Merge branch 'master' into sponsorblock
# Conflicts: # .github/ISSUE_TEMPLATE/feature_request.yml # README.md # app/build.gradle # app/src/main/java/org/schabi/newpipe/DownloaderImpl.java # app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java # app/src/main/java/org/schabi/newpipe/player/Player.java # app/src/main/res/values-es/strings.xml # app/src/main/res/values-so/strings.xml # app/src/main/res/values-zh-rTW/strings.xml # app/src/main/res/values/strings.xml
This commit is contained in:
commit
5b3c971d67
466 changed files with 9933 additions and 8975 deletions
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
name: Bug report
|
name: Bug report
|
||||||
description: Create a bug report to help us improve
|
description: Create a bug report to help us improve
|
||||||
labels: [bug]
|
labels: [bug, needs triage]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -20,6 +20,8 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/polymorphicshade/NewPipe/issues) or [closed](https://github.com/polymorphicshade/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
- label: "I made sure that there are *no existing issues* - [open](https://github.com/polymorphicshade/NewPipe/issues) or [closed](https://github.com/polymorphicshade/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||||
required: true
|
required: true
|
||||||
|
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
|
||||||
|
required: true
|
||||||
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
|
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
|
||||||
required: true
|
required: true
|
||||||
- label: "This issue contains only one bug."
|
- label: "This issue contains only one bug."
|
||||||
|
|
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest an idea for this project
|
description: Suggest an idea for this project
|
||||||
labels: [enhancement]
|
labels: [enhancement, needs triage]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -8,7 +8,6 @@ body:
|
||||||
Thank you for helping to make NewPipe better by suggesting a feature. :hugs:
|
Thank you for helping to make NewPipe better by suggesting a feature. :hugs:
|
||||||
|
|
||||||
Your ideas are highly welcome! The app is made for you, the users, after all.
|
Your ideas are highly welcome! The app is made for you, the users, after all.
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -16,6 +15,8 @@ body:
|
||||||
options:
|
options:
|
||||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/polymorphicshade/NewPipe/issues) or [closed](https://github.com/polymorphicshade/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
- label: "I made sure that there are *no existing issues* - [open](https://github.com/polymorphicshade/NewPipe/issues) or [closed](https://github.com/polymorphicshade/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||||
required: true
|
required: true
|
||||||
|
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
|
||||||
|
required: true
|
||||||
- label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)."
|
- label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)."
|
||||||
required: true
|
required: true
|
||||||
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
|
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
|
||||||
|
|
4
.github/ISSUE_TEMPLATE/question.yml
vendored
4
.github/ISSUE_TEMPLATE/question.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
name: Question
|
name: Question
|
||||||
description: Ask about anything NewPipe-related
|
description: Ask about anything NewPipe-related
|
||||||
labels: [question]
|
labels: [question, needs triage]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -16,6 +16,8 @@ body:
|
||||||
options:
|
options:
|
||||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||||
required: true
|
required: true
|
||||||
|
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my question isn't listed."
|
||||||
|
required: true
|
||||||
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
|
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
|
||||||
required: true
|
required: true
|
||||||
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||||
|
|
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -6,7 +6,7 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- master
|
- master
|
||||||
- release/**
|
- release**
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'README.md'
|
- 'README.md'
|
||||||
- 'doc/**'
|
- 'doc/**'
|
||||||
|
@ -31,6 +31,10 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test-jvm:
|
build-and-test-jvm:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
|
@ -64,6 +68,10 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
# api-level 19 is min sdk, but throws errors related to desugaring
|
# api-level 19 is min sdk, but throws errors related to desugaring
|
||||||
api-level: [ 21, 29 ]
|
api-level: [ 21, 29 ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
@ -91,6 +99,10 @@ jobs:
|
||||||
|
|
||||||
sonar:
|
sonar:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
|
|
4
.github/workflows/image-minimizer.yml
vendored
4
.github/workflows/image-minimizer.yml
vendored
|
@ -6,6 +6,10 @@ on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened, edited]
|
types: [opened, edited]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
try-minimize:
|
try-minimize:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
6
.github/workflows/no-response.yml
vendored
6
.github/workflows/no-response.yml
vendored
|
@ -9,6 +9,10 @@ on:
|
||||||
# Run daily at midnight.
|
# Run daily at midnight.
|
||||||
- cron: '0 0 * * *'
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
noResponse:
|
noResponse:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -17,4 +21,4 @@ jobs:
|
||||||
with:
|
with:
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
daysUntilClose: 14
|
daysUntilClose: 14
|
||||||
responseRequiredLabel: waiting-for-author
|
responseRequiredLabel: waiting for author
|
||||||
|
|
|
@ -14,15 +14,12 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.polymorphicshade.newpipe"
|
applicationId "org.polymorphicshade.newpipe"
|
||||||
resValue "string", "app_name", "NewPipe SponsorBlock"
|
resValue "string", "app_name", "NewPipe SponsorBlock"
|
||||||
minSdk 19
|
minSdk 21
|
||||||
targetSdk 29
|
targetSdk 29
|
||||||
versionCode 989
|
versionCode 990
|
||||||
versionName "0.23.3"
|
versionName "0.24.0"
|
||||||
|
|
||||||
multiDexEnabled true
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
annotationProcessorOptions {
|
annotationProcessorOptions {
|
||||||
|
@ -98,14 +95,14 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
checkstyleVersion = '10.0'
|
checkstyleVersion = '10.3.1'
|
||||||
|
|
||||||
androidxLifecycleVersion = '2.3.1'
|
androidxLifecycleVersion = '2.5.1'
|
||||||
androidxRoomVersion = '2.4.2'
|
androidxRoomVersion = '2.4.3'
|
||||||
androidxWorkVersion = '2.7.1'
|
androidxWorkVersion = '2.7.1'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
exoPlayerVersion = '2.17.1'
|
exoPlayerVersion = '2.18.1'
|
||||||
googleAutoServiceVersion = '1.0.1'
|
googleAutoServiceVersion = '1.0.1'
|
||||||
groupieVersion = '2.10.1'
|
groupieVersion = '2.10.1'
|
||||||
markwonVersion = '4.6.2'
|
markwonVersion = '4.6.2'
|
||||||
|
@ -113,7 +110,7 @@ ext {
|
||||||
leakCanaryVersion = '2.5'
|
leakCanaryVersion = '2.5'
|
||||||
stethoVersion = '1.6.0'
|
stethoVersion = '1.6.0'
|
||||||
mockitoVersion = '4.0.0'
|
mockitoVersion = '4.0.0'
|
||||||
assertJVersion = '3.22.0'
|
assertJVersion = '3.23.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
|
@ -182,7 +179,7 @@ sonarqube {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring **/
|
/** Desugaring **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
|
||||||
|
|
||||||
/** NewPipe libraries **/
|
/** NewPipe libraries **/
|
||||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||||
|
@ -190,27 +187,27 @@ dependencies {
|
||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||||
// This works thanks to JitPack: https://jitpack.io/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:6a858368c86bc9a55abee586eb6c733e86c26b97'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:5c710da160f488bb40ab2cf4469bec9bd4cefd38'
|
||||||
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||||
ktlint 'com.pinterest:ktlint:0.44.0'
|
ktlint 'com.pinterest:ktlint:0.45.2'
|
||||||
|
|
||||||
/** Kotlin **/
|
/** Kotlin **/
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||||
|
|
||||||
/** AndroidX **/
|
/** AndroidX **/
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.core:core-ktx:1.6.0'
|
implementation 'androidx.core:core-ktx:1.8.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
implementation 'androidx.media:media:1.5.0'
|
implementation 'androidx.media:media:1.6.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
|
||||||
implementation 'androidx.preference:preference:1.2.0'
|
implementation 'androidx.preference:preference:1.2.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||||
|
@ -220,10 +217,9 @@ dependencies {
|
||||||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
implementation 'androidx.webkit:webkit:1.4.0'
|
|
||||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
implementation 'com.google.android.material:material:1.6.1'
|
||||||
|
|
||||||
/** Third-party libraries **/
|
/** Third-party libraries **/
|
||||||
// Instance state boilerplate elimination
|
// Instance state boilerplate elimination
|
||||||
|
@ -234,11 +230,16 @@ dependencies {
|
||||||
implementation "org.jsoup:jsoup:1.15.3"
|
implementation "org.jsoup:jsoup:1.15.3"
|
||||||
|
|
||||||
// HTTP client
|
// HTTP client
|
||||||
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
|
implementation "com.squareup.okhttp3:okhttp:4.10.0"
|
||||||
implementation "com.squareup.okhttp3:okhttp:3.12.13"
|
|
||||||
|
|
||||||
// Media player
|
// Media player
|
||||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
|
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}"
|
||||||
|
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}"
|
||||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||||
|
|
||||||
// Metadata generator for service descriptors
|
// Metadata generator for service descriptors
|
||||||
|
@ -257,9 +258,6 @@ dependencies {
|
||||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||||
|
|
||||||
// File picker
|
|
||||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
|
||||||
|
|
||||||
// Crash reporting
|
// Crash reporting
|
||||||
implementation "ch.acra:acra-core:5.9.3"
|
implementation "ch.acra:acra-core:5.9.3"
|
||||||
|
|
||||||
|
@ -273,7 +271,7 @@ dependencies {
|
||||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||||
|
|
||||||
// Date and time formatting
|
// Date and time formatting
|
||||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.2.Final"
|
implementation "org.ocpsoft.prettytime:prettytime:5.0.3.Final"
|
||||||
|
|
||||||
/** Debugging **/
|
/** Debugging **/
|
||||||
// Memory leak detection
|
// Memory leak detection
|
||||||
|
|
11
app/proguard-rules.pro
vendored
11
app/proguard-rules.pro
vendored
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||||
-keep class org.ocpsoft.prettytime.i18n.** { *; }
|
|
||||||
|
|
||||||
-keep class org.mozilla.javascript.** { *; }
|
-keep class org.mozilla.javascript.** { *; }
|
||||||
|
|
||||||
|
@ -26,9 +25,6 @@
|
||||||
-keep class com.google.android.exoplayer2.** { *; }
|
-keep class com.google.android.exoplayer2.** { *; }
|
||||||
|
|
||||||
-dontwarn org.mozilla.javascript.tools.**
|
-dontwarn org.mozilla.javascript.tools.**
|
||||||
-dontwarn android.arch.util.paging.CountedDataSource
|
|
||||||
-dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource
|
|
||||||
|
|
||||||
|
|
||||||
# Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick
|
# Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick
|
||||||
-dontwarn icepick.**
|
-dontwarn icepick.**
|
||||||
|
@ -39,12 +35,11 @@
|
||||||
}
|
}
|
||||||
-keepnames class * { @icepick.State *;}
|
-keepnames class * { @icepick.State *;}
|
||||||
|
|
||||||
# Rules for OkHttp. Copy paste from https://github.com/square/okhttp
|
## Rules for OkHttp. Copy paste from https://github.com/square/okhttp
|
||||||
-dontwarn okhttp3.**
|
-dontwarn okhttp3.**
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
-dontwarn javax.annotation.**
|
##
|
||||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
|
||||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
|
||||||
-keepclassmembers class * implements java.io.Serializable {
|
-keepclassmembers class * implements java.io.Serializable {
|
||||||
static final long serialVersionUID;
|
static final long serialVersionUID;
|
||||||
!static !transient <fields>;
|
!static !transient <fields>;
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".player.MainPlayer"
|
android:name=".player.PlayerService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="mediaPlayback">
|
android:foregroundServiceType="mediaPlayback">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
|
@ -282,11 +282,9 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||||
@Nullable
|
@Nullable
|
||||||
public Parcelable saveState() {
|
public Parcelable saveState() {
|
||||||
Bundle state = null;
|
Bundle state = null;
|
||||||
if (mSavedState.size() > 0) {
|
if (!mSavedState.isEmpty()) {
|
||||||
state = new Bundle();
|
state = new Bundle();
|
||||||
final Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
|
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
|
||||||
mSavedState.toArray(fss);
|
|
||||||
state.putParcelableArray("states", fss);
|
|
||||||
}
|
}
|
||||||
for (int i = 0; i < mFragments.size(); i++) {
|
for (int i = 0; i < mFragments.size(); i++) {
|
||||||
final Fragment f = mFragments.get(i);
|
final Fragment f = mFragments.get(i);
|
||||||
|
|
|
@ -14,7 +14,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
// See https://stackoverflow.com/questions/56849221#57997489
|
// See https://stackoverflow.com/questions/56849221#57997489
|
||||||
|
@ -27,7 +26,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
|
|
||||||
private boolean allowScroll = true;
|
private boolean allowScroll = true;
|
||||||
private final Rect globalRect = new Rect();
|
private final Rect globalRect = new Rect();
|
||||||
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
private final List<Integer> skipInterceptionOfElements = List.of(
|
||||||
R.id.itemsListPanel, R.id.playbackSeekBar,
|
R.id.itemsListPanel, R.id.playbackSeekBar,
|
||||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||||
|
|
||||||
|
@ -67,7 +66,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
||||||
@NonNull final AppBarLayout child,
|
@NonNull final AppBarLayout child,
|
||||||
@NonNull final MotionEvent ev) {
|
@NonNull final MotionEvent ev) {
|
||||||
for (final Integer element : skipInterceptionOfElements) {
|
for (final int element : skipInterceptionOfElements) {
|
||||||
final View view = child.findViewById(element);
|
final View view = child.findViewById(element);
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
||||||
|
@ -132,8 +131,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
try {
|
try {
|
||||||
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
||||||
if (headerBehaviorType != null) {
|
if (headerBehaviorType != null) {
|
||||||
final Field field
|
final Field field =
|
||||||
= headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -7,7 +8,6 @@ import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.app.NotificationChannelCompat;
|
import androidx.core.app.NotificationChannelCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
import androidx.multidex.MultiDexApplication;
|
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||||
|
@ -27,9 +27,8 @@ import org.schabi.newpipe.util.StateSaver;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||||
|
@ -56,7 +55,7 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class App extends MultiDexApplication {
|
public class App extends Application {
|
||||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||||
private static final String TAG = App.class.toString();
|
private static final String TAG = App.class.toString();
|
||||||
private static App app;
|
private static App app;
|
||||||
|
@ -140,7 +139,7 @@ public class App extends MultiDexApplication {
|
||||||
if (throwable instanceof UndeliverableException) {
|
if (throwable instanceof UndeliverableException) {
|
||||||
// As UndeliverableException is a wrapper,
|
// As UndeliverableException is a wrapper,
|
||||||
// get the cause of it to get the "real" exception
|
// get the cause of it to get the "real" exception
|
||||||
actualThrowable = throwable.getCause();
|
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
||||||
} else {
|
} else {
|
||||||
actualThrowable = throwable;
|
actualThrowable = throwable;
|
||||||
}
|
}
|
||||||
|
@ -149,7 +148,7 @@ public class App extends MultiDexApplication {
|
||||||
if (actualThrowable instanceof CompositeException) {
|
if (actualThrowable instanceof CompositeException) {
|
||||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
errors = ((CompositeException) actualThrowable).getExceptions();
|
||||||
} else {
|
} else {
|
||||||
errors = Collections.singletonList(actualThrowable);
|
errors = List.of(actualThrowable);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final Throwable error : errors) {
|
for (final Throwable error : errors) {
|
||||||
|
@ -213,41 +212,37 @@ public class App extends MultiDexApplication {
|
||||||
private void initNotificationChannels() {
|
private void initNotificationChannels() {
|
||||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||||
// the main and update channels
|
// the main and update channels
|
||||||
final List<NotificationChannelCompat> notificationChannelCompats = new ArrayList<>();
|
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
||||||
notificationChannelCompats.add(new NotificationChannelCompat
|
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
||||||
.Builder(getString(R.string.notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||||
.setName(getString(R.string.notification_channel_name))
|
.setName(getString(R.string.notification_channel_name))
|
||||||
.setDescription(getString(R.string.notification_channel_description))
|
.setDescription(getString(R.string.notification_channel_description))
|
||||||
.build());
|
.build(),
|
||||||
|
new NotificationChannelCompat
|
||||||
notificationChannelCompats.add(new NotificationChannelCompat
|
.Builder(getString(R.string.app_update_notification_channel_id),
|
||||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||||
.setName(getString(R.string.app_update_notification_channel_name))
|
.setName(getString(R.string.app_update_notification_channel_name))
|
||||||
.setDescription(getString(R.string.app_update_notification_channel_description))
|
.setDescription(
|
||||||
.build());
|
getString(R.string.app_update_notification_channel_description))
|
||||||
|
.build(),
|
||||||
notificationChannelCompats.add(new NotificationChannelCompat
|
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
||||||
.Builder(getString(R.string.hash_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||||
.setName(getString(R.string.hash_channel_name))
|
.setName(getString(R.string.hash_channel_name))
|
||||||
.setDescription(getString(R.string.hash_channel_description))
|
.setDescription(getString(R.string.hash_channel_description))
|
||||||
.build());
|
.build(),
|
||||||
|
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
||||||
notificationChannelCompats.add(new NotificationChannelCompat
|
|
||||||
.Builder(getString(R.string.error_report_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||||
.setName(getString(R.string.error_report_channel_name))
|
.setName(getString(R.string.error_report_channel_name))
|
||||||
.setDescription(getString(R.string.error_report_channel_description))
|
.setDescription(getString(R.string.error_report_channel_description))
|
||||||
.build());
|
.build(),
|
||||||
|
new NotificationChannelCompat
|
||||||
notificationChannelCompats.add(new NotificationChannelCompat
|
.Builder(getString(R.string.streams_notification_channel_id),
|
||||||
.Builder(getString(R.string.streams_notification_channel_id),
|
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
.setName(getString(R.string.streams_notification_channel_name))
|
||||||
.setName(getString(R.string.streams_notification_channel_name))
|
.setDescription(
|
||||||
.setDescription(getString(R.string.streams_notification_channel_description))
|
getString(R.string.streams_notification_channel_description))
|
||||||
.build());
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -12,40 +11,27 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
import org.schabi.newpipe.extractor.downloader.Request;
|
import org.schabi.newpipe.extractor.downloader.Request;
|
||||||
import org.schabi.newpipe.extractor.downloader.Response;
|
import org.schabi.newpipe.extractor.downloader.Response;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.util.CookieUtils;
|
|
||||||
import org.schabi.newpipe.util.InfoCache;
|
import org.schabi.newpipe.util.InfoCache;
|
||||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
|
||||||
import javax.net.ssl.X509TrustManager;
|
|
||||||
|
|
||||||
import okhttp3.CipherSuite;
|
|
||||||
import okhttp3.ConnectionSpec;
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
public final class DownloaderImpl extends Downloader {
|
public final class DownloaderImpl extends Downloader {
|
||||||
public static final String USER_AGENT
|
public static final String USER_AGENT =
|
||||||
= "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
|
||||||
= "youtube_restricted_mode_key";
|
"youtube_restricted_mode_key";
|
||||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||||
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
||||||
|
|
||||||
|
@ -55,9 +41,6 @@ public final class DownloaderImpl extends Downloader {
|
||||||
private Integer customTimeout;
|
private Integer customTimeout;
|
||||||
|
|
||||||
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
|
||||||
enableModernTLS(builder);
|
|
||||||
}
|
|
||||||
this.client = builder
|
this.client = builder
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
|
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
|
||||||
|
@ -82,74 +65,21 @@ public final class DownloaderImpl extends Downloader {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken
|
|
||||||
* from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_).
|
|
||||||
* <p>
|
|
||||||
* If there is an error, the function will safely fall back to doing nothing
|
|
||||||
* and printing the error to the console.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
|
|
||||||
*/
|
|
||||||
private static void enableModernTLS(final OkHttpClient.Builder builder) {
|
|
||||||
try {
|
|
||||||
// get the default TrustManager
|
|
||||||
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
|
||||||
TrustManagerFactory.getDefaultAlgorithm());
|
|
||||||
trustManagerFactory.init((KeyStore) null);
|
|
||||||
final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
|
||||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
|
||||||
throw new IllegalStateException("Unexpected default trust managers:"
|
|
||||||
+ Arrays.toString(trustManagers));
|
|
||||||
}
|
|
||||||
final X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
|
||||||
|
|
||||||
// insert our own TLSSocketFactory
|
|
||||||
final SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
|
|
||||||
|
|
||||||
builder.sslSocketFactory(sslSocketFactory, trustManager);
|
|
||||||
|
|
||||||
// This will try to enable all modern CipherSuites(+2 more)
|
|
||||||
// that are supported on the device.
|
|
||||||
// Necessary because some servers (e.g. Framatube.org)
|
|
||||||
// don't support the old cipher suites.
|
|
||||||
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
|
|
||||||
final List<CipherSuite> cipherSuites =
|
|
||||||
new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites());
|
|
||||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
|
|
||||||
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
|
|
||||||
final ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
|
||||||
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
|
|
||||||
} catch (final KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
|
||||||
if (DEBUG) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public DownloaderImpl setCustomTimeout(final Integer value) {
|
public DownloaderImpl setCustomTimeout(final Integer value) {
|
||||||
this.customTimeout = value;
|
this.customTimeout = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCookies(final String url) {
|
public String getCookies(final String url) {
|
||||||
final List<String> resultCookies = new ArrayList<>();
|
final String youtubeCookie = url.contains(YOUTUBE_DOMAIN)
|
||||||
if (url.contains(YOUTUBE_DOMAIN)) {
|
? getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) : null;
|
||||||
final String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
|
||||||
if (youtubeCookie != null) {
|
|
||||||
resultCookies.add(youtubeCookie);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
||||||
final String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
|
return Stream.of(youtubeCookie, getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY))
|
||||||
if (recaptchaCookie != null) {
|
.filter(Objects::nonNull)
|
||||||
resultCookies.add(recaptchaCookie);
|
.flatMap(cookies -> Arrays.stream(cookies.split("; *")))
|
||||||
}
|
.distinct()
|
||||||
return CookieUtils.concatCookies(resultCookies);
|
.collect(Collectors.joining("; "));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCookie(final String key) {
|
public String getCookie(final String key) {
|
||||||
|
@ -209,7 +139,7 @@ public final class DownloaderImpl extends Downloader {
|
||||||
|
|
||||||
RequestBody requestBody = null;
|
RequestBody requestBody = null;
|
||||||
if (dataToSend != null) {
|
if (dataToSend != null) {
|
||||||
requestBody = RequestBody.create(null, dataToSend);
|
requestBody = RequestBody.create(dataToSend);
|
||||||
}
|
}
|
||||||
|
|
||||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||||
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
@ -44,11 +43,7 @@ public class ExitActivity extends Activity {
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
finishAndRemoveTask();
|
||||||
finishAndRemoveTask();
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationHelper.restartApp(this);
|
NavigationHelper.restartApp(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
@ -86,7 +85,6 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
import org.schabi.newpipe.util.SerializedCache;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.views.FocusOverlayView;
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
|
@ -131,11 +129,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for media.ccc.de sources
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
|
||||||
TLSSocketFactoryCompat.setAsDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
ThemeHelper.setDayNightMode(this);
|
ThemeHelper.setDayNightMode(this);
|
||||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||||
|
|
||||||
|
@ -381,8 +374,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private void showServices() {
|
private void showServices() {
|
||||||
for (final StreamingService s : NewPipe.getServices()) {
|
for (final StreamingService s : NewPipe.getServices()) {
|
||||||
final String title = s.getServiceInfo().getName()
|
final String title = s.getServiceInfo().getName();
|
||||||
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
|
||||||
|
|
||||||
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
|
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
|
||||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||||
|
@ -390,7 +382,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
// peertube specifics
|
// peertube specifics
|
||||||
if (s.getServiceId() == 3) {
|
if (s.getServiceId() == 3) {
|
||||||
enhancePeertubeMenu(s, menuItem);
|
enhancePeertubeMenu(menuItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drawerLayoutBinding.navigation.getMenu()
|
drawerLayoutBinding.navigation.getMenu()
|
||||||
|
@ -398,9 +390,9 @@ public class MainActivity extends AppCompatActivity {
|
||||||
.setChecked(true);
|
.setChecked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
|
private void enhancePeertubeMenu(final MenuItem menuItem) {
|
||||||
final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
|
final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
|
||||||
menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
menuItem.setTitle(currentInstance.getName());
|
||||||
final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
|
final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
|
||||||
.getRoot();
|
.getRoot();
|
||||||
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||||
|
@ -480,8 +472,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
|
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
final SharedPreferences sharedPreferences
|
final SharedPreferences sharedPreferences =
|
||||||
= PreferenceManager.getDefaultSharedPreferences(this);
|
PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Theme has changed, recreating activity...");
|
Log.d(TAG, "Theme has changed, recreating activity...");
|
||||||
|
@ -653,8 +645,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
super.onCreateOptionsMenu(menu);
|
super.onCreateOptionsMenu(menu);
|
||||||
|
|
||||||
final Fragment fragment
|
final Fragment fragment =
|
||||||
= getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||||
if (!(fragment instanceof SearchFragment)) {
|
if (!(fragment instanceof SearchFragment)) {
|
||||||
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
|
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -40,10 +39,6 @@ public class PanicResponderActivity extends Activity {
|
||||||
ExitActivity.exitAndRemoveFromRecentApps(this);
|
ExitActivity.exitAndRemoveFromRecentApps(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
finishAndRemoveTask();
|
||||||
finishAndRemoveTask();
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.SparseItemUtil;
|
import org.schabi.newpipe.util.SparseItemUtil;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.List;
|
||||||
|
|
||||||
public final class QueueItemMenuUtil {
|
public final class QueueItemMenuUtil {
|
||||||
private QueueItemMenuUtil() {
|
private QueueItemMenuUtil() {
|
||||||
|
@ -53,7 +53,7 @@ public final class QueueItemMenuUtil {
|
||||||
case R.id.menu_item_append_playlist:
|
case R.id.menu_item_append_playlist:
|
||||||
PlaylistDialog.createCorrespondingDialog(
|
PlaylistDialog.createCorrespondingDialog(
|
||||||
context,
|
context,
|
||||||
Collections.singletonList(new StreamEntity(item)),
|
List.of(new StreamEntity(item)),
|
||||||
dialog -> dialog.show(
|
dialog -> dialog.show(
|
||||||
fragmentManager,
|
fragmentManager,
|
||||||
"QueueItemMenuUtil@append_playlist"
|
"QueueItemMenuUtil@append_playlist"
|
||||||
|
|
|
@ -30,6 +30,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.ServiceCompat;
|
import androidx.core.app.ServiceCompat;
|
||||||
|
import androidx.core.math.MathUtils;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||||
|
@ -81,7 +82,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import icepick.Icepick;
|
import icepick.Icepick;
|
||||||
|
@ -452,7 +452,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.size() - 1);
|
selectedRadioPosition = MathUtils.clamp(selectedRadioPosition, -1, choices.size() - 1);
|
||||||
if (selectedRadioPosition != -1) {
|
if (selectedRadioPosition != -1) {
|
||||||
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
|
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
|
||||||
}
|
}
|
||||||
|
@ -630,8 +630,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...the player is not running or in normal Video-mode/type
|
// ...the player is not running or in normal Video-mode/type
|
||||||
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
|
final PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||||
return playerType == null || playerType == MainPlayer.PlayerType.VIDEO;
|
return playerType == null || playerType == PlayerType.MAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openAddToPlaylistDialog() {
|
private void openAddToPlaylistDialog() {
|
||||||
|
@ -649,7 +649,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
.subscribe(
|
.subscribe(
|
||||||
info -> PlaylistDialog.createCorrespondingDialog(
|
info -> PlaylistDialog.createCorrespondingDialog(
|
||||||
getThemeWrapperContext(),
|
getThemeWrapperContext(),
|
||||||
Collections.singletonList(new StreamEntity(info)),
|
List.of(new StreamEntity(info)),
|
||||||
playlistDialog -> {
|
playlistDialog -> {
|
||||||
playlistDialog.setOnDismissListener(dialog -> finish());
|
playlistDialog.setOnDismissListener(dialog -> finish());
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
|
|
||||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||||
|
|
||||||
|
|
|
@ -12,126 +12,92 @@ import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
object LicenseFragmentHelper {
|
/**
|
||||||
/**
|
* @param context the context to use
|
||||||
* @param context the context to use
|
* @param license the license
|
||||||
* @param license the license
|
* @return String which contains a HTML formatted license page
|
||||||
* @return String which contains a HTML formatted license page
|
* styled according to the context's theme
|
||||||
* styled according to the context's theme
|
*/
|
||||||
*/
|
private fun getFormattedLicense(context: Context, license: License): String {
|
||||||
private fun getFormattedLicense(context: Context, license: License): String {
|
try {
|
||||||
val licenseContent = StringBuilder()
|
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||||
val webViewData: String
|
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||||
try {
|
.replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
|
||||||
BufferedReader(
|
} catch (e: IOException) {
|
||||||
InputStreamReader(
|
throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
|
||||||
context.assets.open(license.filename),
|
}
|
||||||
StandardCharsets.UTF_8
|
}
|
||||||
)
|
|
||||||
).use { `in` ->
|
|
||||||
var str: String?
|
|
||||||
while (`in`.readLine().also { str = it } != null) {
|
|
||||||
licenseContent.append(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
/**
|
||||||
webViewData = "$licenseContent".replace(
|
* @param context the Android context
|
||||||
"</head>",
|
* @return String which is a CSS stylesheet according to the context's theme
|
||||||
"<style>" + getLicenseStylesheet(context) + "</style></head>"
|
*/
|
||||||
)
|
private fun getLicenseStylesheet(context: Context): String {
|
||||||
}
|
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||||
} catch (e: IOException) {
|
val licenseBackgroundColor = getHexRGBColor(
|
||||||
throw IllegalArgumentException(
|
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||||
"Could not get license file: " + license.filename, e
|
)
|
||||||
)
|
val licenseTextColor = getHexRGBColor(
|
||||||
|
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
|
||||||
|
)
|
||||||
|
val youtubePrimaryColor = getHexRGBColor(
|
||||||
|
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
|
||||||
|
)
|
||||||
|
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
|
||||||
|
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast R.color to a hexadecimal color value.
|
||||||
|
*
|
||||||
|
* @param context the context to use
|
||||||
|
* @param color the color number from R.color
|
||||||
|
* @return a six characters long String with hexadecimal RGB values
|
||||||
|
*/
|
||||||
|
private fun getHexRGBColor(context: Context, color: Int): String {
|
||||||
|
return context.getString(color).substring(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
||||||
|
return showLicense(context, component.license) {
|
||||||
|
setPositiveButton(R.string.dismiss) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
return webViewData
|
setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||||
}
|
ShareUtils.openUrlInBrowser(context!!, component.link)
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context the Android context
|
|
||||||
* @return String which is a CSS stylesheet according to the context's theme
|
|
||||||
*/
|
|
||||||
private fun getLicenseStylesheet(context: Context): String {
|
|
||||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
|
||||||
return (
|
|
||||||
"body{padding:12px 15px;margin:0;" + "background:#" + getHexRGBColor(
|
|
||||||
context,
|
|
||||||
if (isLightTheme) R.color.light_license_background_color
|
|
||||||
else R.color.dark_license_background_color
|
|
||||||
) + ";" + "color:#" + getHexRGBColor(
|
|
||||||
context,
|
|
||||||
if (isLightTheme) R.color.light_license_text_color
|
|
||||||
else R.color.dark_license_text_color
|
|
||||||
) + "}" + "a[href]{color:#" + getHexRGBColor(
|
|
||||||
context,
|
|
||||||
if (isLightTheme) R.color.light_youtube_primary_color
|
|
||||||
else R.color.dark_youtube_primary_color
|
|
||||||
) + "}" + "pre{white-space:pre-wrap}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cast R.color to a hexadecimal color value.
|
|
||||||
*
|
|
||||||
* @param context the context to use
|
|
||||||
* @param color the color number from R.color
|
|
||||||
* @return a six characters long String with hexadecimal RGB values
|
|
||||||
*/
|
|
||||||
private fun getHexRGBColor(context: Context, color: Int): String {
|
|
||||||
return context.getString(color).substring(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showLicense(context: Context?, license: License): Disposable {
|
|
||||||
return showLicense(context, license) { alertDialog ->
|
|
||||||
alertDialog.setPositiveButton(R.string.ok) { dialog, _ ->
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
|
||||||
return showLicense(context, component.license) { alertDialog ->
|
|
||||||
alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ ->
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
alertDialog.setNeutralButton(R.string.open_website_license) { _, _ ->
|
|
||||||
ShareUtils.openUrlInBrowser(context!!, component.link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLicense(
|
|
||||||
context: Context?,
|
|
||||||
license: License,
|
|
||||||
block: (AlertDialog.Builder) -> Unit
|
|
||||||
): Disposable {
|
|
||||||
return if (context == null) {
|
|
||||||
Disposable.empty()
|
|
||||||
} else {
|
|
||||||
Observable.fromCallable { getFormattedLicense(context, license) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { formattedLicense ->
|
|
||||||
val webViewData = Base64.encodeToString(
|
|
||||||
formattedLicense.toByteArray(StandardCharsets.UTF_8), Base64.NO_PADDING
|
|
||||||
)
|
|
||||||
val webView = WebView(context)
|
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(license.name)
|
|
||||||
setView(webView)
|
|
||||||
Localization.assureCorrectAppLanguage(context)
|
|
||||||
block(this)
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showLicense(context: Context?, license: License) = showLicense(context, license) {
|
||||||
|
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicense(
|
||||||
|
context: Context?,
|
||||||
|
license: License,
|
||||||
|
block: AlertDialog.Builder.() -> AlertDialog.Builder
|
||||||
|
): Disposable {
|
||||||
|
return if (context == null) {
|
||||||
|
Disposable.empty()
|
||||||
|
} else {
|
||||||
|
Observable.fromCallable { getFormattedLicense(context, license) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { formattedLicense ->
|
||||||
|
val webViewData =
|
||||||
|
Base64.encodeToString(formattedLicense.toByteArray(), Base64.NO_PADDING)
|
||||||
|
val webView = WebView(context)
|
||||||
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||||
|
|
||||||
|
Localization.assureCorrectAppLanguage(context)
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setTitle(license.name)
|
||||||
|
.setView(webView)
|
||||||
|
.block()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe.database;
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Delete;
|
import androidx.room.Delete;
|
||||||
import androidx.room.Insert;
|
import androidx.room.Insert;
|
||||||
import androidx.room.OnConflictStrategy;
|
|
||||||
import androidx.room.Update;
|
import androidx.room.Update;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -14,13 +13,10 @@ import io.reactivex.rxjava3.core.Flowable;
|
||||||
@Dao
|
@Dao
|
||||||
public interface BasicDAO<Entity> {
|
public interface BasicDAO<Entity> {
|
||||||
/* Inserts */
|
/* Inserts */
|
||||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
@Insert
|
||||||
long insert(Entity entity);
|
long insert(Entity entity);
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
@Insert
|
||||||
List<Long> insertAll(Entity... entities);
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
|
||||||
List<Long> insertAll(Collection<Entity> entities);
|
List<Long> insertAll(Collection<Entity> entities);
|
||||||
|
|
||||||
/* Searches */
|
/* Searches */
|
||||||
|
@ -32,9 +28,6 @@ public interface BasicDAO<Entity> {
|
||||||
@Delete
|
@Delete
|
||||||
void delete(Entity entity);
|
void delete(Entity entity);
|
||||||
|
|
||||||
@Delete
|
|
||||||
int delete(Collection<Entity> entities);
|
|
||||||
|
|
||||||
int deleteAll();
|
int deleteAll();
|
||||||
|
|
||||||
/* Updates */
|
/* Updates */
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.room.Update
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.core.Maybe
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||||
import org.schabi.newpipe.database.stream.StreamWithState
|
import org.schabi.newpipe.database.stream.StreamWithState
|
||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
|
@ -21,56 +22,16 @@ abstract class FeedDAO {
|
||||||
@Query("DELETE FROM feed")
|
@Query("DELETE FROM feed")
|
||||||
abstract fun deleteAll(): Int
|
abstract fun deleteAll(): Int
|
||||||
|
|
||||||
@Query(
|
|
||||||
"""
|
|
||||||
SELECT s.*, sst.progress_time
|
|
||||||
FROM streams s
|
|
||||||
|
|
||||||
LEFT JOIN stream_state sst
|
|
||||||
ON s.uid = sst.stream_id
|
|
||||||
|
|
||||||
LEFT JOIN stream_history sh
|
|
||||||
ON s.uid = sh.stream_id
|
|
||||||
|
|
||||||
INNER JOIN feed f
|
|
||||||
ON s.uid = f.stream_id
|
|
||||||
|
|
||||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
|
||||||
LIMIT 500
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
abstract fun getAllStreams(): Maybe<List<StreamWithState>>
|
|
||||||
|
|
||||||
@Query(
|
|
||||||
"""
|
|
||||||
SELECT s.*, sst.progress_time
|
|
||||||
FROM streams s
|
|
||||||
|
|
||||||
LEFT JOIN stream_state sst
|
|
||||||
ON s.uid = sst.stream_id
|
|
||||||
|
|
||||||
LEFT JOIN stream_history sh
|
|
||||||
ON s.uid = sh.stream_id
|
|
||||||
|
|
||||||
INNER JOIN feed f
|
|
||||||
ON s.uid = f.stream_id
|
|
||||||
|
|
||||||
INNER JOIN feed_group_subscription_join fgs
|
|
||||||
ON fgs.subscription_id = f.subscription_id
|
|
||||||
|
|
||||||
WHERE fgs.group_id = :groupId
|
|
||||||
|
|
||||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
|
||||||
LIMIT 500
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
abstract fun getAllStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param groupId the group id to get feed streams of; use
|
||||||
|
* [FeedGroupEntity.GROUP_ALL_ID] to not filter by group
|
||||||
|
* @param includePlayed if false, only return all of the live, never-played or non-finished
|
||||||
|
* feed streams (see `@see` items); if true no filter is applied
|
||||||
|
* @param uploadDateBefore get only streams uploaded before this date (useful to filter out
|
||||||
|
* future streams); use null to not filter by upload date
|
||||||
|
* @return the feed streams filtered according to the conditions provided in the parameters
|
||||||
* @see StreamStateEntity.isFinished()
|
* @see StreamStateEntity.isFinished()
|
||||||
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
||||||
* @return all of the non-live, never-played and non-finished streams in the feed
|
|
||||||
* (all of the cited conditions must hold for a stream to be in the returned list)
|
|
||||||
*/
|
*/
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
@ -86,60 +47,37 @@ abstract class FeedDAO {
|
||||||
INNER JOIN feed f
|
INNER JOIN feed f
|
||||||
ON s.uid = f.stream_id
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
LEFT JOIN feed_group_subscription_join fgs
|
||||||
|
ON fgs.subscription_id = f.subscription_id
|
||||||
|
|
||||||
WHERE (
|
WHERE (
|
||||||
sh.stream_id IS NULL
|
:groupId = ${FeedGroupEntity.GROUP_ALL_ID}
|
||||||
OR sst.stream_id IS NULL
|
OR fgs.group_id = :groupId
|
||||||
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
|
||||||
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
|
||||||
OR s.stream_type = 'LIVE_STREAM'
|
|
||||||
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
|
||||||
LIMIT 500
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see StreamStateEntity.isFinished()
|
|
||||||
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
|
||||||
* @param groupId the group id to get streams of
|
|
||||||
* @return all of the non-live, never-played and non-finished streams for the given feed group
|
|
||||||
* (all of the cited conditions must hold for a stream to be in the returned list)
|
|
||||||
*/
|
|
||||||
@Query(
|
|
||||||
"""
|
|
||||||
SELECT s.*, sst.progress_time
|
|
||||||
FROM streams s
|
|
||||||
|
|
||||||
LEFT JOIN stream_state sst
|
|
||||||
ON s.uid = sst.stream_id
|
|
||||||
|
|
||||||
LEFT JOIN stream_history sh
|
|
||||||
ON s.uid = sh.stream_id
|
|
||||||
|
|
||||||
INNER JOIN feed f
|
|
||||||
ON s.uid = f.stream_id
|
|
||||||
|
|
||||||
INNER JOIN feed_group_subscription_join fgs
|
|
||||||
ON fgs.subscription_id = f.subscription_id
|
|
||||||
|
|
||||||
WHERE fgs.group_id = :groupId
|
|
||||||
AND (
|
AND (
|
||||||
sh.stream_id IS NULL
|
:includePlayed
|
||||||
|
OR sh.stream_id IS NULL
|
||||||
OR sst.stream_id IS NULL
|
OR sst.stream_id IS NULL
|
||||||
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||||
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
||||||
OR s.stream_type = 'LIVE_STREAM'
|
OR s.stream_type = 'LIVE_STREAM'
|
||||||
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||||
)
|
)
|
||||||
|
AND (
|
||||||
|
:uploadDateBefore IS NULL
|
||||||
|
OR s.upload_date IS NULL
|
||||||
|
OR s.upload_date < :uploadDateBefore
|
||||||
|
)
|
||||||
|
|
||||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
abstract fun getStreams(
|
||||||
|
groupId: Long,
|
||||||
|
includePlayed: Boolean,
|
||||||
|
uploadDateBefore: OffsetDateTime?
|
||||||
|
): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,10 +3,10 @@ package org.schabi.newpipe.database.playlist;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public interface PlaylistLocalItem extends LocalItem {
|
public interface PlaylistLocalItem extends LocalItem {
|
||||||
String getOrderingName();
|
String getOrderingName();
|
||||||
|
@ -14,14 +14,9 @@ public interface PlaylistLocalItem extends LocalItem {
|
||||||
static List<PlaylistLocalItem> merge(
|
static List<PlaylistLocalItem> merge(
|
||||||
final List<PlaylistMetadataEntry> localPlaylists,
|
final List<PlaylistMetadataEntry> localPlaylists,
|
||||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||||
final List<PlaylistLocalItem> items = new ArrayList<>(
|
return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
|
||||||
localPlaylists.size() + remotePlaylists.size());
|
.sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
||||||
items.addAll(localPlaylists);
|
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
|
||||||
items.addAll(remotePlaylists);
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
|
||||||
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package org.schabi.newpipe.download;
|
package org.schabi.newpipe.download;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -83,10 +87,6 @@ import us.shandian.giga.service.DownloadManagerService;
|
||||||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||||
import us.shandian.giga.service.MissionState;
|
import us.shandian.giga.service.MissionState;
|
||||||
|
|
||||||
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
|
|
||||||
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
|
|
||||||
public class DownloadDialog extends DialogFragment
|
public class DownloadDialog extends DialogFragment
|
||||||
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||||
private static final String TAG = "DialogFragment";
|
private static final String TAG = "DialogFragment";
|
||||||
|
@ -212,8 +212,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams
|
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams =
|
||||||
= new SparseArray<>(4);
|
new SparseArray<>(4);
|
||||||
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||||
|
|
||||||
for (int i = 0; i < videoStreams.size(); i++) {
|
for (int i = 0; i < videoStreams.size(); i++) {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 24.10.15.
|
* Created by Christian Schabesberger on 24.10.15.
|
||||||
|
@ -65,11 +66,11 @@ public class ErrorActivity extends AppCompatActivity {
|
||||||
public static final String ERROR_EMAIL_ADDRESS = "polymorphicshade@gmail.com";
|
public static final String ERROR_EMAIL_ADDRESS = "polymorphicshade@gmail.com";
|
||||||
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
|
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
|
||||||
|
|
||||||
public static final String ERROR_GITHUB_ISSUE_URL
|
public static final String ERROR_GITHUB_ISSUE_URL =
|
||||||
= "https://github.com/polymorphicshade/NewPipe/issues";
|
"https://github.com/polymorphicshade/NewPipe/issues";
|
||||||
|
|
||||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
|
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
|
||||||
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
|
|
||||||
|
|
||||||
private ErrorInfo errorInfo;
|
private ErrorInfo errorInfo;
|
||||||
|
@ -182,14 +183,9 @@ public class ErrorActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String formErrorText(final String[] el) {
|
private String formErrorText(final String[] el) {
|
||||||
final StringBuilder text = new StringBuilder();
|
final String separator = "-------------------------------------";
|
||||||
if (el != null) {
|
return Arrays.stream(el)
|
||||||
for (final String e : el) {
|
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
||||||
text.append("-------------------------------------\n").append(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text.append("-------------------------------------");
|
|
||||||
return text.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,8 +14,6 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
||||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||||
import org.schabi.newpipe.util.ServiceHelper
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.io.StringWriter
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class ErrorInfo(
|
class ErrorInfo(
|
||||||
|
@ -80,19 +78,10 @@ class ErrorInfo(
|
||||||
companion object {
|
companion object {
|
||||||
const val SERVICE_NONE = "none"
|
const val SERVICE_NONE = "none"
|
||||||
|
|
||||||
private fun getStackTrace(throwable: Throwable): String {
|
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
|
||||||
StringWriter().use { stringWriter ->
|
|
||||||
PrintWriter(stringWriter, true).use { printWriter ->
|
|
||||||
throwable.printStackTrace(printWriter)
|
|
||||||
return stringWriter.buffer.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable))
|
fun throwableListToStringList(throwableList: List<Throwable>) =
|
||||||
|
throwableList.map { it.stackTraceToString() }.toTypedArray()
|
||||||
fun throwableListToStringList(throwable: List<Throwable>) =
|
|
||||||
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
|
|
||||||
|
|
||||||
private fun getInfoServiceName(info: Info?) =
|
private fun getInfoServiceName(info: Info?) =
|
||||||
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
|
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
|
||||||
|
|
|
@ -123,13 +123,7 @@ class ErrorUtil {
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.error_report_channel_id)
|
context.getString(R.string.error_report_channel_id)
|
||||||
)
|
)
|
||||||
.setSmallIcon(
|
.setSmallIcon(R.drawable.ic_bug_report)
|
||||||
// the vector drawable icon causes crashes on KitKat devices
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
R.drawable.ic_bug_report
|
|
||||||
else
|
|
||||||
android.R.drawable.stat_notify_error
|
|
||||||
)
|
|
||||||
.setContentTitle(context.getString(R.string.error_report_notification_title))
|
.setContentTitle(context.getString(R.string.error_report_notification_title))
|
||||||
.setContentText(context.getString(errorInfo.messageStringId))
|
.setContentText(context.getString(errorInfo.messageStringId))
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
|
|
|
@ -3,14 +3,15 @@ package org.schabi.newpipe.error;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.webkit.CookieManager;
|
import android.webkit.CookieManager;
|
||||||
|
import android.webkit.WebResourceRequest;
|
||||||
import android.webkit.WebSettings;
|
import android.webkit.WebSettings;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -18,7 +19,6 @@ import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.app.NavUtils;
|
import androidx.core.app.NavUtils;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
import androidx.webkit.WebViewClientCompat;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
||||||
import org.schabi.newpipe.DownloaderImpl;
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
|
@ -86,14 +86,15 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||||
webSettings.setJavaScriptEnabled(true);
|
webSettings.setJavaScriptEnabled(true);
|
||||||
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
|
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
|
||||||
|
|
||||||
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() {
|
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() {
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
|
public boolean shouldOverrideUrlLoading(final WebView view,
|
||||||
|
final WebResourceRequest request) {
|
||||||
if (MainActivity.DEBUG) {
|
if (MainActivity.DEBUG) {
|
||||||
Log.d(TAG, "shouldOverrideUrlLoading: url=" + url);
|
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCookiesFromUrl(url);
|
handleCookiesFromUrl(request.getUrl().toString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,12 +108,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||||
// cleaning cache, history and cookies from webView
|
// cleaning cache, history and cookies from webView
|
||||||
recaptchaBinding.reCaptchaWebView.clearCache(true);
|
recaptchaBinding.reCaptchaWebView.clearCache(true);
|
||||||
recaptchaBinding.reCaptchaWebView.clearHistory();
|
recaptchaBinding.reCaptchaWebView.clearHistory();
|
||||||
final CookieManager cookieManager = CookieManager.getInstance();
|
CookieManager.getInstance().removeAllCookies(null);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
cookieManager.removeAllCookies(value -> { });
|
|
||||||
} else {
|
|
||||||
cookieManager.removeAllCookie();
|
|
||||||
}
|
|
||||||
|
|
||||||
recaptchaBinding.reCaptchaWebView.loadUrl(url);
|
recaptchaBinding.reCaptchaWebView.loadUrl(url);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package org.schabi.newpipe.fragments.detail;
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||||
|
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -26,17 +30,9 @@ import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
|
||||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
|
||||||
|
|
||||||
public class DescriptionFragment extends BaseFragment {
|
public class DescriptionFragment extends BaseFragment {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
|
@ -185,8 +181,8 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ItemMetadataBinding itemBinding
|
final ItemMetadataBinding itemBinding =
|
||||||
= ItemMetadataBinding.inflate(inflater, layout, false);
|
ItemMetadataBinding.inflate(inflater, layout, false);
|
||||||
|
|
||||||
itemBinding.metadataTypeView.setText(type);
|
itemBinding.metadataTypeView.setText(type);
|
||||||
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
|
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
|
||||||
|
@ -206,19 +202,16 @@ public class DescriptionFragment extends BaseFragment {
|
||||||
|
|
||||||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||||
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
|
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
|
||||||
final ItemMetadataTagsBinding itemBinding
|
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||||
= ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
|
||||||
|
|
||||||
final List<String> tags = new ArrayList<>(streamInfo.getTags());
|
streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
||||||
Collections.sort(tags);
|
|
||||||
for (final String tag : tags) {
|
|
||||||
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
|
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
|
||||||
itemBinding.metadataTagsChips, false);
|
itemBinding.metadataTagsChips, false);
|
||||||
chip.setText(tag);
|
chip.setText(tag);
|
||||||
chip.setOnClickListener(this::onTagClick);
|
chip.setOnClickListener(this::onTagClick);
|
||||||
chip.setOnLongClickListener(this::onTagLongClick);
|
chip.setOnLongClickListener(this::onTagLongClick);
|
||||||
itemBinding.metadataTagsChips.addView(chip);
|
itemBinding.metadataTagsChips.addView(chip);
|
||||||
}
|
});
|
||||||
|
|
||||||
layout.addView(itemBinding.getRoot());
|
layout.addView(itemBinding.getRoot());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
package org.schabi.newpipe.fragments.detail;
|
package org.schabi.newpipe.fragments.detail;
|
||||||
|
|
||||||
|
import static android.text.TextUtils.isEmpty;
|
||||||
|
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||||
|
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||||
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
|
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||||
|
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||||
|
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||||
|
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||||
|
|
||||||
import android.animation.ValueAnimator;
|
import android.animation.ValueAnimator;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
|
@ -10,7 +21,6 @@ import android.content.SharedPreferences;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Point;
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
@ -77,9 +87,9 @@ import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
@ -87,6 +97,8 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.player.ui.MainPlayerUi;
|
||||||
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
@ -104,11 +116,11 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
|
@ -118,17 +130,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
|
||||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
|
||||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
|
||||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
|
||||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
|
||||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
|
||||||
|
|
||||||
public final class VideoDetailFragment
|
public final class VideoDetailFragment
|
||||||
extends BaseStateFragment<StreamInfo>
|
extends BaseStateFragment<StreamInfo>
|
||||||
implements BackPressable,
|
implements BackPressable,
|
||||||
|
@ -183,6 +184,8 @@ public final class VideoDetailFragment
|
||||||
@State
|
@State
|
||||||
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||||
@State
|
@State
|
||||||
|
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||||
|
@State
|
||||||
protected boolean autoPlayEnabled = true;
|
protected boolean autoPlayEnabled = true;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -196,6 +199,7 @@ public final class VideoDetailFragment
|
||||||
private Disposable videoSegmentsSubscriber = null;
|
private Disposable videoSegmentsSubscriber = null;
|
||||||
|
|
||||||
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
|
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
|
||||||
|
private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback;
|
||||||
private BroadcastReceiver broadcastReceiver;
|
private BroadcastReceiver broadcastReceiver;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -208,7 +212,7 @@ public final class VideoDetailFragment
|
||||||
|
|
||||||
private ContentObserver settingsContentObserver;
|
private ContentObserver settingsContentObserver;
|
||||||
@Nullable
|
@Nullable
|
||||||
private MainPlayer playerService;
|
private PlayerService playerService;
|
||||||
private Player player;
|
private Player player;
|
||||||
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
||||||
|
|
||||||
|
@ -217,7 +221,7 @@ public final class VideoDetailFragment
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(final Player connectedPlayer,
|
public void onServiceConnected(final Player connectedPlayer,
|
||||||
final MainPlayer connectedPlayerService,
|
final PlayerService connectedPlayerService,
|
||||||
final boolean playAfterConnect) {
|
final boolean playAfterConnect) {
|
||||||
player = connectedPlayer;
|
player = connectedPlayer;
|
||||||
playerService = connectedPlayerService;
|
playerService = connectedPlayerService;
|
||||||
|
@ -225,6 +229,7 @@ public final class VideoDetailFragment
|
||||||
// It will do nothing if the player is not in fullscreen mode
|
// It will do nothing if the player is not in fullscreen mode
|
||||||
hideSystemUiIfNeeded();
|
hideSystemUiIfNeeded();
|
||||||
|
|
||||||
|
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
|
||||||
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -233,22 +238,19 @@ public final class VideoDetailFragment
|
||||||
// If the video is playing but orientation changed
|
// If the video is playing but orientation changed
|
||||||
// let's make the video in fullscreen again
|
// let's make the video in fullscreen again
|
||||||
checkLandscape();
|
checkLandscape();
|
||||||
} else if (player.isFullscreen() && !player.isVerticalVideo()
|
} else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
|
||||||
// Tablet UI has orientation-independent fullscreen
|
// Tablet UI has orientation-independent fullscreen
|
||||||
&& !DeviceUtils.isTablet(activity)) {
|
&& !DeviceUtils.isTablet(activity)) {
|
||||||
// Device is in portrait orientation after rotation but UI is in fullscreen.
|
// Device is in portrait orientation after rotation but UI is in fullscreen.
|
||||||
// Return back to non-fullscreen state
|
// Return back to non-fullscreen state
|
||||||
player.toggleFullscreen();
|
playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
}
|
|
||||||
|
|
||||||
if (playerIsNotStopped() && player.videoPlayerSelected()) {
|
|
||||||
addVideoPlayerView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection SimplifyOptionalCallChains
|
||||||
if (playAfterConnect
|
if (playAfterConnect
|
||||||
|| (currentInfo != null
|
|| (currentInfo != null
|
||||||
&& isAutoplayEnabled()
|
&& isAutoplayEnabled()
|
||||||
&& player.getParentActivity() == null)) {
|
&& !playerUi.isPresent())) {
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
openVideoPlayerAutoFullscreen();
|
openVideoPlayerAutoFullscreen();
|
||||||
}
|
}
|
||||||
|
@ -275,7 +277,7 @@ public final class VideoDetailFragment
|
||||||
|
|
||||||
public static VideoDetailFragment getInstanceInCollapsedState() {
|
public static VideoDetailFragment getInstanceInCollapsedState() {
|
||||||
final VideoDetailFragment instance = new VideoDetailFragment();
|
final VideoDetailFragment instance = new VideoDetailFragment();
|
||||||
instance.bottomSheetState = BottomSheetBehavior.STATE_COLLAPSED;
|
instance.updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,6 +337,9 @@ public final class VideoDetailFragment
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onResume() called");
|
||||||
|
}
|
||||||
|
|
||||||
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
|
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
|
||||||
|
|
||||||
|
@ -392,7 +397,7 @@ public final class VideoDetailFragment
|
||||||
disposables.clear();
|
disposables.clear();
|
||||||
positionSubscriber = null;
|
positionSubscriber = null;
|
||||||
currentWorker = null;
|
currentWorker = null;
|
||||||
bottomSheetBehavior.setBottomSheetCallback(null);
|
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback);
|
||||||
|
|
||||||
if (activity.isFinishing()) {
|
if (activity.isFinishing()) {
|
||||||
playQueue = null;
|
playQueue = null;
|
||||||
|
@ -458,7 +463,7 @@ public final class VideoDetailFragment
|
||||||
disposables.add(
|
disposables.add(
|
||||||
PlaylistDialog.createCorrespondingDialog(
|
PlaylistDialog.createCorrespondingDialog(
|
||||||
getContext(),
|
getContext(),
|
||||||
Collections.singletonList(new StreamEntity(currentInfo)),
|
List.of(new StreamEntity(currentInfo)),
|
||||||
dialog -> dialog.show(getFM(), TAG)
|
dialog -> dialog.show(getFM(), TAG)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -509,12 +514,18 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.detail_thumbnail_root_layout:
|
case R.id.detail_thumbnail_root_layout:
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
// make sure not to open any player if there is nothing currently loaded!
|
||||||
// FIXME Workaround #7427
|
// FIXME removing this `if` causes the player service to start correctly, then stop,
|
||||||
if (isPlayerAvailable()) {
|
// then restart badly without calling `startForeground()`, causing a crash when
|
||||||
player.setRecovery();
|
// later closing the detail fragment
|
||||||
|
if (currentInfo != null) {
|
||||||
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
|
// FIXME Workaround #7427
|
||||||
|
if (isPlayerAvailable()) {
|
||||||
|
player.setRecovery();
|
||||||
|
}
|
||||||
|
openVideoPlayerAutoFullscreen();
|
||||||
}
|
}
|
||||||
openVideoPlayerAutoFullscreen();
|
|
||||||
break;
|
break;
|
||||||
case R.id.detail_title_root_layout:
|
case R.id.detail_title_root_layout:
|
||||||
toggleTitleAndSecondaryControls();
|
toggleTitleAndSecondaryControls();
|
||||||
|
@ -527,7 +538,7 @@ public final class VideoDetailFragment
|
||||||
case R.id.overlay_play_pause_button:
|
case R.id.overlay_play_pause_button:
|
||||||
if (playerIsNotStopped()) {
|
if (playerIsNotStopped()) {
|
||||||
player.playPause();
|
player.playPause();
|
||||||
player.hideControls(0, 0);
|
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||||
showSystemUi();
|
showSystemUi();
|
||||||
} else {
|
} else {
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
|
@ -592,12 +603,12 @@ public final class VideoDetailFragment
|
||||||
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
|
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
|
||||||
binding.detailVideoTitleView.setMaxLines(10);
|
binding.detailVideoTitleView.setMaxLines(10);
|
||||||
animateRotation(binding.detailToggleSecondaryControlsView,
|
animateRotation(binding.detailToggleSecondaryControlsView,
|
||||||
Player.DEFAULT_CONTROLS_DURATION, 180);
|
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
|
||||||
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
|
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
binding.detailVideoTitleView.setMaxLines(1);
|
binding.detailVideoTitleView.setMaxLines(1);
|
||||||
animateRotation(binding.detailToggleSecondaryControlsView,
|
animateRotation(binding.detailToggleSecondaryControlsView,
|
||||||
Player.DEFAULT_CONTROLS_DURATION, 0);
|
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
|
||||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
// view pager height has changed, update the tab layout
|
// view pager height has changed, update the tab layout
|
||||||
|
@ -723,7 +734,7 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
||||||
PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.detailThumbnailImageView, new Callback() {
|
.into(binding.detailThumbnailImageView, new Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess() {
|
public void onSuccess() {
|
||||||
|
@ -755,7 +766,9 @@ public final class VideoDetailFragment
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(final int keyCode) {
|
public boolean onKeyDown(final int keyCode) {
|
||||||
return isPlayerAvailable() && player.onKeyDown(keyCode);
|
return isPlayerAvailable()
|
||||||
|
&& player.UIs().get(VideoPlayerUi.class)
|
||||||
|
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -765,7 +778,7 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are in fullscreen mode just exit from it via first back press
|
// If we are in fullscreen mode just exit from it via first back press
|
||||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
if (isFullscreen()) {
|
||||||
if (!DeviceUtils.isTablet(activity)) {
|
if (!DeviceUtils.isTablet(activity)) {
|
||||||
player.pause();
|
player.pause();
|
||||||
}
|
}
|
||||||
|
@ -1015,8 +1028,7 @@ public final class VideoDetailFragment
|
||||||
getChildFragmentManager().beginTransaction()
|
getChildFragmentManager().beginTransaction()
|
||||||
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
|
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
|
||||||
.commitAllowingStateLoss();
|
.commitAllowingStateLoss();
|
||||||
binding.relatedItemsLayout.setVisibility(
|
binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
|
||||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1056,15 +1068,13 @@ public final class VideoDetailFragment
|
||||||
// call `post()` to be sure `viewPager.getHitRect()`
|
// call `post()` to be sure `viewPager.getHitRect()`
|
||||||
// is up to date and not being currently recomputed
|
// is up to date and not being currently recomputed
|
||||||
binding.tabLayout.post(() -> {
|
binding.tabLayout.post(() -> {
|
||||||
if (getContext() != null) {
|
final var activity = getActivity();
|
||||||
|
if (activity != null) {
|
||||||
final Rect pagerHitRect = new Rect();
|
final Rect pagerHitRect = new Rect();
|
||||||
binding.viewPager.getHitRect(pagerHitRect);
|
binding.viewPager.getHitRect(pagerHitRect);
|
||||||
|
|
||||||
final Point displaySize = new Point();
|
final int height = DeviceUtils.getWindowHeight(activity.getWindowManager());
|
||||||
Objects.requireNonNull(ContextCompat.getSystemService(getContext(),
|
final int viewPagerVisibleHeight = height - pagerHitRect.top;
|
||||||
WindowManager.class)).getDefaultDisplay().getSize(displaySize);
|
|
||||||
|
|
||||||
final int viewPagerVisibleHeight = displaySize.y - pagerHitRect.top;
|
|
||||||
// see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
|
// see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
|
||||||
final float tabLayoutHeight = TypedValue.applyDimension(
|
final float tabLayoutHeight = TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics());
|
TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics());
|
||||||
|
@ -1096,8 +1106,12 @@ public final class VideoDetailFragment
|
||||||
private void toggleFullscreenIfInFullscreenMode() {
|
private void toggleFullscreenIfInFullscreenMode() {
|
||||||
// If a user watched video inside fullscreen mode and than chose another player
|
// If a user watched video inside fullscreen mode and than chose another player
|
||||||
// return to non-fullscreen mode
|
// return to non-fullscreen mode
|
||||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
if (isPlayerAvailable()) {
|
||||||
player.toggleFullscreen();
|
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||||
|
if (playerUi.isFullscreen()) {
|
||||||
|
playerUi.toggleFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1173,7 +1187,7 @@ public final class VideoDetailFragment
|
||||||
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
|
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
|
||||||
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
|
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
|
||||||
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
|
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
|
||||||
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
updateBottomSheetState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
// toggle landscape in order to open directly in fullscreen
|
// toggle landscape in order to open directly in fullscreen
|
||||||
onScreenRotationButtonClicked();
|
onScreenRotationButtonClicked();
|
||||||
}
|
}
|
||||||
|
@ -1223,16 +1237,10 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
final PlayQueue queue = setupPlayQueueForIntent(false);
|
final PlayQueue queue = setupPlayQueueForIntent(false);
|
||||||
|
tryAddVideoPlayerView();
|
||||||
// Video view can have elements visible from popup,
|
|
||||||
// We hide it here but once it ready the view will be shown in handleIntent()
|
|
||||||
if (playerService.getView() != null) {
|
|
||||||
playerService.getView().setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
addVideoPlayerView();
|
|
||||||
|
|
||||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||||
MainPlayer.class, queue, true, autoPlayEnabled);
|
PlayerService.class, queue, true, autoPlayEnabled);
|
||||||
ContextCompat.startForegroundService(activity, playerIntent);
|
ContextCompat.startForegroundService(activity, playerIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1244,8 +1252,8 @@ public final class VideoDetailFragment
|
||||||
* be reused in a few milliseconds and the flickering would be annoying.
|
* be reused in a few milliseconds and the flickering would be annoying.
|
||||||
*/
|
*/
|
||||||
private void hideMainPlayerOnLoadingNewStream() {
|
private void hideMainPlayerOnLoadingNewStream() {
|
||||||
if (!isPlayerServiceAvailable()
|
//noinspection SimplifyOptionalCallChains
|
||||||
|| playerService.getView() == null
|
if (!isPlayerServiceAvailable() || !getRoot().isPresent()
|
||||||
|| !player.videoPlayerSelected()) {
|
|| !player.videoPlayerSelected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1253,7 +1261,7 @@ public final class VideoDetailFragment
|
||||||
removeVideoPlayerView();
|
removeVideoPlayerView();
|
||||||
if (isAutoplayEnabled()) {
|
if (isAutoplayEnabled()) {
|
||||||
playerService.stopForImmediateReusing();
|
playerService.stopForImmediateReusing();
|
||||||
playerService.getView().setVisibility(View.GONE);
|
getRoot().ifPresent(view -> view.setVisibility(View.GONE));
|
||||||
} else {
|
} else {
|
||||||
playerHolder.stopService();
|
playerHolder.stopService();
|
||||||
}
|
}
|
||||||
|
@ -1310,27 +1318,41 @@ public final class VideoDetailFragment
|
||||||
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
|
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addVideoPlayerView() {
|
private void tryAddVideoPlayerView() {
|
||||||
if (!isPlayerAvailable() || getView() == null) {
|
if (isPlayerAvailable() && getView() != null) {
|
||||||
return;
|
// Setup the surface view height, so that it fits the video correctly; this is done also
|
||||||
|
// here, and not only in the Handler, to avoid a choppy fullscreen rotation animation.
|
||||||
|
setHeightThumbnail();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if viewHolder already contains a child
|
// do all the null checks in the posted lambda, too, since the player, the binding and the
|
||||||
if (player.getRootView().getParent() != binding.playerPlaceholder) {
|
// view could be set or unset before the lambda gets executed on the next main thread cycle
|
||||||
playerService.removeViewFromParent();
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||||||
}
|
if (!isPlayerAvailable() || getView() == null) {
|
||||||
setHeightThumbnail();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent from re-adding a view multiple times
|
// setup the surface view height, so that it fits the video correctly
|
||||||
if (player.getRootView().getParent() == null) {
|
setHeightThumbnail();
|
||||||
binding.playerPlaceholder.addView(player.getRootView());
|
|
||||||
}
|
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||||
|
// sometimes binding would be null here, even though getView() != null above u.u
|
||||||
|
if (binding != null) {
|
||||||
|
// prevent from re-adding a view multiple times
|
||||||
|
playerUi.removeViewFromParent();
|
||||||
|
binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
|
||||||
|
playerUi.setupVideoSurfaceIfNeeded();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeVideoPlayerView() {
|
private void removeVideoPlayerView() {
|
||||||
makeDefaultHeightForVideoPlaceholder();
|
makeDefaultHeightForVideoPlaceholder();
|
||||||
|
|
||||||
playerService.removeViewFromParent();
|
if (player != null) {
|
||||||
|
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void makeDefaultHeightForVideoPlaceholder() {
|
private void makeDefaultHeightForVideoPlaceholder() {
|
||||||
|
@ -1371,7 +1393,7 @@ public final class VideoDetailFragment
|
||||||
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
||||||
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
||||||
|
|
||||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
if (isFullscreen()) {
|
||||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||||
? requireView()
|
? requireView()
|
||||||
: activity.getWindow().getDecorView()).getHeight();
|
: activity.getWindow().getDecorView()).getHeight();
|
||||||
|
@ -1396,8 +1418,9 @@ public final class VideoDetailFragment
|
||||||
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
||||||
player.getSurfaceView()
|
player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
|
||||||
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight);
|
ui.getBinding().surfaceView.setHeights(newHeight,
|
||||||
|
ui.isFullscreen() ? newHeight : maxHeight));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1526,7 +1549,7 @@ public final class VideoDetailFragment
|
||||||
if (binding.relatedItemsLayout != null) {
|
if (binding.relatedItemsLayout != null) {
|
||||||
if (showRelatedItems) {
|
if (showRelatedItems) {
|
||||||
binding.relatedItemsLayout.setVisibility(
|
binding.relatedItemsLayout.setVisibility(
|
||||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE);
|
isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||||
} else {
|
} else {
|
||||||
binding.relatedItemsLayout.setVisibility(View.GONE);
|
binding.relatedItemsLayout.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
@ -1560,7 +1583,8 @@ public final class VideoDetailFragment
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy);
|
final Drawable buddyDrawable =
|
||||||
|
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
|
||||||
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
|
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
|
||||||
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
|
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
|
||||||
|
|
||||||
|
@ -1819,6 +1843,11 @@ public final class VideoDetailFragment
|
||||||
// Player event listener
|
// Player event listener
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated() {
|
||||||
|
tryAddVideoPlayerView();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onQueueUpdate(final PlayQueue queue) {
|
public void onQueueUpdate(final PlayQueue queue) {
|
||||||
playQueue = queue;
|
playQueue = queue;
|
||||||
|
@ -1939,15 +1968,10 @@ public final class VideoDetailFragment
|
||||||
@Override
|
@Override
|
||||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||||
setupBrightness();
|
setupBrightness();
|
||||||
|
//noinspection SimplifyOptionalCallChains
|
||||||
if (!isPlayerAndPlayerServiceAvailable()
|
if (!isPlayerAndPlayerServiceAvailable()
|
||||||
|| playerService.getView() == null
|
|| !player.UIs().get(MainPlayerUi.class).isPresent()
|
||||||
|| player.getParentActivity() == null) {
|
|| getRoot().map(View::getParent).orElse(null) == null) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final View view = playerService.getView();
|
|
||||||
final ViewGroup parent = (ViewGroup) view.getParent();
|
|
||||||
if (parent == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1963,13 +1987,7 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
tryAddVideoPlayerView();
|
||||||
addVideoPlayerView();
|
|
||||||
} else {
|
|
||||||
// KitKat needs a delay before addVideoPlayerView call or it reports wrong height in
|
|
||||||
// activity.getWindow().getDecorView().getHeight()
|
|
||||||
new Handler().post(this::addVideoPlayerView);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1981,7 +1999,7 @@ public final class VideoDetailFragment
|
||||||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||||
if (DeviceUtils.isTablet(activity)
|
if (DeviceUtils.isTablet(activity)
|
||||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||||
player.toggleFullscreen();
|
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2032,10 +2050,8 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
activity.getWindow().getDecorView().setSystemUiVisibility(0);
|
activity.getWindow().getDecorView().setSystemUiVisibility(0);
|
||||||
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
|
||||||
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
|
requireContext(), android.R.attr.colorPrimary));
|
||||||
requireContext(), android.R.attr.colorPrimary));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideSystemUi() {
|
private void hideSystemUi() {
|
||||||
|
@ -2066,8 +2082,7 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
if (isInMultiWindow || isFullscreen()) {
|
||||||
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
|
|
||||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||||
}
|
}
|
||||||
|
@ -2075,14 +2090,19 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listener implementation
|
// Listener implementation
|
||||||
|
@Override
|
||||||
public void hideSystemUiIfNeeded() {
|
public void hideSystemUiIfNeeded() {
|
||||||
if (isPlayerAvailable()
|
if (isFullscreen()
|
||||||
&& player.isFullscreen()
|
|
||||||
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
hideSystemUi();
|
hideSystemUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isFullscreen() {
|
||||||
|
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
|
||||||
|
.map(VideoPlayerUi::isFullscreen).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean playerIsNotStopped() {
|
private boolean playerIsNotStopped() {
|
||||||
return isPlayerAvailable() && !player.isStopped();
|
return isPlayerAvailable() && !player.isStopped();
|
||||||
}
|
}
|
||||||
|
@ -2105,10 +2125,7 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
|
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
|
||||||
if (!isPlayerAvailable()
|
if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|| !player.videoPlayerSelected()
|
|
||||||
|| !player.isFullscreen()
|
|
||||||
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
|
||||||
// Apply system brightness when the player is not in fullscreen
|
// Apply system brightness when the player is not in fullscreen
|
||||||
restoreDefaultBrightness();
|
restoreDefaultBrightness();
|
||||||
} else {
|
} else {
|
||||||
|
@ -2132,7 +2149,7 @@ public final class VideoDetailFragment
|
||||||
setAutoPlay(true);
|
setAutoPlay(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.checkLandscape();
|
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
|
||||||
// Let's give a user time to look at video information page if video is not playing
|
// Let's give a user time to look at video information page if video is not playing
|
||||||
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
||||||
player.play();
|
player.play();
|
||||||
|
@ -2211,12 +2228,8 @@ public final class VideoDetailFragment
|
||||||
} else {
|
} else {
|
||||||
final int selectedVideoStreamIndexForExternalPlayers =
|
final int selectedVideoStreamIndexForExternalPlayers =
|
||||||
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
|
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
|
||||||
final CharSequence[] resolutions =
|
final CharSequence[] resolutions = videoStreamsForExternalPlayers.stream()
|
||||||
new CharSequence[videoStreamsForExternalPlayers.size()];
|
.map(VideoStream::getResolution).toArray(CharSequence[]::new);
|
||||||
|
|
||||||
for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
|
|
||||||
resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
|
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
|
||||||
null);
|
null);
|
||||||
|
@ -2320,7 +2333,9 @@ public final class VideoDetailFragment
|
||||||
|
|
||||||
final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder);
|
final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder);
|
||||||
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
|
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
|
||||||
bottomSheetBehavior.setState(bottomSheetState);
|
bottomSheetBehavior.setState(lastStableBottomSheetState);
|
||||||
|
updateBottomSheetState(lastStableBottomSheetState);
|
||||||
|
|
||||||
final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
|
final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
|
||||||
if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) {
|
if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) {
|
||||||
manageSpaceAtTheBottom(false);
|
manageSpaceAtTheBottom(false);
|
||||||
|
@ -2333,10 +2348,10 @@ public final class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onStateChanged(@NonNull final View bottomSheet, final int newState) {
|
public void onStateChanged(@NonNull final View bottomSheet, final int newState) {
|
||||||
bottomSheetState = newState;
|
updateBottomSheetState(newState);
|
||||||
|
|
||||||
switch (newState) {
|
switch (newState) {
|
||||||
case BottomSheetBehavior.STATE_HIDDEN:
|
case BottomSheetBehavior.STATE_HIDDEN:
|
||||||
|
@ -2359,10 +2374,10 @@ public final class VideoDetailFragment
|
||||||
if (DeviceUtils.isLandscape(requireContext())
|
if (DeviceUtils.isLandscape(requireContext())
|
||||||
&& isPlayerAvailable()
|
&& isPlayerAvailable()
|
||||||
&& player.isPlaying()
|
&& player.isPlaying()
|
||||||
&& !player.isFullscreen()
|
&& !isFullscreen()
|
||||||
&& !DeviceUtils.isTablet(activity)
|
&& !DeviceUtils.isTablet(activity)) {
|
||||||
&& player.videoPlayerSelected()) {
|
player.UIs().get(MainPlayerUi.class)
|
||||||
player.toggleFullscreen();
|
.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
}
|
}
|
||||||
setOverlayLook(binding.appBarLayout, behavior, 1);
|
setOverlayLook(binding.appBarLayout, behavior, 1);
|
||||||
break;
|
break;
|
||||||
|
@ -2375,19 +2390,26 @@ public final class VideoDetailFragment
|
||||||
// Re-enable clicks
|
// Re-enable clicks
|
||||||
setOverlayElementsClickable(true);
|
setOverlayElementsClickable(true);
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
player.closeItemsList();
|
player.UIs().get(MainPlayerUi.class)
|
||||||
|
.ifPresent(MainPlayerUi::closeItemsList);
|
||||||
}
|
}
|
||||||
setOverlayLook(binding.appBarLayout, behavior, 0);
|
setOverlayLook(binding.appBarLayout, behavior, 0);
|
||||||
break;
|
break;
|
||||||
case BottomSheetBehavior.STATE_DRAGGING:
|
case BottomSheetBehavior.STATE_DRAGGING:
|
||||||
case BottomSheetBehavior.STATE_SETTLING:
|
case BottomSheetBehavior.STATE_SETTLING:
|
||||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
if (isFullscreen()) {
|
||||||
showSystemUi();
|
showSystemUi();
|
||||||
}
|
}
|
||||||
if (isPlayerAvailable() && player.isControlsVisible()) {
|
if (isPlayerAvailable()) {
|
||||||
player.hideControls(0, 0);
|
player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
|
||||||
|
if (ui.isControlsVisible()) {
|
||||||
|
ui.hideControls(0, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case BottomSheetBehavior.STATE_HALF_EXPANDED:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2395,7 +2417,9 @@ public final class VideoDetailFragment
|
||||||
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
||||||
setOverlayLook(binding.appBarLayout, behavior, slideOffset);
|
setOverlayLook(binding.appBarLayout, behavior, slideOffset);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
|
||||||
|
|
||||||
// User opened a new page and the player will hide itself
|
// User opened a new page and the player will hide itself
|
||||||
activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> {
|
activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> {
|
||||||
|
@ -2410,8 +2434,8 @@ public final class VideoDetailFragment
|
||||||
@Nullable final String thumbnailUrl) {
|
@Nullable final String thumbnailUrl) {
|
||||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||||
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
|
binding.overlayThumbnail.setImageDrawable(null);
|
||||||
PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||||
.into(binding.overlayThumbnail);
|
.into(binding.overlayThumbnail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2459,4 +2483,21 @@ public final class VideoDetailFragment
|
||||||
boolean isPlayerAndPlayerServiceAvailable() {
|
boolean isPlayerAndPlayerServiceAvailable() {
|
||||||
return (player != null && playerService != null);
|
return (player != null && playerService != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<View> getRoot() {
|
||||||
|
if (player == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return player.UIs().get(VideoPlayerUi.class)
|
||||||
|
.map(playerUi -> playerUi.getBinding().getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBottomSheetState(final int newState) {
|
||||||
|
bottomSheetState = newState;
|
||||||
|
if (newState != BottomSheetBehavior.STATE_DRAGGING
|
||||||
|
&& newState != BottomSheetBehavior.STATE_SETTLING) {
|
||||||
|
lastStableBottomSheetState = newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECI
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -28,9 +29,7 @@ import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.List;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,50 +42,34 @@ public final class VideoDetailPlayerCrasher {
|
||||||
// https://stackoverflow.com/a/54744028
|
// https://stackoverflow.com/a/54744028
|
||||||
private static final String TAG = "VideoDetPlayerCrasher";
|
private static final String TAG = "VideoDetPlayerCrasher";
|
||||||
|
|
||||||
private static final Map<String, Supplier<ExoPlaybackException>> AVAILABLE_EXCEPTION_TYPES =
|
private static final String DEFAULT_MSG = "Dummy";
|
||||||
getExceptionTypes();
|
|
||||||
|
private static final List<Pair<String, Supplier<ExoPlaybackException>>>
|
||||||
|
AVAILABLE_EXCEPTION_TYPES = List.of(
|
||||||
|
new Pair<>("Source", () -> ExoPlaybackException.createForSource(
|
||||||
|
new IOException(DEFAULT_MSG),
|
||||||
|
ERROR_CODE_BEHIND_LIVE_WINDOW
|
||||||
|
)),
|
||||||
|
new Pair<>("Renderer", () -> ExoPlaybackException.createForRenderer(
|
||||||
|
new Exception(DEFAULT_MSG),
|
||||||
|
"Dummy renderer",
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
C.FORMAT_HANDLED,
|
||||||
|
/*isRecoverable=*/false,
|
||||||
|
ERROR_CODE_DECODING_FAILED
|
||||||
|
)),
|
||||||
|
new Pair<>("Unexpected", () -> ExoPlaybackException.createForUnexpected(
|
||||||
|
new RuntimeException(DEFAULT_MSG),
|
||||||
|
ERROR_CODE_UNSPECIFIED
|
||||||
|
)),
|
||||||
|
new Pair<>("Remote", () -> ExoPlaybackException.createForRemote(DEFAULT_MSG))
|
||||||
|
);
|
||||||
|
|
||||||
private VideoDetailPlayerCrasher() {
|
private VideoDetailPlayerCrasher() {
|
||||||
// No impls
|
// No impls
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, Supplier<ExoPlaybackException>> getExceptionTypes() {
|
|
||||||
final String defaultMsg = "Dummy";
|
|
||||||
final Map<String, Supplier<ExoPlaybackException>> exceptionTypes = new LinkedHashMap<>();
|
|
||||||
exceptionTypes.put(
|
|
||||||
"Source",
|
|
||||||
() -> ExoPlaybackException.createForSource(
|
|
||||||
new IOException(defaultMsg),
|
|
||||||
ERROR_CODE_BEHIND_LIVE_WINDOW
|
|
||||||
)
|
|
||||||
);
|
|
||||||
exceptionTypes.put(
|
|
||||||
"Renderer",
|
|
||||||
() -> ExoPlaybackException.createForRenderer(
|
|
||||||
new Exception(defaultMsg),
|
|
||||||
"Dummy renderer",
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
C.FORMAT_HANDLED,
|
|
||||||
/*isRecoverable=*/false,
|
|
||||||
ERROR_CODE_DECODING_FAILED
|
|
||||||
)
|
|
||||||
);
|
|
||||||
exceptionTypes.put(
|
|
||||||
"Unexpected",
|
|
||||||
() -> ExoPlaybackException.createForUnexpected(
|
|
||||||
new RuntimeException(defaultMsg),
|
|
||||||
ERROR_CODE_UNSPECIFIED
|
|
||||||
)
|
|
||||||
);
|
|
||||||
exceptionTypes.put(
|
|
||||||
"Remote",
|
|
||||||
() -> ExoPlaybackException.createForRemote(defaultMsg)
|
|
||||||
);
|
|
||||||
|
|
||||||
return Collections.unmodifiableMap(exceptionTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Context getThemeWrapperContext(final Context context) {
|
private static Context getThemeWrapperContext(final Context context) {
|
||||||
return new ContextThemeWrapper(
|
return new ContextThemeWrapper(
|
||||||
context,
|
context,
|
||||||
|
@ -121,10 +104,9 @@ public final class VideoDetailPlayerCrasher {
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
for (final Map.Entry<String, Supplier<ExoPlaybackException>> entry
|
for (final Pair<String, Supplier<ExoPlaybackException>> entry : AVAILABLE_EXCEPTION_TYPES) {
|
||||||
: AVAILABLE_EXCEPTION_TYPES.entrySet()) {
|
|
||||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
||||||
radioButton.setText(entry.getKey());
|
radioButton.setText(entry.first);
|
||||||
radioButton.setChecked(false);
|
radioButton.setChecked(false);
|
||||||
radioButton.setLayoutParams(
|
radioButton.setLayoutParams(
|
||||||
new RadioGroup.LayoutParams(
|
new RadioGroup.LayoutParams(
|
||||||
|
@ -133,7 +115,7 @@ public final class VideoDetailPlayerCrasher {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
radioButton.setOnClickListener(v -> {
|
radioButton.setOnClickListener(v -> {
|
||||||
tryCrashPlayerWith(player, entry.getValue().get());
|
tryCrashPlayerWith(player, entry.second.get());
|
||||||
alertDialog.cancel();
|
alertDialog.cancel();
|
||||||
});
|
});
|
||||||
binding.list.addView(radioButton);
|
binding.list.addView(radioButton);
|
||||||
|
|
|
@ -23,14 +23,11 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.error.ErrorUtil;
|
import org.schabi.newpipe.error.ErrorUtil;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
|
||||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||||
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
@ -264,45 +261,28 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() {
|
infoListAdapter.setOnChannelSelectedListener(selectedItem -> {
|
||||||
@Override
|
try {
|
||||||
public void selected(final ChannelInfoItem selectedItem) {
|
|
||||||
try {
|
|
||||||
onItemSelected(selectedItem);
|
|
||||||
NavigationHelper.openChannelFragment(getFM(),
|
|
||||||
selectedItem.getServiceId(),
|
|
||||||
selectedItem.getUrl(),
|
|
||||||
selectedItem.getName());
|
|
||||||
} catch (final Exception e) {
|
|
||||||
ErrorUtil.showUiErrorSnackbar(
|
|
||||||
BaseListFragment.this, "Opening channel fragment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
|
|
||||||
@Override
|
|
||||||
public void selected(final PlaylistInfoItem selectedItem) {
|
|
||||||
try {
|
|
||||||
onItemSelected(selectedItem);
|
|
||||||
NavigationHelper.openPlaylistFragment(getFM(),
|
|
||||||
selectedItem.getServiceId(),
|
|
||||||
selectedItem.getUrl(),
|
|
||||||
selectedItem.getName());
|
|
||||||
} catch (final Exception e) {
|
|
||||||
ErrorUtil.showUiErrorSnackbar(BaseListFragment.this,
|
|
||||||
"Opening playlist fragment", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
|
|
||||||
@Override
|
|
||||||
public void selected(final CommentsInfoItem selectedItem) {
|
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
|
NavigationHelper.openChannelFragment(getFM(), selectedItem.getServiceId(),
|
||||||
|
selectedItem.getUrl(), selectedItem.getName());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
infoListAdapter.setOnPlaylistSelectedListener(selectedItem -> {
|
||||||
|
try {
|
||||||
|
onItemSelected(selectedItem);
|
||||||
|
NavigationHelper.openPlaylistFragment(getFM(), selectedItem.getServiceId(),
|
||||||
|
selectedItem.getUrl(), selectedItem.getName());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
ErrorUtil.showUiErrorSnackbar(this, "Opening playlist fragment", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
infoListAdapter.setOnCommentsSelectedListener(this::onItemSelected);
|
||||||
|
|
||||||
// Ensure that there is always a scroll listener (e.g. when rotating the device)
|
// Ensure that there is always a scroll listener (e.g. when rotating the device)
|
||||||
useNormalItemListScrollListener();
|
useNormalItemListScrollListener();
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.ktx.AnimationType;
|
import org.schabi.newpipe.ktx.AnimationType;
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
@ -578,17 +578,13 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
private PlayQueue getPlayQueue() {
|
||||||
return getPlayQueue(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PlayQueue getPlayQueue(final int index) {
|
|
||||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||||
.filter(StreamInfoItem.class::isInstance)
|
.filter(StreamInfoItem.class::isInstance)
|
||||||
.map(StreamInfoItem.class::cast)
|
.map(StreamInfoItem.class::cast)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
||||||
currentInfo.getNextPage(), streamItems, index);
|
currentInfo.getNextPage(), streamItems, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -43,7 +43,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
|
@ -200,7 +200,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
|
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
|
||||||
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
|
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
|
||||||
|
|
||||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
suggestionListAdapter = new SuggestionListAdapter();
|
||||||
historyRecordManager = new HistoryRecordManager(context);
|
historyRecordManager = new HistoryRecordManager(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,6 +340,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||||
|
// animations are just strange and useless, since the suggestions keep changing too much
|
||||||
|
searchBinding.suggestionsList.setItemAnimator(null);
|
||||||
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||||
@Override
|
@Override
|
||||||
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
|
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
|
||||||
|
@ -497,9 +499,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
+ lastSearchedString);
|
+ lastSearchedString);
|
||||||
}
|
}
|
||||||
searchEditText.setText(searchString);
|
searchEditText.setText(searchString);
|
||||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
searchEditText.setHintTextColor(searchEditText.getTextColors().withAlpha(128));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||||
searchToolbarContainer.setTranslationX(100);
|
searchToolbarContainer.setTranslationX(100);
|
||||||
|
@ -533,7 +532,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
searchBinding.correctSuggestion.setVisibility(View.GONE);
|
searchBinding.correctSuggestion.setVisibility(View.GONE);
|
||||||
|
|
||||||
searchEditText.setText("");
|
searchEditText.setText("");
|
||||||
suggestionListAdapter.setItems(new ArrayList<>());
|
suggestionListAdapter.submitList(null);
|
||||||
showKeyboardSearch();
|
showKeyboardSearch();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -922,7 +921,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
filterItemCheckedId = item.getItemId();
|
filterItemCheckedId = item.getItemId();
|
||||||
item.setChecked(true);
|
item.setChecked(true);
|
||||||
|
|
||||||
contentFilter = new String[]{theContentFilter.get(0)};
|
contentFilter = theContentFilter.toArray(new String[0]);
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(searchString)) {
|
if (!TextUtils.isEmpty(searchString)) {
|
||||||
search(searchString, contentFilter, sortFilter);
|
search(searchString, contentFilter, sortFilter);
|
||||||
|
@ -947,8 +946,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||||
}
|
}
|
||||||
searchBinding.suggestionsList.smoothScrollToPosition(0);
|
suggestionListAdapter.submitList(suggestions,
|
||||||
searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));
|
() -> searchBinding.suggestionsList.scrollToPosition(0));
|
||||||
|
|
||||||
if (suggestionsPanelVisible && isErrorPanelVisible()) {
|
if (suggestionsPanelVisible && isErrorPanelVisible()) {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
@ -983,8 +982,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
isCorrectedSearch = result.isCorrectedSearch();
|
isCorrectedSearch = result.isCorrectedSearch();
|
||||||
|
|
||||||
// List<MetaInfo> cannot be bundled without creating some containers
|
// List<MetaInfo> cannot be bundled without creating some containers
|
||||||
metaInfo = new MetaInfo[result.getMetaInfo().size()];
|
metaInfo = result.getMetaInfo().toArray(new MetaInfo[0]);
|
||||||
metaInfo = result.getMetaInfo().toArray(metaInfo);
|
|
||||||
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
|
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
|
||||||
searchBinding.searchMetaInfoSeparator, disposables);
|
searchBinding.searchMetaInfoSeparator, disposables);
|
||||||
|
|
||||||
|
@ -1070,14 +1068,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
final SuggestionItem item = suggestionListAdapter.getItem(position);
|
final SuggestionItem item = suggestionListAdapter.getCurrentList().get(position);
|
||||||
return item.fromHistory ? makeMovementFlags(0,
|
return item.fromHistory ? makeMovementFlags(0,
|
||||||
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
|
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||||
final int position = viewHolder.getBindingAdapterPosition();
|
final int position = viewHolder.getBindingAdapterPosition();
|
||||||
final String query = suggestionListAdapter.getItem(position).query;
|
final String query = suggestionListAdapter.getCurrentList().get(position).query;
|
||||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
|
|
@ -1,34 +1,22 @@
|
||||||
package org.schabi.newpipe.fragments.list.search;
|
package org.schabi.newpipe.fragments.list.search;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.ItemSearchSuggestionBinding;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class SuggestionListAdapter
|
public class SuggestionListAdapter
|
||||||
extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
|
extends ListAdapter<SuggestionItem, SuggestionListAdapter.SuggestionItemHolder> {
|
||||||
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
|
||||||
private final Context context;
|
|
||||||
private OnSuggestionItemSelected listener;
|
private OnSuggestionItemSelected listener;
|
||||||
|
|
||||||
public SuggestionListAdapter(final Context context) {
|
public SuggestionListAdapter() {
|
||||||
this.context = context;
|
super(new SuggestionItemCallback());
|
||||||
}
|
|
||||||
|
|
||||||
public void setItems(final List<SuggestionItem> items) {
|
|
||||||
this.items.clear();
|
|
||||||
this.items.addAll(items);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setListener(final OnSuggestionItemSelected listener) {
|
public void setListener(final OnSuggestionItemSelected listener) {
|
||||||
|
@ -39,45 +27,32 @@ public class SuggestionListAdapter
|
||||||
@Override
|
@Override
|
||||||
public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent,
|
public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent,
|
||||||
final int viewType) {
|
final int viewType) {
|
||||||
return new SuggestionItemHolder(LayoutInflater.from(context)
|
return new SuggestionItemHolder(ItemSearchSuggestionBinding
|
||||||
.inflate(R.layout.item_search_suggestion, parent, false));
|
.inflate(LayoutInflater.from(parent.getContext()), parent, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(final SuggestionItemHolder holder, final int position) {
|
public void onBindViewHolder(final SuggestionItemHolder holder, final int position) {
|
||||||
final SuggestionItem currentItem = getItem(position);
|
final SuggestionItem currentItem = getItem(position);
|
||||||
holder.updateFrom(currentItem);
|
holder.updateFrom(currentItem);
|
||||||
holder.queryView.setOnClickListener(v -> {
|
holder.itemBinding.suggestionSearch.setOnClickListener(v -> {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onSuggestionItemSelected(currentItem);
|
listener.onSuggestionItemSelected(currentItem);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
holder.queryView.setOnLongClickListener(v -> {
|
holder.itemBinding.suggestionSearch.setOnLongClickListener(v -> {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onSuggestionItemLongClick(currentItem);
|
listener.onSuggestionItemLongClick(currentItem);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
holder.insertView.setOnClickListener(v -> {
|
holder.itemBinding.suggestionInsert.setOnClickListener(v -> {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onSuggestionItemInserted(currentItem);
|
listener.onSuggestionItemInserted(currentItem);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
SuggestionItem getItem(final int position) {
|
|
||||||
return items.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return items.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return getItemCount() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnSuggestionItemSelected {
|
public interface OnSuggestionItemSelected {
|
||||||
void onSuggestionItemSelected(SuggestionItem item);
|
void onSuggestionItemSelected(SuggestionItem item);
|
||||||
|
|
||||||
|
@ -87,30 +62,32 @@ public class SuggestionListAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class SuggestionItemHolder extends RecyclerView.ViewHolder {
|
public static final class SuggestionItemHolder extends RecyclerView.ViewHolder {
|
||||||
private final TextView itemSuggestionQuery;
|
private final ItemSearchSuggestionBinding itemBinding;
|
||||||
private final ImageView suggestionIcon;
|
|
||||||
private final View queryView;
|
|
||||||
private final View insertView;
|
|
||||||
|
|
||||||
// Cache some ids, as they can potentially be constantly updated/recycled
|
private SuggestionItemHolder(final ItemSearchSuggestionBinding binding) {
|
||||||
private final int historyResId;
|
super(binding.getRoot());
|
||||||
private final int searchResId;
|
this.itemBinding = binding;
|
||||||
|
|
||||||
private SuggestionItemHolder(final View rootView) {
|
|
||||||
super(rootView);
|
|
||||||
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
|
|
||||||
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
|
|
||||||
|
|
||||||
queryView = rootView.findViewById(R.id.suggestion_search);
|
|
||||||
insertView = rootView.findViewById(R.id.suggestion_insert);
|
|
||||||
|
|
||||||
historyResId = R.drawable.ic_history;
|
|
||||||
searchResId = R.drawable.ic_search;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateFrom(final SuggestionItem item) {
|
private void updateFrom(final SuggestionItem item) {
|
||||||
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
|
itemBinding.itemSuggestionIcon.setImageResource(item.fromHistory ? R.drawable.ic_history
|
||||||
itemSuggestionQuery.setText(item.query);
|
: R.drawable.ic_search);
|
||||||
|
itemBinding.itemSuggestionQuery.setText(item.query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SuggestionItemCallback extends DiffUtil.ItemCallback<SuggestionItem> {
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull final SuggestionItem oldItem,
|
||||||
|
@NonNull final SuggestionItem newItem) {
|
||||||
|
return oldItem.fromHistory == newItem.fromHistory
|
||||||
|
&& oldItem.query.equals(newItem.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull final SuggestionItem oldItem,
|
||||||
|
@NonNull final SuggestionItem newItem) {
|
||||||
|
return true; // items' contents never change; the list of items themselves does
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,8 +67,8 @@ public class InfoItemBuilder {
|
||||||
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
||||||
final HistoryRecordManager historyRecordManager,
|
final HistoryRecordManager historyRecordManager,
|
||||||
final boolean useMiniVariant) {
|
final boolean useMiniVariant) {
|
||||||
final InfoItemHolder holder
|
final InfoItemHolder holder =
|
||||||
= holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||||
holder.updateFromItem(infoItem, historyRecordManager);
|
holder.updateFromItem(infoItem, historyRecordManager);
|
||||||
return holder.itemView;
|
return holder.itemView;
|
||||||
}
|
}
|
||||||
|
|
|
@ -321,6 +321,7 @@ public final class InfoItemDialog {
|
||||||
*/
|
*/
|
||||||
public Builder addDefaultEndEntries() {
|
public Builder addDefaultEndEntries() {
|
||||||
addAllEntries(
|
addAllEntries(
|
||||||
|
StreamDialogDefaultEntry.DOWNLOAD,
|
||||||
StreamDialogDefaultEntry.APPEND_PLAYLIST,
|
StreamDialogDefaultEntry.APPEND_PLAYLIST,
|
||||||
StreamDialogDefaultEntry.SHARE,
|
StreamDialogDefaultEntry.SHARE,
|
||||||
StreamDialogDefaultEntry.OPEN_IN_BROWSER
|
StreamDialogDefaultEntry.OPEN_IN_BROWSER
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list.dialog;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment;
|
import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment;
|
||||||
import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse;
|
import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse;
|
||||||
|
import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase;
|
||||||
import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse;
|
import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
@ -11,6 +12,7 @@ import androidx.annotation.StringRes;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.download.DownloadDialog;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
@ -18,7 +20,7 @@ import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
|
||||||
|
@ -87,7 +89,7 @@ public enum StreamDialogDefaultEntry {
|
||||||
APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) ->
|
APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) ->
|
||||||
PlaylistDialog.createCorrespondingDialog(
|
PlaylistDialog.createCorrespondingDialog(
|
||||||
fragment.getContext(),
|
fragment.getContext(),
|
||||||
Collections.singletonList(new StreamEntity(item)),
|
List.of(new StreamEntity(item)),
|
||||||
dialog -> dialog.show(
|
dialog -> dialog.show(
|
||||||
fragment.getParentFragmentManager(),
|
fragment.getParentFragmentManager(),
|
||||||
"StreamDialogEntry@"
|
"StreamDialogEntry@"
|
||||||
|
@ -110,6 +112,15 @@ public enum StreamDialogDefaultEntry {
|
||||||
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
||||||
item.getThumbnailUrl())),
|
item.getThumbnailUrl())),
|
||||||
|
|
||||||
|
DOWNLOAD(R.string.download, (fragment, item) ->
|
||||||
|
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
|
||||||
|
item.getUrl(), info -> {
|
||||||
|
final DownloadDialog downloadDialog =
|
||||||
|
new DownloadDialog(fragment.requireContext(), info);
|
||||||
|
downloadDialog.show(fragment.getChildFragmentManager(), "downloadDialog");
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) ->
|
OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) ->
|
||||||
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
|
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
itemTitleView.setText(item.getName());
|
itemTitleView.setText(item.getName());
|
||||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||||
|
|
||||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||||
|
|
|
@ -23,9 +23,9 @@ import org.schabi.newpipe.util.CommentTextOnTouchListener;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
|
@ -204,8 +204,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||||
boolean hasEllipsis = false;
|
boolean hasEllipsis = false;
|
||||||
|
|
||||||
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||||
final int endOfLastLine
|
final int endOfLastLine = itemContentView
|
||||||
= itemContentView.getLayout().getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
.getLayout()
|
||||||
|
.getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
||||||
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
|
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
|
||||||
if (end == -1) {
|
if (end == -1) {
|
||||||
end = Math.max(endOfLastLine - 2, 0);
|
end = Math.max(endOfLastLine - 2, 0);
|
||||||
|
|
|
@ -14,8 +14,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.ktx.ViewUtils;
|
import org.schabi.newpipe.ktx.ViewUtils;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
@ -111,8 +111,9 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
final HistoryRecordManager historyRecordManager) {
|
final HistoryRecordManager historyRecordManager) {
|
||||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||||
|
|
||||||
final StreamStateEntity state
|
final StreamStateEntity state = historyRecordManager
|
||||||
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
|
.loadStreamState(infoItem)
|
||||||
|
.blockingGet()[0];
|
||||||
if (state != null && item.getDuration() > 0
|
if (state != null && item.getDuration() > 0
|
||||||
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||||
itemProgressView.setMax((int) item.getDuration());
|
itemProgressView.setMax((int) item.getDuration());
|
||||||
|
|
|
@ -21,10 +21,6 @@ import org.schabi.newpipe.MainActivity
|
||||||
|
|
||||||
private const val TAG = "ViewUtils"
|
private const val TAG = "ViewUtils"
|
||||||
|
|
||||||
inline var View.backgroundTintListCompat: ColorStateList?
|
|
||||||
get() = ViewCompat.getBackgroundTintList(this)
|
|
||||||
set(value) = ViewCompat.setBackgroundTintList(this, value)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animate the view.
|
* Animate the view.
|
||||||
*
|
*
|
||||||
|
@ -96,62 +92,43 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
|
||||||
if (MainActivity.DEBUG) {
|
if (MainActivity.DEBUG) {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"animateBackgroundColor() called with: " +
|
"animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
|
||||||
"view = [" + this + "], duration = [" + duration + "], " +
|
"colorStart = [$colorStart], colorEnd = [$colorEnd]"
|
||||||
"colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val empty = arrayOf(IntArray(0))
|
|
||||||
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
|
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
|
||||||
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
|
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
|
||||||
viewPropertyAnimator.duration = duration
|
viewPropertyAnimator.duration = duration
|
||||||
viewPropertyAnimator.addUpdateListener { animation: ValueAnimator ->
|
|
||||||
backgroundTintListCompat = ColorStateList(empty, intArrayOf(animation.animatedValue as Int))
|
fun listenerAction(color: Int) {
|
||||||
|
ViewCompat.setBackgroundTintList(this, ColorStateList.valueOf(color))
|
||||||
}
|
}
|
||||||
viewPropertyAnimator.addListener(
|
viewPropertyAnimator.addUpdateListener { listenerAction(it.animatedValue as Int) }
|
||||||
onCancel = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) },
|
viewPropertyAnimator.addListener(onCancel = { listenerAction(colorEnd) }, onEnd = { listenerAction(colorEnd) })
|
||||||
onEnd = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) }
|
|
||||||
)
|
|
||||||
viewPropertyAnimator.start()
|
viewPropertyAnimator.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
||||||
if (MainActivity.DEBUG) {
|
if (MainActivity.DEBUG) {
|
||||||
Log.d(
|
Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
|
||||||
TAG,
|
|
||||||
"animateHeight: duration = [" + duration + "], " +
|
|
||||||
"from " + height + " to → " + targetHeight + " in: " + this
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
|
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
|
||||||
animator.interpolator = FastOutSlowInInterpolator()
|
animator.interpolator = FastOutSlowInInterpolator()
|
||||||
animator.duration = duration
|
animator.duration = duration
|
||||||
animator.addUpdateListener { animation: ValueAnimator ->
|
|
||||||
val value = animation.animatedValue as Float
|
fun listenerAction(value: Int) {
|
||||||
layoutParams.height = value.toInt()
|
layoutParams.height = value
|
||||||
requestLayout()
|
requestLayout()
|
||||||
}
|
}
|
||||||
animator.addListener(
|
animator.addUpdateListener { listenerAction((it.animatedValue as Float).toInt()) }
|
||||||
onCancel = {
|
animator.addListener(onCancel = { listenerAction(targetHeight) }, onEnd = { listenerAction(targetHeight) })
|
||||||
layoutParams.height = targetHeight
|
|
||||||
requestLayout()
|
|
||||||
},
|
|
||||||
onEnd = {
|
|
||||||
layoutParams.height = targetHeight
|
|
||||||
requestLayout()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
animator.start()
|
animator.start()
|
||||||
return animator
|
return animator
|
||||||
}
|
}
|
||||||
|
|
||||||
fun View.animateRotation(duration: Long, targetRotation: Int) {
|
fun View.animateRotation(duration: Long, targetRotation: Int) {
|
||||||
if (MainActivity.DEBUG) {
|
if (MainActivity.DEBUG) {
|
||||||
Log.d(
|
Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
|
||||||
TAG,
|
|
||||||
"animateRotation: duration = [" + duration + "], " +
|
|
||||||
"from " + rotation + " to → " + targetRotation + " in: " + this
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
animate().setListener(null).cancel()
|
animate().setListener(null).cancel()
|
||||||
animate()
|
animate()
|
||||||
|
@ -172,20 +149,13 @@ private fun View.animateAlpha(enterOrExit: Boolean, duration: Long, delay: Long,
|
||||||
if (enterOrExit) {
|
if (enterOrExit) {
|
||||||
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f)
|
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f)
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(ExecOnEndListener(execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
} else {
|
} else {
|
||||||
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f)
|
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f)
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
isGone = true
|
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,11 +167,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(ExecOnEndListener(execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
} else {
|
} else {
|
||||||
scaleX = 1f
|
scaleX = 1f
|
||||||
scaleY = 1f
|
scaleY = 1f
|
||||||
|
@ -209,12 +176,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
isGone = true
|
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,11 +190,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(ExecOnEndListener(execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
} else {
|
} else {
|
||||||
alpha = 1f
|
alpha = 1f
|
||||||
scaleX = 1f
|
scaleX = 1f
|
||||||
|
@ -240,12 +200,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
.alpha(0f).scaleX(.95f).scaleY(.95f)
|
.alpha(0f).scaleX(.95f).scaleY(.95f)
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
isGone = true
|
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,22 +212,15 @@ private fun View.animateSlideAndAlpha(enterOrExit: Boolean, duration: Long, dela
|
||||||
animate()
|
animate()
|
||||||
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
|
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(ExecOnEndListener(execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
} else {
|
} else {
|
||||||
animate()
|
animate()
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
.alpha(0f).translationY(-height.toFloat())
|
.alpha(0f).translationY(-height.toFloat())
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
isGone = true
|
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,21 +231,14 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
|
||||||
animate()
|
animate()
|
||||||
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
|
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(ExecOnEndListener(execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
} else {
|
} else {
|
||||||
animate().setInterpolator(FastOutSlowInInterpolator())
|
animate().setInterpolator(FastOutSlowInInterpolator())
|
||||||
.alpha(0f).translationY(-height / 2.0f)
|
.alpha(0f).translationY(-height / 2.0f)
|
||||||
.setDuration(duration).setStartDelay(delay)
|
.setDuration(duration).setStartDelay(delay)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
.start()
|
||||||
isGone = true
|
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
}).start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,11 +260,7 @@ fun View.slideUp(
|
||||||
.setStartDelay(delay)
|
.setStartDelay(delay)
|
||||||
.setDuration(duration)
|
.setDuration(duration)
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(ExecOnEndListener(execOnEnd))
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
|
||||||
execOnEnd?.run()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.start()
|
.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,6 +274,20 @@ fun View.animateHideRecyclerViewAllowingScrolling() {
|
||||||
animate().alpha(0.0f).setDuration(200).start()
|
animate().alpha(0.0f).setDuration(200).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private open class ExecOnEndListener(private val execOnEnd: Runnable?) : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
execOnEnd?.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HideAndExecOnEndListener(private val view: View, execOnEnd: Runnable?) :
|
||||||
|
ExecOnEndListener(execOnEnd) {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
view.isGone = true
|
||||||
|
super.onAnimationEnd(animation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum class AnimationType {
|
enum class AnimationType {
|
||||||
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
|
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final LocalItem selectedItem) {
|
public void selected(final LocalItem selectedItem) {
|
||||||
final FragmentManager fragmentManager = getFM();
|
final FragmentManager fragmentManager = getFM();
|
||||||
|
@ -256,8 +256,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||||
final DialogEditTextBinding dialogBinding
|
final DialogEditTextBinding dialogBinding =
|
||||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
DialogEditTextBinding.inflate(getLayoutInflater());
|
||||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||||
dialogBinding.dialogEditText.setText(selectedItem.name);
|
dialogBinding.dialogEditText.setText(selectedItem.name);
|
||||||
|
|
|
@ -13,12 +13,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.local.LocalItemListAdapter;
|
import org.schabi.newpipe.local.LocalItemListAdapter;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -63,18 +61,10 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||||
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||||
|
|
||||||
playlistAdapter = new LocalItemListAdapter(getActivity());
|
playlistAdapter = new LocalItemListAdapter(getActivity());
|
||||||
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
playlistAdapter.setSelectedListener(selectedItem -> {
|
||||||
@Override
|
final List<StreamEntity> entities = getStreamEntities();
|
||||||
public void selected(final LocalItem selectedItem) {
|
if (selectedItem instanceof PlaylistMetadataEntry && entities != null) {
|
||||||
if (!(selectedItem instanceof PlaylistMetadataEntry)
|
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities);
|
||||||
|| getStreamEntities() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onPlaylistSelected(
|
|
||||||
playlistManager,
|
|
||||||
(PlaylistMetadataEntry) selectedItem,
|
|
||||||
getStreamEntities()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -138,14 +128,11 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||||
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
||||||
@NonNull final PlaylistMetadataEntry playlist,
|
@NonNull final PlaylistMetadataEntry playlist,
|
||||||
@NonNull final List<StreamEntity> streams) {
|
@NonNull final List<StreamEntity> streams) {
|
||||||
if (getStreamEntities() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Toast successToast = Toast.makeText(getContext(),
|
final Toast successToast = Toast.makeText(getContext(),
|
||||||
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
|
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
|
||||||
|
|
||||||
if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) {
|
if (playlist.thumbnailUrl
|
||||||
|
.equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) {
|
||||||
playlistDisposables.add(manager
|
playlistDisposables.add(manager
|
||||||
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
|
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
|
@ -45,8 +45,8 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||||
return super.onCreateDialog(savedInstanceState);
|
return super.onCreateDialog(savedInstanceState);
|
||||||
}
|
}
|
||||||
|
|
||||||
final DialogEditTextBinding dialogBinding
|
final DialogEditTextBinding dialogBinding =
|
||||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
DialogEditTextBinding.inflate(getLayoutInflater());
|
||||||
dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
|
dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
|
||||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||||
|
|
|
@ -9,15 +9,20 @@ import android.view.Window;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
@ -131,13 +136,13 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||||
* @param context context used for accessing the database
|
* @param context context used for accessing the database
|
||||||
* @param streamEntities used for crating the dialog
|
* @param streamEntities used for crating the dialog
|
||||||
* @param onExec execution that should occur after a dialog got created, e.g. showing it
|
* @param onExec execution that should occur after a dialog got created, e.g. showing it
|
||||||
* @return Disposable
|
* @return the disposable that was created
|
||||||
*/
|
*/
|
||||||
public static Disposable createCorrespondingDialog(
|
public static Disposable createCorrespondingDialog(
|
||||||
final Context context,
|
final Context context,
|
||||||
final List<StreamEntity> streamEntities,
|
final List<StreamEntity> streamEntities,
|
||||||
final Consumer<PlaylistDialog> onExec
|
final Consumer<PlaylistDialog> onExec) {
|
||||||
) {
|
|
||||||
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
||||||
.hasPlaylists()
|
.hasPlaylists()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -147,4 +152,30 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
||||||
: PlaylistCreationDialog.newInstance(streamEntities))
|
: PlaylistCreationDialog.newInstance(streamEntities))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link PlaylistAppendDialog} when playlists exists,
|
||||||
|
* otherwise a {@link PlaylistCreationDialog}. If the player's play queue is null or empty, no
|
||||||
|
* dialog will be created.
|
||||||
|
*
|
||||||
|
* @param player the player from which to extract the context and the play queue
|
||||||
|
* @param fragmentManager the fragment manager to use to show the dialog
|
||||||
|
* @return the disposable that was created
|
||||||
|
*/
|
||||||
|
public static Disposable showForPlayQueue(
|
||||||
|
final Player player,
|
||||||
|
@NonNull final FragmentManager fragmentManager) {
|
||||||
|
|
||||||
|
final List<StreamEntity> streamEntities = Stream.of(player.getPlayQueue())
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.flatMap(playQueue -> playQueue.getStreams().stream())
|
||||||
|
.map(StreamEntity::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (streamEntities.isEmpty()) {
|
||||||
|
return Disposable.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlaylistDialog.createCorrespondingDialog(player.getContext(), streamEntities,
|
||||||
|
dialog -> dialog.show(fragmentManager, "PlaylistDialog"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,19 +41,15 @@ class FeedDatabaseManager(context: Context) {
|
||||||
fun database() = database
|
fun database() = database
|
||||||
|
|
||||||
fun getStreams(
|
fun getStreams(
|
||||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
groupId: Long,
|
||||||
getPlayedStreams: Boolean = true
|
includePlayedStreams: Boolean,
|
||||||
|
includeFutureStreams: Boolean
|
||||||
): Maybe<List<StreamWithState>> {
|
): Maybe<List<StreamWithState>> {
|
||||||
return when (groupId) {
|
return feedTable.getStreams(
|
||||||
FeedGroupEntity.GROUP_ALL_ID -> {
|
groupId,
|
||||||
if (getPlayedStreams) feedTable.getAllStreams()
|
includePlayedStreams,
|
||||||
else feedTable.getLiveOrNotPlayedStreams()
|
if (includeFutureStreams) null else OffsetDateTime.now()
|
||||||
}
|
)
|
||||||
else -> {
|
|
||||||
if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId)
|
|
||||||
else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)
|
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)
|
||||||
|
|
|
@ -41,6 +41,7 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.core.view.MenuItemCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
@ -98,6 +99,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
|
|
||||||
private lateinit var groupAdapter: GroupieAdapter
|
private lateinit var groupAdapter: GroupieAdapter
|
||||||
@State @JvmField var showPlayedItems: Boolean = true
|
@State @JvmField var showPlayedItems: Boolean = true
|
||||||
|
@State @JvmField var showFutureItems: Boolean = true
|
||||||
|
|
||||||
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
|
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
|
||||||
private var updateListViewModeOnResume = false
|
private var updateListViewModeOnResume = false
|
||||||
|
@ -134,9 +136,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||||
super.onViewCreated(rootView, savedInstanceState)
|
super.onViewCreated(rootView, savedInstanceState)
|
||||||
|
|
||||||
val factory = FeedViewModel.Factory(requireContext(), groupId)
|
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
||||||
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
|
||||||
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
||||||
|
showFutureItems = viewModel.getShowFutureItemsFromPreferences()
|
||||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
|
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
|
||||||
|
|
||||||
groupAdapter = GroupieAdapter().apply {
|
groupAdapter = GroupieAdapter().apply {
|
||||||
|
@ -212,6 +215,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
|
|
||||||
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
||||||
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
|
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
|
||||||
|
updateToggleFutureItemsButton(menu.findItem(R.id.menu_item_feed_toggle_future_items))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
@ -241,6 +245,11 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
updateTogglePlayedItemsButton(item)
|
updateTogglePlayedItemsButton(item)
|
||||||
viewModel.togglePlayedItems(showPlayedItems)
|
viewModel.togglePlayedItems(showPlayedItems)
|
||||||
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
|
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
|
||||||
|
} else if (item.itemId == R.id.menu_item_feed_toggle_future_items) {
|
||||||
|
showFutureItems = !item.isChecked
|
||||||
|
updateToggleFutureItemsButton(item)
|
||||||
|
viewModel.toggleFutureItems(showFutureItems)
|
||||||
|
viewModel.saveShowFutureItemsToPreferences(showFutureItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
|
@ -278,6 +287,32 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
requireContext(),
|
requireContext(),
|
||||||
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
|
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
|
||||||
)
|
)
|
||||||
|
MenuItemCompat.setTooltipText(
|
||||||
|
menuItem,
|
||||||
|
getString(
|
||||||
|
if (showPlayedItems)
|
||||||
|
R.string.feed_toggle_hide_played_items
|
||||||
|
else
|
||||||
|
R.string.feed_toggle_show_played_items
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateToggleFutureItemsButton(menuItem: MenuItem) {
|
||||||
|
menuItem.isChecked = showFutureItems
|
||||||
|
menuItem.icon = AppCompatResources.getDrawable(
|
||||||
|
requireContext(),
|
||||||
|
if (showFutureItems) R.drawable.ic_history_future else R.drawable.ic_history
|
||||||
|
)
|
||||||
|
MenuItemCompat.setTooltipText(
|
||||||
|
menuItem,
|
||||||
|
getString(
|
||||||
|
if (showFutureItems)
|
||||||
|
R.string.feed_toggle_hide_future_items
|
||||||
|
else
|
||||||
|
R.string.feed_toggle_show_future_items
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// //////////////////////////////////////////////////////////////////////////
|
// //////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
package org.schabi.newpipe.local.feed
|
package org.schabi.newpipe.local.feed
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.functions.Function4
|
import io.reactivex.rxjava3.functions.Function5
|
||||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.schabi.newpipe.App
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.stream.StreamWithState
|
import org.schabi.newpipe.database.stream.StreamWithState
|
||||||
|
@ -26,17 +29,23 @@ import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class FeedViewModel(
|
class FeedViewModel(
|
||||||
private val applicationContext: Context,
|
private val application: Application,
|
||||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||||
initialShowPlayedItems: Boolean = true
|
initialShowPlayedItems: Boolean = true,
|
||||||
|
initialShowFutureItems: Boolean = true
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
private val feedDatabaseManager = FeedDatabaseManager(application)
|
||||||
|
|
||||||
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||||
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
|
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
|
||||||
.startWithItem(initialShowPlayedItems)
|
.startWithItem(initialShowPlayedItems)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
||||||
|
private val toggleShowFutureItems = BehaviorProcessor.create<Boolean>()
|
||||||
|
private val toggleShowFutureItemsFlowable = toggleShowFutureItems
|
||||||
|
.startWithItem(initialShowFutureItems)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
||||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||||
|
|
||||||
|
@ -44,21 +53,22 @@ class FeedViewModel(
|
||||||
.combineLatest(
|
.combineLatest(
|
||||||
FeedEventManager.events(),
|
FeedEventManager.events(),
|
||||||
toggleShowPlayedItemsFlowable,
|
toggleShowPlayedItemsFlowable,
|
||||||
|
toggleShowFutureItemsFlowable,
|
||||||
feedDatabaseManager.notLoadedCount(groupId),
|
feedDatabaseManager.notLoadedCount(groupId),
|
||||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||||
|
|
||||||
Function4 { t1: FeedEventManager.Event, t2: Boolean,
|
Function5 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean,
|
||||||
t3: Long, t4: List<OffsetDateTime> ->
|
t4: Long, t5: List<OffsetDateTime> ->
|
||||||
return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
|
return@Function5 CombineResultEventHolder(t1, t2, t3, t4, t5.firstOrNull())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
|
.map { (event, showPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
|
||||||
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
||||||
feedDatabaseManager
|
feedDatabaseManager
|
||||||
.getStreams(groupId, showPlayedItems)
|
.getStreams(groupId, showPlayedItems, showFutureItems)
|
||||||
.blockingGet(arrayListOf())
|
.blockingGet(arrayListOf())
|
||||||
else
|
else
|
||||||
arrayListOf()
|
arrayListOf()
|
||||||
|
@ -89,8 +99,9 @@ class FeedViewModel(
|
||||||
private data class CombineResultEventHolder(
|
private data class CombineResultEventHolder(
|
||||||
val t1: FeedEventManager.Event,
|
val t1: FeedEventManager.Event,
|
||||||
val t2: Boolean,
|
val t2: Boolean,
|
||||||
val t3: Long,
|
val t3: Boolean,
|
||||||
val t4: OffsetDateTime?
|
val t4: Long,
|
||||||
|
val t5: OffsetDateTime?
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class CombineResultDataHolder(
|
private data class CombineResultDataHolder(
|
||||||
|
@ -105,31 +116,42 @@ class FeedViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
|
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
|
||||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems)
|
this.putBoolean(application.getString(R.string.feed_show_played_items_key), showPlayedItems)
|
||||||
this.apply()
|
this.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext)
|
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(application)
|
||||||
|
|
||||||
|
fun toggleFutureItems(showFutureItems: Boolean) {
|
||||||
|
toggleShowFutureItems.onNext(showFutureItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveShowFutureItemsToPreferences(showFutureItems: Boolean) =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
|
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
|
||||||
|
this.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun getShowPlayedItemsFromPreferences(context: Context) =
|
private fun getShowPlayedItemsFromPreferences(context: Context) =
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
|
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
|
||||||
}
|
private fun getShowFutureItemsFromPreferences(context: Context) =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
class Factory(
|
.getBoolean(context.getString(R.string.feed_show_future_items_key), true)
|
||||||
private val context: Context,
|
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
||||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
|
initializer {
|
||||||
) : ViewModelProvider.Factory {
|
FeedViewModel(
|
||||||
@Suppress("UNCHECKED_CAST")
|
App.getApp(),
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
groupId,
|
||||||
return FeedViewModel(
|
// Read initial value from preferences
|
||||||
context.applicationContext,
|
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||||
groupId,
|
getShowFutureItemsFromPreferences(context.applicationContext)
|
||||||
// Read initial value from preferences
|
)
|
||||||
getShowPlayedItemsFromPreferences(context.applicationContext)
|
}
|
||||||
) as T
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
@ -11,6 +13,8 @@ import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import com.squareup.picasso.Target
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||||
|
@ -27,6 +31,8 @@ class NotificationHelper(val context: Context) {
|
||||||
Context.NOTIFICATION_SERVICE
|
Context.NOTIFICATION_SERVICE
|
||||||
) as NotificationManager
|
) as NotificationManager
|
||||||
|
|
||||||
|
private val iconLoadingTargets = ArrayList<Target>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a notification about new streams from a single channel.
|
* Show a notification about new streams from a single channel.
|
||||||
* Opening the notification will open the corresponding channel page.
|
* Opening the notification will open the corresponding channel page.
|
||||||
|
@ -77,10 +83,29 @@ class NotificationHelper(val context: Context) {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
PicassoHelper.loadNotificationIcon(data.avatarUrl) { bitmap ->
|
// a Target is like a listener for image loading events
|
||||||
bitmap?.let { builder.setLargeIcon(it) } // set only if != null
|
val target = object : Target {
|
||||||
manager.notify(data.pseudoId, builder.build())
|
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
|
||||||
|
builder.setLargeIcon(bitmap) // set only if there is actually one
|
||||||
|
manager.notify(data.pseudoId, builder.build())
|
||||||
|
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||||
|
manager.notify(data.pseudoId, builder.build())
|
||||||
|
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add the target to the list to hold a strong reference and prevent it from being garbage
|
||||||
|
// collected, since Picasso only holds weak references to targets
|
||||||
|
iconLoadingTargets.add(target)
|
||||||
|
|
||||||
|
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -28,7 +28,6 @@ import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.feed.dao.FeedDAO;
|
|
||||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
|
@ -51,7 +50,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Completable;
|
import io.reactivex.rxjava3.core.Completable;
|
||||||
|
@ -89,7 +87,6 @@ public class HistoryRecordManager {
|
||||||
* Marks a stream item as watched such that it is hidden from the feed if watched videos are
|
* Marks a stream item as watched such that it is hidden from the feed if watched videos are
|
||||||
* hidden. Adds a history entry and updates the stream progress to 100%.
|
* hidden. Adds a history entry and updates the stream progress to 100%.
|
||||||
*
|
*
|
||||||
* @see FeedDAO#getLiveOrNotPlayedStreams
|
|
||||||
* @see FeedViewModel#togglePlayedItems
|
* @see FeedViewModel#togglePlayedItems
|
||||||
* @param info the item to mark as watched
|
* @param info the item to mark as watched
|
||||||
* @return a Maybe containing the ID of the item if successful
|
* @return a Maybe containing the ID of the item if successful
|
||||||
|
@ -176,10 +173,6 @@ public class HistoryRecordManager {
|
||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flowable<List<StreamHistoryEntry>> getStreamHistory() {
|
|
||||||
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Flowable<List<StreamHistoryEntry>> getStreamHistorySortedById() {
|
public Flowable<List<StreamHistoryEntry>> getStreamHistorySortedById() {
|
||||||
return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io());
|
return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
@ -188,24 +181,6 @@ public class HistoryRecordManager {
|
||||||
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
|
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Single<List<Long>> insertStreamHistory(final Collection<StreamHistoryEntry> entries) {
|
|
||||||
final List<StreamHistoryEntity> entities = new ArrayList<>(entries.size());
|
|
||||||
for (final StreamHistoryEntry entry : entries) {
|
|
||||||
entities.add(entry.toStreamHistoryEntity());
|
|
||||||
}
|
|
||||||
return Single.fromCallable(() -> streamHistoryTable.insertAll(entities))
|
|
||||||
.subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Single<Integer> deleteStreamHistory(final Collection<StreamHistoryEntry> entries) {
|
|
||||||
final List<StreamHistoryEntity> entities = new ArrayList<>(entries.size());
|
|
||||||
for (final StreamHistoryEntry entry : entries) {
|
|
||||||
entities.add(entry.toStreamHistoryEntity());
|
|
||||||
}
|
|
||||||
return Single.fromCallable(() -> streamHistoryTable.delete(entities))
|
|
||||||
.subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isStreamHistoryEnabled() {
|
private boolean isStreamHistoryEnabled() {
|
||||||
return sharedPreferences.getBoolean(streamHistoryKey, false);
|
return sharedPreferences.getBoolean(streamHistoryKey, false);
|
||||||
}
|
}
|
||||||
|
@ -259,13 +234,6 @@ public class HistoryRecordManager {
|
||||||
// Stream State History
|
// Stream State History
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
public Maybe<StreamHistoryEntity> getStreamHistory(final StreamInfo info) {
|
|
||||||
return Maybe.fromCallable(() -> {
|
|
||||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
|
||||||
return streamHistoryTable.getLatestEntry(streamId);
|
|
||||||
}).subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
||||||
return queueItem.getStream()
|
return queueItem.getStream()
|
||||||
.map(info -> streamTable.upsert(new StreamEntity(info)))
|
.map(info -> streamTable.upsert(new StreamEntity(info)))
|
||||||
|
@ -311,28 +279,6 @@ public class HistoryRecordManager {
|
||||||
}).subscribeOn(Schedulers.io());
|
}).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Single<List<StreamStateEntity>> loadStreamStateBatch(final List<InfoItem> infos) {
|
|
||||||
return Single.fromCallable(() -> {
|
|
||||||
final List<StreamStateEntity> result = new ArrayList<>(infos.size());
|
|
||||||
for (final InfoItem info : infos) {
|
|
||||||
final List<StreamEntity> entities = streamTable
|
|
||||||
.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
|
||||||
if (entities.isEmpty()) {
|
|
||||||
result.add(null);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final List<StreamStateEntity> states = streamStateTable
|
|
||||||
.getState(entities.get(0).getUid()).blockingFirst();
|
|
||||||
if (states.isEmpty()) {
|
|
||||||
result.add(null);
|
|
||||||
} else {
|
|
||||||
result.add(states.get(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}).subscribeOn(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(
|
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(
|
||||||
final List<? extends LocalItem> items) {
|
final List<? extends LocalItem> items) {
|
||||||
return Single.fromCallable(() -> {
|
return Single.fromCallable(() -> {
|
||||||
|
|
|
@ -135,7 +135,7 @@ public class StatisticsPlaylistFragment
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final LocalItem selectedItem) {
|
public void selected(final LocalItem selectedItem) {
|
||||||
if (selectedItem instanceof StreamStatisticsEntry) {
|
if (selectedItem instanceof StreamStatisticsEntry) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe.local.playlist;
|
package org.schabi.newpipe.local.playlist;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||||
|
|
||||||
|
@ -41,15 +42,16 @@ import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||||
|
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -57,10 +59,12 @@ import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
@ -163,7 +167,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||||
itemTouchHelper.attachToRecyclerView(itemsList);
|
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||||
|
|
||||||
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(final LocalItem selectedItem) {
|
public void selected(final LocalItem selectedItem) {
|
||||||
if (selectedItem instanceof PlaylistStreamEntry) {
|
if (selectedItem instanceof PlaylistStreamEntry) {
|
||||||
|
@ -345,7 +349,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
if (item.getItemId() == R.id.menu_item_remove_watched) {
|
if (item.getItemId() == R.id.menu_item_share_playlist) {
|
||||||
|
sharePlaylist();
|
||||||
|
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
||||||
|
createRenameDialog();
|
||||||
|
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
|
||||||
if (!isRemovingWatched) {
|
if (!isRemovingWatched) {
|
||||||
new AlertDialog.Builder(requireContext())
|
new AlertDialog.Builder(requireContext())
|
||||||
.setMessage(R.string.remove_watched_popup_warning)
|
.setMessage(R.string.remove_watched_popup_warning)
|
||||||
|
@ -360,14 +368,26 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
|
||||||
createRenameDialog();
|
|
||||||
} else {
|
} else {
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share the playlist as a newline-separated list of stream URLs.
|
||||||
|
*/
|
||||||
|
public void sharePlaylist() {
|
||||||
|
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
||||||
|
.flatMapSingle(playlist -> Single.just(playlist.stream()
|
||||||
|
.map(PlaylistStreamEntry::getStreamEntity)
|
||||||
|
.map(StreamEntity::getUrl)
|
||||||
|
.collect(Collectors.joining("\n"))))
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(urlsText -> ShareUtils.shareText(requireContext(), name, urlsText),
|
||||||
|
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
|
||||||
|
}
|
||||||
|
|
||||||
public void removeWatchedStreams(final boolean removePartiallyWatched) {
|
public void removeWatchedStreams(final boolean removePartiallyWatched) {
|
||||||
if (isRemovingWatched) {
|
if (isRemovingWatched) {
|
||||||
return;
|
return;
|
||||||
|
@ -382,8 +402,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
final Iterator<PlaylistStreamEntry> playlistIter = playlist.iterator();
|
final Iterator<PlaylistStreamEntry> playlistIter = playlist.iterator();
|
||||||
|
|
||||||
// History data
|
// History data
|
||||||
final HistoryRecordManager recordManager
|
final HistoryRecordManager recordManager =
|
||||||
= new HistoryRecordManager(getContext());
|
new HistoryRecordManager(getContext());
|
||||||
final Iterator<StreamHistoryEntry> historyIter = recordManager
|
final Iterator<StreamHistoryEntry> historyIter = recordManager
|
||||||
.getStreamHistorySortedById().blockingFirst().iterator();
|
.getStreamHistorySortedById().blockingFirst().iterator();
|
||||||
|
|
||||||
|
@ -524,8 +544,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final DialogEditTextBinding dialogBinding
|
final DialogEditTextBinding dialogBinding =
|
||||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
DialogEditTextBinding.inflate(getLayoutInflater());
|
||||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||||
dialogBinding.dialogEditText.setSelection(dialogBinding.dialogEditText.getText().length());
|
dialogBinding.dialogEditText.setSelection(dialogBinding.dialogEditText.getText().length());
|
||||||
|
@ -593,7 +613,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0))
|
newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0))
|
||||||
.getStreamEntity().getThumbnailUrl();
|
.getStreamEntity().getThumbnailUrl();
|
||||||
} else {
|
} else {
|
||||||
newThumbnailUrl = "drawable://" + R.drawable.dummy_thumbnail_playlist;
|
newThumbnailUrl = "drawable://" + R.drawable.placeholder_thumbnail_playlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeThumbnailUrl(newThumbnailUrl);
|
changeThumbnailUrl(newThumbnailUrl);
|
||||||
|
|
|
@ -346,7 +346,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
override fun doInitialLoadLogic() = Unit
|
override fun doInitialLoadLogic() = Unit
|
||||||
override fun startLoading(forceLoad: Boolean) = Unit
|
override fun startLoading(forceLoad: Boolean) = Unit
|
||||||
|
|
||||||
private val listenerFeedGroups = object : OnClickGesture<Item<*>>() {
|
private val listenerFeedGroups = object : OnClickGesture<Item<*>> {
|
||||||
override fun selected(selectedItem: Item<*>?) {
|
override fun selected(selectedItem: Item<*>?) {
|
||||||
when (selectedItem) {
|
when (selectedItem) {
|
||||||
is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name)
|
is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name)
|
||||||
|
@ -361,7 +361,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem>() {
|
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> {
|
||||||
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
|
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
|
||||||
fm,
|
fm,
|
||||||
selectedItem.serviceId, selectedItem.url, selectedItem.name
|
selectedItem.serviceId, selectedItem.url, selectedItem.name
|
||||||
|
|
|
@ -8,12 +8,10 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
@ -124,14 +122,6 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
||||||
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
|
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
|
||||||
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
|
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
|
||||||
|
|
||||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
// KitKat doesn't apply container's theme to <include> content
|
|
||||||
val contrastColor = AppCompatResources.getColorStateList(requireContext(), R.color.contrastColor)
|
|
||||||
searchLayoutBinding.toolbarSearchEditText.setTextColor(contrastColor)
|
|
||||||
searchLayoutBinding.toolbarSearchEditText.setHintTextColor(contrastColor.withAlpha(128))
|
|
||||||
ImageViewCompat.setImageTintList(searchLayoutBinding.toolbarSearchClearIcon, contrastColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel = ViewModelProvider(
|
viewModel = ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
FeedGroupDialogViewModel.Factory(
|
FeedGroupDialogViewModel.Factory(
|
||||||
|
|
|
@ -122,7 +122,7 @@ class FeedGroupDialogViewModel(
|
||||||
private val initialShowOnlyUngrouped: Boolean = false
|
private val initialShowOnlyUngrouped: Boolean = false
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
return FeedGroupDialogViewModel(
|
return FeedGroupDialogViewModel(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
groupId, initialQuery, initialShowOnlyUngrouped
|
groupId, initialQuery, initialShowOnlyUngrouped
|
||||||
|
|
|
@ -39,7 +39,7 @@ class ChannelItem(
|
||||||
itemChannelDescriptionView.text = infoItem.description
|
itemChannelDescriptionView.text = infoItem.description
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView)
|
PicassoHelper.loadAvatar(infoItem.thumbnailUrl).into(itemThumbnailView)
|
||||||
|
|
||||||
gesturesListener?.run {
|
gesturesListener?.run {
|
||||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
|
|
||||||
package org.schabi.newpipe.local.subscription.services;
|
package org.schabi.newpipe.local.subscription.services;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -43,8 +45,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.functions.Function;
|
import io.reactivex.rxjava3.functions.Function;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
public class SubscriptionsExportService extends BaseImportExportService {
|
public class SubscriptionsExportService extends BaseImportExportService {
|
||||||
public static final String KEY_FILE_PATH = "key_file_path";
|
public static final String KEY_FILE_PATH = "key_file_path";
|
||||||
|
|
||||||
|
@ -109,8 +109,8 @@ public class SubscriptionsExportService extends BaseImportExportService {
|
||||||
|
|
||||||
subscriptionManager.subscriptionTable().getAll().take(1)
|
subscriptionManager.subscriptionTable().getAll().take(1)
|
||||||
.map(subscriptionEntities -> {
|
.map(subscriptionEntities -> {
|
||||||
final List<SubscriptionItem> result
|
final List<SubscriptionItem> result =
|
||||||
= new ArrayList<>(subscriptionEntities.size());
|
new ArrayList<>(subscriptionEntities.size());
|
||||||
for (final SubscriptionEntity entity : subscriptionEntities) {
|
for (final SubscriptionEntity entity : subscriptionEntities) {
|
||||||
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(),
|
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(),
|
||||||
entity.getName()));
|
entity.getName()));
|
||||||
|
|
|
@ -1,259 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
|
||||||
* Part of NewPipe
|
|
||||||
*
|
|
||||||
* License: GPL-3.0+
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.player;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.App;
|
|
||||||
import org.schabi.newpipe.databinding.PlayerBinding;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One service for all players.
|
|
||||||
*
|
|
||||||
* @author mauriciocolli
|
|
||||||
*/
|
|
||||||
public final class MainPlayer extends Service {
|
|
||||||
private static final String TAG = "MainPlayer";
|
|
||||||
private static final boolean DEBUG = Player.DEBUG;
|
|
||||||
|
|
||||||
private Player player;
|
|
||||||
private WindowManager windowManager;
|
|
||||||
|
|
||||||
private final IBinder mBinder = new MainPlayer.LocalBinder();
|
|
||||||
|
|
||||||
public enum PlayerType {
|
|
||||||
VIDEO,
|
|
||||||
AUDIO,
|
|
||||||
POPUP
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Notification
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
static final String ACTION_CLOSE
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
|
|
||||||
static final String ACTION_PLAY_PAUSE
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
|
|
||||||
static final String ACTION_REPEAT
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
|
|
||||||
static final String ACTION_PLAY_NEXT
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
|
|
||||||
static final String ACTION_PLAY_PREVIOUS
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
|
|
||||||
static final String ACTION_FAST_REWIND
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
|
|
||||||
static final String ACTION_FAST_FORWARD
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
|
|
||||||
static final String ACTION_SHUFFLE
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
|
|
||||||
public static final String ACTION_RECREATE_NOTIFICATION
|
|
||||||
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Service's LifeCycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onCreate() called");
|
|
||||||
}
|
|
||||||
assureCorrectAppLanguage(this);
|
|
||||||
windowManager = ContextCompat.getSystemService(this, WindowManager.class);
|
|
||||||
|
|
||||||
ThemeHelper.setTheme(this);
|
|
||||||
createView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createView() {
|
|
||||||
final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this));
|
|
||||||
|
|
||||||
player = new Player(this);
|
|
||||||
player.setupFromView(binding);
|
|
||||||
|
|
||||||
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
|
||||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
|
||||||
}
|
|
||||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
|
||||||
&& player.getPlayQueue() == null) {
|
|
||||||
// Player is not working, no need to process media button's action
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
|
||||||
|| intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) {
|
|
||||||
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
player.handleIntent(intent);
|
|
||||||
if (player.getMediaSessionManager() != null) {
|
|
||||||
player.getMediaSessionManager().handleMediaButtonIntent(intent);
|
|
||||||
}
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopForImmediateReusing() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "stopForImmediateReusing() called");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!player.exoPlayerIsNull()) {
|
|
||||||
player.saveWasPlaying();
|
|
||||||
|
|
||||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
|
||||||
// We can't just pause the player here because it will make transition
|
|
||||||
// from one stream to a new stream not smooth
|
|
||||||
player.smoothStopPlayer();
|
|
||||||
player.setRecovery();
|
|
||||||
|
|
||||||
// Android TV will handle back button in case controls will be visible
|
|
||||||
// (one more additional unneeded click while the player is hidden)
|
|
||||||
player.hideControls(0, 0);
|
|
||||||
player.closeItemsList();
|
|
||||||
|
|
||||||
// Notification shows information about old stream but if a user selects
|
|
||||||
// a stream from backStack it's not actual anymore
|
|
||||||
// So we should hide the notification at all.
|
|
||||||
// When autoplay enabled such notification flashing is annoying so skip this case
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTaskRemoved(final Intent rootIntent) {
|
|
||||||
super.onTaskRemoved(rootIntent);
|
|
||||||
if (!player.videoPlayerSelected()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onDestroy();
|
|
||||||
// Unload from memory completely
|
|
||||||
Runtime.getRuntime().halt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "destroy() called");
|
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanup() {
|
|
||||||
if (player != null) {
|
|
||||||
// Exit from fullscreen when user closes the player via notification
|
|
||||||
if (player.isFullscreen()) {
|
|
||||||
player.toggleFullscreen();
|
|
||||||
}
|
|
||||||
removeViewFromParent();
|
|
||||||
|
|
||||||
player.saveStreamProgressState();
|
|
||||||
player.setRecovery();
|
|
||||||
player.stopActivityBinding();
|
|
||||||
player.removePopupFromView();
|
|
||||||
player.destroy();
|
|
||||||
|
|
||||||
player = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopService() {
|
|
||||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
|
||||||
cleanup();
|
|
||||||
stopSelf();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(final Context base) {
|
|
||||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(final Intent intent) {
|
|
||||||
return mBinder;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
boolean isLandscape() {
|
|
||||||
// DisplayMetrics from activity context knows about MultiWindow feature
|
|
||||||
// while DisplayMetrics from app context doesn't
|
|
||||||
return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
|
|
||||||
? player.getParentActivity() : this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public View getView() {
|
|
||||||
if (player == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return player.getRootView();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeViewFromParent() {
|
|
||||||
if (getView() != null && getView().getParent() != null) {
|
|
||||||
if (player.getParentActivity() != null) {
|
|
||||||
// This means view was added to fragment
|
|
||||||
final ViewGroup parent = (ViewGroup) getView().getParent();
|
|
||||||
parent.removeView(getView());
|
|
||||||
} else {
|
|
||||||
// This means view was added by windowManager for popup player
|
|
||||||
windowManager.removeViewImmediate(getView());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class LocalBinder extends Binder {
|
|
||||||
|
|
||||||
public MainPlayer getService() {
|
|
||||||
return MainPlayer.this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Player getPlayer() {
|
|
||||||
return MainPlayer.this.player;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -32,6 +32,7 @@ import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
@ -56,7 +57,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
|
|
||||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||||
|
|
||||||
protected Player player;
|
private Player player;
|
||||||
|
|
||||||
private boolean serviceBound;
|
private boolean serviceBound;
|
||||||
private ServiceConnection serviceConnection;
|
private ServiceConnection serviceConnection;
|
||||||
|
@ -131,13 +132,13 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
NavigationHelper.openSettings(this);
|
NavigationHelper.openSettings(this);
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_append_playlist:
|
case R.id.action_append_playlist:
|
||||||
player.onAddToPlaylistClicked(getSupportFragmentManager());
|
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_playback_speed:
|
case R.id.action_playback_speed:
|
||||||
openPlaybackParameterDialog();
|
openPlaybackParameterDialog();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_mute:
|
case R.id.action_mute:
|
||||||
player.onMuteUnmuteButtonClicked();
|
player.toggleMute();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_system_audio:
|
case R.id.action_system_audio:
|
||||||
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
|
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
|
||||||
|
@ -173,7 +174,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void bind() {
|
private void bind() {
|
||||||
final Intent bindIntent = new Intent(this, MainPlayer.class);
|
final Intent bindIntent = new Intent(this, PlayerService.class);
|
||||||
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
|
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
unbindService(serviceConnection);
|
unbindService(serviceConnection);
|
||||||
|
@ -189,10 +190,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
player.removeActivityListener(this);
|
player.removeActivityListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player != null && player.getPlayQueueAdapter() != null) {
|
onQueueUpdate(null);
|
||||||
player.getPlayQueueAdapter().unsetSelectedListener();
|
|
||||||
}
|
|
||||||
queueControlBinding.playQueue.setAdapter(null);
|
|
||||||
if (itemTouchHelper != null) {
|
if (itemTouchHelper != null) {
|
||||||
itemTouchHelper.attachToRecyclerView(null);
|
itemTouchHelper.attachToRecyclerView(null);
|
||||||
}
|
}
|
||||||
|
@ -213,17 +211,15 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
public void onServiceConnected(final ComponentName name, final IBinder service) {
|
public void onServiceConnected(final ComponentName name, final IBinder service) {
|
||||||
Log.d(TAG, "Player service is connected");
|
Log.d(TAG, "Player service is connected");
|
||||||
|
|
||||||
if (service instanceof PlayerServiceBinder) {
|
if (service instanceof PlayerService.LocalBinder) {
|
||||||
player = ((PlayerServiceBinder) service).getPlayerInstance();
|
player = ((PlayerService.LocalBinder) service).getPlayer();
|
||||||
} else if (service instanceof MainPlayer.LocalBinder) {
|
|
||||||
player = ((MainPlayer.LocalBinder) service).getPlayer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player == null || player.getPlayQueue() == null
|
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|
||||||
|| player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
|
|
||||||
unbind();
|
unbind();
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
|
onQueueUpdate(player.getPlayQueue());
|
||||||
buildComponents();
|
buildComponents();
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
final PlayQueueItem item = player.getPlayQueue().getItem();
|
final PlayQueueItem item = player.getPlayQueue().getItem();
|
||||||
|
@ -252,7 +248,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
|
|
||||||
private void buildQueue() {
|
private void buildQueue() {
|
||||||
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
|
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
|
||||||
queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
|
|
||||||
queueControlBinding.playQueue.setClickable(true);
|
queueControlBinding.playQueue.setClickable(true);
|
||||||
queueControlBinding.playQueue.setLongClickable(true);
|
queueControlBinding.playQueue.setLongClickable(true);
|
||||||
queueControlBinding.playQueue.clearOnScrollListeners();
|
queueControlBinding.playQueue.clearOnScrollListeners();
|
||||||
|
@ -260,8 +255,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
|
|
||||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||||
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
|
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
|
||||||
|
|
||||||
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildMetadata() {
|
private void buildMetadata() {
|
||||||
|
@ -381,7 +374,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
|
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
|
||||||
player.onRepeatClicked();
|
player.cycleNextRepeatMode();
|
||||||
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
|
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
|
||||||
player.playPrevious();
|
player.playPrevious();
|
||||||
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
|
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
|
||||||
|
@ -393,7 +386,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
|
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
|
||||||
player.playNext();
|
player.playNext();
|
||||||
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
|
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
|
||||||
player.onShuffleClicked();
|
player.toggleShuffleModeEnabled();
|
||||||
} else if (view.getId() == queueControlBinding.metadata.getId()) {
|
} else if (view.getId() == queueControlBinding.metadata.getId()) {
|
||||||
scrollToSelected();
|
scrollToSelected();
|
||||||
} else if (view.getId() == queueControlBinding.liveSync.getId()) {
|
} else if (view.getId() == queueControlBinding.liveSync.getId()) {
|
||||||
|
@ -456,7 +449,14 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onQueueUpdate(final PlayQueue queue) {
|
public void onQueueUpdate(@Nullable final PlayQueue queue) {
|
||||||
|
if (queue == null) {
|
||||||
|
queueControlBinding.playQueue.setAdapter(null);
|
||||||
|
} else {
|
||||||
|
final PlayQueueAdapter adapter = new PlayQueueAdapter(this, queue);
|
||||||
|
adapter.setSelectedListener(getOnSelectedListener());
|
||||||
|
queueControlBinding.playQueue.setAdapter(adapter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -465,7 +465,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
onStateChanged(state);
|
onStateChanged(state);
|
||||||
onPlayModeChanged(repeatMode, shuffled);
|
onPlayModeChanged(repeatMode, shuffled);
|
||||||
onPlaybackParameterChanged(parameters);
|
onPlaybackParameterChanged(parameters);
|
||||||
onMaybePlaybackAdapterChanged();
|
|
||||||
onMaybeMuteChanged();
|
onMaybeMuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -593,17 +592,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMaybePlaybackAdapterChanged() {
|
|
||||||
if (player == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
|
|
||||||
if (maybeNewAdapter != null
|
|
||||||
&& queueControlBinding.playQueue.getAdapter() != maybeNewAdapter) {
|
|
||||||
queueControlBinding.playQueue.setAdapter(maybeNewAdapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onMaybeMuteChanged() {
|
private void onMaybeMuteChanged() {
|
||||||
if (menu != null && player != null) {
|
if (menu != null && player != null) {
|
||||||
final MenuItem item = menu.findItem(R.id.action_mute);
|
final MenuItem item = menu.findItem(R.id.action_mute);
|
||||||
|
|
File diff suppressed because it is too large
Load diff
149
app/src/main/java/org/schabi/newpipe/player/PlayerService.java
Normal file
149
app/src/main/java/org/schabi/newpipe/player/PlayerService.java
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||||
|
* Part of NewPipe
|
||||||
|
*
|
||||||
|
* License: GPL-3.0+
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One service for all players.
|
||||||
|
*/
|
||||||
|
public final class PlayerService extends Service {
|
||||||
|
private static final String TAG = PlayerService.class.getSimpleName();
|
||||||
|
private static final boolean DEBUG = Player.DEBUG;
|
||||||
|
|
||||||
|
private Player player;
|
||||||
|
|
||||||
|
private final IBinder mBinder = new PlayerService.LocalBinder();
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Service's LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreate() called");
|
||||||
|
}
|
||||||
|
assureCorrectAppLanguage(this);
|
||||||
|
ThemeHelper.setTheme(this);
|
||||||
|
|
||||||
|
player = new Player(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
||||||
|
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||||
|
&& player.getPlayQueue() == null) {
|
||||||
|
// No need to process media button's actions if the player is not working, otherwise the
|
||||||
|
// player service would strangely start with nothing to play
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.handleIntent(intent);
|
||||||
|
player.UIs().get(MediaSessionPlayerUi.class)
|
||||||
|
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||||
|
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopForImmediateReusing() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "stopForImmediateReusing() called");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player.exoPlayerIsNull()) {
|
||||||
|
player.saveWasPlaying();
|
||||||
|
|
||||||
|
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||||
|
// We can't just pause the player here because it will make transition
|
||||||
|
// from one stream to a new stream not smooth
|
||||||
|
player.smoothStopForImmediateReusing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTaskRemoved(final Intent rootIntent) {
|
||||||
|
super.onTaskRemoved(rootIntent);
|
||||||
|
if (!player.videoPlayerSelected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onDestroy();
|
||||||
|
// Unload from memory completely
|
||||||
|
Runtime.getRuntime().halt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "destroy() called");
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup() {
|
||||||
|
if (player != null) {
|
||||||
|
player.destroy();
|
||||||
|
player = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopService() {
|
||||||
|
cleanup();
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void attachBaseContext(final Context base) {
|
||||||
|
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(final Intent intent) {
|
||||||
|
return mBinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LocalBinder extends Binder {
|
||||||
|
|
||||||
|
public PlayerService getService() {
|
||||||
|
return PlayerService.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Player getPlayer() {
|
||||||
|
return PlayerService.this.player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
package org.schabi.newpipe.player;
|
|
||||||
|
|
||||||
import android.os.Binder;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
class PlayerServiceBinder extends Binder {
|
|
||||||
private final Player player;
|
|
||||||
|
|
||||||
PlayerServiceBinder(@NonNull final Player player) {
|
|
||||||
this.player = player;
|
|
||||||
}
|
|
||||||
|
|
||||||
Player getPlayerInstance() {
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package org.schabi.newpipe.player;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
public class PlayerState implements Serializable {
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final PlayQueue playQueue;
|
|
||||||
private final int repeatMode;
|
|
||||||
private final float playbackSpeed;
|
|
||||||
private final float playbackPitch;
|
|
||||||
@Nullable
|
|
||||||
private final String playbackQuality;
|
|
||||||
private final boolean playbackSkipSilence;
|
|
||||||
private final boolean wasPlaying;
|
|
||||||
|
|
||||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
|
||||||
final float playbackSpeed, final float playbackPitch,
|
|
||||||
final boolean playbackSkipSilence, final boolean wasPlaying) {
|
|
||||||
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null,
|
|
||||||
playbackSkipSilence, wasPlaying);
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
|
||||||
final float playbackSpeed, final float playbackPitch,
|
|
||||||
@Nullable final String playbackQuality, final boolean playbackSkipSilence,
|
|
||||||
final boolean wasPlaying) {
|
|
||||||
this.playQueue = playQueue;
|
|
||||||
this.repeatMode = repeatMode;
|
|
||||||
this.playbackSpeed = playbackSpeed;
|
|
||||||
this.playbackPitch = playbackPitch;
|
|
||||||
this.playbackQuality = playbackQuality;
|
|
||||||
this.playbackSkipSilence = playbackSkipSilence;
|
|
||||||
this.wasPlaying = wasPlaying;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Serdes
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Getters
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public PlayQueue getPlayQueue() {
|
|
||||||
return playQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRepeatMode() {
|
|
||||||
return repeatMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getPlaybackSpeed() {
|
|
||||||
return playbackSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getPlaybackPitch() {
|
|
||||||
return playbackPitch;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getPlaybackQuality() {
|
|
||||||
return playbackQuality;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPlaybackSkipSilence() {
|
|
||||||
return playbackSkipSilence;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean wasPlaying() {
|
|
||||||
return wasPlaying;
|
|
||||||
}
|
|
||||||
}
|
|
32
app/src/main/java/org/schabi/newpipe/player/PlayerType.java
Normal file
32
app/src/main/java/org/schabi/newpipe/player/PlayerType.java
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
public enum PlayerType {
|
||||||
|
MAIN,
|
||||||
|
AUDIO,
|
||||||
|
POPUP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an integer representing this {@link PlayerType}, to be used to save it in intents
|
||||||
|
* @see #retrieveFromIntent(Intent) Use retrieveFromIntent() to retrieve and convert player type
|
||||||
|
* integers from an intent
|
||||||
|
*/
|
||||||
|
public int valueForIntent() {
|
||||||
|
return ordinal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param intent the intent to retrieve a player type from
|
||||||
|
* @return the player type integer retrieved from the intent, converted back into a {@link
|
||||||
|
* PlayerType}, or {@link PlayerType#MAIN} if there is no player type extra in the
|
||||||
|
* intent
|
||||||
|
* @throws ArrayIndexOutOfBoundsException if the intent contains an invalid player type integer
|
||||||
|
* @see #valueForIntent() Use valueForIntent() to obtain valid player type integers
|
||||||
|
*/
|
||||||
|
public static PlayerType retrieveFromIntent(final Intent intent) {
|
||||||
|
return values()[intent.getIntExtra(PLAYER_TYPE, MAIN.valueForIntent())];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Based on ExoPlayer's DefaultHttpDataSource, version 2.17.1.
|
* Based on ExoPlayer's DefaultHttpDataSource, version 2.18.1.
|
||||||
*
|
*
|
||||||
* Original source code copyright (C) 2016 The Android Open Source Project, licensed under the
|
* Original source code copyright (C) 2016 The Android Open Source Project, licensed under the
|
||||||
* Apache License, Version 2.0.
|
* Apache License, Version 2.0.
|
||||||
|
|
|
@ -1,520 +0,0 @@
|
||||||
package org.schabi.newpipe.player.event
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Handler
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.GestureDetector
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewConfiguration
|
|
||||||
import org.schabi.newpipe.ktx.animate
|
|
||||||
import org.schabi.newpipe.player.MainPlayer
|
|
||||||
import org.schabi.newpipe.player.Player
|
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper
|
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.hypot
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base gesture handling for [Player]
|
|
||||||
*
|
|
||||||
* This class contains the logic for the player gestures like View preparations
|
|
||||||
* and provides some abstract methods to make it easier separating the logic from the UI.
|
|
||||||
*/
|
|
||||||
abstract class BasePlayerGestureListener(
|
|
||||||
@JvmField
|
|
||||||
protected val player: Player,
|
|
||||||
@JvmField
|
|
||||||
protected val service: MainPlayer
|
|
||||||
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
|
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
// Abstract methods for VIDEO and POPUP
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion)
|
|
||||||
|
|
||||||
abstract fun onSingleTap(playerType: MainPlayer.PlayerType)
|
|
||||||
|
|
||||||
abstract fun onScroll(
|
|
||||||
playerType: MainPlayer.PlayerType,
|
|
||||||
portion: DisplayPortion,
|
|
||||||
initialEvent: MotionEvent,
|
|
||||||
movingEvent: MotionEvent,
|
|
||||||
distanceX: Float,
|
|
||||||
distanceY: Float
|
|
||||||
)
|
|
||||||
|
|
||||||
abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent)
|
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
// Abstract methods for POPUP (exclusive)
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
abstract fun onPopupResizingStart()
|
|
||||||
|
|
||||||
abstract fun onPopupResizingEnd()
|
|
||||||
|
|
||||||
private var initialPopupX: Int = -1
|
|
||||||
private var initialPopupY: Int = -1
|
|
||||||
|
|
||||||
private var isMovingInMain = false
|
|
||||||
private var isMovingInPopup = false
|
|
||||||
private var isResizing = false
|
|
||||||
|
|
||||||
private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity()
|
|
||||||
|
|
||||||
// [popup] initial coordinates and distance between fingers
|
|
||||||
private var initPointerDistance = -1.0
|
|
||||||
private var initFirstPointerX = -1f
|
|
||||||
private var initFirstPointerY = -1f
|
|
||||||
private var initSecPointerX = -1f
|
|
||||||
private var initSecPointerY = -1f
|
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
// onTouch implementation
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
|
||||||
return if (player.popupPlayerSelected()) {
|
|
||||||
onTouchInPopup(v, event)
|
|
||||||
} else {
|
|
||||||
onTouchInMain(v, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
|
|
||||||
player.gestureDetector.onTouchEvent(event)
|
|
||||||
if (event.action == MotionEvent.ACTION_UP && isMovingInMain) {
|
|
||||||
isMovingInMain = false
|
|
||||||
onScrollEnd(MainPlayer.PlayerType.VIDEO, event)
|
|
||||||
}
|
|
||||||
return when (event.action) {
|
|
||||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
|
||||||
v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_UP -> {
|
|
||||||
v.parent.requestDisallowInterceptTouchEvent(false)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onTouchInPopup(v: View, event: MotionEvent): Boolean {
|
|
||||||
player.gestureDetector.onTouchEvent(event)
|
|
||||||
if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
|
|
||||||
}
|
|
||||||
onPopupResizingStart()
|
|
||||||
|
|
||||||
// record coordinates of fingers
|
|
||||||
initFirstPointerX = event.getX(0)
|
|
||||||
initFirstPointerY = event.getY(0)
|
|
||||||
initSecPointerX = event.getX(1)
|
|
||||||
initSecPointerY = event.getY(1)
|
|
||||||
// record distance between fingers
|
|
||||||
initPointerDistance = hypot(
|
|
||||||
initFirstPointerX - initSecPointerX.toDouble(),
|
|
||||||
initFirstPointerY - initSecPointerY.toDouble()
|
|
||||||
)
|
|
||||||
|
|
||||||
isResizing = true
|
|
||||||
}
|
|
||||||
if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
|
|
||||||
"[${event.rawX}, ${event.rawY}]"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return handleMultiDrag(event)
|
|
||||||
}
|
|
||||||
if (event.action == MotionEvent.ACTION_UP) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
|
|
||||||
" [${event.rawX}, ${event.rawY}]"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (isMovingInPopup) {
|
|
||||||
isMovingInPopup = false
|
|
||||||
onScrollEnd(MainPlayer.PlayerType.POPUP, event)
|
|
||||||
}
|
|
||||||
if (isResizing) {
|
|
||||||
isResizing = false
|
|
||||||
|
|
||||||
initPointerDistance = (-1).toDouble()
|
|
||||||
initFirstPointerX = (-1).toFloat()
|
|
||||||
initFirstPointerY = (-1).toFloat()
|
|
||||||
initSecPointerX = (-1).toFloat()
|
|
||||||
initSecPointerY = (-1).toFloat()
|
|
||||||
|
|
||||||
onPopupResizingEnd()
|
|
||||||
player.changeState(player.currentState)
|
|
||||||
}
|
|
||||||
if (!player.isPopupClosing) {
|
|
||||||
savePopupPositionAndSizeToPrefs(player)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v.performClick()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleMultiDrag(event: MotionEvent): Boolean {
|
|
||||||
if (initPointerDistance != -1.0 && event.pointerCount == 2) {
|
|
||||||
// get the movements of the fingers
|
|
||||||
val firstPointerMove = hypot(
|
|
||||||
event.getX(0) - initFirstPointerX.toDouble(),
|
|
||||||
event.getY(0) - initFirstPointerY.toDouble()
|
|
||||||
)
|
|
||||||
val secPointerMove = hypot(
|
|
||||||
event.getX(1) - initSecPointerX.toDouble(),
|
|
||||||
event.getY(1) - initSecPointerY.toDouble()
|
|
||||||
)
|
|
||||||
|
|
||||||
// minimum threshold beyond which pinch gesture will work
|
|
||||||
val minimumMove = ViewConfiguration.get(service).scaledTouchSlop
|
|
||||||
|
|
||||||
if (max(firstPointerMove, secPointerMove) > minimumMove) {
|
|
||||||
// calculate current distance between the pointers
|
|
||||||
val currentPointerDistance = hypot(
|
|
||||||
event.getX(0) - event.getX(1).toDouble(),
|
|
||||||
event.getY(0) - event.getY(1).toDouble()
|
|
||||||
)
|
|
||||||
|
|
||||||
val popupWidth = player.popupLayoutParams!!.width.toDouble()
|
|
||||||
// change co-ordinates of popup so the center stays at the same position
|
|
||||||
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
|
|
||||||
initPointerDistance = currentPointerDistance
|
|
||||||
player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt()
|
|
||||||
|
|
||||||
player.checkPopupPositionBounds()
|
|
||||||
player.updateScreenSize()
|
|
||||||
player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
// Simple gestures
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
override fun onDown(e: MotionEvent): Boolean {
|
|
||||||
if (DEBUG)
|
|
||||||
Log.d(TAG, "onDown called with e = [$e]")
|
|
||||||
|
|
||||||
if (isDoubleTapping && isDoubleTapEnabled) {
|
|
||||||
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (player.popupPlayerSelected())
|
|
||||||
onDownInPopup(e)
|
|
||||||
else
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDownInPopup(e: MotionEvent): Boolean {
|
|
||||||
// Fix popup position when the user touch it, it may have the wrong one
|
|
||||||
// because the soft input is visible (the draggable area is currently resized).
|
|
||||||
player.updateScreenSize()
|
|
||||||
player.checkPopupPositionBounds()
|
|
||||||
player.popupLayoutParams?.let {
|
|
||||||
initialPopupX = it.x
|
|
||||||
initialPopupY = it.y
|
|
||||||
}
|
|
||||||
return super.onDown(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
|
||||||
if (DEBUG)
|
|
||||||
Log.d(TAG, "onDoubleTap called with e = [$e]")
|
|
||||||
|
|
||||||
onDoubleTap(e, getDisplayPortion(e))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
|
||||||
if (DEBUG)
|
|
||||||
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
|
|
||||||
|
|
||||||
if (isDoubleTapping)
|
|
||||||
return true
|
|
||||||
|
|
||||||
if (player.popupPlayerSelected()) {
|
|
||||||
if (player.exoPlayerIsNull())
|
|
||||||
return false
|
|
||||||
|
|
||||||
onSingleTap(MainPlayer.PlayerType.POPUP)
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
super.onSingleTapConfirmed(e)
|
|
||||||
if (player.currentState == Player.STATE_BLOCKED)
|
|
||||||
return true
|
|
||||||
|
|
||||||
onSingleTap(MainPlayer.PlayerType.VIDEO)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongPress(e: MotionEvent?) {
|
|
||||||
if (player.popupPlayerSelected()) {
|
|
||||||
player.updateScreenSize()
|
|
||||||
player.checkPopupPositionBounds()
|
|
||||||
player.changePopupSize(player.screenWidth.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScroll(
|
|
||||||
initialEvent: MotionEvent,
|
|
||||||
movingEvent: MotionEvent,
|
|
||||||
distanceX: Float,
|
|
||||||
distanceY: Float
|
|
||||||
): Boolean {
|
|
||||||
return if (player.popupPlayerSelected()) {
|
|
||||||
onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY)
|
|
||||||
} else {
|
|
||||||
onScrollInMain(initialEvent, movingEvent, distanceX, distanceY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFling(
|
|
||||||
e1: MotionEvent?,
|
|
||||||
e2: MotionEvent?,
|
|
||||||
velocityX: Float,
|
|
||||||
velocityY: Float
|
|
||||||
): Boolean {
|
|
||||||
return if (player.popupPlayerSelected()) {
|
|
||||||
val absVelocityX = abs(velocityX)
|
|
||||||
val absVelocityY = abs(velocityY)
|
|
||||||
if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) {
|
|
||||||
if (absVelocityX > tossFlingVelocity) {
|
|
||||||
player.popupLayoutParams!!.x = velocityX.toInt()
|
|
||||||
}
|
|
||||||
if (absVelocityY > tossFlingVelocity) {
|
|
||||||
player.popupLayoutParams!!.y = velocityY.toInt()
|
|
||||||
}
|
|
||||||
player.checkPopupPositionBounds()
|
|
||||||
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onScrollInMain(
|
|
||||||
initialEvent: MotionEvent,
|
|
||||||
movingEvent: MotionEvent,
|
|
||||||
distanceX: Float,
|
|
||||||
distanceY: Float
|
|
||||||
): Boolean {
|
|
||||||
|
|
||||||
if (!player.isFullscreen) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
|
|
||||||
val isTouchingNavigationBar: Boolean =
|
|
||||||
initialEvent.y > (player.rootView.height - getNavigationBarHeight(service))
|
|
||||||
if (isTouchingStatusBar || isTouchingNavigationBar) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
|
|
||||||
if (
|
|
||||||
!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
|
|
||||||
player.currentState == Player.STATE_COMPLETED
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
isMovingInMain = true
|
|
||||||
|
|
||||||
onScroll(
|
|
||||||
MainPlayer.PlayerType.VIDEO,
|
|
||||||
getDisplayHalfPortion(initialEvent),
|
|
||||||
initialEvent,
|
|
||||||
movingEvent,
|
|
||||||
distanceX,
|
|
||||||
distanceY
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onScrollInPopup(
|
|
||||||
initialEvent: MotionEvent,
|
|
||||||
movingEvent: MotionEvent,
|
|
||||||
distanceX: Float,
|
|
||||||
distanceY: Float
|
|
||||||
): Boolean {
|
|
||||||
|
|
||||||
if (isResizing) {
|
|
||||||
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMovingInPopup) {
|
|
||||||
player.closeOverlayButton.animate(true, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
isMovingInPopup = true
|
|
||||||
|
|
||||||
val diffX: Float = (movingEvent.rawX - initialEvent.rawX)
|
|
||||||
var posX: Float = (initialPopupX + diffX)
|
|
||||||
val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
|
|
||||||
var posY: Float = (initialPopupY + diffY)
|
|
||||||
|
|
||||||
if (posX > player.screenWidth - player.popupLayoutParams!!.width) {
|
|
||||||
posX = (player.screenWidth - player.popupLayoutParams!!.width)
|
|
||||||
} else if (posX < 0) {
|
|
||||||
posX = 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
if (posY > player.screenHeight - player.popupLayoutParams!!.height) {
|
|
||||||
posY = (player.screenHeight - player.popupLayoutParams!!.height)
|
|
||||||
} else if (posY < 0) {
|
|
||||||
posY = 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
player.popupLayoutParams!!.x = posX.toInt()
|
|
||||||
player.popupLayoutParams!!.y = posY.toInt()
|
|
||||||
|
|
||||||
onScroll(
|
|
||||||
MainPlayer.PlayerType.POPUP,
|
|
||||||
getDisplayHalfPortion(initialEvent),
|
|
||||||
initialEvent,
|
|
||||||
movingEvent,
|
|
||||||
distanceX,
|
|
||||||
distanceY
|
|
||||||
)
|
|
||||||
|
|
||||||
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
// Multi double tapping
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
var doubleTapControls: DoubleTapListener? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val isDoubleTapEnabled: Boolean
|
|
||||||
get() = doubleTapDelay > 0
|
|
||||||
|
|
||||||
var isDoubleTapping = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
fun doubleTapControls(listener: DoubleTapListener) = apply {
|
|
||||||
doubleTapControls = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
private var doubleTapDelay = DOUBLE_TAP_DELAY
|
|
||||||
private val doubleTapHandler: Handler = Handler()
|
|
||||||
private val doubleTapRunnable = Runnable {
|
|
||||||
if (DEBUG)
|
|
||||||
Log.d(TAG, "doubleTapRunnable called")
|
|
||||||
|
|
||||||
isDoubleTapping = false
|
|
||||||
doubleTapControls?.onDoubleTapFinished()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startMultiDoubleTap(e: MotionEvent) {
|
|
||||||
if (!isDoubleTapping) {
|
|
||||||
if (DEBUG)
|
|
||||||
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
|
|
||||||
|
|
||||||
keepInDoubleTapMode()
|
|
||||||
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun keepInDoubleTapMode() {
|
|
||||||
if (DEBUG)
|
|
||||||
Log.d(TAG, "keepInDoubleTapMode called")
|
|
||||||
|
|
||||||
isDoubleTapping = true
|
|
||||||
doubleTapHandler.removeCallbacks(doubleTapRunnable)
|
|
||||||
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun endMultiDoubleTap() {
|
|
||||||
if (DEBUG)
|
|
||||||
Log.d(TAG, "endMultiDoubleTap called")
|
|
||||||
|
|
||||||
isDoubleTapping = false
|
|
||||||
doubleTapHandler.removeCallbacks(doubleTapRunnable)
|
|
||||||
doubleTapControls?.onDoubleTapFinished()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
// Utils
|
|
||||||
// ///////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
private fun getDisplayPortion(e: MotionEvent): DisplayPortion {
|
|
||||||
return if (player.playerType == MainPlayer.PlayerType.POPUP && player.popupLayoutParams != null) {
|
|
||||||
when {
|
|
||||||
e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT
|
|
||||||
e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
|
|
||||||
else -> DisplayPortion.MIDDLE
|
|
||||||
}
|
|
||||||
} else /* MainPlayer.PlayerType.VIDEO */ {
|
|
||||||
when {
|
|
||||||
e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT
|
|
||||||
e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
|
|
||||||
else -> DisplayPortion.MIDDLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Currently needed for scrolling since there is no action more the middle portion
|
|
||||||
private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
|
|
||||||
return if (player.playerType == MainPlayer.PlayerType.POPUP) {
|
|
||||||
when {
|
|
||||||
e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF
|
|
||||||
else -> DisplayPortion.RIGHT_HALF
|
|
||||||
}
|
|
||||||
} else /* MainPlayer.PlayerType.VIDEO */ {
|
|
||||||
when {
|
|
||||||
e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
|
|
||||||
else -> DisplayPortion.RIGHT_HALF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNavigationBarHeight(context: Context): Int {
|
|
||||||
val resId = context.resources
|
|
||||||
.getIdentifier("navigation_bar_height", "dimen", "android")
|
|
||||||
return if (resId > 0) {
|
|
||||||
context.resources.getDimensionPixelSize(resId)
|
|
||||||
} else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getStatusBarHeight(context: Context): Int {
|
|
||||||
val resId = context.resources
|
|
||||||
.getIdentifier("status_bar_height", "dimen", "android")
|
|
||||||
return if (resId > 0) {
|
|
||||||
context.resources.getDimensionPixelSize(resId)
|
|
||||||
} else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "BasePlayerGestListener"
|
|
||||||
private val DEBUG = Player.DEBUG
|
|
||||||
|
|
||||||
private const val DOUBLE_TAP_DELAY = 550L
|
|
||||||
private const val MOVEMENT_THRESHOLD = 40
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package org.schabi.newpipe.player.event
|
|
||||||
|
|
||||||
interface DoubleTapListener {
|
|
||||||
fun onDoubleTapStarted(portion: DisplayPortion) {}
|
|
||||||
fun onDoubleTapProgressDown(portion: DisplayPortion) {}
|
|
||||||
fun onDoubleTapFinished() {}
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.schabi.newpipe.player.event;
|
package org.schabi.newpipe.player.event;
|
||||||
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
|
|
@ -1,256 +0,0 @@
|
||||||
package org.schabi.newpipe.player.event;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.ktx.AnimationType.ALPHA;
|
|
||||||
import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA;
|
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|
||||||
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION;
|
|
||||||
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
|
|
||||||
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
|
||||||
import org.schabi.newpipe.player.Player;
|
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GestureListener for the player
|
|
||||||
*
|
|
||||||
* While {@link BasePlayerGestureListener} contains the logic behind the single gestures
|
|
||||||
* this class focuses on the visual aspect like hiding and showing the controls or changing
|
|
||||||
* volume/brightness during scrolling for specific events.
|
|
||||||
*/
|
|
||||||
public class PlayerGestureListener
|
|
||||||
extends BasePlayerGestureListener
|
|
||||||
implements View.OnTouchListener {
|
|
||||||
private static final String TAG = PlayerGestureListener.class.getSimpleName();
|
|
||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
|
||||||
|
|
||||||
private final int maxVolume;
|
|
||||||
|
|
||||||
public PlayerGestureListener(final Player player, final MainPlayer service) {
|
|
||||||
super(player, service);
|
|
||||||
maxVolume = player.getAudioReactor().getMaxVolume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDoubleTap(@NonNull final MotionEvent event,
|
|
||||||
@NonNull final DisplayPortion portion) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onDoubleTap called with playerType = ["
|
|
||||||
+ player.getPlayerType() + "], portion = [" + portion + "]");
|
|
||||||
}
|
|
||||||
if (player.isSomePopupMenuVisible()) {
|
|
||||||
player.hideControls(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
|
|
||||||
startMultiDoubleTap(event);
|
|
||||||
} else if (portion == DisplayPortion.MIDDLE) {
|
|
||||||
player.playPause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSingleTap(@NonNull final MainPlayer.PlayerType playerType) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (player.isControlsVisible()) {
|
|
||||||
player.hideControls(150, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// -- Controls are not visible --
|
|
||||||
|
|
||||||
// When player is completed show controls and don't hide them later
|
|
||||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
|
||||||
player.showControls(0);
|
|
||||||
} else {
|
|
||||||
player.showControlsThenHide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onScroll(@NonNull final MainPlayer.PlayerType playerType,
|
|
||||||
@NonNull final DisplayPortion portion,
|
|
||||||
@NonNull final MotionEvent initialEvent,
|
|
||||||
@NonNull final MotionEvent movingEvent,
|
|
||||||
final float distanceX, final float distanceY) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onScroll called with playerType = ["
|
|
||||||
+ player.getPlayerType() + "], portion = [" + portion + "]");
|
|
||||||
}
|
|
||||||
if (playerType == MainPlayer.PlayerType.VIDEO) {
|
|
||||||
|
|
||||||
// -- Brightness and Volume control --
|
|
||||||
final boolean isBrightnessGestureEnabled =
|
|
||||||
PlayerHelper.isBrightnessGestureEnabled(service);
|
|
||||||
final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
|
|
||||||
|
|
||||||
if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
|
|
||||||
if (portion == DisplayPortion.LEFT_HALF) {
|
|
||||||
onScrollMainBrightness(distanceX, distanceY);
|
|
||||||
|
|
||||||
} else /* DisplayPortion.RIGHT_HALF */ {
|
|
||||||
onScrollMainVolume(distanceX, distanceY);
|
|
||||||
}
|
|
||||||
} else if (isBrightnessGestureEnabled) {
|
|
||||||
onScrollMainBrightness(distanceX, distanceY);
|
|
||||||
} else if (isVolumeGestureEnabled) {
|
|
||||||
onScrollMainVolume(distanceX, distanceY);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else /* MainPlayer.PlayerType.POPUP */ {
|
|
||||||
|
|
||||||
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
|
|
||||||
final View closingOverlayView = player.getClosingOverlayView();
|
|
||||||
final boolean showClosingOverlayView = player.isInsideClosingRadius(movingEvent);
|
|
||||||
// Check if an view is in expected state and if not animate it into the correct state
|
|
||||||
final int expectedVisibility = showClosingOverlayView ? View.VISIBLE : View.GONE;
|
|
||||||
if (closingOverlayView.getVisibility() != expectedVisibility) {
|
|
||||||
animate(closingOverlayView, showClosingOverlayView, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onScrollMainVolume(final float distanceX, final float distanceY) {
|
|
||||||
// If we just started sliding, change the progress bar to match the system volume
|
|
||||||
if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
|
|
||||||
final float volumePercent = player
|
|
||||||
.getAudioReactor().getVolume() / (float) maxVolume;
|
|
||||||
player.getVolumeProgressBar().setProgress(
|
|
||||||
(int) (volumePercent * player.getMaxGestureLength()));
|
|
||||||
}
|
|
||||||
|
|
||||||
player.getVolumeProgressBar().incrementProgressBy((int) distanceY);
|
|
||||||
final float currentProgressPercent = (float) player
|
|
||||||
.getVolumeProgressBar().getProgress() / player.getMaxGestureLength();
|
|
||||||
final int currentVolume = (int) (maxVolume * currentProgressPercent);
|
|
||||||
player.getAudioReactor().setVolume(currentVolume);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
|
|
||||||
}
|
|
||||||
|
|
||||||
player.getVolumeImageView().setImageDrawable(
|
|
||||||
AppCompatResources.getDrawable(service, currentProgressPercent <= 0
|
|
||||||
? R.drawable.ic_volume_off
|
|
||||||
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute
|
|
||||||
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down
|
|
||||||
: R.drawable.ic_volume_up)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
|
|
||||||
animate(player.getVolumeRelativeLayout(), true, 200, SCALE_AND_ALPHA);
|
|
||||||
}
|
|
||||||
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
|
|
||||||
player.getBrightnessRelativeLayout().setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onScrollMainBrightness(final float distanceX, final float distanceY) {
|
|
||||||
final Activity parent = player.getParentActivity();
|
|
||||||
if (parent == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Window window = parent.getWindow();
|
|
||||||
final WindowManager.LayoutParams layoutParams = window.getAttributes();
|
|
||||||
final ProgressBar bar = player.getBrightnessProgressBar();
|
|
||||||
final float oldBrightness = layoutParams.screenBrightness;
|
|
||||||
bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness))));
|
|
||||||
bar.incrementProgressBy((int) distanceY);
|
|
||||||
|
|
||||||
final float currentProgressPercent = (float) bar.getProgress() / bar.getMax();
|
|
||||||
layoutParams.screenBrightness = currentProgressPercent;
|
|
||||||
window.setAttributes(layoutParams);
|
|
||||||
|
|
||||||
// Save current brightness level
|
|
||||||
PlayerHelper.setScreenBrightness(parent, currentProgressPercent);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onScroll().brightnessControl, "
|
|
||||||
+ "currentBrightness = " + currentProgressPercent);
|
|
||||||
}
|
|
||||||
|
|
||||||
player.getBrightnessImageView().setImageDrawable(
|
|
||||||
AppCompatResources.getDrawable(service,
|
|
||||||
currentProgressPercent < 0.25
|
|
||||||
? R.drawable.ic_brightness_low
|
|
||||||
: currentProgressPercent < 0.75
|
|
||||||
? R.drawable.ic_brightness_medium
|
|
||||||
: R.drawable.ic_brightness_high)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
|
|
||||||
animate(player.getBrightnessRelativeLayout(), true, 200, SCALE_AND_ALPHA);
|
|
||||||
}
|
|
||||||
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
|
|
||||||
player.getVolumeRelativeLayout().setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onScrollEnd(@NonNull final MainPlayer.PlayerType playerType,
|
|
||||||
@NonNull final MotionEvent event) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onScrollEnd called with playerType = ["
|
|
||||||
+ player.getPlayerType() + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
|
|
||||||
player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerType == MainPlayer.PlayerType.VIDEO) {
|
|
||||||
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
|
|
||||||
animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA,
|
|
||||||
200);
|
|
||||||
}
|
|
||||||
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
|
|
||||||
animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA,
|
|
||||||
200);
|
|
||||||
}
|
|
||||||
} else /* Popup-Player */ {
|
|
||||||
if (player.isInsideClosingRadius(event)) {
|
|
||||||
player.closePopup();
|
|
||||||
} else if (!player.isPopupClosing()) {
|
|
||||||
animate(player.getCloseOverlayButton(), false, 200);
|
|
||||||
animate(player.getClosingOverlayView(), false, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPopupResizingStart() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onPopupResizingStart called");
|
|
||||||
}
|
|
||||||
player.getLoadingPanel().setVisibility(View.GONE);
|
|
||||||
|
|
||||||
player.hideControls(0, 0);
|
|
||||||
animate(player.getFastSeekOverlay(), false, 0);
|
|
||||||
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPopupResizingEnd() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onPopupResizingEnd called");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ package org.schabi.newpipe.player.event;
|
||||||
import com.google.android.exoplayer2.PlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
|
|
||||||
public interface PlayerServiceEventListener extends PlayerEventListener {
|
public interface PlayerServiceEventListener extends PlayerEventListener {
|
||||||
|
void onViewCreated();
|
||||||
|
|
||||||
void onFullscreenStateChanged(boolean fullscreen);
|
void onFullscreenStateChanged(boolean fullscreen);
|
||||||
|
|
||||||
void onScreenRotationButtonClicked();
|
void onScreenRotationButtonClicked();
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package org.schabi.newpipe.player.event;
|
package org.schabi.newpipe.player.event;
|
||||||
|
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
|
||||||
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
|
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
|
||||||
void onServiceConnected(Player player,
|
void onServiceConnected(Player player,
|
||||||
MainPlayer playerService,
|
PlayerService playerService,
|
||||||
boolean playAfterConnect);
|
boolean playAfterConnect);
|
||||||
void onServiceDisconnected();
|
void onServiceDisconnected();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import org.schabi.newpipe.databinding.PlayerBinding
|
||||||
|
import org.schabi.newpipe.player.Player
|
||||||
|
import org.schabi.newpipe.player.ui.VideoPlayerUi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base gesture handling for [Player]
|
||||||
|
*
|
||||||
|
* This class contains the logic for the player gestures like View preparations
|
||||||
|
* and provides some abstract methods to make it easier separating the logic from the UI.
|
||||||
|
*/
|
||||||
|
abstract class BasePlayerGestureListener(
|
||||||
|
private val playerUi: VideoPlayerUi,
|
||||||
|
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
|
||||||
|
|
||||||
|
protected val player: Player = playerUi.player
|
||||||
|
protected val binding: PlayerBinding = playerUi.binding
|
||||||
|
|
||||||
|
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||||
|
playerUi.gestureDetector.onTouchEvent(event)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDoubleTap(
|
||||||
|
event: MotionEvent,
|
||||||
|
portion: DisplayPortion
|
||||||
|
) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"onDoubleTap called with playerType = [" +
|
||||||
|
player.playerType + "], portion = [" + portion + "]"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (playerUi.isSomePopupMenuVisible) {
|
||||||
|
playerUi.hideControls(0, 0)
|
||||||
|
}
|
||||||
|
if (portion === DisplayPortion.LEFT || portion === DisplayPortion.RIGHT) {
|
||||||
|
startMultiDoubleTap(event)
|
||||||
|
} else if (portion === DisplayPortion.MIDDLE) {
|
||||||
|
player.playPause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun onSingleTap() {
|
||||||
|
if (playerUi.isControlsVisible) {
|
||||||
|
playerUi.hideControls(150, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// -- Controls are not visible --
|
||||||
|
|
||||||
|
// When player is completed show controls and don't hide them later
|
||||||
|
if (player.currentState == Player.STATE_COMPLETED) {
|
||||||
|
playerUi.showControls(0)
|
||||||
|
} else {
|
||||||
|
playerUi.showControlsThenHide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onScrollEnd(event: MotionEvent) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"onScrollEnd called with playerType = [" +
|
||||||
|
player.playerType + "]"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (playerUi.isControlsVisible && player.currentState == Player.STATE_PLAYING) {
|
||||||
|
playerUi.hideControls(
|
||||||
|
VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
|
||||||
|
VideoPlayerUi.DEFAULT_CONTROLS_HIDE_TIME
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ///////////////////////////////////////////////////////////////////
|
||||||
|
// Simple gestures
|
||||||
|
// ///////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
override fun onDown(e: MotionEvent): Boolean {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "onDown called with e = [$e]")
|
||||||
|
|
||||||
|
if (isDoubleTapping && isDoubleTapEnabled) {
|
||||||
|
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDownNotDoubleTapping(e)) {
|
||||||
|
return super.onDown(e)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if `super.onDown(e)` should be called, false otherwise
|
||||||
|
*/
|
||||||
|
open fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
|
||||||
|
return false // do not call super.onDown(e) by default, overridden for popup player
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "onDoubleTap called with e = [$e]")
|
||||||
|
|
||||||
|
onDoubleTap(e, getDisplayPortion(e))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ///////////////////////////////////////////////////////////////////
|
||||||
|
// Multi double tapping
|
||||||
|
// ///////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private var doubleTapControls: DoubleTapListener? = null
|
||||||
|
|
||||||
|
private val isDoubleTapEnabled: Boolean
|
||||||
|
get() = doubleTapDelay > 0
|
||||||
|
|
||||||
|
var isDoubleTapping = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun doubleTapControls(listener: DoubleTapListener) = apply {
|
||||||
|
doubleTapControls = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
private var doubleTapDelay = DOUBLE_TAP_DELAY
|
||||||
|
private val doubleTapHandler: Handler = Handler(Looper.getMainLooper())
|
||||||
|
private val doubleTapRunnable = Runnable {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "doubleTapRunnable called")
|
||||||
|
|
||||||
|
isDoubleTapping = false
|
||||||
|
doubleTapControls?.onDoubleTapFinished()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startMultiDoubleTap(e: MotionEvent) {
|
||||||
|
if (!isDoubleTapping) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
|
||||||
|
|
||||||
|
keepInDoubleTapMode()
|
||||||
|
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun keepInDoubleTapMode() {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "keepInDoubleTapMode called")
|
||||||
|
|
||||||
|
isDoubleTapping = true
|
||||||
|
doubleTapHandler.removeCallbacks(doubleTapRunnable)
|
||||||
|
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun endMultiDoubleTap() {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "endMultiDoubleTap called")
|
||||||
|
|
||||||
|
isDoubleTapping = false
|
||||||
|
doubleTapHandler.removeCallbacks(doubleTapRunnable)
|
||||||
|
doubleTapControls?.onDoubleTapFinished()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ///////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
// ///////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
abstract fun getDisplayPortion(e: MotionEvent): DisplayPortion
|
||||||
|
|
||||||
|
// Currently needed for scrolling since there is no action more the middle portion
|
||||||
|
abstract fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "BasePlayerGestListener"
|
||||||
|
private val DEBUG = Player.DEBUG
|
||||||
|
|
||||||
|
private const val DOUBLE_TAP_DELAY = 550L
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.schabi.newpipe.player.event;
|
package org.schabi.newpipe.player.gesture;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
@ -8,24 +8,25 @@ import android.view.View;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout> {
|
public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout> {
|
||||||
|
|
||||||
public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) {
|
public CustomBottomSheetBehavior(@NonNull final Context context,
|
||||||
|
@Nullable final AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect globalRect = new Rect();
|
Rect globalRect = new Rect();
|
||||||
private boolean skippingInterception = false;
|
private boolean skippingInterception = false;
|
||||||
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
private final List<Integer> skipInterceptionOfElements = List.of(
|
||||||
R.id.detail_content_root_layout, R.id.relatedItemsLayout,
|
R.id.detail_content_root_layout, R.id.relatedItemsLayout,
|
||||||
R.id.itemsListPanel, R.id.view_pager, R.id.tab_layout, R.id.bottomControls,
|
R.id.itemsListPanel, R.id.view_pager, R.id.tab_layout, R.id.bottomControls,
|
||||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||||
|
@ -33,7 +34,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
|
||||||
@Override
|
@Override
|
||||||
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
||||||
@NonNull final FrameLayout child,
|
@NonNull final FrameLayout child,
|
||||||
final MotionEvent event) {
|
@NonNull final MotionEvent event) {
|
||||||
// Drop following when action ends
|
// Drop following when action ends
|
||||||
if (event.getAction() == MotionEvent.ACTION_CANCEL
|
if (event.getAction() == MotionEvent.ACTION_CANCEL
|
||||||
|| event.getAction() == MotionEvent.ACTION_UP) {
|
|| event.getAction() == MotionEvent.ACTION_UP) {
|
||||||
|
@ -57,7 +58,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
|
||||||
if (getState() == BottomSheetBehavior.STATE_EXPANDED
|
if (getState() == BottomSheetBehavior.STATE_EXPANDED
|
||||||
&& event.getAction() == MotionEvent.ACTION_DOWN) {
|
&& event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||||
// Without overriding scrolling will not work when user touches these elements
|
// Without overriding scrolling will not work when user touches these elements
|
||||||
for (final Integer element : skipInterceptionOfElements) {
|
for (final int element : skipInterceptionOfElements) {
|
||||||
final View view = child.findViewById(element);
|
final View view = child.findViewById(element);
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
|
@ -1,4 +1,4 @@
|
||||||
package org.schabi.newpipe.player.event
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
enum class DisplayPortion {
|
enum class DisplayPortion {
|
||||||
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF
|
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
|
interface DoubleTapListener {
|
||||||
|
fun onDoubleTapStarted(portion: DisplayPortion)
|
||||||
|
fun onDoubleTapProgressDown(portion: DisplayPortion)
|
||||||
|
fun onDoubleTapFinished()
|
||||||
|
}
|
|
@ -0,0 +1,234 @@
|
||||||
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.OnTouchListener
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import org.schabi.newpipe.MainActivity
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ktx.AnimationType
|
||||||
|
import org.schabi.newpipe.ktx.animate
|
||||||
|
import org.schabi.newpipe.player.Player
|
||||||
|
import org.schabi.newpipe.player.helper.AudioReactor
|
||||||
|
import org.schabi.newpipe.player.helper.PlayerHelper
|
||||||
|
import org.schabi.newpipe.player.ui.MainPlayerUi
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GestureListener for the player
|
||||||
|
*
|
||||||
|
* While [BasePlayerGestureListener] contains the logic behind the single gestures
|
||||||
|
* this class focuses on the visual aspect like hiding and showing the controls or changing
|
||||||
|
* volume/brightness during scrolling for specific events.
|
||||||
|
*/
|
||||||
|
class MainPlayerGestureListener(
|
||||||
|
private val playerUi: MainPlayerUi
|
||||||
|
) : BasePlayerGestureListener(playerUi), OnTouchListener {
|
||||||
|
private var isMoving = false
|
||||||
|
|
||||||
|
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||||
|
super.onTouch(v, event)
|
||||||
|
if (event.action == MotionEvent.ACTION_UP && isMoving) {
|
||||||
|
isMoving = false
|
||||||
|
onScrollEnd(event)
|
||||||
|
}
|
||||||
|
return when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
||||||
|
v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
v.parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
|
||||||
|
|
||||||
|
if (isDoubleTapping)
|
||||||
|
return true
|
||||||
|
super.onSingleTapConfirmed(e)
|
||||||
|
|
||||||
|
if (player.currentState != Player.STATE_BLOCKED)
|
||||||
|
onSingleTap()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onScrollVolume(distanceY: Float) {
|
||||||
|
val bar: ProgressBar = binding.volumeProgressBar
|
||||||
|
val audioReactor: AudioReactor = player.audioReactor
|
||||||
|
|
||||||
|
// If we just started sliding, change the progress bar to match the system volume
|
||||||
|
if (!binding.volumeRelativeLayout.isVisible) {
|
||||||
|
val volumePercent: Float = audioReactor.volume / audioReactor.maxVolume.toFloat()
|
||||||
|
bar.progress = (volumePercent * bar.max).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
|
||||||
|
|
||||||
|
// Update volume
|
||||||
|
val currentProgressPercent: Float = bar.progress / bar.max.toFloat()
|
||||||
|
val currentVolume = (audioReactor.maxVolume * currentProgressPercent).toInt()
|
||||||
|
audioReactor.volume = currentVolume
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player center image
|
||||||
|
binding.volumeImageView.setImageDrawable(
|
||||||
|
AppCompatResources.getDrawable(
|
||||||
|
player.context,
|
||||||
|
when {
|
||||||
|
currentProgressPercent <= 0 -> R.drawable.ic_volume_off
|
||||||
|
currentProgressPercent < 0.25 -> R.drawable.ic_volume_mute
|
||||||
|
currentProgressPercent < 0.75 -> R.drawable.ic_volume_down
|
||||||
|
else -> R.drawable.ic_volume_up
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make sure the correct layout is visible
|
||||||
|
if (!binding.volumeRelativeLayout.isVisible) {
|
||||||
|
binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
|
||||||
|
}
|
||||||
|
binding.brightnessRelativeLayout.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onScrollBrightness(distanceY: Float) {
|
||||||
|
val parent: AppCompatActivity = playerUi.parentActivity.orElse(null) ?: return
|
||||||
|
val window = parent.window
|
||||||
|
val layoutParams = window.attributes
|
||||||
|
val bar: ProgressBar = binding.brightnessProgressBar
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
val oldBrightness = layoutParams.screenBrightness
|
||||||
|
bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt()
|
||||||
|
bar.incrementProgressBy(distanceY.toInt())
|
||||||
|
|
||||||
|
// Update brightness
|
||||||
|
val currentProgressPercent = bar.progress.toFloat() / bar.max
|
||||||
|
layoutParams.screenBrightness = currentProgressPercent
|
||||||
|
window.attributes = layoutParams
|
||||||
|
|
||||||
|
// Save current brightness level
|
||||||
|
PlayerHelper.setScreenBrightness(parent, currentProgressPercent)
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"onScroll().brightnessControl, " +
|
||||||
|
"currentBrightness = " + currentProgressPercent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player center image
|
||||||
|
binding.brightnessImageView.setImageDrawable(
|
||||||
|
AppCompatResources.getDrawable(
|
||||||
|
player.context,
|
||||||
|
when {
|
||||||
|
currentProgressPercent < 0.25 -> R.drawable.ic_brightness_low
|
||||||
|
currentProgressPercent < 0.75 -> R.drawable.ic_brightness_medium
|
||||||
|
else -> R.drawable.ic_brightness_high
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make sure the correct layout is visible
|
||||||
|
if (!binding.brightnessRelativeLayout.isVisible) {
|
||||||
|
binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
|
||||||
|
}
|
||||||
|
binding.volumeRelativeLayout.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrollEnd(event: MotionEvent) {
|
||||||
|
super.onScrollEnd(event)
|
||||||
|
if (binding.volumeRelativeLayout.isVisible) {
|
||||||
|
binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
|
||||||
|
}
|
||||||
|
if (binding.brightnessRelativeLayout.isVisible) {
|
||||||
|
binding.brightnessRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScroll(
|
||||||
|
initialEvent: MotionEvent,
|
||||||
|
movingEvent: MotionEvent,
|
||||||
|
distanceX: Float,
|
||||||
|
distanceY: Float
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
if (!playerUi.isFullscreen) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate heights of status and navigation bars
|
||||||
|
val statusBarHeight = getAndroidDimenPx(player.context, "status_bar_height")
|
||||||
|
val navigationBarHeight = getAndroidDimenPx(player.context, "navigation_bar_height")
|
||||||
|
|
||||||
|
// Do not handle this event if initially it started from status or navigation bars
|
||||||
|
val isTouchingStatusBar = initialEvent.y < statusBarHeight
|
||||||
|
val isTouchingNavigationBar = initialEvent.y > (binding.root.height - navigationBarHeight)
|
||||||
|
if (isTouchingStatusBar || isTouchingNavigationBar) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
|
||||||
|
if (
|
||||||
|
!isMoving && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
|
||||||
|
player.currentState == Player.STATE_COMPLETED
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
isMoving = true
|
||||||
|
|
||||||
|
// -- Brightness and Volume control --
|
||||||
|
val isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(player.context)
|
||||||
|
val isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(player.context)
|
||||||
|
if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
|
||||||
|
if (getDisplayHalfPortion(initialEvent) === DisplayPortion.LEFT_HALF) {
|
||||||
|
onScrollBrightness(distanceY)
|
||||||
|
} else /* DisplayPortion.RIGHT_HALF */ {
|
||||||
|
onScrollVolume(distanceY)
|
||||||
|
}
|
||||||
|
} else if (isBrightnessGestureEnabled) {
|
||||||
|
onScrollBrightness(distanceY)
|
||||||
|
} else if (isVolumeGestureEnabled) {
|
||||||
|
onScrollVolume(distanceY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
|
||||||
|
return when {
|
||||||
|
e.x < binding.root.width / 3.0 -> DisplayPortion.LEFT
|
||||||
|
e.x > binding.root.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
|
||||||
|
else -> DisplayPortion.MIDDLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
|
||||||
|
return when {
|
||||||
|
e.x < binding.root.width / 2.0 -> DisplayPortion.LEFT_HALF
|
||||||
|
else -> DisplayPortion.RIGHT_HALF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = MainPlayerGestureListener::class.java.simpleName
|
||||||
|
private val DEBUG = MainActivity.DEBUG
|
||||||
|
private const val MOVEMENT_THRESHOLD = 40
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,283 @@
|
||||||
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewConfiguration
|
||||||
|
import androidx.core.math.MathUtils
|
||||||
|
import org.schabi.newpipe.MainActivity
|
||||||
|
import org.schabi.newpipe.ktx.AnimationType
|
||||||
|
import org.schabi.newpipe.ktx.animate
|
||||||
|
import org.schabi.newpipe.player.ui.PopupPlayerUi
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.hypot
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class PopupPlayerGestureListener(
|
||||||
|
private val playerUi: PopupPlayerUi,
|
||||||
|
) : BasePlayerGestureListener(playerUi) {
|
||||||
|
|
||||||
|
private var isMoving = false
|
||||||
|
|
||||||
|
private var initialPopupX: Int = -1
|
||||||
|
private var initialPopupY: Int = -1
|
||||||
|
private var isResizing = false
|
||||||
|
|
||||||
|
// initial coordinates and distance between fingers
|
||||||
|
private var initPointerDistance = -1.0
|
||||||
|
private var initFirstPointerX = -1f
|
||||||
|
private var initFirstPointerY = -1f
|
||||||
|
private var initSecPointerX = -1f
|
||||||
|
private var initSecPointerY = -1f
|
||||||
|
|
||||||
|
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||||
|
super.onTouch(v, event)
|
||||||
|
if (event.pointerCount == 2 && !isMoving && !isResizing) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
|
||||||
|
}
|
||||||
|
onPopupResizingStart()
|
||||||
|
|
||||||
|
// record coordinates of fingers
|
||||||
|
initFirstPointerX = event.getX(0)
|
||||||
|
initFirstPointerY = event.getY(0)
|
||||||
|
initSecPointerX = event.getX(1)
|
||||||
|
initSecPointerY = event.getY(1)
|
||||||
|
// record distance between fingers
|
||||||
|
initPointerDistance = hypot(
|
||||||
|
initFirstPointerX - initSecPointerX.toDouble(),
|
||||||
|
initFirstPointerY - initSecPointerY.toDouble()
|
||||||
|
)
|
||||||
|
|
||||||
|
isResizing = true
|
||||||
|
}
|
||||||
|
if (event.action == MotionEvent.ACTION_MOVE && !isMoving && isResizing) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
|
||||||
|
"[${event.rawX}, ${event.rawY}]"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return handleMultiDrag(event)
|
||||||
|
}
|
||||||
|
if (event.action == MotionEvent.ACTION_UP) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
|
||||||
|
" [${event.rawX}, ${event.rawY}]"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isMoving) {
|
||||||
|
isMoving = false
|
||||||
|
onScrollEnd(event)
|
||||||
|
}
|
||||||
|
if (isResizing) {
|
||||||
|
isResizing = false
|
||||||
|
|
||||||
|
initPointerDistance = (-1).toDouble()
|
||||||
|
initFirstPointerX = (-1).toFloat()
|
||||||
|
initFirstPointerY = (-1).toFloat()
|
||||||
|
initSecPointerX = (-1).toFloat()
|
||||||
|
initSecPointerY = (-1).toFloat()
|
||||||
|
|
||||||
|
onPopupResizingEnd()
|
||||||
|
player.changeState(player.currentState)
|
||||||
|
}
|
||||||
|
if (!playerUi.isPopupClosing) {
|
||||||
|
playerUi.savePopupPositionAndSizeToPrefs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.performClick()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrollEnd(event: MotionEvent) {
|
||||||
|
super.onScrollEnd(event)
|
||||||
|
if (playerUi.isInsideClosingRadius(event)) {
|
||||||
|
playerUi.closePopup()
|
||||||
|
} else if (!playerUi.isPopupClosing) {
|
||||||
|
playerUi.closeOverlayBinding.closeButton.animate(false, 200)
|
||||||
|
binding.closingOverlay.animate(false, 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMultiDrag(event: MotionEvent): Boolean {
|
||||||
|
if (initPointerDistance == -1.0 || event.pointerCount != 2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the movements of the fingers
|
||||||
|
val firstPointerMove = hypot(
|
||||||
|
event.getX(0) - initFirstPointerX.toDouble(),
|
||||||
|
event.getY(0) - initFirstPointerY.toDouble()
|
||||||
|
)
|
||||||
|
val secPointerMove = hypot(
|
||||||
|
event.getX(1) - initSecPointerX.toDouble(),
|
||||||
|
event.getY(1) - initSecPointerY.toDouble()
|
||||||
|
)
|
||||||
|
|
||||||
|
// minimum threshold beyond which pinch gesture will work
|
||||||
|
val minimumMove = ViewConfiguration.get(player.context).scaledTouchSlop
|
||||||
|
if (max(firstPointerMove, secPointerMove) <= minimumMove) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate current distance between the pointers
|
||||||
|
val currentPointerDistance = hypot(
|
||||||
|
event.getX(0) - event.getX(1).toDouble(),
|
||||||
|
event.getY(0) - event.getY(1).toDouble()
|
||||||
|
)
|
||||||
|
|
||||||
|
val popupWidth = playerUi.popupLayoutParams.width.toDouble()
|
||||||
|
// change co-ordinates of popup so the center stays at the same position
|
||||||
|
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
|
||||||
|
initPointerDistance = currentPointerDistance
|
||||||
|
playerUi.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt()
|
||||||
|
|
||||||
|
playerUi.checkPopupPositionBounds()
|
||||||
|
playerUi.updateScreenSize()
|
||||||
|
playerUi.changePopupSize(min(playerUi.screenWidth.toDouble(), newWidth).toInt())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPopupResizingStart() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onPopupResizingStart called")
|
||||||
|
}
|
||||||
|
binding.loadingPanel.visibility = View.GONE
|
||||||
|
playerUi.hideControls(0, 0)
|
||||||
|
binding.fastSeekOverlay.animate(false, 0)
|
||||||
|
binding.currentDisplaySeek.animate(false, 0, AnimationType.ALPHA, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPopupResizingEnd() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onPopupResizingEnd called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongPress(e: MotionEvent?) {
|
||||||
|
playerUi.updateScreenSize()
|
||||||
|
playerUi.checkPopupPositionBounds()
|
||||||
|
playerUi.changePopupSize(playerUi.screenWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFling(
|
||||||
|
e1: MotionEvent?,
|
||||||
|
e2: MotionEvent?,
|
||||||
|
velocityX: Float,
|
||||||
|
velocityY: Float
|
||||||
|
): Boolean {
|
||||||
|
return if (player.popupPlayerSelected()) {
|
||||||
|
val absVelocityX = abs(velocityX)
|
||||||
|
val absVelocityY = abs(velocityY)
|
||||||
|
if (absVelocityX.coerceAtLeast(absVelocityY) > TOSS_FLING_VELOCITY) {
|
||||||
|
if (absVelocityX > TOSS_FLING_VELOCITY) {
|
||||||
|
playerUi.popupLayoutParams.x = velocityX.toInt()
|
||||||
|
}
|
||||||
|
if (absVelocityY > TOSS_FLING_VELOCITY) {
|
||||||
|
playerUi.popupLayoutParams.y = velocityY.toInt()
|
||||||
|
}
|
||||||
|
playerUi.checkPopupPositionBounds()
|
||||||
|
playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
|
||||||
|
// Fix popup position when the user touch it, it may have the wrong one
|
||||||
|
// because the soft input is visible (the draggable area is currently resized).
|
||||||
|
playerUi.updateScreenSize()
|
||||||
|
playerUi.checkPopupPositionBounds()
|
||||||
|
playerUi.popupLayoutParams.let {
|
||||||
|
initialPopupX = it.x
|
||||||
|
initialPopupY = it.y
|
||||||
|
}
|
||||||
|
return true // we want `super.onDown(e)` to be called
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
|
||||||
|
|
||||||
|
if (isDoubleTapping)
|
||||||
|
return true
|
||||||
|
if (player.exoPlayerIsNull())
|
||||||
|
return false
|
||||||
|
|
||||||
|
onSingleTap()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScroll(
|
||||||
|
initialEvent: MotionEvent,
|
||||||
|
movingEvent: MotionEvent,
|
||||||
|
distanceX: Float,
|
||||||
|
distanceY: Float
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
if (isResizing) {
|
||||||
|
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMoving) {
|
||||||
|
playerUi.closeOverlayBinding.closeButton.animate(true, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
isMoving = true
|
||||||
|
|
||||||
|
val diffX = (movingEvent.rawX - initialEvent.rawX)
|
||||||
|
val posX = MathUtils.clamp(
|
||||||
|
initialPopupX + diffX,
|
||||||
|
0f, (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
|
||||||
|
)
|
||||||
|
val diffY = (movingEvent.rawY - initialEvent.rawY)
|
||||||
|
val posY = MathUtils.clamp(
|
||||||
|
initialPopupY + diffY,
|
||||||
|
0f, (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
|
||||||
|
)
|
||||||
|
|
||||||
|
playerUi.popupLayoutParams.x = posX.toInt()
|
||||||
|
playerUi.popupLayoutParams.y = posY.toInt()
|
||||||
|
|
||||||
|
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
|
||||||
|
val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent)
|
||||||
|
// Check if an view is in expected state and if not animate it into the correct state
|
||||||
|
val expectedVisibility = if (showClosingOverlayView) View.VISIBLE else View.GONE
|
||||||
|
if (binding.closingOverlay.visibility != expectedVisibility) {
|
||||||
|
binding.closingOverlay.animate(showClosingOverlayView, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
|
||||||
|
return when {
|
||||||
|
e.x < playerUi.popupLayoutParams.width / 3.0 -> DisplayPortion.LEFT
|
||||||
|
e.x > playerUi.popupLayoutParams.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
|
||||||
|
else -> DisplayPortion.MIDDLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
|
||||||
|
return when {
|
||||||
|
e.x < playerUi.popupLayoutParams.width / 2.0 -> DisplayPortion.LEFT_HALF
|
||||||
|
else -> DisplayPortion.RIGHT_HALF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PopupPlayerGestureListener::class.java.simpleName
|
||||||
|
private val DEBUG = MainActivity.DEBUG
|
||||||
|
private const val TOSS_FLING_VELOCITY = 2500
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,8 +39,8 @@ final class CacheFactory implements DataSource.Factory {
|
||||||
.createDataSource();
|
.createDataSource();
|
||||||
|
|
||||||
final FileDataSource fileSource = new FileDataSource();
|
final FileDataSource fileSource = new FileDataSource();
|
||||||
final CacheDataSink dataSink
|
final CacheDataSink dataSink =
|
||||||
= new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
|
new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
|
||||||
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
|
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,226 +0,0 @@
|
||||||
package org.schabi.newpipe.player.helper;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.support.v4.media.MediaMetadataCompat;
|
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
|
||||||
import android.support.v4.media.session.PlaybackStateCompat;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.media.session.MediaButtonReceiver;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ForwardingPlayer;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
|
||||||
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class MediaSessionManager {
|
|
||||||
private static final String TAG = MediaSessionManager.class.getSimpleName();
|
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final MediaSessionCompat mediaSession;
|
|
||||||
@NonNull
|
|
||||||
private final MediaSessionConnector sessionConnector;
|
|
||||||
|
|
||||||
private int lastTitleHashCode;
|
|
||||||
private int lastArtistHashCode;
|
|
||||||
private long lastDuration;
|
|
||||||
private int lastAlbumArtHashCode;
|
|
||||||
|
|
||||||
public MediaSessionManager(@NonNull final Context context,
|
|
||||||
@NonNull final Player player,
|
|
||||||
@NonNull final MediaSessionCallback callback) {
|
|
||||||
mediaSession = new MediaSessionCompat(context, TAG);
|
|
||||||
mediaSession.setActive(true);
|
|
||||||
|
|
||||||
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
|
|
||||||
.setState(PlaybackStateCompat.STATE_NONE, -1, 1)
|
|
||||||
.setActions(PlaybackStateCompat.ACTION_SEEK_TO
|
|
||||||
| PlaybackStateCompat.ACTION_PLAY
|
|
||||||
| PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause
|
|
||||||
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
|
||||||
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
|
||||||
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
|
|
||||||
| PlaybackStateCompat.ACTION_STOP)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
sessionConnector = new MediaSessionConnector(mediaSession);
|
|
||||||
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
|
|
||||||
sessionConnector.setPlayer(new ForwardingPlayer(player) {
|
|
||||||
@Override
|
|
||||||
public void play() {
|
|
||||||
callback.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pause() {
|
|
||||||
callback.pause();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@SuppressWarnings("UnusedReturnValue")
|
|
||||||
public KeyEvent handleMediaButtonIntent(final Intent intent) {
|
|
||||||
return MediaButtonReceiver.handleIntent(mediaSession, intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MediaSessionCompat.Token getSessionToken() {
|
|
||||||
return mediaSession.getSessionToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sets the Metadata - if required.
|
|
||||||
*
|
|
||||||
* @param title {@link MediaMetadataCompat#METADATA_KEY_TITLE}
|
|
||||||
* @param artist {@link MediaMetadataCompat#METADATA_KEY_ARTIST}
|
|
||||||
* @param optAlbumArt {@link MediaMetadataCompat#METADATA_KEY_ALBUM_ART}
|
|
||||||
* @param duration {@link MediaMetadataCompat#METADATA_KEY_DURATION}
|
|
||||||
* - should be a negative value for unknown durations, e.g. for livestreams
|
|
||||||
*/
|
|
||||||
public void setMetadata(@NonNull final String title,
|
|
||||||
@NonNull final String artist,
|
|
||||||
@NonNull final Optional<Bitmap> optAlbumArt,
|
|
||||||
final long duration
|
|
||||||
) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "setMetadata called:"
|
|
||||||
+ " t: " + title
|
|
||||||
+ " a: " + artist
|
|
||||||
+ " thumb: " + (
|
|
||||||
optAlbumArt.isPresent()
|
|
||||||
? optAlbumArt.get().hashCode()
|
|
||||||
: "<none>")
|
|
||||||
+ " d: " + duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mediaSession.isActive()) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "setMetadata: mediaSession not active - exiting");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkIfMetadataShouldBeSet(title, artist, optAlbumArt, duration)) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "setMetadata: No update required - exiting");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "setMetadata: N_Metadata update:"
|
|
||||||
+ " t: " + title
|
|
||||||
+ " a: " + artist
|
|
||||||
+ " thumb: " + (
|
|
||||||
optAlbumArt.isPresent()
|
|
||||||
? optAlbumArt.get().hashCode()
|
|
||||||
: "<none>")
|
|
||||||
+ " d: " + duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
|
|
||||||
|
|
||||||
if (optAlbumArt.isPresent()) {
|
|
||||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, optAlbumArt.get());
|
|
||||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, optAlbumArt.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSession.setMetadata(builder.build());
|
|
||||||
|
|
||||||
lastTitleHashCode = title.hashCode();
|
|
||||||
lastArtistHashCode = artist.hashCode();
|
|
||||||
lastDuration = duration;
|
|
||||||
optAlbumArt.ifPresent(bitmap -> lastAlbumArtHashCode = bitmap.hashCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkIfMetadataShouldBeSet(
|
|
||||||
@NonNull final String title,
|
|
||||||
@NonNull final String artist,
|
|
||||||
@NonNull final Optional<Bitmap> optAlbumArt,
|
|
||||||
final long duration
|
|
||||||
) {
|
|
||||||
// Check if the values have changed since the last time
|
|
||||||
if (title.hashCode() != lastTitleHashCode
|
|
||||||
|| artist.hashCode() != lastArtistHashCode
|
|
||||||
|| duration != lastDuration
|
|
||||||
|| (optAlbumArt.isPresent() && optAlbumArt.get().hashCode() != lastAlbumArtHashCode)
|
|
||||||
) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG,
|
|
||||||
"checkIfMetadataShouldBeSet: true - reason: changed values since last");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the currently set metadata is valid
|
|
||||||
if (getMetadataTitle() == null
|
|
||||||
|| getMetadataArtist() == null
|
|
||||||
// Note that the duration can be <= 0 for live streams
|
|
||||||
) {
|
|
||||||
if (DEBUG) {
|
|
||||||
if (getMetadataTitle() == null) {
|
|
||||||
Log.d(TAG,
|
|
||||||
"N_getMetadataTitle: title == null");
|
|
||||||
} else if (getMetadataArtist() == null) {
|
|
||||||
Log.d(TAG,
|
|
||||||
"N_getMetadataArtist: artist == null");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got an album art check if the current set AlbumArt is null
|
|
||||||
if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default - no update required
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Bitmap getMetadataAlbumArt() {
|
|
||||||
return mediaSession.getController().getMetadata()
|
|
||||||
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String getMetadataTitle() {
|
|
||||||
return mediaSession.getController().getMetadata()
|
|
||||||
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String getMetadataArtist() {
|
|
||||||
return mediaSession.getController().getMetadata()
|
|
||||||
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should be called on player destruction to prevent leakage.
|
|
||||||
*/
|
|
||||||
public void dispose() {
|
|
||||||
sessionConnector.setPlayer(null);
|
|
||||||
sessionConnector.setQueueNavigator(null);
|
|
||||||
mediaSession.setActive(false);
|
|
||||||
mediaSession.release();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,7 +11,6 @@ import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.LayerDrawable;
|
import android.graphics.drawable.LayerDrawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
|
@ -21,16 +20,16 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.core.math.MathUtils;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
|
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||||
import org.schabi.newpipe.util.SliderStrategy;
|
import org.schabi.newpipe.util.SliderStrategy;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
@ -149,7 +148,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
assureCorrectAppLanguage(getContext());
|
assureCorrectAppLanguage(getContext());
|
||||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||||
|
|
||||||
binding = DialogPlaybackParameterBinding.inflate(LayoutInflater.from(getContext()));
|
binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
|
||||||
initUI();
|
initUI();
|
||||||
|
|
||||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
|
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
|
||||||
|
@ -207,7 +206,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
? View.VISIBLE
|
? View.VISIBLE
|
||||||
: View.GONE);
|
: View.GONE);
|
||||||
animateRotation(binding.pitchToogleControlModes,
|
animateRotation(binding.pitchToogleControlModes,
|
||||||
Player.DEFAULT_CONTROLS_DURATION,
|
VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
|
||||||
isCurrentlyVisible ? 180 : 0);
|
isCurrentlyVisible ? 180 : 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -334,10 +333,8 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Boolean, TextView> getPitchControlModeComponentMappings() {
|
private Map<Boolean, TextView> getPitchControlModeComponentMappings() {
|
||||||
final Map<Boolean, TextView> mappings = new HashMap<>();
|
return Map.of(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent,
|
||||||
mappings.put(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent);
|
PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
|
||||||
mappings.put(PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
|
|
||||||
return mappings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changePitchControlMode(final boolean semitones) {
|
private void changePitchControlMode(final boolean semitones) {
|
||||||
|
@ -407,13 +404,11 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Double, TextView> getStepSizeComponentMappings() {
|
private Map<Double, TextView> getStepSizeComponentMappings() {
|
||||||
final Map<Double, TextView> mappings = new HashMap<>();
|
return Map.of(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent,
|
||||||
mappings.put(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent);
|
STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent,
|
||||||
mappings.put(STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent);
|
STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent,
|
||||||
mappings.put(STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent);
|
STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent,
|
||||||
mappings.put(STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent);
|
STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
|
||||||
mappings.put(STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
|
|
||||||
return mappings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setStepSizeToUI(final double newStepSize) {
|
private void setStepSizeToUI(final double newStepSize) {
|
||||||
|
@ -532,7 +527,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAndUpdateTempo(final double newTempo) {
|
private void setAndUpdateTempo(final double newTempo) {
|
||||||
this.tempo = calcValidTempo(newTempo);
|
this.tempo = MathUtils.clamp(newTempo, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED);
|
||||||
|
|
||||||
binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo));
|
binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo));
|
||||||
setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo);
|
setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo);
|
||||||
|
@ -551,13 +546,8 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
pitchPercent);
|
pitchPercent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calcValidTempo(final double newTempo) {
|
|
||||||
return Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newTempo));
|
|
||||||
}
|
|
||||||
|
|
||||||
private double calcValidPitch(final double newPitch) {
|
private double calcValidPitch(final double newPitch) {
|
||||||
final double calcPitch =
|
final double calcPitch = MathUtils.clamp(newPitch, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED);
|
||||||
Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newPitch));
|
|
||||||
|
|
||||||
if (!isCurrentPitchControlModeSemitone()) {
|
if (!isCurrentPitchControlModeSemitone()) {
|
||||||
return calcPitch;
|
return calcPitch;
|
||||||
|
|
|
@ -208,8 +208,8 @@ public class PlayerDataSource {
|
||||||
Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir");
|
Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
final LeastRecentlyUsedCacheEvictor evictor
|
final LeastRecentlyUsedCacheEvictor evictor =
|
||||||
= new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
|
new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
|
||||||
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
|
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ package org.schabi.newpipe.player.helper;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||||
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
|
|
||||||
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
||||||
|
@ -15,14 +13,8 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.PixelFormat;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.view.accessibility.CaptioningManager;
|
import android.view.accessibility.CaptioningManager;
|
||||||
|
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
|
@ -49,7 +41,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
@ -71,25 +62,11 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public final class PlayerHelper {
|
public final class PlayerHelper {
|
||||||
private static final StringBuilder STRING_BUILDER = new StringBuilder();
|
private static final StringBuilder STRING_BUILDER = new StringBuilder();
|
||||||
private static final Formatter STRING_FORMATTER
|
private static final Formatter STRING_FORMATTER =
|
||||||
= new Formatter(STRING_BUILDER, Locale.getDefault());
|
new Formatter(STRING_BUILDER, Locale.getDefault());
|
||||||
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
|
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
|
||||||
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
|
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
|
|
||||||
* NewPipe's popup player.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* This value is hardcoded instead of being get dynamically with the method linked of the
|
|
||||||
* constant documentation below, because it is not static and popup player layout parameters
|
|
||||||
* are generated with static methods.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
|
|
||||||
*/
|
|
||||||
private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
|
|
||||||
|
|
||||||
@Retention(SOURCE)
|
@Retention(SOURCE)
|
||||||
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
|
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
|
||||||
AUTOPLAY_TYPE_NEVER})
|
AUTOPLAY_TYPE_NEVER})
|
||||||
|
@ -339,10 +316,6 @@ public final class PlayerHelper {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getTossFlingVelocity() {
|
|
||||||
return 2500;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
|
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
|
||||||
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
|
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
|
||||||
|
@ -452,12 +425,6 @@ public final class PlayerHelper {
|
||||||
// Utils used by player
|
// Utils used by player
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) {
|
|
||||||
// If you want to open popup from the app just include Constants.POPUP_ONLY into an extra
|
|
||||||
return MainPlayer.PlayerType.values()[
|
|
||||||
intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isPlaybackResumeEnabled(final Player player) {
|
public static boolean isPlaybackResumeEnabled(final Player player) {
|
||||||
return player.getPrefs().getBoolean(
|
return player.getPrefs().getBoolean(
|
||||||
player.getContext().getString(R.string.enable_watch_history_key), true)
|
player.getContext().getString(R.string.enable_watch_history_key), true)
|
||||||
|
@ -528,90 +495,10 @@ public final class PlayerHelper {
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param player {@code screenWidth} and {@code screenHeight} must have been initialized
|
|
||||||
* @return the popup starting layout params
|
|
||||||
*/
|
|
||||||
@SuppressLint("RtlHardcoded")
|
|
||||||
public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
|
|
||||||
final Player player) {
|
|
||||||
final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean(
|
|
||||||
player.getContext().getString(R.string.popup_remember_size_pos_key), true);
|
|
||||||
final float defaultSize =
|
|
||||||
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
|
|
||||||
final float popupWidth = popupRememberSizeAndPos
|
|
||||||
? player.getPrefs().getFloat(player.getContext().getString(
|
|
||||||
R.string.popup_saved_width_key), defaultSize)
|
|
||||||
: defaultSize;
|
|
||||||
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
|
||||||
|
|
||||||
final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams(
|
|
||||||
(int) popupWidth, (int) popupHeight,
|
|
||||||
popupLayoutParamType(),
|
|
||||||
IDLE_WINDOW_FLAGS,
|
|
||||||
PixelFormat.TRANSLUCENT);
|
|
||||||
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
|
||||||
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
|
||||||
|
|
||||||
final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f);
|
|
||||||
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
|
|
||||||
popupLayoutParams.x = popupRememberSizeAndPos
|
|
||||||
? player.getPrefs().getInt(player.getContext().getString(
|
|
||||||
R.string.popup_saved_x_key), centerX) : centerX;
|
|
||||||
popupLayoutParams.y = popupRememberSizeAndPos
|
|
||||||
? player.getPrefs().getInt(player.getContext().getString(
|
|
||||||
R.string.popup_saved_y_key), centerY) : centerY;
|
|
||||||
|
|
||||||
return popupLayoutParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void savePopupPositionAndSizeToPrefs(final Player player) {
|
|
||||||
if (player.getPopupLayoutParams() != null) {
|
|
||||||
player.getPrefs().edit()
|
|
||||||
.putFloat(player.getContext().getString(R.string.popup_saved_width_key),
|
|
||||||
player.getPopupLayoutParams().width)
|
|
||||||
.putInt(player.getContext().getString(R.string.popup_saved_x_key),
|
|
||||||
player.getPopupLayoutParams().x)
|
|
||||||
.putInt(player.getContext().getString(R.string.popup_saved_y_key),
|
|
||||||
player.getPopupLayoutParams().y)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static float getMinimumVideoHeight(final float width) {
|
public static float getMinimumVideoHeight(final float width) {
|
||||||
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
|
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RtlHardcoded")
|
|
||||||
public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
|
|
||||||
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
||||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
|
||||||
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
|
||||||
|
|
||||||
final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
popupLayoutParamType(),
|
|
||||||
flags,
|
|
||||||
PixelFormat.TRANSLUCENT);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
// Setting maximum opacity allowed for touch events to other apps for Android 12 and
|
|
||||||
// higher to prevent non interaction when using other apps with the popup player
|
|
||||||
closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
|
||||||
closeOverlayLayoutParams.softInputMode =
|
|
||||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
|
||||||
return closeOverlayLayoutParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int popupLayoutParamType() {
|
|
||||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
|
||||||
? WindowManager.LayoutParams.TYPE_PHONE
|
|
||||||
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int retrieveSeekDurationFromPreferences(final Player player) {
|
public static int retrieveSeekDurationFromPreferences(final Player player) {
|
||||||
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
|
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
|
||||||
player.getContext().getString(R.string.seek_duration_key),
|
player.getContext().getString(R.string.seek_duration_key),
|
||||||
|
|
|
@ -16,8 +16,9 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import org.schabi.newpipe.App;
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.player.MainPlayer;
|
import org.schabi.newpipe.player.PlayerService;
|
||||||
import org.schabi.newpipe.player.Player;
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
@ -42,17 +43,17 @@ public final class PlayerHolder {
|
||||||
|
|
||||||
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
|
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
|
||||||
private boolean bound;
|
private boolean bound;
|
||||||
@Nullable private MainPlayer playerService;
|
@Nullable private PlayerService playerService;
|
||||||
@Nullable private Player player;
|
@Nullable private Player player;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
|
* Returns the current {@link PlayerType} of the {@link PlayerService} service,
|
||||||
* otherwise `null` if no service running.
|
* otherwise `null` if no service is running.
|
||||||
*
|
*
|
||||||
* @return Current PlayerType
|
* @return Current PlayerType
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public MainPlayer.PlayerType getType() {
|
public PlayerType getType() {
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -122,7 +123,7 @@ public final class PlayerHolder {
|
||||||
// and NullPointerExceptions inside the service because the service will be
|
// and NullPointerExceptions inside the service because the service will be
|
||||||
// bound twice. Prevent it with unbinding first
|
// bound twice. Prevent it with unbinding first
|
||||||
unbind(context);
|
unbind(context);
|
||||||
ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class));
|
ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
|
||||||
serviceConnection.doPlayAfterConnect(playAfterConnect);
|
serviceConnection.doPlayAfterConnect(playAfterConnect);
|
||||||
bind(context);
|
bind(context);
|
||||||
}
|
}
|
||||||
|
@ -130,7 +131,7 @@ public final class PlayerHolder {
|
||||||
public void stopService() {
|
public void stopService() {
|
||||||
final Context context = getCommonContext();
|
final Context context = getCommonContext();
|
||||||
unbind(context);
|
unbind(context);
|
||||||
context.stopService(new Intent(context, MainPlayer.class));
|
context.stopService(new Intent(context, PlayerService.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayerServiceConnection implements ServiceConnection {
|
class PlayerServiceConnection implements ServiceConnection {
|
||||||
|
@ -156,7 +157,7 @@ public final class PlayerHolder {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Player service is connected");
|
Log.d(TAG, "Player service is connected");
|
||||||
}
|
}
|
||||||
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
|
final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
|
||||||
|
|
||||||
playerService = localBinder.getService();
|
playerService = localBinder.getService();
|
||||||
player = localBinder.getPlayer();
|
player = localBinder.getPlayer();
|
||||||
|
@ -172,7 +173,7 @@ public final class PlayerHolder {
|
||||||
Log.d(TAG, "bind() called");
|
Log.d(TAG, "bind() called");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Intent serviceIntent = new Intent(context, MainPlayer.class);
|
final Intent serviceIntent = new Intent(context, PlayerService.class);
|
||||||
bound = context.bindService(serviceIntent, serviceConnection,
|
bound = context.bindService(serviceIntent, serviceConnection,
|
||||||
Context.BIND_AUTO_CREATE);
|
Context.BIND_AUTO_CREATE);
|
||||||
if (!bound) {
|
if (!bound) {
|
||||||
|
@ -211,6 +212,13 @@ public final class PlayerHolder {
|
||||||
|
|
||||||
private final PlayerServiceEventListener internalListener =
|
private final PlayerServiceEventListener internalListener =
|
||||||
new PlayerServiceEventListener() {
|
new PlayerServiceEventListener() {
|
||||||
|
@Override
|
||||||
|
public void onViewCreated() {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onViewCreated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.schabi.newpipe.player.helper;
|
package org.schabi.newpipe.player.helper;
|
||||||
|
|
||||||
|
import androidx.core.math.MathUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts between percent and 12-tone equal temperament semitones.
|
* Converts between percent and 12-tone equal temperament semitones.
|
||||||
* <br/>
|
* <br/>
|
||||||
|
@ -33,6 +35,6 @@ public final class PlayerSemitoneHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ensureSemitonesInRange(final int semitones) {
|
private static int ensureSemitonesInRange(final int semitones) {
|
||||||
return Math.max(-SEMITONE_COUNT, Math.min(SEMITONE_COUNT, semitones));
|
return MathUtils.clamp(semitones, -SEMITONE_COUNT, SEMITONE_COUNT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
package org.schabi.newpipe.player.listeners.view
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import org.schabi.newpipe.MainActivity
|
|
||||||
import org.schabi.newpipe.player.Player
|
|
||||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click listener for the playbackSpeed textview of the player
|
|
||||||
*/
|
|
||||||
class PlaybackSpeedClickListener(
|
|
||||||
private val player: Player,
|
|
||||||
private val playbackSpeedPopupMenu: PopupMenu
|
|
||||||
) : View.OnClickListener {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG: String = "PlaybSpeedClickListener"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
if (MainActivity.DEBUG) {
|
|
||||||
Log.d(TAG, "onPlaybackSpeedClicked() called")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (player.videoPlayerSelected()) {
|
|
||||||
PlaybackParameterDialog.newInstance(
|
|
||||||
player.playbackSpeed.toDouble(),
|
|
||||||
player.playbackPitch.toDouble(),
|
|
||||||
player.playbackSkipSilence
|
|
||||||
) { speed: Float, pitch: Float, skipSilence: Boolean ->
|
|
||||||
player.setPlaybackParameters(
|
|
||||||
speed,
|
|
||||||
pitch,
|
|
||||||
skipSilence
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.show(player.parentActivity!!.supportFragmentManager, null)
|
|
||||||
} else {
|
|
||||||
playbackSpeedPopupMenu.show()
|
|
||||||
player.isSomePopupMenuVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
player.manageControlsAfterOnClick(v)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package org.schabi.newpipe.player.listeners.view
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import org.schabi.newpipe.MainActivity
|
|
||||||
import org.schabi.newpipe.extractor.MediaFormat
|
|
||||||
import org.schabi.newpipe.player.Player
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click listener for the qualityTextView of the player
|
|
||||||
*/
|
|
||||||
class QualityClickListener(
|
|
||||||
private val player: Player,
|
|
||||||
private val qualityPopupMenu: PopupMenu
|
|
||||||
) : View.OnClickListener {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG: String = "QualityClickListener"
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n") // we don't need I18N because of a " "
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
if (MainActivity.DEBUG) {
|
|
||||||
Log.d(TAG, "onQualitySelectorClicked() called")
|
|
||||||
}
|
|
||||||
|
|
||||||
qualityPopupMenu.show()
|
|
||||||
player.isSomePopupMenuVisible = true
|
|
||||||
|
|
||||||
val videoStream = player.selectedVideoStream
|
|
||||||
if (videoStream != null) {
|
|
||||||
player.binding.qualityTextView.text =
|
|
||||||
MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution()
|
|
||||||
}
|
|
||||||
|
|
||||||
player.saveWasPlaying()
|
|
||||||
player.manageControlsAfterOnClick(v)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ package org.schabi.newpipe.player.mediaitem;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.MediaItem.RequestMetadata;
|
||||||
import com.google.android.exoplayer2.MediaMetadata;
|
import com.google.android.exoplayer2.MediaMetadata;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
|
||||||
|
@ -76,7 +77,6 @@ public interface MediaItemTag {
|
||||||
@NonNull
|
@NonNull
|
||||||
default MediaItem asMediaItem() {
|
default MediaItem asMediaItem() {
|
||||||
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setMediaUri(Uri.parse(getStreamUrl()))
|
|
||||||
.setArtworkUri(Uri.parse(getThumbnailUrl()))
|
.setArtworkUri(Uri.parse(getThumbnailUrl()))
|
||||||
.setArtist(getUploaderName())
|
.setArtist(getUploaderName())
|
||||||
.setDescription(getTitle())
|
.setDescription(getTitle())
|
||||||
|
@ -84,10 +84,15 @@ public interface MediaItemTag {
|
||||||
.setTitle(getTitle())
|
.setTitle(getTitle())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
final RequestMetadata requestMetaData = new RequestMetadata.Builder()
|
||||||
|
.setMediaUri(Uri.parse(getStreamUrl()))
|
||||||
|
.build();
|
||||||
|
|
||||||
return MediaItem.fromUri(getStreamUrl())
|
return MediaItem.fromUri(getStreamUrl())
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.setMediaId(makeMediaId())
|
.setMediaId(makeMediaId())
|
||||||
.setMediaMetadata(mediaMetadata)
|
.setMediaMetadata(mediaMetadata)
|
||||||
|
.setRequestMetadata(requestMetaData)
|
||||||
.setTag(this)
|
.setTag(this)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
package org.schabi.newpipe.player.mediasession;
|
|
||||||
|
|
||||||
import android.support.v4.media.MediaDescriptionCompat;
|
|
||||||
|
|
||||||
public interface MediaSessionCallback {
|
|
||||||
void playPrevious();
|
|
||||||
|
|
||||||
void playNext();
|
|
||||||
|
|
||||||
void playItemAtIndex(int index);
|
|
||||||
|
|
||||||
int getCurrentPlayingIndex();
|
|
||||||
|
|
||||||
int getQueueSize();
|
|
||||||
|
|
||||||
MediaDescriptionCompat getQueueMetadata(int index);
|
|
||||||
|
|
||||||
void play();
|
|
||||||
|
|
||||||
void pause();
|
|
||||||
}
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
package org.schabi.newpipe.player.mediasession;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.support.v4.media.MediaMetadataCompat;
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media.session.MediaButtonReceiver;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.ForwardingPlayer;
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||||
|
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||||
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class MediaSessionPlayerUi extends PlayerUi {
|
||||||
|
private static final String TAG = "MediaSessUi";
|
||||||
|
|
||||||
|
private MediaSessionCompat mediaSession;
|
||||||
|
private MediaSessionConnector sessionConnector;
|
||||||
|
|
||||||
|
public MediaSessionPlayerUi(@NonNull final Player player) {
|
||||||
|
super(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initPlayer() {
|
||||||
|
super.initPlayer();
|
||||||
|
destroyPlayer(); // release previously used resources
|
||||||
|
|
||||||
|
mediaSession = new MediaSessionCompat(context, TAG);
|
||||||
|
mediaSession.setActive(true);
|
||||||
|
|
||||||
|
sessionConnector = new MediaSessionConnector(mediaSession);
|
||||||
|
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, player));
|
||||||
|
sessionConnector.setPlayer(getForwardingPlayer());
|
||||||
|
|
||||||
|
sessionConnector.setMetadataDeduplicationEnabled(true);
|
||||||
|
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyPlayer() {
|
||||||
|
super.destroyPlayer();
|
||||||
|
if (sessionConnector != null) {
|
||||||
|
sessionConnector.setPlayer(null);
|
||||||
|
sessionConnector.setQueueNavigator(null);
|
||||||
|
sessionConnector = null;
|
||||||
|
}
|
||||||
|
if (mediaSession != null) {
|
||||||
|
mediaSession.setActive(false);
|
||||||
|
mediaSession.release();
|
||||||
|
mediaSession = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
||||||
|
super.onThumbnailLoaded(bitmap);
|
||||||
|
if (sessionConnector != null) {
|
||||||
|
// the thumbnail is now loaded: invalidate the metadata to trigger a metadata update
|
||||||
|
sessionConnector.invalidateMediaSessionMetadata();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void handleMediaButtonIntent(final Intent intent) {
|
||||||
|
MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<MediaSessionCompat.Token> getSessionToken() {
|
||||||
|
return Optional.ofNullable(mediaSession).map(MediaSessionCompat::getSessionToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ForwardingPlayer getForwardingPlayer() {
|
||||||
|
// ForwardingPlayer means that all media session actions called on this player are
|
||||||
|
// forwarded directly to the connected exoplayer, except for the overridden methods. So
|
||||||
|
// override play and pause since our player adds more functionality to them over exoplayer.
|
||||||
|
return new ForwardingPlayer(player.getExoPlayer()) {
|
||||||
|
@Override
|
||||||
|
public void play() {
|
||||||
|
player.play();
|
||||||
|
// hide the player controls even if the play command came from the media session
|
||||||
|
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pause() {
|
||||||
|
player.pause();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaMetadataCompat buildMediaMetadata() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "buildMediaMetadata called");
|
||||||
|
}
|
||||||
|
|
||||||
|
// set title and artist
|
||||||
|
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, player.getVideoTitle())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, player.getUploaderName());
|
||||||
|
|
||||||
|
// set duration (-1 for livestreams or if unknown, see the METADATA_KEY_DURATION docs)
|
||||||
|
final long duration = player.getCurrentStreamInfo()
|
||||||
|
.filter(info -> !StreamTypeUtil.isLiveStream(info.getStreamType()))
|
||||||
|
.map(info -> info.getDuration() * 1000L)
|
||||||
|
.orElse(-1L);
|
||||||
|
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
|
||||||
|
|
||||||
|
// set album art, unless the user asked not to, or there is no thumbnail available
|
||||||
|
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||||
|
context.getString(R.string.show_thumbnail_key), true);
|
||||||
|
Optional.ofNullable(player.getThumbnail())
|
||||||
|
.filter(bitmap -> showThumbnail)
|
||||||
|
.ifPresent(bitmap -> {
|
||||||
|
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap);
|
||||||
|
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap);
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,106 +1,152 @@
|
||||||
package org.schabi.newpipe.player.mediasession;
|
package org.schabi.newpipe.player.mediasession;
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.ResultReceiver;
|
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
|
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
|
||||||
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
|
||||||
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
|
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
|
import android.support.v4.media.MediaMetadataCompat;
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
|
public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
|
||||||
public static final int DEFAULT_MAX_QUEUE_SIZE = 10;
|
private static final int MAX_QUEUE_SIZE = 10;
|
||||||
|
|
||||||
private final MediaSessionCompat mediaSession;
|
private final MediaSessionCompat mediaSession;
|
||||||
private final MediaSessionCallback callback;
|
private final Player player;
|
||||||
private final int maxQueueSize;
|
|
||||||
|
|
||||||
private long activeQueueItemId;
|
private long activeQueueItemId;
|
||||||
|
|
||||||
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
|
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
|
||||||
@NonNull final MediaSessionCallback callback) {
|
@NonNull final Player player) {
|
||||||
this.mediaSession = mediaSession;
|
this.mediaSession = mediaSession;
|
||||||
this.callback = callback;
|
this.player = player;
|
||||||
this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
|
|
||||||
|
|
||||||
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSupportedQueueNavigatorActions(@Nullable final Player player) {
|
public long getSupportedQueueNavigatorActions(
|
||||||
|
@Nullable final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
|
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(@NonNull final Player player) {
|
public void onTimelineChanged(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
publishFloatingQueueWindow();
|
publishFloatingQueueWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCurrentMediaItemIndexChanged(@NonNull final Player player) {
|
public void onCurrentMediaItemIndexChanged(
|
||||||
|
@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|
||||||
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
|
|| exoPlayer.getCurrentTimeline().getWindowCount() > MAX_QUEUE_SIZE) {
|
||||||
publishFloatingQueueWindow();
|
publishFloatingQueueWindow();
|
||||||
} else if (!player.getCurrentTimeline().isEmpty()) {
|
} else if (!exoPlayer.getCurrentTimeline().isEmpty()) {
|
||||||
activeQueueItemId = player.getCurrentMediaItemIndex();
|
activeQueueItemId = exoPlayer.getCurrentMediaItemIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getActiveQueueItemId(@Nullable final Player player) {
|
public long getActiveQueueItemId(
|
||||||
return callback.getCurrentPlayingIndex();
|
@Nullable final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
|
return Optional.ofNullable(player.getPlayQueue()).map(PlayQueue::getIndex).orElse(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkipToPrevious(@NonNull final Player player) {
|
public void onSkipToPrevious(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
callback.playPrevious();
|
player.playPrevious();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkipToQueueItem(@NonNull final Player player, final long id) {
|
public void onSkipToQueueItem(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
|
||||||
callback.playItemAtIndex((int) id);
|
final long id) {
|
||||||
|
if (player.getPlayQueue() != null) {
|
||||||
|
player.selectQueueItem(player.getPlayQueue().getItem((int) id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSkipToNext(@NonNull final Player player) {
|
public void onSkipToNext(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||||
callback.playNext();
|
player.playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void publishFloatingQueueWindow() {
|
private void publishFloatingQueueWindow() {
|
||||||
if (callback.getQueueSize() == 0) {
|
final int windowCount = Optional.ofNullable(player.getPlayQueue())
|
||||||
|
.map(PlayQueue::size)
|
||||||
|
.orElse(0);
|
||||||
|
if (windowCount == 0) {
|
||||||
mediaSession.setQueue(Collections.emptyList());
|
mediaSession.setQueue(Collections.emptyList());
|
||||||
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yes this is almost a copypasta, got a problem with that? =\
|
// Yes this is almost a copypasta, got a problem with that? =\
|
||||||
final int windowCount = callback.getQueueSize();
|
final int currentWindowIndex = player.getPlayQueue().getIndex();
|
||||||
final int currentWindowIndex = callback.getCurrentPlayingIndex();
|
final int queueSize = Math.min(MAX_QUEUE_SIZE, windowCount);
|
||||||
final int queueSize = Math.min(maxQueueSize, windowCount);
|
|
||||||
final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
|
final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
|
||||||
windowCount - queueSize);
|
windowCount - queueSize);
|
||||||
|
|
||||||
final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
||||||
for (int i = startIndex; i < startIndex + queueSize; i++) {
|
for (int i = startIndex; i < startIndex + queueSize; i++) {
|
||||||
queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i));
|
queue.add(new MediaSessionCompat.QueueItem(getQueueMetadata(i), i));
|
||||||
}
|
}
|
||||||
mediaSession.setQueue(queue);
|
mediaSession.setQueue(queue);
|
||||||
activeQueueItemId = currentWindowIndex;
|
activeQueueItemId = currentWindowIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MediaDescriptionCompat getQueueMetadata(final int index) {
|
||||||
|
if (player.getPlayQueue() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final PlayQueueItem item = player.getPlayQueue().getItem(index);
|
||||||
|
if (item == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder()
|
||||||
|
.setMediaId(String.valueOf(index))
|
||||||
|
.setTitle(item.getTitle())
|
||||||
|
.setSubtitle(item.getUploader());
|
||||||
|
|
||||||
|
// set additional metadata for A2DP/AVRCP (Audio/Video Bluetooth profiles)
|
||||||
|
final Bundle additionalMetadata = new Bundle();
|
||||||
|
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
|
||||||
|
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
|
||||||
|
additionalMetadata
|
||||||
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
|
||||||
|
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1L);
|
||||||
|
additionalMetadata
|
||||||
|
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
||||||
|
descBuilder.setExtras(additionalMetadata);
|
||||||
|
|
||||||
|
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
||||||
|
if (thumbnailUri != null) {
|
||||||
|
descBuilder.setIconUri(thumbnailUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return descBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(@NonNull final Player player,
|
public boolean onCommand(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
|
||||||
@NonNull final String command,
|
@NonNull final String command,
|
||||||
@Nullable final Bundle extras,
|
@Nullable final Bundle extras,
|
||||||
@Nullable final ResultReceiver cb) {
|
@Nullable final ResultReceiver cb) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.player.mediaitem.ExceptionTag;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -56,9 +56,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
|
||||||
this.playQueueItem = playQueueItem;
|
this.playQueueItem = playQueueItem;
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.retryTimestamp = retryTimestamp;
|
this.retryTimestamp = retryTimestamp;
|
||||||
this.mediaItem = ExceptionTag
|
this.mediaItem = ExceptionTag.of(playQueueItem, List.of(error)).withExtras(this)
|
||||||
.of(playQueueItem, Collections.singletonList(error))
|
|
||||||
.withExtras(this)
|
|
||||||
.asMediaItem();
|
.asMediaItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player.notification;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
@ -7,20 +7,48 @@ import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
public final class NotificationConstants {
|
public final class NotificationConstants {
|
||||||
|
|
||||||
private NotificationConstants() { }
|
private NotificationConstants() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Intent actions
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private static final String BASE_ACTION =
|
||||||
|
App.PACKAGE_NAME + ".player.MainPlayer.";
|
||||||
|
public static final String ACTION_CLOSE =
|
||||||
|
BASE_ACTION + "CLOSE";
|
||||||
|
public static final String ACTION_PLAY_PAUSE =
|
||||||
|
BASE_ACTION + ".player.MainPlayer.PLAY_PAUSE";
|
||||||
|
public static final String ACTION_REPEAT =
|
||||||
|
BASE_ACTION + ".player.MainPlayer.REPEAT";
|
||||||
|
public static final String ACTION_PLAY_NEXT =
|
||||||
|
BASE_ACTION + ".player.MainPlayer.ACTION_PLAY_NEXT";
|
||||||
|
public static final String ACTION_PLAY_PREVIOUS =
|
||||||
|
BASE_ACTION + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
|
||||||
|
public static final String ACTION_FAST_REWIND =
|
||||||
|
BASE_ACTION + ".player.MainPlayer.ACTION_FAST_REWIND";
|
||||||
|
public static final String ACTION_FAST_FORWARD =
|
||||||
|
BASE_ACTION + ".player.MainPlayer.ACTION_FAST_FORWARD";
|
||||||
|
public static final String ACTION_SHUFFLE =
|
||||||
|
BASE_ACTION + ".player.MainPlayer.ACTION_SHUFFLE";
|
||||||
|
public static final String ACTION_RECREATE_NOTIFICATION =
|
||||||
|
BASE_ACTION + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
||||||
|
|
||||||
|
|
||||||
public static final int NOTHING = 0;
|
public static final int NOTHING = 0;
|
||||||
|
@ -86,7 +114,7 @@ public final class NotificationConstants {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
public static final Integer[] SLOT_COMPACT_DEFAULTS = {0, 1, 2};
|
public static final List<Integer> SLOT_COMPACT_DEFAULTS = List.of(0, 1, 2);
|
||||||
|
|
||||||
public static final int[] SLOT_COMPACT_PREF_KEYS = {
|
public static final int[] SLOT_COMPACT_PREF_KEYS = {
|
||||||
R.string.notification_slot_compact_0_key,
|
R.string.notification_slot_compact_0_key,
|
||||||
|
@ -152,7 +180,7 @@ public final class NotificationConstants {
|
||||||
|
|
||||||
if (compactSlot == Integer.MAX_VALUE) {
|
if (compactSlot == Integer.MAX_VALUE) {
|
||||||
// settings not yet populated, return default values
|
// settings not yet populated, return default values
|
||||||
return new ArrayList<>(Arrays.asList(SLOT_COMPACT_DEFAULTS));
|
return new ArrayList<>(SLOT_COMPACT_DEFAULTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// a negative value (-1) is set when the user does not want a particular compact slot
|
// a negative value (-1) is set when the user does not want a particular compact slot
|
|
@ -0,0 +1,125 @@
|
||||||
|
package org.schabi.newpipe.player.notification;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||||
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
import org.schabi.newpipe.player.ui.PlayerUi;
|
||||||
|
|
||||||
|
public final class NotificationPlayerUi extends PlayerUi {
|
||||||
|
private boolean foregroundNotificationAlreadyCreated = false;
|
||||||
|
private final NotificationUtil notificationUtil;
|
||||||
|
|
||||||
|
public NotificationPlayerUi(@NonNull final Player player) {
|
||||||
|
super(player);
|
||||||
|
notificationUtil = new NotificationUtil(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initPlayer() {
|
||||||
|
super.initPlayer();
|
||||||
|
if (!foregroundNotificationAlreadyCreated) {
|
||||||
|
notificationUtil.createNotificationAndStartForeground();
|
||||||
|
foregroundNotificationAlreadyCreated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
super.destroy();
|
||||||
|
notificationUtil.cancelNotificationAndStopForeground();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
|
||||||
|
super.onThumbnailLoaded(bitmap);
|
||||||
|
notificationUtil.updateThumbnail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlocked() {
|
||||||
|
super.onBlocked();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaying() {
|
||||||
|
super.onPlaying();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBuffering() {
|
||||||
|
super.onBuffering();
|
||||||
|
if (notificationUtil.shouldUpdateBufferingSlot()) {
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPaused() {
|
||||||
|
super.onPaused();
|
||||||
|
|
||||||
|
// Remove running notification when user does not want minimization to background or popup
|
||||||
|
if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE
|
||||||
|
&& player.videoPlayerSelected()) {
|
||||||
|
notificationUtil.cancelNotificationAndStopForeground();
|
||||||
|
} else {
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPausedSeek() {
|
||||||
|
super.onPausedSeek();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompleted() {
|
||||||
|
super.onCompleted();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
|
||||||
|
super.onRepeatModeChanged(repeatMode);
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
|
||||||
|
super.onShuffleModeEnabledChanged(shuffleModeEnabled);
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBroadcastReceived(final Intent intent) {
|
||||||
|
super.onBroadcastReceived(intent);
|
||||||
|
if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) {
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMetadataChanged(@NonNull final StreamInfo info) {
|
||||||
|
super.onMetadataChanged(info);
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayQueueEdited() {
|
||||||
|
super.onPlayQueueEdited();
|
||||||
|
notificationUtil.createNotificationIfNeededAndUpdate(false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,15 @@
|
||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player.notification;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ServiceInfo;
|
import android.content.pm.ServiceInfo;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Matrix;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
@ -20,48 +19,45 @@ import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.player.Player;
|
||||||
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
import static androidx.media.app.NotificationCompat.MediaStyle;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
||||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a utility class for player notifications.
|
* This is a utility class for player notifications.
|
||||||
*
|
|
||||||
* @author cool-student
|
|
||||||
*/
|
*/
|
||||||
public final class NotificationUtil {
|
public final class NotificationUtil {
|
||||||
private static final String TAG = NotificationUtil.class.getSimpleName();
|
private static final String TAG = NotificationUtil.class.getSimpleName();
|
||||||
private static final boolean DEBUG = Player.DEBUG;
|
private static final boolean DEBUG = Player.DEBUG;
|
||||||
private static final int NOTIFICATION_ID = 123789;
|
private static final int NOTIFICATION_ID = 123789;
|
||||||
|
|
||||||
@Nullable private static NotificationUtil instance = null;
|
|
||||||
|
|
||||||
@NotificationConstants.Action
|
@NotificationConstants.Action
|
||||||
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
|
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
|
||||||
|
|
||||||
private NotificationManagerCompat notificationManager;
|
private NotificationManagerCompat notificationManager;
|
||||||
private NotificationCompat.Builder notificationBuilder;
|
private NotificationCompat.Builder notificationBuilder;
|
||||||
|
|
||||||
private NotificationUtil() {
|
private final Player player;
|
||||||
}
|
|
||||||
|
|
||||||
public static NotificationUtil getInstance() {
|
public NotificationUtil(final Player player) {
|
||||||
if (instance == null) {
|
this.player = player;
|
||||||
instance = new NotificationUtil();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,20 +68,31 @@ public final class NotificationUtil {
|
||||||
/**
|
/**
|
||||||
* Creates the notification if it does not exist already and recreates it if forceRecreate is
|
* Creates the notification if it does not exist already and recreates it if forceRecreate is
|
||||||
* true. Updates the notification with the data in the player.
|
* true. Updates the notification with the data in the player.
|
||||||
* @param player the player currently open, to take data from
|
|
||||||
* @param forceRecreate whether to force the recreation of the notification even if it already
|
* @param forceRecreate whether to force the recreation of the notification even if it already
|
||||||
* exists
|
* exists
|
||||||
*/
|
*/
|
||||||
synchronized void createNotificationIfNeededAndUpdate(final Player player,
|
public synchronized void createNotificationIfNeededAndUpdate(final boolean forceRecreate) {
|
||||||
final boolean forceRecreate) {
|
|
||||||
if (forceRecreate || notificationBuilder == null) {
|
if (forceRecreate || notificationBuilder == null) {
|
||||||
notificationBuilder = createNotification(player);
|
notificationBuilder = createNotification();
|
||||||
}
|
}
|
||||||
updateNotification(player);
|
updateNotification();
|
||||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized NotificationCompat.Builder createNotification(final Player player) {
|
public synchronized void updateThumbnail() {
|
||||||
|
if (notificationBuilder != null) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "updateThumbnail() called with thumbnail = [" + Integer.toHexString(
|
||||||
|
Optional.ofNullable(player.getThumbnail()).map(Objects::hashCode).orElse(0))
|
||||||
|
+ "], title = [" + player.getVideoTitle() + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
setLargeIcon(notificationBuilder);
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized NotificationCompat.Builder createNotification() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "createNotification()");
|
Log.d(TAG, "createNotification()");
|
||||||
}
|
}
|
||||||
|
@ -94,7 +101,7 @@ public final class NotificationUtil {
|
||||||
new NotificationCompat.Builder(player.getContext(),
|
new NotificationCompat.Builder(player.getContext(),
|
||||||
player.getContext().getString(R.string.notification_channel_id));
|
player.getContext().getString(R.string.notification_channel_id));
|
||||||
|
|
||||||
initializeNotificationSlots(player);
|
initializeNotificationSlots();
|
||||||
|
|
||||||
// count the number of real slots, to make sure compact slots indices are not out of bound
|
// count the number of real slots, to make sure compact slots indices are not out of bound
|
||||||
int nonNothingSlotCount = 5;
|
int nonNothingSlotCount = 5;
|
||||||
|
@ -108,14 +115,15 @@ public final class NotificationUtil {
|
||||||
// build the compact slot indices array (need code to convert from Integer... because Java)
|
// build the compact slot indices array (need code to convert from Integer... because Java)
|
||||||
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
|
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
|
||||||
player.getContext(), player.getPrefs(), nonNothingSlotCount);
|
player.getContext(), player.getPrefs(), nonNothingSlotCount);
|
||||||
final int[] compactSlots = new int[compactSlotList.size()];
|
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
|
||||||
for (int i = 0; i < compactSlotList.size(); i++) {
|
|
||||||
compactSlots[i] = compactSlotList.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
|
final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
|
||||||
.setMediaSession(player.getMediaSessionManager().getSessionToken())
|
player.UIs()
|
||||||
.setShowActionsInCompactView(compactSlots))
|
.get(MediaSessionPlayerUi.class)
|
||||||
|
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
||||||
|
.ifPresent(mediaStyle::setMediaSession);
|
||||||
|
|
||||||
|
builder.setStyle(mediaStyle)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
||||||
|
@ -128,35 +136,33 @@ public final class NotificationUtil {
|
||||||
.setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
|
.setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
|
||||||
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
|
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
|
||||||
|
|
||||||
|
// set the initial value for the video thumbnail, updatable with updateNotificationThumbnail
|
||||||
|
setLargeIcon(builder);
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the notification builder and the button icons depending on the playback state.
|
* Updates the notification builder and the button icons depending on the playback state.
|
||||||
* @param player the player currently open, to take data from
|
|
||||||
*/
|
*/
|
||||||
private synchronized void updateNotification(final Player player) {
|
private synchronized void updateNotification() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "updateNotification()");
|
Log.d(TAG, "updateNotification()");
|
||||||
}
|
}
|
||||||
|
|
||||||
// also update content intent, in case the user switched players
|
// also update content intent, in case the user switched players
|
||||||
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
|
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
|
||||||
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
|
NOTIFICATION_ID, getIntentForNotification(), FLAG_UPDATE_CURRENT));
|
||||||
notificationBuilder.setContentTitle(player.getVideoTitle());
|
notificationBuilder.setContentTitle(player.getVideoTitle());
|
||||||
notificationBuilder.setContentText(player.getUploaderName());
|
notificationBuilder.setContentText(player.getUploaderName());
|
||||||
notificationBuilder.setTicker(player.getVideoTitle());
|
notificationBuilder.setTicker(player.getVideoTitle());
|
||||||
updateActions(notificationBuilder, player);
|
|
||||||
final boolean showThumbnail = player.getPrefs().getBoolean(
|
updateActions(notificationBuilder);
|
||||||
player.getContext().getString(R.string.show_thumbnail_key), true);
|
|
||||||
if (showThumbnail) {
|
|
||||||
setLargeIcon(notificationBuilder, player);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
boolean shouldUpdateBufferingSlot() {
|
public boolean shouldUpdateBufferingSlot() {
|
||||||
if (notificationBuilder == null) {
|
if (notificationBuilder == null) {
|
||||||
// if there is no notification active, there is no point in updating it
|
// if there is no notification active, there is no point in updating it
|
||||||
return false;
|
return false;
|
||||||
|
@ -174,22 +180,22 @@ public final class NotificationUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void createNotificationAndStartForeground(final Player player, final Service service) {
|
public void createNotificationAndStartForeground() {
|
||||||
if (notificationBuilder == null) {
|
if (notificationBuilder == null) {
|
||||||
notificationBuilder = createNotification(player);
|
notificationBuilder = createNotification();
|
||||||
}
|
}
|
||||||
updateNotification(player);
|
updateNotification();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build(),
|
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(),
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||||
} else {
|
} else {
|
||||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancelNotificationAndStopForeground(final Service service) {
|
public void cancelNotificationAndStopForeground() {
|
||||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
ServiceCompat.stopForeground(player.getService(), ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||||
|
|
||||||
if (notificationManager != null) {
|
if (notificationManager != null) {
|
||||||
notificationManager.cancel(NOTIFICATION_ID);
|
notificationManager.cancel(NOTIFICATION_ID);
|
||||||
|
@ -203,7 +209,7 @@ public final class NotificationUtil {
|
||||||
// ACTIONS
|
// ACTIONS
|
||||||
/////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void initializeNotificationSlots(final Player player) {
|
private void initializeNotificationSlots() {
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
notificationSlots[i] = player.getPrefs().getInt(
|
notificationSlots[i] = player.getPrefs().getInt(
|
||||||
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||||
|
@ -212,17 +218,16 @@ public final class NotificationUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private void updateActions(final NotificationCompat.Builder builder, final Player player) {
|
private void updateActions(final NotificationCompat.Builder builder) {
|
||||||
builder.mActions.clear();
|
builder.mActions.clear();
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
addAction(builder, player, notificationSlots[i]);
|
addAction(builder, notificationSlots[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addAction(final NotificationCompat.Builder builder,
|
private void addAction(final NotificationCompat.Builder builder,
|
||||||
final Player player,
|
|
||||||
@NotificationConstants.Action final int slot) {
|
@NotificationConstants.Action final int slot) {
|
||||||
final NotificationCompat.Action action = getAction(player, slot);
|
final NotificationCompat.Action action = getAction(slot);
|
||||||
if (action != null) {
|
if (action != null) {
|
||||||
builder.addAction(action);
|
builder.addAction(action);
|
||||||
}
|
}
|
||||||
|
@ -230,41 +235,40 @@ public final class NotificationUtil {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private NotificationCompat.Action getAction(
|
private NotificationCompat.Action getAction(
|
||||||
final Player player,
|
|
||||||
@NotificationConstants.Action final int selectedAction) {
|
@NotificationConstants.Action final int selectedAction) {
|
||||||
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
||||||
switch (selectedAction) {
|
switch (selectedAction) {
|
||||||
case NotificationConstants.PREVIOUS:
|
case NotificationConstants.PREVIOUS:
|
||||||
return getAction(player, baseActionIcon,
|
return getAction(baseActionIcon,
|
||||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||||
|
|
||||||
case NotificationConstants.NEXT:
|
case NotificationConstants.NEXT:
|
||||||
return getAction(player, baseActionIcon,
|
return getAction(baseActionIcon,
|
||||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||||
|
|
||||||
case NotificationConstants.REWIND:
|
case NotificationConstants.REWIND:
|
||||||
return getAction(player, baseActionIcon,
|
return getAction(baseActionIcon,
|
||||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||||
|
|
||||||
case NotificationConstants.FORWARD:
|
case NotificationConstants.FORWARD:
|
||||||
return getAction(player, baseActionIcon,
|
return getAction(baseActionIcon,
|
||||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||||
|
|
||||||
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||||
return getAction(player, R.drawable.exo_notification_previous,
|
return getAction(R.drawable.exo_notification_previous,
|
||||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||||
} else {
|
} else {
|
||||||
return getAction(player, R.drawable.exo_controls_rewind,
|
return getAction(R.drawable.exo_controls_rewind,
|
||||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||||
}
|
}
|
||||||
|
|
||||||
case NotificationConstants.SMART_FORWARD_NEXT:
|
case NotificationConstants.SMART_FORWARD_NEXT:
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||||
return getAction(player, R.drawable.exo_notification_next,
|
return getAction(R.drawable.exo_notification_next,
|
||||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||||
} else {
|
} else {
|
||||||
return getAction(player, R.drawable.exo_controls_fastforward,
|
return getAction(R.drawable.exo_controls_fastforward,
|
||||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,44 +282,45 @@ public final class NotificationUtil {
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fallthrough
|
||||||
case NotificationConstants.PLAY_PAUSE:
|
case NotificationConstants.PLAY_PAUSE:
|
||||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
||||||
return getAction(player, R.drawable.ic_replay,
|
return getAction(R.drawable.ic_replay,
|
||||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||||
} else if (player.isPlaying()
|
} else if (player.isPlaying()
|
||||||
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||||
return getAction(player, R.drawable.exo_notification_pause,
|
return getAction(R.drawable.exo_notification_pause,
|
||||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||||
} else {
|
} else {
|
||||||
return getAction(player, R.drawable.exo_notification_play,
|
return getAction(R.drawable.exo_notification_play,
|
||||||
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
|
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
case NotificationConstants.REPEAT:
|
case NotificationConstants.REPEAT:
|
||||||
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
||||||
return getAction(player, R.drawable.exo_media_action_repeat_all,
|
return getAction(R.drawable.exo_media_action_repeat_all,
|
||||||
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
|
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
|
||||||
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
||||||
return getAction(player, R.drawable.exo_media_action_repeat_one,
|
return getAction(R.drawable.exo_media_action_repeat_one,
|
||||||
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
|
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
|
||||||
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
||||||
return getAction(player, R.drawable.exo_media_action_repeat_off,
|
return getAction(R.drawable.exo_media_action_repeat_off,
|
||||||
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
|
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
case NotificationConstants.SHUFFLE:
|
case NotificationConstants.SHUFFLE:
|
||||||
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
||||||
return getAction(player, R.drawable.exo_controls_shuffle_on,
|
return getAction(R.drawable.exo_controls_shuffle_on,
|
||||||
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
||||||
} else {
|
} else {
|
||||||
return getAction(player, R.drawable.exo_controls_shuffle_off,
|
return getAction(R.drawable.exo_controls_shuffle_off,
|
||||||
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
|
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
case NotificationConstants.CLOSE:
|
case NotificationConstants.CLOSE:
|
||||||
return getAction(player, R.drawable.ic_close,
|
return getAction(R.drawable.ic_close,
|
||||||
R.string.close, ACTION_CLOSE);
|
R.string.close, ACTION_CLOSE);
|
||||||
|
|
||||||
case NotificationConstants.NOTHING:
|
case NotificationConstants.NOTHING:
|
||||||
|
@ -325,8 +330,7 @@ public final class NotificationUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private NotificationCompat.Action getAction(final Player player,
|
private NotificationCompat.Action getAction(@DrawableRes final int drawable,
|
||||||
@DrawableRes final int drawable,
|
|
||||||
@StringRes final int title,
|
@StringRes final int title,
|
||||||
final String intentAction) {
|
final String intentAction) {
|
||||||
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
|
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
|
||||||
|
@ -334,7 +338,7 @@ public final class NotificationUtil {
|
||||||
new Intent(intentAction), FLAG_UPDATE_CURRENT));
|
new Intent(intentAction), FLAG_UPDATE_CURRENT));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getIntentForNotification(final Player player) {
|
private Intent getIntentForNotification() {
|
||||||
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
|
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
|
||||||
// Means we play in popup or audio only. Let's show the play queue
|
// Means we play in popup or audio only. Let's show the play queue
|
||||||
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
|
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
|
||||||
|
@ -354,28 +358,34 @@ public final class NotificationUtil {
|
||||||
// BITMAP
|
// BITMAP
|
||||||
/////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) {
|
private void setLargeIcon(final NotificationCompat.Builder builder) {
|
||||||
|
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||||
|
player.getContext().getString(R.string.show_thumbnail_key), true);
|
||||||
|
final Bitmap thumbnail = player.getThumbnail();
|
||||||
|
if (thumbnail == null || !showThumbnail) {
|
||||||
|
// since the builder is reused, make sure the thumbnail is unset if there is not one
|
||||||
|
builder.setLargeIcon(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
|
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
|
||||||
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
||||||
false);
|
false);
|
||||||
if (scaleImageToSquareAspectRatio) {
|
if (scaleImageToSquareAspectRatio) {
|
||||||
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));
|
builder.setLargeIcon(getBitmapWithSquareAspectRatio(thumbnail));
|
||||||
} else {
|
} else {
|
||||||
builder.setLargeIcon(player.getThumbnail());
|
builder.setLargeIcon(thumbnail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) {
|
private Bitmap getBitmapWithSquareAspectRatio(@NonNull final Bitmap bitmap) {
|
||||||
return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth());
|
// Find the smaller dimension and then take a center portion of the image that
|
||||||
}
|
// has that size.
|
||||||
|
final int w = bitmap.getWidth();
|
||||||
private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) {
|
final int h = bitmap.getHeight();
|
||||||
final int width = bitmap.getWidth();
|
final int dstSize = Math.min(w, h);
|
||||||
final int height = bitmap.getHeight();
|
final int x = (w - dstSize) / 2;
|
||||||
final float scaleWidth = ((float) newWidth) / width;
|
final int y = (h - dstSize) / 2;
|
||||||
final float scaleHeight = ((float) newHeight) / height;
|
return Bitmap.createBitmap(bitmap, x, y, dstSize, dstSize);
|
||||||
final Matrix matrix = new Matrix();
|
|
||||||
matrix.postScale(scaleWidth, scaleHeight);
|
|
||||||
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,99 +0,0 @@
|
||||||
package org.schabi.newpipe.player.playback;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v4.media.MediaDescriptionCompat;
|
|
||||||
import android.support.v4.media.MediaMetadataCompat;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.player.Player;
|
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|
||||||
|
|
||||||
public class PlayerMediaSession implements MediaSessionCallback {
|
|
||||||
private final Player player;
|
|
||||||
|
|
||||||
public PlayerMediaSession(final Player player) {
|
|
||||||
this.player = player;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playPrevious() {
|
|
||||||
player.playPrevious();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playNext() {
|
|
||||||
player.playNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playItemAtIndex(final int index) {
|
|
||||||
if (player.getPlayQueue() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
player.selectQueueItem(player.getPlayQueue().getItem(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCurrentPlayingIndex() {
|
|
||||||
if (player.getPlayQueue() == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return player.getPlayQueue().getIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getQueueSize() {
|
|
||||||
if (player.getPlayQueue() == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return player.getPlayQueue().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MediaDescriptionCompat getQueueMetadata(final int index) {
|
|
||||||
if (player.getPlayQueue() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final PlayQueueItem item = player.getPlayQueue().getItem(index);
|
|
||||||
if (item == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final MediaDescriptionCompat.Builder descriptionBuilder
|
|
||||||
= new MediaDescriptionCompat.Builder()
|
|
||||||
.setMediaId(String.valueOf(index))
|
|
||||||
.setTitle(item.getTitle())
|
|
||||||
.setSubtitle(item.getUploader());
|
|
||||||
|
|
||||||
// set additional metadata for A2DP/AVRCP
|
|
||||||
final Bundle additionalMetadata = new Bundle();
|
|
||||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
|
|
||||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
|
|
||||||
additionalMetadata
|
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
|
|
||||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1);
|
|
||||||
additionalMetadata
|
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
|
||||||
descriptionBuilder.setExtras(additionalMetadata);
|
|
||||||
|
|
||||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
|
||||||
if (thumbnailUri != null) {
|
|
||||||
descriptionBuilder.setIconUri(thumbnailUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
return descriptionBuilder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void play() {
|
|
||||||
player.play();
|
|
||||||
// hide the player controls even if the play command came from the media session
|
|
||||||
player.hideControls(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void pause() {
|
|
||||||
player.pause();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import android.content.Context;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.video.DummySurface;
|
import com.google.android.exoplayer2.video.PlaceholderSurface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent error message: 'Unrecoverable player error occurred'
|
* Prevent error message: 'Unrecoverable player error occurred'
|
||||||
|
@ -26,7 +26,7 @@ public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Player player;
|
private final Player player;
|
||||||
private DummySurface dummySurface;
|
private PlaceholderSurface placeholderSurface;
|
||||||
|
|
||||||
public SurfaceHolderCallback(final Context context, final Player player) {
|
public SurfaceHolderCallback(final Context context, final Player player) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
@ -47,16 +47,16 @@ public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(final SurfaceHolder holder) {
|
public void surfaceDestroyed(final SurfaceHolder holder) {
|
||||||
if (dummySurface == null) {
|
if (placeholderSurface == null) {
|
||||||
dummySurface = DummySurface.newInstanceV17(context, false);
|
placeholderSurface = PlaceholderSurface.newInstanceV17(context, false);
|
||||||
}
|
}
|
||||||
player.setVideoSurface(dummySurface);
|
player.setVideoSurface(placeholderSurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void release() {
|
public void release() {
|
||||||
if (dummySurface != null) {
|
if (placeholderSurface != null) {
|
||||||
dummySurface.release();
|
placeholderSurface.release();
|
||||||
dummySurface = null;
|
placeholderSurface = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
public void onError(@NonNull final Throwable e) {
|
public void onError(@NonNull final Throwable e) {
|
||||||
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
append(); // Notify change
|
notifyChange();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
||||||
public void onError(@NonNull final Throwable e) {
|
public void onError(@NonNull final Throwable e) {
|
||||||
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
append(); // Notify change
|
notifyChange();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ import org.schabi.newpipe.player.playqueue.events.SelectEvent;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
@ -258,13 +257,10 @@ public abstract class PlayQueue implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends the given {@link PlayQueueItem}s to the current play queue.
|
* Notifies that a change has occurred.
|
||||||
*
|
|
||||||
* @see #append(List items)
|
|
||||||
* @param items {@link PlayQueueItem}s to append
|
|
||||||
*/
|
*/
|
||||||
public synchronized void append(@NonNull final PlayQueueItem... items) {
|
public synchronized void notifyChange() {
|
||||||
append(Arrays.asList(items));
|
broadcast(new AppendEvent(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue