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
|
||||
description: Create a bug report to help us improve
|
||||
labels: [bug]
|
||||
labels: [bug, needs triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@ -20,6 +20,8 @@ body:
|
|||
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."
|
||||
required: true
|
||||
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
|
||||
required: true
|
||||
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
|
||||
required: true
|
||||
- label: "This issue contains only one bug."
|
||||
|
|
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
labels: [enhancement]
|
||||
labels: [enhancement, needs triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@ -8,7 +8,6 @@ body:
|
|||
Thank you for helping to make NewPipe better by suggesting a feature. :hugs:
|
||||
|
||||
Your ideas are highly welcome! The app is made for you, the users, after all.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
|
@ -16,6 +15,8 @@ body:
|
|||
options:
|
||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/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
|
||||
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
|
||||
required: true
|
||||
- label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)."
|
||||
required: true
|
||||
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
|
||||
|
|
4
.github/ISSUE_TEMPLATE/question.yml
vendored
4
.github/ISSUE_TEMPLATE/question.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: Question
|
||||
description: Ask about anything NewPipe-related
|
||||
labels: [question]
|
||||
labels: [question, needs triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@ -16,6 +16,8 @@ body:
|
|||
options:
|
||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||
required: true
|
||||
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my question isn't listed."
|
||||
required: true
|
||||
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
|
||||
required: true
|
||||
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
|
||||
|
|
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -6,7 +6,7 @@ on:
|
|||
branches:
|
||||
- dev
|
||||
- master
|
||||
- release/**
|
||||
- release**
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'doc/**'
|
||||
|
@ -31,6 +31,10 @@ on:
|
|||
jobs:
|
||||
build-and-test-jvm:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
|
@ -64,6 +68,10 @@ jobs:
|
|||
matrix:
|
||||
# api-level 19 is min sdk, but throws errors related to desugaring
|
||||
api-level: [ 21, 29 ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
@ -91,6 +99,10 @@ jobs:
|
|||
|
||||
sonar:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
|
4
.github/workflows/image-minimizer.yml
vendored
4
.github/workflows/image-minimizer.yml
vendored
|
@ -6,6 +6,10 @@ on:
|
|||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
try-minimize:
|
||||
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.
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
noResponse:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -17,4 +21,4 @@ jobs:
|
|||
with:
|
||||
token: ${{ github.token }}
|
||||
daysUntilClose: 14
|
||||
responseRequiredLabel: waiting-for-author
|
||||
responseRequiredLabel: waiting for author
|
||||
|
|
|
@ -14,15 +14,12 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "org.polymorphicshade.newpipe"
|
||||
resValue "string", "app_name", "NewPipe SponsorBlock"
|
||||
minSdk 19
|
||||
minSdk 21
|
||||
targetSdk 29
|
||||
versionCode 989
|
||||
versionName "0.23.3"
|
||||
|
||||
multiDexEnabled true
|
||||
versionCode 990
|
||||
versionName "0.24.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
|
@ -98,14 +95,14 @@ android {
|
|||
}
|
||||
|
||||
ext {
|
||||
checkstyleVersion = '10.0'
|
||||
checkstyleVersion = '10.3.1'
|
||||
|
||||
androidxLifecycleVersion = '2.3.1'
|
||||
androidxRoomVersion = '2.4.2'
|
||||
androidxLifecycleVersion = '2.5.1'
|
||||
androidxRoomVersion = '2.4.3'
|
||||
androidxWorkVersion = '2.7.1'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.17.1'
|
||||
exoPlayerVersion = '2.18.1'
|
||||
googleAutoServiceVersion = '1.0.1'
|
||||
groupieVersion = '2.10.1'
|
||||
markwonVersion = '4.6.2'
|
||||
|
@ -113,7 +110,7 @@ ext {
|
|||
leakCanaryVersion = '2.5'
|
||||
stethoVersion = '1.6.0'
|
||||
mockitoVersion = '4.0.0'
|
||||
assertJVersion = '3.22.0'
|
||||
assertJVersion = '3.23.1'
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
@ -182,7 +179,7 @@ sonarqube {
|
|||
|
||||
dependencies {
|
||||
/** Desugaring **/
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
|
||||
|
||||
/** NewPipe libraries **/
|
||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||
|
@ -190,27 +187,27 @@ dependencies {
|
|||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:6a858368c86bc9a55abee586eb6c733e86c26b97'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:5c710da160f488bb40ab2cf4469bec9bd4cefd38'
|
||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
ktlint 'com.pinterest:ktlint:0.44.0'
|
||||
ktlint 'com.pinterest:ktlint:0.45.2'
|
||||
|
||||
/** Kotlin **/
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||
|
||||
/** AndroidX **/
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||
implementation 'androidx.media:media:1.5.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
|
@ -220,10 +217,9 @@ dependencies {
|
|||
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
|
||||
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
|
||||
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
|
||||
/** Third-party libraries **/
|
||||
// Instance state boilerplate elimination
|
||||
|
@ -234,11 +230,16 @@ dependencies {
|
|||
implementation "org.jsoup:jsoup:1.15.3"
|
||||
|
||||
// HTTP client
|
||||
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
|
||||
implementation "com.squareup.okhttp3:okhttp:3.12.13"
|
||||
implementation "com.squareup.okhttp3:okhttp:4.10.0"
|
||||
|
||||
// Media player
|
||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||
|
||||
// Metadata generator for service descriptors
|
||||
|
@ -257,9 +258,6 @@ dependencies {
|
|||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||
|
||||
// File picker
|
||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||
|
||||
// Crash reporting
|
||||
implementation "ch.acra:acra-core:5.9.3"
|
||||
|
||||
|
@ -273,7 +271,7 @@ dependencies {
|
|||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
// Date and time formatting
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.2.Final"
|
||||
implementation "org.ocpsoft.prettytime:prettytime:5.0.3.Final"
|
||||
|
||||
/** Debugging **/
|
||||
// Memory leak detection
|
||||
|
|
11
app/proguard-rules.pro
vendored
11
app/proguard-rules.pro
vendored
|
@ -18,7 +18,6 @@
|
|||
|
||||
-dontobfuscate
|
||||
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||
-keep class org.ocpsoft.prettytime.i18n.** { *; }
|
||||
|
||||
-keep class org.mozilla.javascript.** { *; }
|
||||
|
||||
|
@ -26,9 +25,6 @@
|
|||
-keep class com.google.android.exoplayer2.** { *; }
|
||||
|
||||
-dontwarn org.mozilla.javascript.tools.**
|
||||
-dontwarn android.arch.util.paging.CountedDataSource
|
||||
-dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource
|
||||
|
||||
|
||||
# Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick
|
||||
-dontwarn icepick.**
|
||||
|
@ -39,12 +35,11 @@
|
|||
}
|
||||
-keepnames class * { @icepick.State *;}
|
||||
|
||||
# Rules for OkHttp. Copy paste from https://github.com/square/okhttp
|
||||
## Rules for OkHttp. Copy paste from https://github.com/square/okhttp
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.**
|
||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
##
|
||||
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
!static !transient <fields>;
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".player.MainPlayer"
|
||||
android:name=".player.PlayerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
|
|
|
@ -282,11 +282,9 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
|||
@Nullable
|
||||
public Parcelable saveState() {
|
||||
Bundle state = null;
|
||||
if (mSavedState.size() > 0) {
|
||||
if (!mSavedState.isEmpty()) {
|
||||
state = new Bundle();
|
||||
final Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
|
||||
mSavedState.toArray(fss);
|
||||
state.putParcelableArray("states", fss);
|
||||
state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
|
||||
}
|
||||
for (int i = 0; i < mFragments.size(); i++) {
|
||||
final Fragment f = mFragments.get(i);
|
||||
|
|
|
@ -14,7 +14,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
|||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
// See https://stackoverflow.com/questions/56849221#57997489
|
||||
|
@ -27,7 +26,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
|||
|
||||
private boolean allowScroll = true;
|
||||
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.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,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull final MotionEvent ev) {
|
||||
for (final Integer element : skipInterceptionOfElements) {
|
||||
for (final int element : skipInterceptionOfElements) {
|
||||
final View view = child.findViewById(element);
|
||||
if (view != null) {
|
||||
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
||||
|
@ -132,8 +131,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
|||
try {
|
||||
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
||||
if (headerBehaviorType != null) {
|
||||
final Field field
|
||||
= headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||
final Field field =
|
||||
headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
@ -7,7 +8,6 @@ import android.util.Log;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationChannelCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
||||
|
@ -27,9 +27,8 @@ import org.schabi.newpipe.util.StateSaver;
|
|||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
|
@ -56,7 +55,7 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
|||
* 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;
|
||||
private static final String TAG = App.class.toString();
|
||||
private static App app;
|
||||
|
@ -140,7 +139,7 @@ public class App extends MultiDexApplication {
|
|||
if (throwable instanceof UndeliverableException) {
|
||||
// As UndeliverableException is a wrapper,
|
||||
// get the cause of it to get the "real" exception
|
||||
actualThrowable = throwable.getCause();
|
||||
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
||||
} else {
|
||||
actualThrowable = throwable;
|
||||
}
|
||||
|
@ -149,7 +148,7 @@ public class App extends MultiDexApplication {
|
|||
if (actualThrowable instanceof CompositeException) {
|
||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
||||
} else {
|
||||
errors = Collections.singletonList(actualThrowable);
|
||||
errors = List.of(actualThrowable);
|
||||
}
|
||||
|
||||
for (final Throwable error : errors) {
|
||||
|
@ -213,41 +212,37 @@ public class App extends MultiDexApplication {
|
|||
private void initNotificationChannels() {
|
||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||
// the main and update channels
|
||||
final List<NotificationChannelCompat> notificationChannelCompats = new ArrayList<>();
|
||||
notificationChannelCompats.add(new NotificationChannelCompat
|
||||
.Builder(getString(R.string.notification_channel_id),
|
||||
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
||||
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build());
|
||||
|
||||
notificationChannelCompats.add(new NotificationChannelCompat
|
||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
||||
.setName(getString(R.string.notification_channel_name))
|
||||
.setDescription(getString(R.string.notification_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat
|
||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||
.build());
|
||||
|
||||
notificationChannelCompats.add(new NotificationChannelCompat
|
||||
.Builder(getString(R.string.hash_channel_id),
|
||||
.setName(getString(R.string.app_update_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.app_update_notification_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
||||
.setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build());
|
||||
|
||||
notificationChannelCompats.add(new NotificationChannelCompat
|
||||
.Builder(getString(R.string.error_report_channel_id),
|
||||
.setName(getString(R.string.hash_channel_name))
|
||||
.setDescription(getString(R.string.hash_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.error_report_channel_name))
|
||||
.setDescription(getString(R.string.error_report_channel_description))
|
||||
.build());
|
||||
|
||||
notificationChannelCompats.add(new NotificationChannelCompat
|
||||
.Builder(getString(R.string.streams_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(getString(R.string.streams_notification_channel_name))
|
||||
.setDescription(getString(R.string.streams_notification_channel_description))
|
||||
.build());
|
||||
.setName(getString(R.string.error_report_channel_name))
|
||||
.setDescription(getString(R.string.error_report_channel_description))
|
||||
.build(),
|
||||
new NotificationChannelCompat
|
||||
.Builder(getString(R.string.streams_notification_channel_id),
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(getString(R.string.streams_notification_channel_name))
|
||||
.setDescription(
|
||||
getString(R.string.streams_notification_channel_description))
|
||||
.build()
|
||||
);
|
||||
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -12,40 +11,27 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
|
|||
import org.schabi.newpipe.extractor.downloader.Request;
|
||||
import org.schabi.newpipe.extractor.downloader.Response;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.util.CookieUtils;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import okhttp3.CipherSuite;
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public final class DownloaderImpl extends Downloader {
|
||||
public static final String USER_AGENT
|
||||
= "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
||||
= "youtube_restricted_mode_key";
|
||||
public static final String USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
|
||||
"youtube_restricted_mode_key";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
||||
|
||||
|
@ -55,9 +41,6 @@ public final class DownloaderImpl extends Downloader {
|
|||
private Integer customTimeout;
|
||||
|
||||
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
enableModernTLS(builder);
|
||||
}
|
||||
this.client = builder
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
|
||||
|
@ -82,74 +65,21 @@ public final class DownloaderImpl extends Downloader {
|
|||
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) {
|
||||
this.customTimeout = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCookies(final String url) {
|
||||
final List<String> resultCookies = new ArrayList<>();
|
||||
if (url.contains(YOUTUBE_DOMAIN)) {
|
||||
final String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
||||
if (youtubeCookie != null) {
|
||||
resultCookies.add(youtubeCookie);
|
||||
}
|
||||
}
|
||||
final String youtubeCookie = url.contains(YOUTUBE_DOMAIN)
|
||||
? getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) : null;
|
||||
|
||||
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
||||
final String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
|
||||
if (recaptchaCookie != null) {
|
||||
resultCookies.add(recaptchaCookie);
|
||||
}
|
||||
return CookieUtils.concatCookies(resultCookies);
|
||||
return Stream.of(youtubeCookie, getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY))
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(cookies -> Arrays.stream(cookies.split("; *")))
|
||||
.distinct()
|
||||
.collect(Collectors.joining("; "));
|
||||
}
|
||||
|
||||
public String getCookie(final String key) {
|
||||
|
@ -209,7 +139,7 @@ public final class DownloaderImpl extends Downloader {
|
|||
|
||||
RequestBody requestBody = null;
|
||||
if (dataToSend != null) {
|
||||
requestBody = RequestBody.create(null, dataToSend);
|
||||
requestBody = RequestBody.create(dataToSend);
|
||||
}
|
||||
|
||||
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
@ -44,11 +43,7 @@ public class ExitActivity extends Activity {
|
|||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
finishAndRemoveTask();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
finishAndRemoveTask();
|
||||
|
||||
NavigationHelper.restartApp(this);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import android.content.Intent;
|
|||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
@ -86,7 +85,6 @@ import org.schabi.newpipe.util.PermissionHelper;
|
|||
import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
|
@ -131,11 +129,6 @@ public class MainActivity extends AppCompatActivity {
|
|||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
|
||||
// enable TLS1.1/1.2 for kitkat devices, to fix download and play for media.ccc.de sources
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
|
||||
TLSSocketFactoryCompat.setAsDefault();
|
||||
}
|
||||
|
||||
ThemeHelper.setDayNightMode(this);
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
|
||||
|
@ -381,8 +374,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
private void showServices() {
|
||||
for (final StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName()
|
||||
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
final String title = s.getServiceInfo().getName();
|
||||
|
||||
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||
|
@ -390,7 +382,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
// peertube specifics
|
||||
if (s.getServiceId() == 3) {
|
||||
enhancePeertubeMenu(s, menuItem);
|
||||
enhancePeertubeMenu(menuItem);
|
||||
}
|
||||
}
|
||||
drawerLayoutBinding.navigation.getMenu()
|
||||
|
@ -398,9 +390,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
.setChecked(true);
|
||||
}
|
||||
|
||||
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
|
||||
private void enhancePeertubeMenu(final MenuItem menuItem) {
|
||||
final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
|
||||
menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""));
|
||||
menuItem.setTitle(currentInstance.getName());
|
||||
final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
|
||||
.getRoot();
|
||||
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||
|
@ -480,8 +472,8 @@ public class MainActivity extends AppCompatActivity {
|
|||
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
|
||||
}
|
||||
|
||||
final SharedPreferences sharedPreferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final SharedPreferences sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Theme has changed, recreating activity...");
|
||||
|
@ -653,8 +645,8 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
final Fragment fragment
|
||||
= getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
final Fragment fragment =
|
||||
getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (!(fragment instanceof SearchFragment)) {
|
||||
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
/*
|
||||
|
@ -40,10 +39,6 @@ public class PanicResponderActivity extends Activity {
|
|||
ExitActivity.exitAndRemoveFromRecentApps(this);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
finishAndRemoveTask();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
|||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.SparseItemUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class QueueItemMenuUtil {
|
||||
private QueueItemMenuUtil() {
|
||||
|
@ -53,7 +53,7 @@ public final class QueueItemMenuUtil {
|
|||
case R.id.menu_item_append_playlist:
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
context,
|
||||
Collections.singletonList(new StreamEntity(item)),
|
||||
List.of(new StreamEntity(item)),
|
||||
dialog -> dialog.show(
|
||||
fragmentManager,
|
||||
"QueueItemMenuUtil@append_playlist"
|
||||
|
|
|
@ -30,6 +30,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.core.math.MathUtils;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
|
@ -60,7 +61,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
|||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
|
@ -81,7 +82,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
|
|||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
|
@ -452,7 +452,7 @@ 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) {
|
||||
((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
|
||||
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
return playerType == null || playerType == MainPlayer.PlayerType.VIDEO;
|
||||
final PlayerType playerType = PlayerHolder.getInstance().getType();
|
||||
return playerType == null || playerType == PlayerType.MAIN;
|
||||
}
|
||||
|
||||
private void openAddToPlaylistDialog() {
|
||||
|
@ -649,7 +649,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||
.subscribe(
|
||||
info -> PlaylistDialog.createCorrespondingDialog(
|
||||
getThemeWrapperContext(),
|
||||
Collections.singletonList(new StreamEntity(info)),
|
||||
List.of(new StreamEntity(info)),
|
||||
playlistDialog -> {
|
||||
playlistDialog.setOnDismissListener(dialog -> finish());
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import androidx.core.os.bundleOf
|
|||
import androidx.fragment.app.Fragment
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
|
||||
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||
|
||||
|
|
|
@ -12,126 +12,92 @@ import org.schabi.newpipe.R
|
|||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
object LicenseFragmentHelper {
|
||||
/**
|
||||
* @param context the context to use
|
||||
* @param license the license
|
||||
* @return String which contains a HTML formatted license page
|
||||
* styled according to the context's theme
|
||||
*/
|
||||
private fun getFormattedLicense(context: Context, license: License): String {
|
||||
val licenseContent = StringBuilder()
|
||||
val webViewData: String
|
||||
try {
|
||||
BufferedReader(
|
||||
InputStreamReader(
|
||||
context.assets.open(license.filename),
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
).use { `in` ->
|
||||
var str: String?
|
||||
while (`in`.readLine().also { str = it } != null) {
|
||||
licenseContent.append(str)
|
||||
}
|
||||
/**
|
||||
* @param context the context to use
|
||||
* @param license the license
|
||||
* @return String which contains a HTML formatted license page
|
||||
* styled according to the context's theme
|
||||
*/
|
||||
private fun getFormattedLicense(context: Context, license: License): String {
|
||||
try {
|
||||
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||
.replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
|
||||
} catch (e: IOException) {
|
||||
throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
|
||||
}
|
||||
}
|
||||
|
||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||
webViewData = "$licenseContent".replace(
|
||||
"</head>",
|
||||
"<style>" + getLicenseStylesheet(context) + "</style></head>"
|
||||
)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw IllegalArgumentException(
|
||||
"Could not get license file: " + license.filename, e
|
||||
)
|
||||
/**
|
||||
* @param context the Android context
|
||||
* @return String which is a CSS stylesheet according to the context's theme
|
||||
*/
|
||||
private fun getLicenseStylesheet(context: Context): String {
|
||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||
val licenseBackgroundColor = getHexRGBColor(
|
||||
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||
)
|
||||
val licenseTextColor = getHexRGBColor(
|
||||
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
|
||||
)
|
||||
val youtubePrimaryColor = getHexRGBColor(
|
||||
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
|
||||
)
|
||||
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
|
||||
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast R.color to a hexadecimal color value.
|
||||
*
|
||||
* @param context the context to use
|
||||
* @param color the color number from R.color
|
||||
* @return a six characters long String with hexadecimal RGB values
|
||||
*/
|
||||
private fun getHexRGBColor(context: Context, color: Int): String {
|
||||
return context.getString(color).substring(3)
|
||||
}
|
||||
|
||||
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
||||
return showLicense(context, component.license) {
|
||||
setPositiveButton(R.string.dismiss) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
return webViewData
|
||||
}
|
||||
|
||||
/**
|
||||
* @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()
|
||||
}
|
||||
}
|
||||
setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||
ShareUtils.openUrlInBrowser(context!!, component.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Update;
|
||||
|
||||
import java.util.Collection;
|
||||
|
@ -14,13 +13,10 @@ import io.reactivex.rxjava3.core.Flowable;
|
|||
@Dao
|
||||
public interface BasicDAO<Entity> {
|
||||
/* Inserts */
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
@Insert
|
||||
long insert(Entity entity);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
List<Long> insertAll(Entity... entities);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
@Insert
|
||||
List<Long> insertAll(Collection<Entity> entities);
|
||||
|
||||
/* Searches */
|
||||
|
@ -32,9 +28,6 @@ public interface BasicDAO<Entity> {
|
|||
@Delete
|
||||
void delete(Entity entity);
|
||||
|
||||
@Delete
|
||||
int delete(Collection<Entity> entities);
|
||||
|
||||
int deleteAll();
|
||||
|
||||
/* Updates */
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.room.Update
|
|||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
|
@ -21,56 +22,16 @@ abstract class FeedDAO {
|
|||
@Query("DELETE FROM feed")
|
||||
abstract fun deleteAll(): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.*, sst.progress_time
|
||||
FROM streams s
|
||||
|
||||
LEFT JOIN stream_state sst
|
||||
ON s.uid = sst.stream_id
|
||||
|
||||
LEFT JOIN stream_history sh
|
||||
ON s.uid = sh.stream_id
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Maybe<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.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(
|
||||
"""
|
||||
|
@ -86,60 +47,37 @@ abstract class FeedDAO {
|
|||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = f.subscription_id
|
||||
|
||||
WHERE (
|
||||
sh.stream_id IS NULL
|
||||
OR sst.stream_id IS NULL
|
||||
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
||||
OR s.stream_type = 'LIVE_STREAM'
|
||||
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||
:groupId = ${FeedGroupEntity.GROUP_ALL_ID}
|
||||
OR fgs.group_id = :groupId
|
||||
)
|
||||
|
||||
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 (
|
||||
sh.stream_id IS NULL
|
||||
:includePlayed
|
||||
OR sh.stream_id IS NULL
|
||||
OR sst.stream_id IS NULL
|
||||
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
||||
OR s.stream_type = 'LIVE_STREAM'
|
||||
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||
)
|
||||
AND (
|
||||
:uploadDateBefore IS NULL
|
||||
OR s.upload_date IS NULL
|
||||
OR s.upload_date < :uploadDateBefore
|
||||
)
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||
abstract fun getStreams(
|
||||
groupId: Long,
|
||||
includePlayed: Boolean,
|
||||
uploadDateBefore: OffsetDateTime?
|
||||
): Maybe<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
|
|
@ -3,10 +3,10 @@ package org.schabi.newpipe.database.playlist;
|
|||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public interface PlaylistLocalItem extends LocalItem {
|
||||
String getOrderingName();
|
||||
|
@ -14,14 +14,9 @@ public interface PlaylistLocalItem extends LocalItem {
|
|||
static List<PlaylistLocalItem> merge(
|
||||
final List<PlaylistMetadataEntry> localPlaylists,
|
||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||
final List<PlaylistLocalItem> items = new ArrayList<>(
|
||||
localPlaylists.size() + remotePlaylists.size());
|
||||
items.addAll(localPlaylists);
|
||||
items.addAll(remotePlaylists);
|
||||
|
||||
Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
||||
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
|
||||
|
||||
return items;
|
||||
return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
|
||||
.sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
||||
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package org.schabi.newpipe.download;
|
||||
|
||||
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
|
||||
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
|
@ -83,10 +87,6 @@ import us.shandian.giga.service.DownloadManagerService;
|
|||
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
|
||||
import us.shandian.giga.service.MissionState;
|
||||
|
||||
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
|
||||
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class DownloadDialog extends DialogFragment
|
||||
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
private static final String TAG = "DialogFragment";
|
||||
|
@ -212,8 +212,8 @@ public class DownloadDialog extends DialogFragment
|
|||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams
|
||||
= new SparseArray<>(4);
|
||||
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams =
|
||||
new SparseArray<>(4);
|
||||
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||
|
||||
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.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 24.10.15.
|
||||
|
@ -65,11 +66,11 @@ public class ErrorActivity extends AppCompatActivity {
|
|||
public static final String ERROR_EMAIL_ADDRESS = "polymorphicshade@gmail.com";
|
||||
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
|
||||
|
||||
public static final String ERROR_GITHUB_ISSUE_URL
|
||||
= "https://github.com/polymorphicshade/NewPipe/issues";
|
||||
public static final String ERROR_GITHUB_ISSUE_URL =
|
||||
"https://github.com/polymorphicshade/NewPipe/issues";
|
||||
|
||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
|
||||
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
|
||||
private ErrorInfo errorInfo;
|
||||
|
@ -182,14 +183,9 @@ public class ErrorActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private String formErrorText(final String[] el) {
|
||||
final StringBuilder text = new StringBuilder();
|
||||
if (el != null) {
|
||||
for (final String e : el) {
|
||||
text.append("-------------------------------------\n").append(e);
|
||||
}
|
||||
}
|
||||
text.append("-------------------------------------");
|
||||
return text.toString();
|
||||
final String separator = "-------------------------------------";
|
||||
return Arrays.stream(el)
|
||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,8 +14,6 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException
|
|||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
|
||||
import org.schabi.newpipe.ktx.isNetworkRelated
|
||||
import org.schabi.newpipe.util.ServiceHelper
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
@Parcelize
|
||||
class ErrorInfo(
|
||||
|
@ -80,19 +78,10 @@ class ErrorInfo(
|
|||
companion object {
|
||||
const val SERVICE_NONE = "none"
|
||||
|
||||
private fun getStackTrace(throwable: Throwable): String {
|
||||
StringWriter().use { stringWriter ->
|
||||
PrintWriter(stringWriter, true).use { printWriter ->
|
||||
throwable.printStackTrace(printWriter)
|
||||
return stringWriter.buffer.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
|
||||
|
||||
fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable))
|
||||
|
||||
fun throwableListToStringList(throwable: List<Throwable>) =
|
||||
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
|
||||
fun throwableListToStringList(throwableList: List<Throwable>) =
|
||||
throwableList.map { it.stackTraceToString() }.toTypedArray()
|
||||
|
||||
private fun getInfoServiceName(info: Info?) =
|
||||
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
|
||||
|
|
|
@ -123,13 +123,7 @@ class ErrorUtil {
|
|||
context,
|
||||
context.getString(R.string.error_report_channel_id)
|
||||
)
|
||||
.setSmallIcon(
|
||||
// the vector drawable icon causes crashes on KitKat devices
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||
R.drawable.ic_bug_report
|
||||
else
|
||||
android.R.drawable.stat_notify_error
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_bug_report)
|
||||
.setContentTitle(context.getString(R.string.error_report_notification_title))
|
||||
.setContentText(context.getString(errorInfo.messageStringId))
|
||||
.setAutoCancel(true)
|
||||
|
|
|
@ -3,14 +3,15 @@ package org.schabi.newpipe.error;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -18,7 +19,6 @@ import androidx.appcompat.app.ActionBar;
|
|||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.webkit.WebViewClientCompat;
|
||||
|
||||
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
||||
import org.schabi.newpipe.DownloaderImpl;
|
||||
|
@ -86,14 +86,15 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
|||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
|
||||
|
||||
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() {
|
||||
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
|
||||
public boolean shouldOverrideUrlLoading(final WebView view,
|
||||
final WebResourceRequest request) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "shouldOverrideUrlLoading: url=" + url);
|
||||
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString());
|
||||
}
|
||||
|
||||
handleCookiesFromUrl(url);
|
||||
handleCookiesFromUrl(request.getUrl().toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -107,12 +108,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
|||
// cleaning cache, history and cookies from webView
|
||||
recaptchaBinding.reCaptchaWebView.clearCache(true);
|
||||
recaptchaBinding.reCaptchaWebView.clearHistory();
|
||||
final CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeAllCookies(value -> { });
|
||||
} else {
|
||||
cookieManager.removeAllCookie();
|
||||
}
|
||||
CookieManager.getInstance().removeAllCookies(null);
|
||||
|
||||
recaptchaBinding.reCaptchaWebView.loadUrl(url);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -26,17 +30,9 @@ import org.schabi.newpipe.util.NavigationHelper;
|
|||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.external_communication.TextLinkifier;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
|
||||
|
||||
public class DescriptionFragment extends BaseFragment {
|
||||
|
||||
@State
|
||||
|
@ -185,8 +181,8 @@ public class DescriptionFragment extends BaseFragment {
|
|||
return;
|
||||
}
|
||||
|
||||
final ItemMetadataBinding itemBinding
|
||||
= ItemMetadataBinding.inflate(inflater, layout, false);
|
||||
final ItemMetadataBinding itemBinding =
|
||||
ItemMetadataBinding.inflate(inflater, layout, false);
|
||||
|
||||
itemBinding.metadataTypeView.setText(type);
|
||||
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
|
||||
|
@ -206,19 +202,16 @@ public class DescriptionFragment extends BaseFragment {
|
|||
|
||||
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
|
||||
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
|
||||
final ItemMetadataTagsBinding itemBinding
|
||||
= ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
|
||||
|
||||
final List<String> tags = new ArrayList<>(streamInfo.getTags());
|
||||
Collections.sort(tags);
|
||||
for (final String tag : tags) {
|
||||
streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
|
||||
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
|
||||
itemBinding.metadataTagsChips, false);
|
||||
chip.setText(tag);
|
||||
chip.setOnClickListener(this::onTagClick);
|
||||
chip.setOnLongClickListener(this::onTagLongClick);
|
||||
itemBinding.metadataTagsChips.addView(chip);
|
||||
}
|
||||
});
|
||||
|
||||
layout.addView(itemBinding.getRoot());
|
||||
}
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
|
@ -10,7 +21,6 @@ import android.content.SharedPreferences;
|
|||
import android.content.pm.ActivityInfo;
|
||||
import android.database.ContentObserver;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
|
@ -77,9 +87,9 @@ import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
|
|||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
|
@ -87,6 +97,8 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
|
|||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.ui.MainPlayerUi;
|
||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
@ -104,11 +116,11 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
|
@ -118,17 +130,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||
|
||||
public final class VideoDetailFragment
|
||||
extends BaseStateFragment<StreamInfo>
|
||||
implements BackPressable,
|
||||
|
@ -183,6 +184,8 @@ public final class VideoDetailFragment
|
|||
@State
|
||||
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||
@State
|
||||
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||
@State
|
||||
protected boolean autoPlayEnabled = true;
|
||||
|
||||
@Nullable
|
||||
|
@ -196,6 +199,7 @@ public final class VideoDetailFragment
|
|||
private Disposable videoSegmentsSubscriber = null;
|
||||
|
||||
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
|
||||
private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback;
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -208,7 +212,7 @@ public final class VideoDetailFragment
|
|||
|
||||
private ContentObserver settingsContentObserver;
|
||||
@Nullable
|
||||
private MainPlayer playerService;
|
||||
private PlayerService playerService;
|
||||
private Player player;
|
||||
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
|
||||
|
||||
|
@ -217,7 +221,7 @@ public final class VideoDetailFragment
|
|||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@Override
|
||||
public void onServiceConnected(final Player connectedPlayer,
|
||||
final MainPlayer connectedPlayerService,
|
||||
final PlayerService connectedPlayerService,
|
||||
final boolean playAfterConnect) {
|
||||
player = connectedPlayer;
|
||||
playerService = connectedPlayerService;
|
||||
|
@ -225,6 +229,7 @@ public final class VideoDetailFragment
|
|||
// It will do nothing if the player is not in fullscreen mode
|
||||
hideSystemUiIfNeeded();
|
||||
|
||||
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
|
||||
if (!player.videoPlayerSelected() && !playAfterConnect) {
|
||||
return;
|
||||
}
|
||||
|
@ -233,22 +238,19 @@ public final class VideoDetailFragment
|
|||
// If the video is playing but orientation changed
|
||||
// let's make the video in fullscreen again
|
||||
checkLandscape();
|
||||
} else if (player.isFullscreen() && !player.isVerticalVideo()
|
||||
} else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
|
||||
// Tablet UI has orientation-independent fullscreen
|
||||
&& !DeviceUtils.isTablet(activity)) {
|
||||
// Device is in portrait orientation after rotation but UI is in fullscreen.
|
||||
// Return back to non-fullscreen state
|
||||
player.toggleFullscreen();
|
||||
}
|
||||
|
||||
if (playerIsNotStopped() && player.videoPlayerSelected()) {
|
||||
addVideoPlayerView();
|
||||
playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
}
|
||||
|
||||
//noinspection SimplifyOptionalCallChains
|
||||
if (playAfterConnect
|
||||
|| (currentInfo != null
|
||||
&& isAutoplayEnabled()
|
||||
&& player.getParentActivity() == null)) {
|
||||
&& !playerUi.isPresent())) {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
|
@ -275,7 +277,7 @@ public final class VideoDetailFragment
|
|||
|
||||
public static VideoDetailFragment getInstanceInCollapsedState() {
|
||||
final VideoDetailFragment instance = new VideoDetailFragment();
|
||||
instance.bottomSheetState = BottomSheetBehavior.STATE_COLLAPSED;
|
||||
instance.updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
@ -335,6 +337,9 @@ public final class VideoDetailFragment
|
|||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onResume() called");
|
||||
}
|
||||
|
||||
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
|
||||
|
||||
|
@ -392,7 +397,7 @@ public final class VideoDetailFragment
|
|||
disposables.clear();
|
||||
positionSubscriber = null;
|
||||
currentWorker = null;
|
||||
bottomSheetBehavior.setBottomSheetCallback(null);
|
||||
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback);
|
||||
|
||||
if (activity.isFinishing()) {
|
||||
playQueue = null;
|
||||
|
@ -458,7 +463,7 @@ public final class VideoDetailFragment
|
|||
disposables.add(
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
getContext(),
|
||||
Collections.singletonList(new StreamEntity(currentInfo)),
|
||||
List.of(new StreamEntity(currentInfo)),
|
||||
dialog -> dialog.show(getFM(), TAG)
|
||||
)
|
||||
);
|
||||
|
@ -509,12 +514,18 @@ public final class VideoDetailFragment
|
|||
}
|
||||
break;
|
||||
case R.id.detail_thumbnail_root_layout:
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
// FIXME Workaround #7427
|
||||
if (isPlayerAvailable()) {
|
||||
player.setRecovery();
|
||||
// make sure not to open any player if there is nothing currently loaded!
|
||||
// FIXME removing this `if` causes the player service to start correctly, then stop,
|
||||
// then restart badly without calling `startForeground()`, causing a crash when
|
||||
// later closing the detail fragment
|
||||
if (currentInfo != null) {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
// FIXME Workaround #7427
|
||||
if (isPlayerAvailable()) {
|
||||
player.setRecovery();
|
||||
}
|
||||
openVideoPlayerAutoFullscreen();
|
||||
}
|
||||
openVideoPlayerAutoFullscreen();
|
||||
break;
|
||||
case R.id.detail_title_root_layout:
|
||||
toggleTitleAndSecondaryControls();
|
||||
|
@ -527,7 +538,7 @@ public final class VideoDetailFragment
|
|||
case R.id.overlay_play_pause_button:
|
||||
if (playerIsNotStopped()) {
|
||||
player.playPause();
|
||||
player.hideControls(0, 0);
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
||||
showSystemUi();
|
||||
} else {
|
||||
autoPlayEnabled = true; // forcefully start playing
|
||||
|
@ -592,12 +603,12 @@ public final class VideoDetailFragment
|
|||
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
|
||||
binding.detailVideoTitleView.setMaxLines(10);
|
||||
animateRotation(binding.detailToggleSecondaryControlsView,
|
||||
Player.DEFAULT_CONTROLS_DURATION, 180);
|
||||
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
|
||||
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.detailVideoTitleView.setMaxLines(1);
|
||||
animateRotation(binding.detailToggleSecondaryControlsView,
|
||||
Player.DEFAULT_CONTROLS_DURATION, 0);
|
||||
VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
|
||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||
}
|
||||
// view pager height has changed, update the tab layout
|
||||
|
@ -723,7 +734,7 @@ public final class VideoDetailFragment
|
|||
}
|
||||
|
||||
private void initThumbnailViews(@NonNull final StreamInfo info) {
|
||||
PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.detailThumbnailImageView, new Callback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
|
@ -755,7 +766,9 @@ public final class VideoDetailFragment
|
|||
|
||||
@Override
|
||||
public boolean onKeyDown(final int keyCode) {
|
||||
return isPlayerAvailable() && player.onKeyDown(keyCode);
|
||||
return isPlayerAvailable()
|
||||
&& player.UIs().get(VideoPlayerUi.class)
|
||||
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -765,7 +778,7 @@ public final class VideoDetailFragment
|
|||
}
|
||||
|
||||
// If we are in fullscreen mode just exit from it via first back press
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
if (isFullscreen()) {
|
||||
if (!DeviceUtils.isTablet(activity)) {
|
||||
player.pause();
|
||||
}
|
||||
|
@ -1015,8 +1028,7 @@ public final class VideoDetailFragment
|
|||
getChildFragmentManager().beginTransaction()
|
||||
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
|
||||
.commitAllowingStateLoss();
|
||||
binding.relatedItemsLayout.setVisibility(
|
||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
|
||||
binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1056,15 +1068,13 @@ public final class VideoDetailFragment
|
|||
// call `post()` to be sure `viewPager.getHitRect()`
|
||||
// is up to date and not being currently recomputed
|
||||
binding.tabLayout.post(() -> {
|
||||
if (getContext() != null) {
|
||||
final var activity = getActivity();
|
||||
if (activity != null) {
|
||||
final Rect pagerHitRect = new Rect();
|
||||
binding.viewPager.getHitRect(pagerHitRect);
|
||||
|
||||
final Point displaySize = new Point();
|
||||
Objects.requireNonNull(ContextCompat.getSystemService(getContext(),
|
||||
WindowManager.class)).getDefaultDisplay().getSize(displaySize);
|
||||
|
||||
final int viewPagerVisibleHeight = displaySize.y - pagerHitRect.top;
|
||||
final int height = DeviceUtils.getWindowHeight(activity.getWindowManager());
|
||||
final int viewPagerVisibleHeight = height - pagerHitRect.top;
|
||||
// see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
|
||||
final float tabLayoutHeight = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics());
|
||||
|
@ -1096,8 +1106,12 @@ public final class VideoDetailFragment
|
|||
private void toggleFullscreenIfInFullscreenMode() {
|
||||
// If a user watched video inside fullscreen mode and than chose another player
|
||||
// return to non-fullscreen mode
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
player.toggleFullscreen();
|
||||
if (isPlayerAvailable()) {
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||
if (playerUi.isFullscreen()) {
|
||||
playerUi.toggleFullscreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1173,7 +1187,7 @@ public final class VideoDetailFragment
|
|||
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
|
||||
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
|
||||
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
|
||||
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||
updateBottomSheetState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
// toggle landscape in order to open directly in fullscreen
|
||||
onScreenRotationButtonClicked();
|
||||
}
|
||||
|
@ -1223,16 +1237,10 @@ public final class VideoDetailFragment
|
|||
}
|
||||
|
||||
final PlayQueue queue = setupPlayQueueForIntent(false);
|
||||
|
||||
// Video view can have elements visible from popup,
|
||||
// We hide it here but once it ready the view will be shown in handleIntent()
|
||||
if (playerService.getView() != null) {
|
||||
playerService.getView().setVisibility(View.GONE);
|
||||
}
|
||||
addVideoPlayerView();
|
||||
tryAddVideoPlayerView();
|
||||
|
||||
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
|
||||
MainPlayer.class, queue, true, autoPlayEnabled);
|
||||
PlayerService.class, queue, true, autoPlayEnabled);
|
||||
ContextCompat.startForegroundService(activity, playerIntent);
|
||||
}
|
||||
|
||||
|
@ -1244,8 +1252,8 @@ public final class VideoDetailFragment
|
|||
* be reused in a few milliseconds and the flickering would be annoying.
|
||||
*/
|
||||
private void hideMainPlayerOnLoadingNewStream() {
|
||||
if (!isPlayerServiceAvailable()
|
||||
|| playerService.getView() == null
|
||||
//noinspection SimplifyOptionalCallChains
|
||||
if (!isPlayerServiceAvailable() || !getRoot().isPresent()
|
||||
|| !player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
|
@ -1253,7 +1261,7 @@ public final class VideoDetailFragment
|
|||
removeVideoPlayerView();
|
||||
if (isAutoplayEnabled()) {
|
||||
playerService.stopForImmediateReusing();
|
||||
playerService.getView().setVisibility(View.GONE);
|
||||
getRoot().ifPresent(view -> view.setVisibility(View.GONE));
|
||||
} else {
|
||||
playerHolder.stopService();
|
||||
}
|
||||
|
@ -1310,27 +1318,41 @@ public final class VideoDetailFragment
|
|||
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
|
||||
}
|
||||
|
||||
private void addVideoPlayerView() {
|
||||
if (!isPlayerAvailable() || getView() == null) {
|
||||
return;
|
||||
private void tryAddVideoPlayerView() {
|
||||
if (isPlayerAvailable() && getView() != null) {
|
||||
// Setup the surface view height, so that it fits the video correctly; this is done also
|
||||
// here, and not only in the Handler, to avoid a choppy fullscreen rotation animation.
|
||||
setHeightThumbnail();
|
||||
}
|
||||
|
||||
// Check if viewHolder already contains a child
|
||||
if (player.getRootView().getParent() != binding.playerPlaceholder) {
|
||||
playerService.removeViewFromParent();
|
||||
}
|
||||
setHeightThumbnail();
|
||||
// do all the null checks in the posted lambda, too, since the player, the binding and the
|
||||
// view could be set or unset before the lambda gets executed on the next main thread cycle
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
if (!isPlayerAvailable() || getView() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent from re-adding a view multiple times
|
||||
if (player.getRootView().getParent() == null) {
|
||||
binding.playerPlaceholder.addView(player.getRootView());
|
||||
}
|
||||
// setup the surface view height, so that it fits the video correctly
|
||||
setHeightThumbnail();
|
||||
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||
// sometimes binding would be null here, even though getView() != null above u.u
|
||||
if (binding != null) {
|
||||
// prevent from re-adding a view multiple times
|
||||
playerUi.removeViewFromParent();
|
||||
binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
|
||||
playerUi.setupVideoSurfaceIfNeeded();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void removeVideoPlayerView() {
|
||||
makeDefaultHeightForVideoPlaceholder();
|
||||
|
||||
playerService.removeViewFromParent();
|
||||
if (player != null) {
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
|
||||
}
|
||||
}
|
||||
|
||||
private void makeDefaultHeightForVideoPlaceholder() {
|
||||
|
@ -1371,7 +1393,7 @@ public final class VideoDetailFragment
|
|||
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
||||
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
|
||||
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
if (isFullscreen()) {
|
||||
final int height = (DeviceUtils.isInMultiWindow(activity)
|
||||
? requireView()
|
||||
: activity.getWindow().getDecorView()).getHeight();
|
||||
|
@ -1396,8 +1418,9 @@ public final class VideoDetailFragment
|
|||
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
|
||||
if (isPlayerAvailable()) {
|
||||
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
|
||||
player.getSurfaceView()
|
||||
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight);
|
||||
player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
|
||||
ui.getBinding().surfaceView.setHeights(newHeight,
|
||||
ui.isFullscreen() ? newHeight : maxHeight));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1526,7 +1549,7 @@ public final class VideoDetailFragment
|
|||
if (binding.relatedItemsLayout != null) {
|
||||
if (showRelatedItems) {
|
||||
binding.relatedItemsLayout.setVisibility(
|
||||
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||
isFullscreen() ? View.GONE : View.INVISIBLE);
|
||||
} else {
|
||||
binding.relatedItemsLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
@ -1560,7 +1583,8 @@ public final class VideoDetailFragment
|
|||
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy);
|
||||
final Drawable buddyDrawable =
|
||||
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
|
||||
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
|
||||
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
|
||||
|
||||
|
@ -1819,6 +1843,11 @@ public final class VideoDetailFragment
|
|||
// Player event listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onViewCreated() {
|
||||
tryAddVideoPlayerView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueueUpdate(final PlayQueue queue) {
|
||||
playQueue = queue;
|
||||
|
@ -1939,15 +1968,10 @@ public final class VideoDetailFragment
|
|||
@Override
|
||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||
setupBrightness();
|
||||
//noinspection SimplifyOptionalCallChains
|
||||
if (!isPlayerAndPlayerServiceAvailable()
|
||||
|| playerService.getView() == null
|
||||
|| player.getParentActivity() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final View view = playerService.getView();
|
||||
final ViewGroup parent = (ViewGroup) view.getParent();
|
||||
if (parent == null) {
|
||||
|| !player.UIs().get(MainPlayerUi.class).isPresent()
|
||||
|| getRoot().map(View::getParent).orElse(null) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1963,13 +1987,7 @@ public final class VideoDetailFragment
|
|||
}
|
||||
scrollToTop();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addVideoPlayerView();
|
||||
} else {
|
||||
// KitKat needs a delay before addVideoPlayerView call or it reports wrong height in
|
||||
// activity.getWindow().getDecorView().getHeight()
|
||||
new Handler().post(this::addVideoPlayerView);
|
||||
}
|
||||
tryAddVideoPlayerView();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1981,7 +1999,7 @@ public final class VideoDetailFragment
|
|||
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||
if (DeviceUtils.isTablet(activity)
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||
player.toggleFullscreen();
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2032,10 +2050,8 @@ public final class VideoDetailFragment
|
|||
}
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(0);
|
||||
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
|
||||
requireContext(), android.R.attr.colorPrimary));
|
||||
}
|
||||
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
|
||||
requireContext(), android.R.attr.colorPrimary));
|
||||
}
|
||||
|
||||
private void hideSystemUi() {
|
||||
|
@ -2066,8 +2082,7 @@ public final class VideoDetailFragment
|
|||
}
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
|
||||
if (isInMultiWindow || isFullscreen()) {
|
||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||
}
|
||||
|
@ -2075,14 +2090,19 @@ public final class VideoDetailFragment
|
|||
}
|
||||
|
||||
// Listener implementation
|
||||
@Override
|
||||
public void hideSystemUiIfNeeded() {
|
||||
if (isPlayerAvailable()
|
||||
&& player.isFullscreen()
|
||||
if (isFullscreen()
|
||||
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
hideSystemUi();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFullscreen() {
|
||||
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
|
||||
.map(VideoPlayerUi::isFullscreen).orElse(false);
|
||||
}
|
||||
|
||||
private boolean playerIsNotStopped() {
|
||||
return isPlayerAvailable() && !player.isStopped();
|
||||
}
|
||||
|
@ -2105,10 +2125,7 @@ public final class VideoDetailFragment
|
|||
}
|
||||
|
||||
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
|
||||
if (!isPlayerAvailable()
|
||||
|| !player.videoPlayerSelected()
|
||||
|| !player.isFullscreen()
|
||||
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
||||
if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|
||||
// Apply system brightness when the player is not in fullscreen
|
||||
restoreDefaultBrightness();
|
||||
} else {
|
||||
|
@ -2132,7 +2149,7 @@ public final class VideoDetailFragment
|
|||
setAutoPlay(true);
|
||||
}
|
||||
|
||||
player.checkLandscape();
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
|
||||
// Let's give a user time to look at video information page if video is not playing
|
||||
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
||||
player.play();
|
||||
|
@ -2211,12 +2228,8 @@ public final class VideoDetailFragment
|
|||
} else {
|
||||
final int selectedVideoStreamIndexForExternalPlayers =
|
||||
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
|
||||
final CharSequence[] resolutions =
|
||||
new CharSequence[videoStreamsForExternalPlayers.size()];
|
||||
|
||||
for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
|
||||
resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
|
||||
}
|
||||
final CharSequence[] resolutions = videoStreamsForExternalPlayers.stream()
|
||||
.map(VideoStream::getResolution).toArray(CharSequence[]::new);
|
||||
|
||||
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
|
||||
null);
|
||||
|
@ -2320,7 +2333,9 @@ public final class VideoDetailFragment
|
|||
|
||||
final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder);
|
||||
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
|
||||
bottomSheetBehavior.setState(bottomSheetState);
|
||||
bottomSheetBehavior.setState(lastStableBottomSheetState);
|
||||
updateBottomSheetState(lastStableBottomSheetState);
|
||||
|
||||
final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
|
||||
if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) {
|
||||
manageSpaceAtTheBottom(false);
|
||||
|
@ -2333,10 +2348,10 @@ public final class VideoDetailFragment
|
|||
}
|
||||
}
|
||||
|
||||
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
||||
bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
|
||||
@Override
|
||||
public void onStateChanged(@NonNull final View bottomSheet, final int newState) {
|
||||
bottomSheetState = newState;
|
||||
updateBottomSheetState(newState);
|
||||
|
||||
switch (newState) {
|
||||
case BottomSheetBehavior.STATE_HIDDEN:
|
||||
|
@ -2359,10 +2374,10 @@ public final class VideoDetailFragment
|
|||
if (DeviceUtils.isLandscape(requireContext())
|
||||
&& isPlayerAvailable()
|
||||
&& player.isPlaying()
|
||||
&& !player.isFullscreen()
|
||||
&& !DeviceUtils.isTablet(activity)
|
||||
&& player.videoPlayerSelected()) {
|
||||
player.toggleFullscreen();
|
||||
&& !isFullscreen()
|
||||
&& !DeviceUtils.isTablet(activity)) {
|
||||
player.UIs().get(MainPlayerUi.class)
|
||||
.ifPresent(MainPlayerUi::toggleFullscreen);
|
||||
}
|
||||
setOverlayLook(binding.appBarLayout, behavior, 1);
|
||||
break;
|
||||
|
@ -2375,19 +2390,26 @@ public final class VideoDetailFragment
|
|||
// Re-enable clicks
|
||||
setOverlayElementsClickable(true);
|
||||
if (isPlayerAvailable()) {
|
||||
player.closeItemsList();
|
||||
player.UIs().get(MainPlayerUi.class)
|
||||
.ifPresent(MainPlayerUi::closeItemsList);
|
||||
}
|
||||
setOverlayLook(binding.appBarLayout, behavior, 0);
|
||||
break;
|
||||
case BottomSheetBehavior.STATE_DRAGGING:
|
||||
case BottomSheetBehavior.STATE_SETTLING:
|
||||
if (isPlayerAvailable() && player.isFullscreen()) {
|
||||
if (isFullscreen()) {
|
||||
showSystemUi();
|
||||
}
|
||||
if (isPlayerAvailable() && player.isControlsVisible()) {
|
||||
player.hideControls(0, 0);
|
||||
if (isPlayerAvailable()) {
|
||||
player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
|
||||
if (ui.isControlsVisible()) {
|
||||
ui.hideControls(0, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case BottomSheetBehavior.STATE_HALF_EXPANDED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2395,7 +2417,9 @@ public final class VideoDetailFragment
|
|||
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
|
||||
setOverlayLook(binding.appBarLayout, behavior, slideOffset);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
|
||||
|
||||
// User opened a new page and the player will hide itself
|
||||
activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> {
|
||||
|
@ -2410,8 +2434,8 @@ public final class VideoDetailFragment
|
|||
@Nullable final String thumbnailUrl) {
|
||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
binding.overlayThumbnail.setImageDrawable(null);
|
||||
PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
|
||||
.into(binding.overlayThumbnail);
|
||||
}
|
||||
|
||||
|
@ -2459,4 +2483,21 @@ public final class VideoDetailFragment
|
|||
boolean isPlayerAndPlayerServiceAvailable() {
|
||||
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.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -28,9 +29,7 @@ import org.schabi.newpipe.player.Player;
|
|||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
|
@ -43,50 +42,34 @@ public final class VideoDetailPlayerCrasher {
|
|||
// https://stackoverflow.com/a/54744028
|
||||
private static final String TAG = "VideoDetPlayerCrasher";
|
||||
|
||||
private static final Map<String, Supplier<ExoPlaybackException>> AVAILABLE_EXCEPTION_TYPES =
|
||||
getExceptionTypes();
|
||||
private static final String DEFAULT_MSG = "Dummy";
|
||||
|
||||
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() {
|
||||
// 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) {
|
||||
return new ContextThemeWrapper(
|
||||
context,
|
||||
|
@ -121,10 +104,9 @@ public final class VideoDetailPlayerCrasher {
|
|||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
for (final Map.Entry<String, Supplier<ExoPlaybackException>> entry
|
||||
: AVAILABLE_EXCEPTION_TYPES.entrySet()) {
|
||||
for (final Pair<String, Supplier<ExoPlaybackException>> entry : AVAILABLE_EXCEPTION_TYPES) {
|
||||
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
|
||||
radioButton.setText(entry.getKey());
|
||||
radioButton.setText(entry.first);
|
||||
radioButton.setChecked(false);
|
||||
radioButton.setLayoutParams(
|
||||
new RadioGroup.LayoutParams(
|
||||
|
@ -133,7 +115,7 @@ public final class VideoDetailPlayerCrasher {
|
|||
)
|
||||
);
|
||||
radioButton.setOnClickListener(v -> {
|
||||
tryCrashPlayerWith(player, entry.getValue().get());
|
||||
tryCrashPlayerWith(player, entry.second.get());
|
||||
alertDialog.cancel();
|
||||
});
|
||||
binding.list.addView(radioButton);
|
||||
|
|
|
@ -23,14 +23,11 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
@ -264,45 +261,28 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
|||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final ChannelInfoItem selectedItem) {
|
||||
try {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openChannelFragment(getFM(),
|
||||
selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(
|
||||
BaseListFragment.this, "Opening channel fragment", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final PlaylistInfoItem selectedItem) {
|
||||
try {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(getFM(),
|
||||
selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(BaseListFragment.this,
|
||||
"Opening playlist fragment", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final CommentsInfoItem selectedItem) {
|
||||
infoListAdapter.setOnChannelSelectedListener(selectedItem -> {
|
||||
try {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openChannelFragment(getFM(), selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(), selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnPlaylistSelectedListener(selectedItem -> {
|
||||
try {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(getFM(), selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(), selectedItem.getName());
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Opening playlist fragment", e);
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnCommentsSelectedListener(this::onItemSelected);
|
||||
|
||||
// Ensure that there is always a scroll listener (e.g. when rotating the device)
|
||||
useNormalItemListScrollListener();
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
|||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
@ -578,17 +578,13 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
|||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
|
||||
.filter(StreamInfoItem.class::isInstance)
|
||||
.map(StreamInfoItem.class::cast)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
||||
currentInfo.getNextPage(), streamItems, index);
|
||||
currentInfo.getNextPage(), streamItems, 0);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -43,7 +43,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
|||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
|
|
@ -200,7 +200,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
|
||||
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
|
||||
|
||||
suggestionListAdapter = new SuggestionListAdapter(activity);
|
||||
suggestionListAdapter = new SuggestionListAdapter();
|
||||
historyRecordManager = new HistoryRecordManager(context);
|
||||
}
|
||||
|
||||
|
@ -340,6 +340,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
|
||||
// animations are just strange and useless, since the suggestions keep changing too much
|
||||
searchBinding.suggestionsList.setItemAnimator(null);
|
||||
new ItemTouchHelper(new ItemTouchHelper.Callback() {
|
||||
@Override
|
||||
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
|
||||
|
@ -497,9 +499,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
+ lastSearchedString);
|
||||
}
|
||||
searchEditText.setText(searchString);
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
searchEditText.setHintTextColor(searchEditText.getTextColors().withAlpha(128));
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||
searchToolbarContainer.setTranslationX(100);
|
||||
|
@ -533,7 +532,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
searchBinding.correctSuggestion.setVisibility(View.GONE);
|
||||
|
||||
searchEditText.setText("");
|
||||
suggestionListAdapter.setItems(new ArrayList<>());
|
||||
suggestionListAdapter.submitList(null);
|
||||
showKeyboardSearch();
|
||||
});
|
||||
|
||||
|
@ -922,7 +921,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
filterItemCheckedId = item.getItemId();
|
||||
item.setChecked(true);
|
||||
|
||||
contentFilter = new String[]{theContentFilter.get(0)};
|
||||
contentFilter = theContentFilter.toArray(new String[0]);
|
||||
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
|
@ -947,8 +946,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
if (DEBUG) {
|
||||
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||
}
|
||||
searchBinding.suggestionsList.smoothScrollToPosition(0);
|
||||
searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));
|
||||
suggestionListAdapter.submitList(suggestions,
|
||||
() -> searchBinding.suggestionsList.scrollToPosition(0));
|
||||
|
||||
if (suggestionsPanelVisible && isErrorPanelVisible()) {
|
||||
hideLoading();
|
||||
|
@ -983,8 +982,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
isCorrectedSearch = result.isCorrectedSearch();
|
||||
|
||||
// List<MetaInfo> cannot be bundled without creating some containers
|
||||
metaInfo = new MetaInfo[result.getMetaInfo().size()];
|
||||
metaInfo = result.getMetaInfo().toArray(metaInfo);
|
||||
metaInfo = result.getMetaInfo().toArray(new MetaInfo[0]);
|
||||
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
|
||||
searchBinding.searchMetaInfoSeparator, disposables);
|
||||
|
||||
|
@ -1070,14 +1068,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
return 0;
|
||||
}
|
||||
|
||||
final SuggestionItem item = suggestionListAdapter.getItem(position);
|
||||
final SuggestionItem item = suggestionListAdapter.getCurrentList().get(position);
|
||||
return item.fromHistory ? makeMovementFlags(0,
|
||||
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
|
||||
}
|
||||
|
||||
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getBindingAdapterPosition();
|
||||
final String query = suggestionListAdapter.getItem(position).query;
|
||||
final String query = suggestionListAdapter.getCurrentList().get(position).query;
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
|
|
|
@ -1,34 +1,22 @@
|
|||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.schabi.newpipe.databinding.ItemSearchSuggestionBinding;
|
||||
|
||||
public class SuggestionListAdapter
|
||||
extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
|
||||
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
||||
private final Context context;
|
||||
extends ListAdapter<SuggestionItem, SuggestionListAdapter.SuggestionItemHolder> {
|
||||
private OnSuggestionItemSelected listener;
|
||||
|
||||
public SuggestionListAdapter(final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setItems(final List<SuggestionItem> items) {
|
||||
this.items.clear();
|
||||
this.items.addAll(items);
|
||||
notifyDataSetChanged();
|
||||
public SuggestionListAdapter() {
|
||||
super(new SuggestionItemCallback());
|
||||
}
|
||||
|
||||
public void setListener(final OnSuggestionItemSelected listener) {
|
||||
|
@ -39,45 +27,32 @@ public class SuggestionListAdapter
|
|||
@Override
|
||||
public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent,
|
||||
final int viewType) {
|
||||
return new SuggestionItemHolder(LayoutInflater.from(context)
|
||||
.inflate(R.layout.item_search_suggestion, parent, false));
|
||||
return new SuggestionItemHolder(ItemSearchSuggestionBinding
|
||||
.inflate(LayoutInflater.from(parent.getContext()), parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final SuggestionItemHolder holder, final int position) {
|
||||
final SuggestionItem currentItem = getItem(position);
|
||||
holder.updateFrom(currentItem);
|
||||
holder.queryView.setOnClickListener(v -> {
|
||||
holder.itemBinding.suggestionSearch.setOnClickListener(v -> {
|
||||
if (listener != null) {
|
||||
listener.onSuggestionItemSelected(currentItem);
|
||||
}
|
||||
});
|
||||
holder.queryView.setOnLongClickListener(v -> {
|
||||
holder.itemBinding.suggestionSearch.setOnLongClickListener(v -> {
|
||||
if (listener != null) {
|
||||
listener.onSuggestionItemLongClick(currentItem);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
holder.insertView.setOnClickListener(v -> {
|
||||
holder.itemBinding.suggestionInsert.setOnClickListener(v -> {
|
||||
if (listener != null) {
|
||||
listener.onSuggestionItemInserted(currentItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SuggestionItem getItem(final int position) {
|
||||
return items.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return getItemCount() == 0;
|
||||
}
|
||||
|
||||
public interface OnSuggestionItemSelected {
|
||||
void onSuggestionItemSelected(SuggestionItem item);
|
||||
|
||||
|
@ -87,30 +62,32 @@ public class SuggestionListAdapter
|
|||
}
|
||||
|
||||
public static final class SuggestionItemHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView itemSuggestionQuery;
|
||||
private final ImageView suggestionIcon;
|
||||
private final View queryView;
|
||||
private final View insertView;
|
||||
private final ItemSearchSuggestionBinding itemBinding;
|
||||
|
||||
// Cache some ids, as they can potentially be constantly updated/recycled
|
||||
private final int historyResId;
|
||||
private final int searchResId;
|
||||
|
||||
private SuggestionItemHolder(final View rootView) {
|
||||
super(rootView);
|
||||
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
|
||||
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
|
||||
|
||||
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 SuggestionItemHolder(final ItemSearchSuggestionBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.itemBinding = binding;
|
||||
}
|
||||
|
||||
private void updateFrom(final SuggestionItem item) {
|
||||
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
|
||||
itemSuggestionQuery.setText(item.query);
|
||||
itemBinding.itemSuggestionIcon.setImageResource(item.fromHistory ? R.drawable.ic_history
|
||||
: 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,
|
||||
final HistoryRecordManager historyRecordManager,
|
||||
final boolean useMiniVariant) {
|
||||
final InfoItemHolder holder
|
||||
= holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||
final InfoItemHolder holder =
|
||||
holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||
holder.updateFromItem(infoItem, historyRecordManager);
|
||||
return holder.itemView;
|
||||
}
|
||||
|
|
|
@ -321,6 +321,7 @@ public final class InfoItemDialog {
|
|||
*/
|
||||
public Builder addDefaultEndEntries() {
|
||||
addAllEntries(
|
||||
StreamDialogDefaultEntry.DOWNLOAD,
|
||||
StreamDialogDefaultEntry.APPEND_PLAYLIST,
|
||||
StreamDialogDefaultEntry.SHARE,
|
||||
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.SparseItemUtil.fetchItemInfoIfSparse;
|
||||
import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase;
|
||||
import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse;
|
||||
|
||||
import android.net.Uri;
|
||||
|
@ -11,6 +12,7 @@ import androidx.annotation.StringRes;
|
|||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
|
@ -18,7 +20,7 @@ import org.schabi.newpipe.util.NavigationHelper;
|
|||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
|
||||
|
@ -87,7 +89,7 @@ public enum StreamDialogDefaultEntry {
|
|||
APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) ->
|
||||
PlaylistDialog.createCorrespondingDialog(
|
||||
fragment.getContext(),
|
||||
Collections.singletonList(new StreamEntity(item)),
|
||||
List.of(new StreamEntity(item)),
|
||||
dialog -> dialog.show(
|
||||
fragment.getParentFragmentManager(),
|
||||
"StreamDialogEntry@"
|
||||
|
@ -110,6 +112,15 @@ public enum StreamDialogDefaultEntry {
|
|||
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
||||
item.getThumbnailUrl())),
|
||||
|
||||
DOWNLOAD(R.string.download, (fragment, item) ->
|
||||
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
|
||||
item.getUrl(), info -> {
|
||||
final DownloadDialog downloadDialog =
|
||||
new DownloadDialog(fragment.requireContext(), info);
|
||||
downloadDialog.show(fragment.getChildFragmentManager(), "downloadDialog");
|
||||
})
|
||||
),
|
||||
|
||||
OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) ->
|
||||
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
|||
itemTitleView.setText(item.getName());
|
||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||
|
||||
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||
|
|
|
@ -23,9 +23,9 @@ import org.schabi.newpipe.util.CommentTextOnTouchListener;
|
|||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
|
@ -204,8 +204,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||
boolean hasEllipsis = false;
|
||||
|
||||
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||
final int endOfLastLine
|
||||
= itemContentView.getLayout().getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
||||
final int endOfLastLine = itemContentView
|
||||
.getLayout()
|
||||
.getLineEnd(COMMENT_DEFAULT_LINES - 1);
|
||||
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
|
||||
if (end == -1) {
|
||||
end = Math.max(endOfLastLine - 2, 0);
|
||||
|
|
|
@ -14,8 +14,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||
|
||||
|
@ -111,8 +111,9 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||
final HistoryRecordManager historyRecordManager) {
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
||||
final StreamStateEntity state
|
||||
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadStreamState(infoItem)
|
||||
.blockingGet()[0];
|
||||
if (state != null && item.getDuration() > 0
|
||||
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
|
|
|
@ -21,10 +21,6 @@ import org.schabi.newpipe.MainActivity
|
|||
|
||||
private const val TAG = "ViewUtils"
|
||||
|
||||
inline var View.backgroundTintListCompat: ColorStateList?
|
||||
get() = ViewCompat.getBackgroundTintList(this)
|
||||
set(value) = ViewCompat.setBackgroundTintList(this, value)
|
||||
|
||||
/**
|
||||
* Animate the view.
|
||||
*
|
||||
|
@ -96,62 +92,43 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
|
|||
if (MainActivity.DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"animateBackgroundColor() called with: " +
|
||||
"view = [" + this + "], duration = [" + duration + "], " +
|
||||
"colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"
|
||||
"animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
|
||||
"colorStart = [$colorStart], colorEnd = [$colorEnd]"
|
||||
)
|
||||
}
|
||||
val empty = arrayOf(IntArray(0))
|
||||
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
|
||||
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
|
||||
viewPropertyAnimator.duration = duration
|
||||
viewPropertyAnimator.addUpdateListener { animation: ValueAnimator ->
|
||||
backgroundTintListCompat = ColorStateList(empty, intArrayOf(animation.animatedValue as Int))
|
||||
|
||||
fun listenerAction(color: Int) {
|
||||
ViewCompat.setBackgroundTintList(this, ColorStateList.valueOf(color))
|
||||
}
|
||||
viewPropertyAnimator.addListener(
|
||||
onCancel = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) },
|
||||
onEnd = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) }
|
||||
)
|
||||
viewPropertyAnimator.addUpdateListener { listenerAction(it.animatedValue as Int) }
|
||||
viewPropertyAnimator.addListener(onCancel = { listenerAction(colorEnd) }, onEnd = { listenerAction(colorEnd) })
|
||||
viewPropertyAnimator.start()
|
||||
}
|
||||
|
||||
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"animateHeight: duration = [" + duration + "], " +
|
||||
"from " + height + " to → " + targetHeight + " in: " + this
|
||||
)
|
||||
Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
|
||||
}
|
||||
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
|
||||
animator.interpolator = FastOutSlowInInterpolator()
|
||||
animator.duration = duration
|
||||
animator.addUpdateListener { animation: ValueAnimator ->
|
||||
val value = animation.animatedValue as Float
|
||||
layoutParams.height = value.toInt()
|
||||
|
||||
fun listenerAction(value: Int) {
|
||||
layoutParams.height = value
|
||||
requestLayout()
|
||||
}
|
||||
animator.addListener(
|
||||
onCancel = {
|
||||
layoutParams.height = targetHeight
|
||||
requestLayout()
|
||||
},
|
||||
onEnd = {
|
||||
layoutParams.height = targetHeight
|
||||
requestLayout()
|
||||
}
|
||||
)
|
||||
animator.addUpdateListener { listenerAction((it.animatedValue as Float).toInt()) }
|
||||
animator.addListener(onCancel = { listenerAction(targetHeight) }, onEnd = { listenerAction(targetHeight) })
|
||||
animator.start()
|
||||
return animator
|
||||
}
|
||||
|
||||
fun View.animateRotation(duration: Long, targetRotation: Int) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"animateRotation: duration = [" + duration + "], " +
|
||||
"from " + rotation + " to → " + targetRotation + " in: " + this
|
||||
)
|
||||
Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
|
||||
}
|
||||
animate().setListener(null).cancel()
|
||||
animate()
|
||||
|
@ -172,20 +149,13 @@ private fun View.animateAlpha(enterOrExit: Boolean, duration: Long, delay: Long,
|
|||
if (enterOrExit) {
|
||||
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(ExecOnEndListener(execOnEnd))
|
||||
.start()
|
||||
} else {
|
||||
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,11 +167,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
|
|||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(ExecOnEndListener(execOnEnd))
|
||||
.start()
|
||||
} else {
|
||||
scaleX = 1f
|
||||
scaleY = 1f
|
||||
|
@ -209,12 +176,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
|
|||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,11 +190,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
|
|||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(ExecOnEndListener(execOnEnd))
|
||||
.start()
|
||||
} else {
|
||||
alpha = 1f
|
||||
scaleX = 1f
|
||||
|
@ -240,12 +200,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
|
|||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).scaleX(.95f).scaleY(.95f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -256,22 +212,15 @@ private fun View.animateSlideAndAlpha(enterOrExit: Boolean, duration: Long, dela
|
|||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(ExecOnEndListener(execOnEnd))
|
||||
.start()
|
||||
} else {
|
||||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).translationY(-height.toFloat())
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,21 +231,14 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
|
|||
animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(ExecOnEndListener(execOnEnd))
|
||||
.start()
|
||||
} else {
|
||||
animate().setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).translationY(-height / 2.0f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
isGone = true
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}).start()
|
||||
.setListener(HideAndExecOnEndListener(this, execOnEnd))
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -318,11 +260,7 @@ fun View.slideUp(
|
|||
.setStartDelay(delay)
|
||||
.setDuration(duration)
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
})
|
||||
.setListener(ExecOnEndListener(execOnEnd))
|
||||
.start()
|
||||
}
|
||||
|
||||
|
@ -336,6 +274,20 @@ fun View.animateHideRecyclerViewAllowingScrolling() {
|
|||
animate().alpha(0.0f).setDuration(200).start()
|
||||
}
|
||||
|
||||
private open class ExecOnEndListener(private val execOnEnd: Runnable?) : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
execOnEnd?.run()
|
||||
}
|
||||
}
|
||||
|
||||
private class HideAndExecOnEndListener(private val view: View, execOnEnd: Runnable?) :
|
||||
ExecOnEndListener(execOnEnd) {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
view.isGone = true
|
||||
super.onAnimationEnd(animation)
|
||||
}
|
||||
}
|
||||
|
||||
enum class AnimationType {
|
||||
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final LocalItem selectedItem) {
|
||||
final FragmentManager fragmentManager = getFM();
|
||||
|
@ -256,8 +256,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
|||
}
|
||||
|
||||
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
|
||||
final DialogEditTextBinding dialogBinding
|
||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
final DialogEditTextBinding dialogBinding =
|
||||
DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
dialogBinding.dialogEditText.setText(selectedItem.name);
|
||||
|
|
|
@ -13,12 +13,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.local.LocalItemListAdapter;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -63,18 +61,10 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
|
||||
|
||||
playlistAdapter = new LocalItemListAdapter(getActivity());
|
||||
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
@Override
|
||||
public void selected(final LocalItem selectedItem) {
|
||||
if (!(selectedItem instanceof PlaylistMetadataEntry)
|
||||
|| getStreamEntities() == null) {
|
||||
return;
|
||||
}
|
||||
onPlaylistSelected(
|
||||
playlistManager,
|
||||
(PlaylistMetadataEntry) selectedItem,
|
||||
getStreamEntities()
|
||||
);
|
||||
playlistAdapter.setSelectedListener(selectedItem -> {
|
||||
final List<StreamEntity> entities = getStreamEntities();
|
||||
if (selectedItem instanceof PlaylistMetadataEntry && entities != null) {
|
||||
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -138,14 +128,11 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
||||
@NonNull final PlaylistMetadataEntry playlist,
|
||||
@NonNull final List<StreamEntity> streams) {
|
||||
if (getStreamEntities() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Toast successToast = Toast.makeText(getContext(),
|
||||
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
|
||||
|
||||
if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) {
|
||||
if (playlist.thumbnailUrl
|
||||
.equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) {
|
||||
playlistDisposables.add(manager
|
||||
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
|
|
@ -45,8 +45,8 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
|
|||
return super.onCreateDialog(savedInstanceState);
|
||||
}
|
||||
|
||||
final DialogEditTextBinding dialogBinding
|
||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
final DialogEditTextBinding dialogBinding =
|
||||
DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
|
|
|
@ -9,15 +9,20 @@ import android.view.Window;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
@ -131,13 +136,13 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
|||
* @param context context used for accessing the database
|
||||
* @param streamEntities used for crating the dialog
|
||||
* @param onExec execution that should occur after a dialog got created, e.g. showing it
|
||||
* @return Disposable
|
||||
* @return the disposable that was created
|
||||
*/
|
||||
public static Disposable createCorrespondingDialog(
|
||||
final Context context,
|
||||
final List<StreamEntity> streamEntities,
|
||||
final Consumer<PlaylistDialog> onExec
|
||||
) {
|
||||
final Consumer<PlaylistDialog> onExec) {
|
||||
|
||||
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
|
||||
.hasPlaylists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -147,4 +152,30 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
|
|||
: 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 getStreams(
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
getPlayedStreams: Boolean = true
|
||||
groupId: Long,
|
||||
includePlayedStreams: Boolean,
|
||||
includeFutureStreams: Boolean
|
||||
): Maybe<List<StreamWithState>> {
|
||||
return when (groupId) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> {
|
||||
if (getPlayedStreams) feedTable.getAllStreams()
|
||||
else feedTable.getLiveOrNotPlayedStreams()
|
||||
}
|
||||
else -> {
|
||||
if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId)
|
||||
else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId)
|
||||
}
|
||||
}
|
||||
return feedTable.getStreams(
|
||||
groupId,
|
||||
includePlayedStreams,
|
||||
if (includeFutureStreams) null else OffsetDateTime.now()
|
||||
)
|
||||
}
|
||||
|
||||
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)
|
||||
|
|
|
@ -41,6 +41,7 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.MenuItemCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
|
@ -98,6 +99,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||
|
||||
private lateinit var groupAdapter: GroupieAdapter
|
||||
@State @JvmField var showPlayedItems: Boolean = true
|
||||
@State @JvmField var showFutureItems: Boolean = true
|
||||
|
||||
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
|
||||
private var updateListViewModeOnResume = false
|
||||
|
@ -134,9 +136,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
|
||||
val factory = FeedViewModel.Factory(requireContext(), groupId)
|
||||
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
||||
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
||||
viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
|
||||
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
||||
showFutureItems = viewModel.getShowFutureItemsFromPreferences()
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
|
||||
|
||||
groupAdapter = GroupieAdapter().apply {
|
||||
|
@ -212,6 +215,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||
|
||||
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
||||
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
|
||||
updateToggleFutureItemsButton(menu.findItem(R.id.menu_item_feed_toggle_future_items))
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
@ -241,6 +245,11 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||
updateTogglePlayedItemsButton(item)
|
||||
viewModel.togglePlayedItems(showPlayedItems)
|
||||
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
|
||||
} else if (item.itemId == R.id.menu_item_feed_toggle_future_items) {
|
||||
showFutureItems = !item.isChecked
|
||||
updateToggleFutureItemsButton(item)
|
||||
viewModel.toggleFutureItems(showFutureItems)
|
||||
viewModel.saveShowFutureItemsToPreferences(showFutureItems)
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
|
@ -278,6 +287,32 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||
requireContext(),
|
||||
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
|
||||
)
|
||||
MenuItemCompat.setTooltipText(
|
||||
menuItem,
|
||||
getString(
|
||||
if (showPlayedItems)
|
||||
R.string.feed_toggle_hide_played_items
|
||||
else
|
||||
R.string.feed_toggle_show_played_items
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateToggleFutureItemsButton(menuItem: MenuItem) {
|
||||
menuItem.isChecked = showFutureItems
|
||||
menuItem.icon = AppCompatResources.getDrawable(
|
||||
requireContext(),
|
||||
if (showFutureItems) R.drawable.ic_history_future else R.drawable.ic_history
|
||||
)
|
||||
MenuItemCompat.setTooltipText(
|
||||
menuItem,
|
||||
getString(
|
||||
if (showFutureItems)
|
||||
R.string.feed_toggle_hide_future_items
|
||||
else
|
||||
R.string.feed_toggle_show_future_items
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.functions.Function4
|
||||
import io.reactivex.rxjava3.functions.Function5
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.App
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
|
@ -26,17 +29,23 @@ import java.time.OffsetDateTime
|
|||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(
|
||||
private val applicationContext: Context,
|
||||
private val application: Application,
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
initialShowPlayedItems: Boolean = true
|
||||
initialShowPlayedItems: Boolean = true,
|
||||
initialShowFutureItems: Boolean = true
|
||||
) : ViewModel() {
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
||||
private val feedDatabaseManager = FeedDatabaseManager(application)
|
||||
|
||||
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
|
||||
.startWithItem(initialShowPlayedItems)
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val toggleShowFutureItems = BehaviorProcessor.create<Boolean>()
|
||||
private val toggleShowFutureItemsFlowable = toggleShowFutureItems
|
||||
.startWithItem(initialShowFutureItems)
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||
|
||||
|
@ -44,21 +53,22 @@ class FeedViewModel(
|
|||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
toggleShowPlayedItemsFlowable,
|
||||
toggleShowFutureItemsFlowable,
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
|
||||
Function4 { t1: FeedEventManager.Event, t2: Boolean,
|
||||
t3: Long, t4: List<OffsetDateTime> ->
|
||||
return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
|
||||
Function5 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean,
|
||||
t4: Long, t5: List<OffsetDateTime> ->
|
||||
return@Function5 CombineResultEventHolder(t1, t2, t3, t4, t5.firstOrNull())
|
||||
}
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
|
||||
.map { (event, showPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
|
||||
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
||||
feedDatabaseManager
|
||||
.getStreams(groupId, showPlayedItems)
|
||||
.getStreams(groupId, showPlayedItems, showFutureItems)
|
||||
.blockingGet(arrayListOf())
|
||||
else
|
||||
arrayListOf()
|
||||
|
@ -89,8 +99,9 @@ class FeedViewModel(
|
|||
private data class CombineResultEventHolder(
|
||||
val t1: FeedEventManager.Event,
|
||||
val t2: Boolean,
|
||||
val t3: Long,
|
||||
val t4: OffsetDateTime?
|
||||
val t3: Boolean,
|
||||
val t4: Long,
|
||||
val t5: OffsetDateTime?
|
||||
)
|
||||
|
||||
private data class CombineResultDataHolder(
|
||||
|
@ -105,31 +116,42 @@ class FeedViewModel(
|
|||
}
|
||||
|
||||
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
|
||||
this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems)
|
||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||
this.putBoolean(application.getString(R.string.feed_show_played_items_key), showPlayedItems)
|
||||
this.apply()
|
||||
}
|
||||
|
||||
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(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 {
|
||||
private fun getShowPlayedItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return FeedViewModel(
|
||||
context.applicationContext,
|
||||
groupId,
|
||||
// Read initial value from preferences
|
||||
getShowPlayedItemsFromPreferences(context.applicationContext)
|
||||
) as T
|
||||
private fun getShowFutureItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_future_items_key), true)
|
||||
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
||||
initializer {
|
||||
FeedViewModel(
|
||||
App.getApp(),
|
||||
groupId,
|
||||
// Read initial value from preferences
|
||||
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||
getShowFutureItemsFromPreferences(context.applicationContext)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import android.app.NotificationManager
|
|||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
|
@ -11,6 +13,8 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.squareup.picasso.Target
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||
|
@ -27,6 +31,8 @@ class NotificationHelper(val context: Context) {
|
|||
Context.NOTIFICATION_SERVICE
|
||||
) as NotificationManager
|
||||
|
||||
private val iconLoadingTargets = ArrayList<Target>()
|
||||
|
||||
/**
|
||||
* Show a notification about new streams from a single channel.
|
||||
* Opening the notification will open the corresponding channel page.
|
||||
|
@ -77,10 +83,29 @@ class NotificationHelper(val context: Context) {
|
|||
)
|
||||
)
|
||||
|
||||
PicassoHelper.loadNotificationIcon(data.avatarUrl) { bitmap ->
|
||||
bitmap?.let { builder.setLargeIcon(it) } // set only if != null
|
||||
manager.notify(data.pseudoId, builder.build())
|
||||
// a Target is like a listener for image loading events
|
||||
val target = object : Target {
|
||||
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
|
||||
builder.setLargeIcon(bitmap) // set only if there is actually one
|
||||
manager.notify(data.pseudoId, builder.build())
|
||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||
}
|
||||
|
||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||
manager.notify(data.pseudoId, builder.build())
|
||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||
}
|
||||
|
||||
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
// add the target to the list to hold a strong reference and prevent it from being garbage
|
||||
// collected, since Picasso only holds weak references to targets
|
||||
iconLoadingTargets.add(target)
|
||||
|
||||
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.schabi.newpipe.NewPipeDatabase;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.feed.dao.FeedDAO;
|
||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
|
@ -51,7 +50,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
|||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
|
@ -89,7 +87,6 @@ public class HistoryRecordManager {
|
|||
* Marks a stream item as watched such that it is hidden from the feed if watched videos are
|
||||
* hidden. Adds a history entry and updates the stream progress to 100%.
|
||||
*
|
||||
* @see FeedDAO#getLiveOrNotPlayedStreams
|
||||
* @see FeedViewModel#togglePlayedItems
|
||||
* @param info the item to mark as watched
|
||||
* @return a Maybe containing the ID of the item if successful
|
||||
|
@ -176,10 +173,6 @@ public class HistoryRecordManager {
|
|||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<StreamHistoryEntry>> getStreamHistory() {
|
||||
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<StreamHistoryEntry>> getStreamHistorySortedById() {
|
||||
return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
@ -188,24 +181,6 @@ public class HistoryRecordManager {
|
|||
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() {
|
||||
return sharedPreferences.getBoolean(streamHistoryKey, false);
|
||||
}
|
||||
|
@ -259,13 +234,6 @@ public class HistoryRecordManager {
|
|||
// 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) {
|
||||
return queueItem.getStream()
|
||||
.map(info -> streamTable.upsert(new StreamEntity(info)))
|
||||
|
@ -311,28 +279,6 @@ public class HistoryRecordManager {
|
|||
}).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(
|
||||
final List<? extends LocalItem> items) {
|
||||
return Single.fromCallable(() -> {
|
||||
|
|
|
@ -135,7 +135,7 @@ public class StatisticsPlaylistFragment
|
|||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final LocalItem selectedItem) {
|
||||
if (selectedItem instanceof StreamStatisticsEntry) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.schabi.newpipe.local.playlist;
|
||||
|
||||
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
|
||||
|
||||
|
@ -41,15 +42,16 @@ import org.schabi.newpipe.error.ErrorInfo;
|
|||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.MainPlayer.PlayerType;
|
||||
import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -57,10 +59,12 @@ import java.util.Iterator;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
@ -163,7 +167,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||
|
||||
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
itemListAdapter.setSelectedListener(new OnClickGesture<>() {
|
||||
@Override
|
||||
public void selected(final LocalItem selectedItem) {
|
||||
if (selectedItem instanceof PlaylistStreamEntry) {
|
||||
|
@ -345,7 +349,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_item_remove_watched) {
|
||||
if (item.getItemId() == R.id.menu_item_share_playlist) {
|
||||
sharePlaylist();
|
||||
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
||||
createRenameDialog();
|
||||
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
|
||||
if (!isRemovingWatched) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.remove_watched_popup_warning)
|
||||
|
@ -360,14 +368,26 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
.create()
|
||||
.show();
|
||||
}
|
||||
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
|
||||
createRenameDialog();
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Share the playlist as a newline-separated list of stream URLs.
|
||||
*/
|
||||
public void sharePlaylist() {
|
||||
disposables.add(playlistManager.getPlaylistStreams(playlistId)
|
||||
.flatMapSingle(playlist -> Single.just(playlist.stream()
|
||||
.map(PlaylistStreamEntry::getStreamEntity)
|
||||
.map(StreamEntity::getUrl)
|
||||
.collect(Collectors.joining("\n"))))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(urlsText -> ShareUtils.shareText(requireContext(), name, urlsText),
|
||||
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
|
||||
}
|
||||
|
||||
public void removeWatchedStreams(final boolean removePartiallyWatched) {
|
||||
if (isRemovingWatched) {
|
||||
return;
|
||||
|
@ -382,8 +402,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
final Iterator<PlaylistStreamEntry> playlistIter = playlist.iterator();
|
||||
|
||||
// History data
|
||||
final HistoryRecordManager recordManager
|
||||
= new HistoryRecordManager(getContext());
|
||||
final HistoryRecordManager recordManager =
|
||||
new HistoryRecordManager(getContext());
|
||||
final Iterator<StreamHistoryEntry> historyIter = recordManager
|
||||
.getStreamHistorySortedById().blockingFirst().iterator();
|
||||
|
||||
|
@ -524,8 +544,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
return;
|
||||
}
|
||||
|
||||
final DialogEditTextBinding dialogBinding
|
||||
= DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
final DialogEditTextBinding dialogBinding =
|
||||
DialogEditTextBinding.inflate(getLayoutInflater());
|
||||
dialogBinding.dialogEditText.setHint(R.string.name);
|
||||
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
dialogBinding.dialogEditText.setSelection(dialogBinding.dialogEditText.getText().length());
|
||||
|
@ -593,7 +613,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||
newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0))
|
||||
.getStreamEntity().getThumbnailUrl();
|
||||
} else {
|
||||
newThumbnailUrl = "drawable://" + R.drawable.dummy_thumbnail_playlist;
|
||||
newThumbnailUrl = "drawable://" + R.drawable.placeholder_thumbnail_playlist;
|
||||
}
|
||||
|
||||
changeThumbnailUrl(newThumbnailUrl);
|
||||
|
|
|
@ -346,7 +346,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
|
|||
override fun doInitialLoadLogic() = Unit
|
||||
override fun startLoading(forceLoad: Boolean) = Unit
|
||||
|
||||
private val listenerFeedGroups = object : OnClickGesture<Item<*>>() {
|
||||
private val listenerFeedGroups = object : OnClickGesture<Item<*>> {
|
||||
override fun selected(selectedItem: Item<*>?) {
|
||||
when (selectedItem) {
|
||||
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(
|
||||
fm,
|
||||
selectedItem.serviceId, selectedItem.url, selectedItem.name
|
||||
|
|
|
@ -8,12 +8,10 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.Observer
|
||||
|
@ -124,14 +122,6 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
|
||||
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
// KitKat doesn't apply container's theme to <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(
|
||||
this,
|
||||
FeedGroupDialogViewModel.Factory(
|
||||
|
|
|
@ -122,7 +122,7 @@ class FeedGroupDialogViewModel(
|
|||
private val initialShowOnlyUngrouped: Boolean = false
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return FeedGroupDialogViewModel(
|
||||
context.applicationContext,
|
||||
groupId, initialQuery, initialShowOnlyUngrouped
|
||||
|
|
|
@ -39,7 +39,7 @@ class ChannelItem(
|
|||
itemChannelDescriptionView.text = infoItem.description
|
||||
}
|
||||
|
||||
PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView)
|
||||
PicassoHelper.loadAvatar(infoItem.thumbnailUrl).into(itemThumbnailView)
|
||||
|
||||
gesturesListener?.run {
|
||||
viewHolder.root.setOnClickListener { selected(infoItem) }
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
package org.schabi.newpipe.local.subscription.services;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
@ -43,8 +45,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
|||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public class SubscriptionsExportService extends BaseImportExportService {
|
||||
public static final String KEY_FILE_PATH = "key_file_path";
|
||||
|
||||
|
@ -109,8 +109,8 @@ public class SubscriptionsExportService extends BaseImportExportService {
|
|||
|
||||
subscriptionManager.subscriptionTable().getAll().take(1)
|
||||
.map(subscriptionEntities -> {
|
||||
final List<SubscriptionItem> result
|
||||
= new ArrayList<>(subscriptionEntities.size());
|
||||
final List<SubscriptionItem> result =
|
||||
new ArrayList<>(subscriptionEntities.size());
|
||||
for (final SubscriptionEntity entity : subscriptionEntities) {
|
||||
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(),
|
||||
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.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
@ -56,7 +57,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
|
||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||
|
||||
protected Player player;
|
||||
private Player player;
|
||||
|
||||
private boolean serviceBound;
|
||||
private ServiceConnection serviceConnection;
|
||||
|
@ -131,13 +132,13 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
case R.id.action_append_playlist:
|
||||
player.onAddToPlaylistClicked(getSupportFragmentManager());
|
||||
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
|
||||
return true;
|
||||
case R.id.action_playback_speed:
|
||||
openPlaybackParameterDialog();
|
||||
return true;
|
||||
case R.id.action_mute:
|
||||
player.onMuteUnmuteButtonClicked();
|
||||
player.toggleMute();
|
||||
return true;
|
||||
case R.id.action_system_audio:
|
||||
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
|
||||
|
@ -173,7 +174,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void bind() {
|
||||
final Intent bindIntent = new Intent(this, MainPlayer.class);
|
||||
final Intent bindIntent = new Intent(this, PlayerService.class);
|
||||
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
|
||||
if (!success) {
|
||||
unbindService(serviceConnection);
|
||||
|
@ -189,10 +190,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
player.removeActivityListener(this);
|
||||
}
|
||||
|
||||
if (player != null && player.getPlayQueueAdapter() != null) {
|
||||
player.getPlayQueueAdapter().unsetSelectedListener();
|
||||
}
|
||||
queueControlBinding.playQueue.setAdapter(null);
|
||||
onQueueUpdate(null);
|
||||
if (itemTouchHelper != null) {
|
||||
itemTouchHelper.attachToRecyclerView(null);
|
||||
}
|
||||
|
@ -213,17 +211,15 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
public void onServiceConnected(final ComponentName name, final IBinder service) {
|
||||
Log.d(TAG, "Player service is connected");
|
||||
|
||||
if (service instanceof PlayerServiceBinder) {
|
||||
player = ((PlayerServiceBinder) service).getPlayerInstance();
|
||||
} else if (service instanceof MainPlayer.LocalBinder) {
|
||||
player = ((MainPlayer.LocalBinder) service).getPlayer();
|
||||
if (service instanceof PlayerService.LocalBinder) {
|
||||
player = ((PlayerService.LocalBinder) service).getPlayer();
|
||||
}
|
||||
|
||||
if (player == null || player.getPlayQueue() == null
|
||||
|| player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
|
||||
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|
||||
unbind();
|
||||
finish();
|
||||
} else {
|
||||
onQueueUpdate(player.getPlayQueue());
|
||||
buildComponents();
|
||||
if (player != null) {
|
||||
final PlayQueueItem item = player.getPlayQueue().getItem();
|
||||
|
@ -252,7 +248,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
|
||||
private void buildQueue() {
|
||||
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
|
||||
queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
|
||||
queueControlBinding.playQueue.setClickable(true);
|
||||
queueControlBinding.playQueue.setLongClickable(true);
|
||||
queueControlBinding.playQueue.clearOnScrollListeners();
|
||||
|
@ -260,8 +255,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
|
||||
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
|
||||
|
||||
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
|
||||
}
|
||||
|
||||
private void buildMetadata() {
|
||||
|
@ -381,7 +374,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
}
|
||||
|
||||
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
|
||||
player.onRepeatClicked();
|
||||
player.cycleNextRepeatMode();
|
||||
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
|
||||
player.playPrevious();
|
||||
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
|
||||
|
@ -393,7 +386,7 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
|
||||
player.playNext();
|
||||
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
|
||||
player.onShuffleClicked();
|
||||
player.toggleShuffleModeEnabled();
|
||||
} else if (view.getId() == queueControlBinding.metadata.getId()) {
|
||||
scrollToSelected();
|
||||
} else if (view.getId() == queueControlBinding.liveSync.getId()) {
|
||||
|
@ -456,7 +449,14 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@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
|
||||
|
@ -465,7 +465,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||
onStateChanged(state);
|
||||
onPlayModeChanged(repeatMode, shuffled);
|
||||
onPlaybackParameterChanged(parameters);
|
||||
onMaybePlaybackAdapterChanged();
|
||||
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() {
|
||||
if (menu != null && player != null) {
|
||||
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
|
||||
* 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;
|
||||
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
|
||||
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;
|
||||
|
||||
public interface PlayerServiceEventListener extends PlayerEventListener {
|
||||
void onViewCreated();
|
||||
|
||||
void onFullscreenStateChanged(boolean fullscreen);
|
||||
|
||||
void onScreenRotationButtonClicked();
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package org.schabi.newpipe.player.event;
|
||||
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
|
||||
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
|
||||
void onServiceConnected(Player player,
|
||||
MainPlayer playerService,
|
||||
PlayerService playerService,
|
||||
boolean playAfterConnect);
|
||||
void onServiceDisconnected();
|
||||
}
|
||||
|
|
|
@ -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.graphics.Rect;
|
||||
|
@ -8,24 +8,25 @@ import android.view.View;
|
|||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout> {
|
||||
|
||||
public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) {
|
||||
public CustomBottomSheetBehavior(@NonNull final Context context,
|
||||
@Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
Rect globalRect = new Rect();
|
||||
private boolean skippingInterception = false;
|
||||
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
||||
private final List<Integer> skipInterceptionOfElements = List.of(
|
||||
R.id.detail_content_root_layout, R.id.relatedItemsLayout,
|
||||
R.id.itemsListPanel, R.id.view_pager, R.id.tab_layout, R.id.bottomControls,
|
||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||
|
@ -33,7 +34,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
|
|||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
||||
@NonNull final FrameLayout child,
|
||||
final MotionEvent event) {
|
||||
@NonNull final MotionEvent event) {
|
||||
// Drop following when action ends
|
||||
if (event.getAction() == MotionEvent.ACTION_CANCEL
|
||||
|| event.getAction() == MotionEvent.ACTION_UP) {
|
||||
|
@ -57,7 +58,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
|
|||
if (getState() == BottomSheetBehavior.STATE_EXPANDED
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
// Without overriding scrolling will not work when user touches these elements
|
||||
for (final Integer element : skipInterceptionOfElements) {
|
||||
for (final int element : skipInterceptionOfElements) {
|
||||
final View view = child.findViewById(element);
|
||||
if (view != null) {
|
||||
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
|
@ -1,4 +1,4 @@
|
|||
package org.schabi.newpipe.player.event
|
||||
package org.schabi.newpipe.player.gesture
|
||||
|
||||
enum class DisplayPortion {
|
||||
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF
|
|
@ -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();
|
||||
|
||||
final FileDataSource fileSource = new FileDataSource();
|
||||
final CacheDataSink dataSink
|
||||
= new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
|
||||
final CacheDataSink dataSink =
|
||||
new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
|
||||
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.SeekBar;
|
||||
|
@ -21,16 +20,16 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.math.MathUtils;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.ui.VideoPlayerUi;
|
||||
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
|
||||
import org.schabi.newpipe.util.SliderStrategy;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -149,7 +148,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||
assureCorrectAppLanguage(getContext());
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
binding = DialogPlaybackParameterBinding.inflate(LayoutInflater.from(getContext()));
|
||||
binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
|
||||
initUI();
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
|
||||
|
@ -207,7 +206,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
animateRotation(binding.pitchToogleControlModes,
|
||||
Player.DEFAULT_CONTROLS_DURATION,
|
||||
VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
|
||||
isCurrentlyVisible ? 180 : 0);
|
||||
});
|
||||
|
||||
|
@ -334,10 +333,8 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||
}
|
||||
|
||||
private Map<Boolean, TextView> getPitchControlModeComponentMappings() {
|
||||
final Map<Boolean, TextView> mappings = new HashMap<>();
|
||||
mappings.put(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent);
|
||||
mappings.put(PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
|
||||
return mappings;
|
||||
return Map.of(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent,
|
||||
PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
|
||||
}
|
||||
|
||||
private void changePitchControlMode(final boolean semitones) {
|
||||
|
@ -407,13 +404,11 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||
}
|
||||
|
||||
private Map<Double, TextView> getStepSizeComponentMappings() {
|
||||
final Map<Double, TextView> mappings = new HashMap<>();
|
||||
mappings.put(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent);
|
||||
mappings.put(STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent);
|
||||
mappings.put(STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent);
|
||||
mappings.put(STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent);
|
||||
mappings.put(STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
|
||||
return mappings;
|
||||
return Map.of(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent,
|
||||
STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent,
|
||||
STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent,
|
||||
STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent,
|
||||
STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
|
||||
}
|
||||
|
||||
private void setStepSizeToUI(final double newStepSize) {
|
||||
|
@ -532,7 +527,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||
}
|
||||
|
||||
private void setAndUpdateTempo(final double newTempo) {
|
||||
this.tempo = calcValidTempo(newTempo);
|
||||
this.tempo = MathUtils.clamp(newTempo, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED);
|
||||
|
||||
binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo));
|
||||
setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo);
|
||||
|
@ -551,13 +546,8 @@ public class PlaybackParameterDialog extends DialogFragment {
|
|||
pitchPercent);
|
||||
}
|
||||
|
||||
private double calcValidTempo(final double newTempo) {
|
||||
return Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newTempo));
|
||||
}
|
||||
|
||||
private double calcValidPitch(final double newPitch) {
|
||||
final double calcPitch =
|
||||
Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newPitch));
|
||||
final double calcPitch = MathUtils.clamp(newPitch, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED);
|
||||
|
||||
if (!isCurrentPitchControlModeSemitone()) {
|
||||
return calcPitch;
|
||||
|
|
|
@ -208,8 +208,8 @@ public class PlayerDataSource {
|
|||
Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir");
|
||||
}
|
||||
|
||||
final LeastRecentlyUsedCacheEvictor evictor
|
||||
= new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
|
||||
final LeastRecentlyUsedCacheEvictor evictor =
|
||||
new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
|
||||
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
|
||||
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
||||
|
@ -15,14 +13,8 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
|
@ -49,7 +41,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
@ -71,25 +62,11 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
public final class PlayerHelper {
|
||||
private static final StringBuilder STRING_BUILDER = new StringBuilder();
|
||||
private static final Formatter STRING_FORMATTER
|
||||
= new Formatter(STRING_BUILDER, Locale.getDefault());
|
||||
private static final Formatter STRING_FORMATTER =
|
||||
new Formatter(STRING_BUILDER, Locale.getDefault());
|
||||
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
|
||||
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
|
||||
|
||||
/**
|
||||
* Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
|
||||
* NewPipe's popup player.
|
||||
*
|
||||
* <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)
|
||||
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
|
||||
AUTOPLAY_TYPE_NEVER})
|
||||
|
@ -339,10 +316,6 @@ public final class PlayerHelper {
|
|||
return true;
|
||||
}
|
||||
|
||||
public static int getTossFlingVelocity() {
|
||||
return 2500;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
|
||||
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
|
||||
|
@ -452,12 +425,6 @@ public final class PlayerHelper {
|
|||
// Utils used by player
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) {
|
||||
// If you want to open popup from the app just include Constants.POPUP_ONLY into an extra
|
||||
return MainPlayer.PlayerType.values()[
|
||||
intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())];
|
||||
}
|
||||
|
||||
public static boolean isPlaybackResumeEnabled(final Player player) {
|
||||
return player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.enable_watch_history_key), true)
|
||||
|
@ -528,90 +495,10 @@ public final class PlayerHelper {
|
|||
.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player {@code screenWidth} and {@code screenHeight} must have been initialized
|
||||
* @return the popup starting layout params
|
||||
*/
|
||||
@SuppressLint("RtlHardcoded")
|
||||
public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
|
||||
final Player player) {
|
||||
final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.popup_remember_size_pos_key), true);
|
||||
final float defaultSize =
|
||||
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
|
||||
final float popupWidth = popupRememberSizeAndPos
|
||||
? player.getPrefs().getFloat(player.getContext().getString(
|
||||
R.string.popup_saved_width_key), defaultSize)
|
||||
: defaultSize;
|
||||
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
||||
|
||||
final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams(
|
||||
(int) popupWidth, (int) popupHeight,
|
||||
popupLayoutParamType(),
|
||||
IDLE_WINDOW_FLAGS,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
|
||||
final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f);
|
||||
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
|
||||
popupLayoutParams.x = popupRememberSizeAndPos
|
||||
? player.getPrefs().getInt(player.getContext().getString(
|
||||
R.string.popup_saved_x_key), centerX) : centerX;
|
||||
popupLayoutParams.y = popupRememberSizeAndPos
|
||||
? player.getPrefs().getInt(player.getContext().getString(
|
||||
R.string.popup_saved_y_key), centerY) : centerY;
|
||||
|
||||
return popupLayoutParams;
|
||||
}
|
||||
|
||||
public static void savePopupPositionAndSizeToPrefs(final Player player) {
|
||||
if (player.getPopupLayoutParams() != null) {
|
||||
player.getPrefs().edit()
|
||||
.putFloat(player.getContext().getString(R.string.popup_saved_width_key),
|
||||
player.getPopupLayoutParams().width)
|
||||
.putInt(player.getContext().getString(R.string.popup_saved_x_key),
|
||||
player.getPopupLayoutParams().x)
|
||||
.putInt(player.getContext().getString(R.string.popup_saved_y_key),
|
||||
player.getPopupLayoutParams().y)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
public static float getMinimumVideoHeight(final float width) {
|
||||
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
|
||||
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
||||
|
||||
final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
popupLayoutParamType(),
|
||||
flags,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Setting maximum opacity allowed for touch events to other apps for Android 12 and
|
||||
// higher to prevent non interaction when using other apps with the popup player
|
||||
closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER;
|
||||
}
|
||||
|
||||
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
closeOverlayLayoutParams.softInputMode =
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
return closeOverlayLayoutParams;
|
||||
}
|
||||
|
||||
public static int popupLayoutParamType() {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
? WindowManager.LayoutParams.TYPE_PHONE
|
||||
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
}
|
||||
|
||||
public static int retrieveSeekDurationFromPreferences(final Player player) {
|
||||
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
|
||||
player.getContext().getString(R.string.seek_duration_key),
|
||||
|
|
|
@ -16,8 +16,9 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
|||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.PlayerService;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.PlayerType;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
|
||||
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
@ -42,17 +43,17 @@ public final class PlayerHolder {
|
|||
|
||||
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
|
||||
private boolean bound;
|
||||
@Nullable private MainPlayer playerService;
|
||||
@Nullable private PlayerService playerService;
|
||||
@Nullable private Player player;
|
||||
|
||||
/**
|
||||
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
|
||||
* otherwise `null` if no service running.
|
||||
* Returns the current {@link PlayerType} of the {@link PlayerService} service,
|
||||
* otherwise `null` if no service is running.
|
||||
*
|
||||
* @return Current PlayerType
|
||||
*/
|
||||
@Nullable
|
||||
public MainPlayer.PlayerType getType() {
|
||||
public PlayerType getType() {
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -122,7 +123,7 @@ public final class PlayerHolder {
|
|||
// and NullPointerExceptions inside the service because the service will be
|
||||
// bound twice. Prevent it with unbinding first
|
||||
unbind(context);
|
||||
ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class));
|
||||
ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
|
||||
serviceConnection.doPlayAfterConnect(playAfterConnect);
|
||||
bind(context);
|
||||
}
|
||||
|
@ -130,7 +131,7 @@ public final class PlayerHolder {
|
|||
public void stopService() {
|
||||
final Context context = getCommonContext();
|
||||
unbind(context);
|
||||
context.stopService(new Intent(context, MainPlayer.class));
|
||||
context.stopService(new Intent(context, PlayerService.class));
|
||||
}
|
||||
|
||||
class PlayerServiceConnection implements ServiceConnection {
|
||||
|
@ -156,7 +157,7 @@ public final class PlayerHolder {
|
|||
if (DEBUG) {
|
||||
Log.d(TAG, "Player service is connected");
|
||||
}
|
||||
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
|
||||
final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
|
||||
|
||||
playerService = localBinder.getService();
|
||||
player = localBinder.getPlayer();
|
||||
|
@ -172,7 +173,7 @@ public final class PlayerHolder {
|
|||
Log.d(TAG, "bind() called");
|
||||
}
|
||||
|
||||
final Intent serviceIntent = new Intent(context, MainPlayer.class);
|
||||
final Intent serviceIntent = new Intent(context, PlayerService.class);
|
||||
bound = context.bindService(serviceIntent, serviceConnection,
|
||||
Context.BIND_AUTO_CREATE);
|
||||
if (!bound) {
|
||||
|
@ -211,6 +212,13 @@ public final class PlayerHolder {
|
|||
|
||||
private final PlayerServiceEventListener internalListener =
|
||||
new PlayerServiceEventListener() {
|
||||
@Override
|
||||
public void onViewCreated() {
|
||||
if (listener != null) {
|
||||
listener.onViewCreated();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullscreenStateChanged(final boolean fullscreen) {
|
||||
if (listener != null) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import androidx.core.math.MathUtils;
|
||||
|
||||
/**
|
||||
* Converts between percent and 12-tone equal temperament semitones.
|
||||
* <br/>
|
||||
|
@ -33,6 +35,6 @@ public final class PlayerSemitoneHelper {
|
|||
}
|
||||
|
||||
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 com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaItem.RequestMetadata;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
||||
|
@ -76,7 +77,6 @@ public interface MediaItemTag {
|
|||
@NonNull
|
||||
default MediaItem asMediaItem() {
|
||||
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setMediaUri(Uri.parse(getStreamUrl()))
|
||||
.setArtworkUri(Uri.parse(getThumbnailUrl()))
|
||||
.setArtist(getUploaderName())
|
||||
.setDescription(getTitle())
|
||||
|
@ -84,10 +84,15 @@ public interface MediaItemTag {
|
|||
.setTitle(getTitle())
|
||||
.build();
|
||||
|
||||
final RequestMetadata requestMetaData = new RequestMetadata.Builder()
|
||||
.setMediaUri(Uri.parse(getStreamUrl()))
|
||||
.build();
|
||||
|
||||
return MediaItem.fromUri(getStreamUrl())
|
||||
.buildUpon()
|
||||
.setMediaId(makeMediaId())
|
||||
.setMediaMetadata(mediaMetadata)
|
||||
.setRequestMetadata(requestMetaData)
|
||||
.setTag(this)
|
||||
.build();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
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_PREVIOUS;
|
||||
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.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 static final int DEFAULT_MAX_QUEUE_SIZE = 10;
|
||||
private static final int MAX_QUEUE_SIZE = 10;
|
||||
|
||||
private final MediaSessionCompat mediaSession;
|
||||
private final MediaSessionCallback callback;
|
||||
private final int maxQueueSize;
|
||||
private final Player player;
|
||||
|
||||
private long activeQueueItemId;
|
||||
|
||||
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
|
||||
@NonNull final MediaSessionCallback callback) {
|
||||
@NonNull final Player player) {
|
||||
this.mediaSession = mediaSession;
|
||||
this.callback = callback;
|
||||
this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
|
||||
this.player = player;
|
||||
|
||||
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSupportedQueueNavigatorActions(@Nullable final Player player) {
|
||||
public long getSupportedQueueNavigatorActions(
|
||||
@Nullable final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(@NonNull final Player player) {
|
||||
public void onTimelineChanged(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
publishFloatingQueueWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCurrentMediaItemIndexChanged(@NonNull final Player player) {
|
||||
public void onCurrentMediaItemIndexChanged(
|
||||
@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|
||||
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
|
||||
|| exoPlayer.getCurrentTimeline().getWindowCount() > MAX_QUEUE_SIZE) {
|
||||
publishFloatingQueueWindow();
|
||||
} else if (!player.getCurrentTimeline().isEmpty()) {
|
||||
activeQueueItemId = player.getCurrentMediaItemIndex();
|
||||
} else if (!exoPlayer.getCurrentTimeline().isEmpty()) {
|
||||
activeQueueItemId = exoPlayer.getCurrentMediaItemIndex();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getActiveQueueItemId(@Nullable final Player player) {
|
||||
return callback.getCurrentPlayingIndex();
|
||||
public long getActiveQueueItemId(
|
||||
@Nullable final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
return Optional.ofNullable(player.getPlayQueue()).map(PlayQueue::getIndex).orElse(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToPrevious(@NonNull final Player player) {
|
||||
callback.playPrevious();
|
||||
public void onSkipToPrevious(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
player.playPrevious();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToQueueItem(@NonNull final Player player, final long id) {
|
||||
callback.playItemAtIndex((int) id);
|
||||
public void onSkipToQueueItem(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
|
||||
final long id) {
|
||||
if (player.getPlayQueue() != null) {
|
||||
player.selectQueueItem(player.getPlayQueue().getItem((int) id));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToNext(@NonNull final Player player) {
|
||||
callback.playNext();
|
||||
public void onSkipToNext(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
|
||||
player.playNext();
|
||||
}
|
||||
|
||||
private void publishFloatingQueueWindow() {
|
||||
if (callback.getQueueSize() == 0) {
|
||||
final int windowCount = Optional.ofNullable(player.getPlayQueue())
|
||||
.map(PlayQueue::size)
|
||||
.orElse(0);
|
||||
if (windowCount == 0) {
|
||||
mediaSession.setQueue(Collections.emptyList());
|
||||
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
|
||||
return;
|
||||
}
|
||||
|
||||
// Yes this is almost a copypasta, got a problem with that? =\
|
||||
final int windowCount = callback.getQueueSize();
|
||||
final int currentWindowIndex = callback.getCurrentPlayingIndex();
|
||||
final int queueSize = Math.min(maxQueueSize, windowCount);
|
||||
final int currentWindowIndex = player.getPlayQueue().getIndex();
|
||||
final int queueSize = Math.min(MAX_QUEUE_SIZE, windowCount);
|
||||
final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
|
||||
windowCount - queueSize);
|
||||
|
||||
final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
||||
for (int i = startIndex; i < startIndex + queueSize; i++) {
|
||||
queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i));
|
||||
queue.add(new MediaSessionCompat.QueueItem(getQueueMetadata(i), i));
|
||||
}
|
||||
mediaSession.setQueue(queue);
|
||||
activeQueueItemId = currentWindowIndex;
|
||||
}
|
||||
|
||||
public MediaDescriptionCompat getQueueMetadata(final int index) {
|
||||
if (player.getPlayQueue() == null) {
|
||||
return null;
|
||||
}
|
||||
final PlayQueueItem item = player.getPlayQueue().getItem(index);
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder()
|
||||
.setMediaId(String.valueOf(index))
|
||||
.setTitle(item.getTitle())
|
||||
.setSubtitle(item.getUploader());
|
||||
|
||||
// set additional metadata for A2DP/AVRCP (Audio/Video Bluetooth profiles)
|
||||
final Bundle additionalMetadata = new Bundle();
|
||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
|
||||
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
|
||||
additionalMetadata
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
|
||||
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1L);
|
||||
additionalMetadata
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
|
||||
descBuilder.setExtras(additionalMetadata);
|
||||
|
||||
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
|
||||
if (thumbnailUri != null) {
|
||||
descBuilder.setIconUri(thumbnailUri);
|
||||
}
|
||||
|
||||
return descBuilder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NonNull final Player player,
|
||||
public boolean onCommand(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
|
||||
@NonNull final String command,
|
||||
@Nullable final Bundle extras,
|
||||
@Nullable final ResultReceiver cb) {
|
||||
|
|
|
@ -16,7 +16,7 @@ import org.schabi.newpipe.player.mediaitem.ExceptionTag;
|
|||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -56,9 +56,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
|
|||
this.playQueueItem = playQueueItem;
|
||||
this.error = error;
|
||||
this.retryTimestamp = retryTimestamp;
|
||||
this.mediaItem = ExceptionTag
|
||||
.of(playQueueItem, Collections.singletonList(error))
|
||||
.withExtras(this)
|
||||
this.mediaItem = ExceptionTag.of(playQueueItem, List.of(error)).withExtras(this)
|
||||
.asMediaItem();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package org.schabi.newpipe.player;
|
||||
package org.schabi.newpipe.player.notification;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -7,20 +7,48 @@ import androidx.annotation.DrawableRes;
|
|||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class NotificationConstants {
|
||||
|
||||
private NotificationConstants() { }
|
||||
private NotificationConstants() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Intent actions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static final String BASE_ACTION =
|
||||
App.PACKAGE_NAME + ".player.MainPlayer.";
|
||||
public static final String ACTION_CLOSE =
|
||||
BASE_ACTION + "CLOSE";
|
||||
public static final String ACTION_PLAY_PAUSE =
|
||||
BASE_ACTION + ".player.MainPlayer.PLAY_PAUSE";
|
||||
public static final String ACTION_REPEAT =
|
||||
BASE_ACTION + ".player.MainPlayer.REPEAT";
|
||||
public static final String ACTION_PLAY_NEXT =
|
||||
BASE_ACTION + ".player.MainPlayer.ACTION_PLAY_NEXT";
|
||||
public static final String ACTION_PLAY_PREVIOUS =
|
||||
BASE_ACTION + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
|
||||
public static final String ACTION_FAST_REWIND =
|
||||
BASE_ACTION + ".player.MainPlayer.ACTION_FAST_REWIND";
|
||||
public static final String ACTION_FAST_FORWARD =
|
||||
BASE_ACTION + ".player.MainPlayer.ACTION_FAST_FORWARD";
|
||||
public static final String ACTION_SHUFFLE =
|
||||
BASE_ACTION + ".player.MainPlayer.ACTION_SHUFFLE";
|
||||
public static final String ACTION_RECREATE_NOTIFICATION =
|
||||
BASE_ACTION + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
||||
|
||||
|
||||
public static final int NOTHING = 0;
|
||||
|
@ -86,7 +114,7 @@ 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 = {
|
||||
R.string.notification_slot_compact_0_key,
|
||||
|
@ -152,7 +180,7 @@ public final class NotificationConstants {
|
|||
|
||||
if (compactSlot == Integer.MAX_VALUE) {
|
||||
// settings not yet populated, return default values
|
||||
return new ArrayList<>(Arrays.asList(SLOT_COMPACT_DEFAULTS));
|
||||
return new ArrayList<>(SLOT_COMPACT_DEFAULTS);
|
||||
}
|
||||
|
||||
// a negative value (-1) is set when the user does not want a particular compact slot
|
|
@ -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.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
@ -20,48 +19,45 @@ import androidx.core.content.ContextCompat;
|
|||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.player.Player;
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static androidx.media.app.NotificationCompat.MediaStyle;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
|
||||
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
|
||||
|
||||
/**
|
||||
* This is a utility class for player notifications.
|
||||
*
|
||||
* @author cool-student
|
||||
*/
|
||||
public final class NotificationUtil {
|
||||
private static final String TAG = NotificationUtil.class.getSimpleName();
|
||||
private static final boolean DEBUG = Player.DEBUG;
|
||||
private static final int NOTIFICATION_ID = 123789;
|
||||
|
||||
@Nullable private static NotificationUtil instance = null;
|
||||
|
||||
@NotificationConstants.Action
|
||||
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
|
||||
|
||||
private NotificationManagerCompat notificationManager;
|
||||
private NotificationCompat.Builder notificationBuilder;
|
||||
|
||||
private NotificationUtil() {
|
||||
}
|
||||
private final Player player;
|
||||
|
||||
public static NotificationUtil getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new NotificationUtil();
|
||||
}
|
||||
return instance;
|
||||
public NotificationUtil(final Player player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
|
||||
|
@ -72,20 +68,31 @@ public final class NotificationUtil {
|
|||
/**
|
||||
* Creates the notification if it does not exist already and recreates it if forceRecreate is
|
||||
* true. Updates the notification with the data in the player.
|
||||
* @param player the player currently open, to take data from
|
||||
* @param forceRecreate whether to force the recreation of the notification even if it already
|
||||
* exists
|
||||
*/
|
||||
synchronized void createNotificationIfNeededAndUpdate(final Player player,
|
||||
final boolean forceRecreate) {
|
||||
public synchronized void createNotificationIfNeededAndUpdate(final boolean forceRecreate) {
|
||||
if (forceRecreate || notificationBuilder == null) {
|
||||
notificationBuilder = createNotification(player);
|
||||
notificationBuilder = createNotification();
|
||||
}
|
||||
updateNotification(player);
|
||||
updateNotification();
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
|
||||
private synchronized NotificationCompat.Builder createNotification(final Player player) {
|
||||
public synchronized void updateThumbnail() {
|
||||
if (notificationBuilder != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateThumbnail() called with thumbnail = [" + Integer.toHexString(
|
||||
Optional.ofNullable(player.getThumbnail()).map(Objects::hashCode).orElse(0))
|
||||
+ "], title = [" + player.getVideoTitle() + "]");
|
||||
}
|
||||
|
||||
setLargeIcon(notificationBuilder);
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized NotificationCompat.Builder createNotification() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "createNotification()");
|
||||
}
|
||||
|
@ -94,7 +101,7 @@ public final class NotificationUtil {
|
|||
new NotificationCompat.Builder(player.getContext(),
|
||||
player.getContext().getString(R.string.notification_channel_id));
|
||||
|
||||
initializeNotificationSlots(player);
|
||||
initializeNotificationSlots();
|
||||
|
||||
// count the number of real slots, to make sure compact slots indices are not out of bound
|
||||
int nonNothingSlotCount = 5;
|
||||
|
@ -108,14 +115,15 @@ public final class NotificationUtil {
|
|||
// build the compact slot indices array (need code to convert from Integer... because Java)
|
||||
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
|
||||
player.getContext(), player.getPrefs(), nonNothingSlotCount);
|
||||
final int[] compactSlots = new int[compactSlotList.size()];
|
||||
for (int i = 0; i < compactSlotList.size(); i++) {
|
||||
compactSlots[i] = compactSlotList.get(i);
|
||||
}
|
||||
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
|
||||
|
||||
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(player.getMediaSessionManager().getSessionToken())
|
||||
.setShowActionsInCompactView(compactSlots))
|
||||
final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
|
||||
player.UIs()
|
||||
.get(MediaSessionPlayerUi.class)
|
||||
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
||||
.ifPresent(mediaStyle::setMediaSession);
|
||||
|
||||
builder.setStyle(mediaStyle)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
||||
|
@ -128,35 +136,33 @@ public final class NotificationUtil {
|
|||
.setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
|
||||
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
|
||||
|
||||
// set the initial value for the video thumbnail, updatable with updateNotificationThumbnail
|
||||
setLargeIcon(builder);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the notification builder and the button icons depending on the playback state.
|
||||
* @param player the player currently open, to take data from
|
||||
*/
|
||||
private synchronized void updateNotification(final Player player) {
|
||||
private synchronized void updateNotification() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateNotification()");
|
||||
}
|
||||
|
||||
// also update content intent, in case the user switched players
|
||||
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
|
||||
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
|
||||
NOTIFICATION_ID, getIntentForNotification(), FLAG_UPDATE_CURRENT));
|
||||
notificationBuilder.setContentTitle(player.getVideoTitle());
|
||||
notificationBuilder.setContentText(player.getUploaderName());
|
||||
notificationBuilder.setTicker(player.getVideoTitle());
|
||||
updateActions(notificationBuilder, player);
|
||||
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.show_thumbnail_key), true);
|
||||
if (showThumbnail) {
|
||||
setLargeIcon(notificationBuilder, player);
|
||||
}
|
||||
|
||||
updateActions(notificationBuilder);
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
boolean shouldUpdateBufferingSlot() {
|
||||
public boolean shouldUpdateBufferingSlot() {
|
||||
if (notificationBuilder == null) {
|
||||
// if there is no notification active, there is no point in updating it
|
||||
return false;
|
||||
|
@ -174,22 +180,22 @@ public final class NotificationUtil {
|
|||
}
|
||||
|
||||
|
||||
void createNotificationAndStartForeground(final Player player, final Service service) {
|
||||
public void createNotificationAndStartForeground() {
|
||||
if (notificationBuilder == null) {
|
||||
notificationBuilder = createNotification(player);
|
||||
notificationBuilder = createNotification();
|
||||
}
|
||||
updateNotification(player);
|
||||
updateNotification();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build(),
|
||||
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||
} else {
|
||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||
player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
void cancelNotificationAndStopForeground(final Service service) {
|
||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
public void cancelNotificationAndStopForeground() {
|
||||
ServiceCompat.stopForeground(player.getService(), ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
|
@ -203,7 +209,7 @@ public final class NotificationUtil {
|
|||
// ACTIONS
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
private void initializeNotificationSlots(final Player player) {
|
||||
private void initializeNotificationSlots() {
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
notificationSlots[i] = player.getPrefs().getInt(
|
||||
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||
|
@ -212,17 +218,16 @@ public final class NotificationUtil {
|
|||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private void updateActions(final NotificationCompat.Builder builder, final Player player) {
|
||||
private void updateActions(final NotificationCompat.Builder builder) {
|
||||
builder.mActions.clear();
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
addAction(builder, player, notificationSlots[i]);
|
||||
addAction(builder, notificationSlots[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void addAction(final NotificationCompat.Builder builder,
|
||||
final Player player,
|
||||
@NotificationConstants.Action final int slot) {
|
||||
final NotificationCompat.Action action = getAction(player, slot);
|
||||
final NotificationCompat.Action action = getAction(slot);
|
||||
if (action != null) {
|
||||
builder.addAction(action);
|
||||
}
|
||||
|
@ -230,41 +235,40 @@ public final class NotificationUtil {
|
|||
|
||||
@Nullable
|
||||
private NotificationCompat.Action getAction(
|
||||
final Player player,
|
||||
@NotificationConstants.Action final int selectedAction) {
|
||||
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
||||
switch (selectedAction) {
|
||||
case NotificationConstants.PREVIOUS:
|
||||
return getAction(player, baseActionIcon,
|
||||
return getAction(baseActionIcon,
|
||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||
|
||||
case NotificationConstants.NEXT:
|
||||
return getAction(player, baseActionIcon,
|
||||
return getAction(baseActionIcon,
|
||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||
|
||||
case NotificationConstants.REWIND:
|
||||
return getAction(player, baseActionIcon,
|
||||
return getAction(baseActionIcon,
|
||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||
|
||||
case NotificationConstants.FORWARD:
|
||||
return getAction(player, baseActionIcon,
|
||||
return getAction(baseActionIcon,
|
||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||
|
||||
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||
return getAction(player, R.drawable.exo_notification_previous,
|
||||
return getAction(R.drawable.exo_notification_previous,
|
||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_controls_rewind,
|
||||
return getAction(R.drawable.exo_controls_rewind,
|
||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||
}
|
||||
|
||||
case NotificationConstants.SMART_FORWARD_NEXT:
|
||||
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
|
||||
return getAction(player, R.drawable.exo_notification_next,
|
||||
return getAction(R.drawable.exo_notification_next,
|
||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_controls_fastforward,
|
||||
return getAction(R.drawable.exo_controls_fastforward,
|
||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||
}
|
||||
|
||||
|
@ -278,44 +282,45 @@ public final class NotificationUtil {
|
|||
null);
|
||||
}
|
||||
|
||||
// fallthrough
|
||||
case NotificationConstants.PLAY_PAUSE:
|
||||
if (player.getCurrentState() == Player.STATE_COMPLETED) {
|
||||
return getAction(player, R.drawable.ic_replay,
|
||||
return getAction(R.drawable.ic_replay,
|
||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||
} else if (player.isPlaying()
|
||||
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|
||||
|| player.getCurrentState() == Player.STATE_BLOCKED
|
||||
|| player.getCurrentState() == Player.STATE_BUFFERING) {
|
||||
return getAction(player, R.drawable.exo_notification_pause,
|
||||
return getAction(R.drawable.exo_notification_pause,
|
||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_notification_play,
|
||||
return getAction(R.drawable.exo_notification_play,
|
||||
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
|
||||
}
|
||||
|
||||
case NotificationConstants.REPEAT:
|
||||
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
||||
return getAction(player, R.drawable.exo_media_action_repeat_all,
|
||||
return getAction(R.drawable.exo_media_action_repeat_all,
|
||||
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
|
||||
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
||||
return getAction(player, R.drawable.exo_media_action_repeat_one,
|
||||
return getAction(R.drawable.exo_media_action_repeat_one,
|
||||
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
|
||||
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
||||
return getAction(player, R.drawable.exo_media_action_repeat_off,
|
||||
return getAction(R.drawable.exo_media_action_repeat_off,
|
||||
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
|
||||
}
|
||||
|
||||
case NotificationConstants.SHUFFLE:
|
||||
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
|
||||
return getAction(player, R.drawable.exo_controls_shuffle_on,
|
||||
return getAction(R.drawable.exo_controls_shuffle_on,
|
||||
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_controls_shuffle_off,
|
||||
return getAction(R.drawable.exo_controls_shuffle_off,
|
||||
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
|
||||
}
|
||||
|
||||
case NotificationConstants.CLOSE:
|
||||
return getAction(player, R.drawable.ic_close,
|
||||
return getAction(R.drawable.ic_close,
|
||||
R.string.close, ACTION_CLOSE);
|
||||
|
||||
case NotificationConstants.NOTHING:
|
||||
|
@ -325,8 +330,7 @@ public final class NotificationUtil {
|
|||
}
|
||||
}
|
||||
|
||||
private NotificationCompat.Action getAction(final Player player,
|
||||
@DrawableRes final int drawable,
|
||||
private NotificationCompat.Action getAction(@DrawableRes final int drawable,
|
||||
@StringRes final int title,
|
||||
final String intentAction) {
|
||||
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
|
||||
|
@ -334,7 +338,7 @@ public final class NotificationUtil {
|
|||
new Intent(intentAction), FLAG_UPDATE_CURRENT));
|
||||
}
|
||||
|
||||
private Intent getIntentForNotification(final Player player) {
|
||||
private Intent getIntentForNotification() {
|
||||
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
|
||||
// Means we play in popup or audio only. Let's show the play queue
|
||||
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
|
||||
|
@ -354,28 +358,34 @@ public final class NotificationUtil {
|
|||
// BITMAP
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) {
|
||||
private void setLargeIcon(final NotificationCompat.Builder builder) {
|
||||
final boolean showThumbnail = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.show_thumbnail_key), true);
|
||||
final Bitmap thumbnail = player.getThumbnail();
|
||||
if (thumbnail == null || !showThumbnail) {
|
||||
// since the builder is reused, make sure the thumbnail is unset if there is not one
|
||||
builder.setLargeIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
|
||||
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
|
||||
false);
|
||||
if (scaleImageToSquareAspectRatio) {
|
||||
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));
|
||||
builder.setLargeIcon(getBitmapWithSquareAspectRatio(thumbnail));
|
||||
} else {
|
||||
builder.setLargeIcon(player.getThumbnail());
|
||||
builder.setLargeIcon(thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) {
|
||||
return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth());
|
||||
}
|
||||
|
||||
private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) {
|
||||
final int width = bitmap.getWidth();
|
||||
final int height = bitmap.getHeight();
|
||||
final float scaleWidth = ((float) newWidth) / width;
|
||||
final float scaleHeight = ((float) newHeight) / height;
|
||||
final Matrix matrix = new Matrix();
|
||||
matrix.postScale(scaleWidth, scaleHeight);
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
|
||||
private Bitmap getBitmapWithSquareAspectRatio(@NonNull final Bitmap bitmap) {
|
||||
// Find the smaller dimension and then take a center portion of the image that
|
||||
// has that size.
|
||||
final int w = bitmap.getWidth();
|
||||
final int h = bitmap.getHeight();
|
||||
final int dstSize = Math.min(w, h);
|
||||
final int x = (w - dstSize) / 2;
|
||||
final int y = (h - dstSize) / 2;
|
||||
return Bitmap.createBitmap(bitmap, x, y, dstSize, dstSize);
|
||||
}
|
||||
}
|
|
@ -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 com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.video.DummySurface;
|
||||
import com.google.android.exoplayer2.video.PlaceholderSurface;
|
||||
|
||||
/**
|
||||
* Prevent error message: 'Unrecoverable player error occurred'
|
||||
|
@ -26,7 +26,7 @@ public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
|
|||
|
||||
private final Context context;
|
||||
private final Player player;
|
||||
private DummySurface dummySurface;
|
||||
private PlaceholderSurface placeholderSurface;
|
||||
|
||||
public SurfaceHolderCallback(final Context context, final Player player) {
|
||||
this.context = context;
|
||||
|
@ -47,16 +47,16 @@ public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
|
|||
|
||||
@Override
|
||||
public void surfaceDestroyed(final SurfaceHolder holder) {
|
||||
if (dummySurface == null) {
|
||||
dummySurface = DummySurface.newInstanceV17(context, false);
|
||||
if (placeholderSurface == null) {
|
||||
placeholderSurface = PlaceholderSurface.newInstanceV17(context, false);
|
||||
}
|
||||
player.setVideoSurface(dummySurface);
|
||||
player.setVideoSurface(placeholderSurface);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (dummySurface != null) {
|
||||
dummySurface.release();
|
||||
dummySurface = null;
|
||||
if (placeholderSurface != null) {
|
||||
placeholderSurface.release();
|
||||
placeholderSurface = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
|||
public void onError(@NonNull final Throwable e) {
|
||||
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
||||
isComplete = true;
|
||||
append(); // Notify change
|
||||
notifyChange();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
|||
public void onError(@NonNull final Throwable e) {
|
||||
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
||||
isComplete = true;
|
||||
append(); // Notify change
|
||||
notifyChange();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import org.schabi.newpipe.player.playqueue.events.SelectEvent;
|
|||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
@ -258,13 +257,10 @@ public abstract class PlayQueue implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Appends the given {@link PlayQueueItem}s to the current play queue.
|
||||
*
|
||||
* @see #append(List items)
|
||||
* @param items {@link PlayQueueItem}s to append
|
||||
* Notifies that a change has occurred.
|
||||
*/
|
||||
public synchronized void append(@NonNull final PlayQueueItem... items) {
|
||||
append(Arrays.asList(items));
|
||||
public synchronized void notifyChange() {
|
||||
broadcast(new AppendEvent(0));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue