diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f61e320c9..93af9d872 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -33,6 +33,7 @@ with your GitHub account. ## Code contribution +* If you want to add a feature or change one, please open an issue describing your change. This gives the team and community a chance to give feedback before you spend any time on something that could be done differently or not done at all. It also prevents two contributors from working on the same thing and one being disappointed when only one user's code can be added. * Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project. * Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google libraries. diff --git a/app/build.gradle b/app/build.gradle index 6e0801049..80171be62 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { resValue "string", "app_name", "NewPipe" minSdkVersion 19 targetSdkVersion 29 - versionCode 950 - versionName "0.19.5" + versionCode 953 + versionName "0.19.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -50,7 +50,7 @@ android { // TODO: update Gradle version release { minifyEnabled true - shrinkResources true + shrinkResources false // disabled to fix F-Droid's reproducible build proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' archivesBaseName = 'app' } @@ -84,11 +84,17 @@ ext { checkstyleVersion = '8.32' stethoVersion = '1.5.1' leakCanaryVersion = '2.2' - exoPlayerVersion = '2.11.6' + exoPlayerVersion = '2.11.8' androidxLifecycleVersion = '2.2.0' androidxRoomVersion = '2.2.5' groupieVersion = '2.8.0' markwonVersion = '4.3.1' + googleAutoServiceVersion = '1.0-rc7' +} + +configurations { + checkstyle + ktlint } checkstyle { @@ -106,8 +112,7 @@ task runCheckstyle(type: Checkstyle) { exclude '**/BuildConfig.java' exclude 'main/java/us/shandian/giga/**' - // empty classpath - classpath = files() + classpath = configurations.checkstyle showViolations true @@ -117,10 +122,6 @@ task runCheckstyle(type: Checkstyle) { } } -configurations { - ktlint -} - task runKtlint(type: JavaExec) { main = "com.pinterest.ktlint.Main" classpath = configurations.ktlint @@ -138,12 +139,12 @@ afterEvaluate { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "frankiesardo:icepick:${icepickVersion}" kapt "frankiesardo:icepick-processor:${icepickVersion}" - debugImplementation "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" + checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" ktlint "com.pinterest:ktlint:0.35.0" debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" @@ -163,18 +164,21 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:bda83fe6a5b9a8a0751669fbc444fa49d72d0d2f' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:6633f26ec5a73a8e932de575b7a0643b6ad6c890' implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" implementation "org.jsoup:jsoup:1.13.1" - implementation "com.squareup.okhttp3:okhttp:3.12.11" + implementation "com.squareup.okhttp3:okhttp:3.12.12" implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}" implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" implementation "com.google.android.material:material:1.1.0" + compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}" + kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}" + implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.preference:preference:1.1.1" implementation "androidx.recyclerview:recyclerview:1.1.0" @@ -183,7 +187,6 @@ dependencies { implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" - implementation "androidx.lifecycle:lifecycle-extensions:${androidxLifecycleVersion}" implementation "androidx.room:room-runtime:${androidxRoomVersion}" implementation "androidx.room:room-rxjava2:${androidxRoomVersion}" diff --git a/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java b/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java index ab20d2ff3..a3aa28059 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java +++ b/app/src/androidTest/java/org/schabi/newpipe/report/ErrorInfoTest.java @@ -21,13 +21,13 @@ public class ErrorInfoTest { @Test public void errorInfoTestParcelable() { - ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request", + final ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request", R.string.general_error); // Obtain a Parcel object and write the parcelable object to it: - Parcel parcel = Parcel.obtain(); + final Parcel parcel = Parcel.obtain(); info.writeToParcel(parcel, 0); parcel.setDataPosition(0); - ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel); + final ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel); assertEquals(UserAction.USER_REPORT, infoFromParcel.userAction); assertEquals("youtube", infoFromParcel.serviceName); diff --git a/app/src/debug/java/org/schabi/newpipe/DebugApp.kt b/app/src/debug/java/org/schabi/newpipe/DebugApp.kt index 5cfde80b8..9ea3bdabe 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.kt +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.kt @@ -1,6 +1,5 @@ package org.schabi.newpipe -import android.content.Context import androidx.multidex.MultiDex import androidx.preference.PreferenceManager import com.facebook.stetho.Stetho @@ -11,11 +10,6 @@ import okhttp3.OkHttpClient import org.schabi.newpipe.extractor.downloader.Downloader class DebugApp : App() { - override fun attachBaseContext(base: Context) { - super.attachBaseContext(base) - MultiDex.install(this) - } - override fun onCreate() { super.onCreate() initStetho() @@ -34,6 +28,12 @@ class DebugApp : App() { return downloader } + override fun initACRA() { + // install MultiDex before initializing ACRA + MultiDex.install(this) + super.initACRA() + } + private fun initStetho() { // Create an InitializerBuilder val initializerBuilder = Stetho.newInitializerBuilder(this) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3ffed3c9d..1b3b80d88 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ android:logo="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true" android:theme="@style/OpeningTheme" + android:resizeableActivity="true" tools:ignore="AllowBackup"> + android:name=".player.MainPlayer" + android:exported="false"> @@ -52,25 +53,9 @@ - - - - - - @@ -242,17 +227,19 @@ - - - - - - - - + + + - + + + + + + + + @@ -319,6 +306,7 @@ + @@ -331,5 +319,11 @@ + + + + + + diff --git a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java index 11f457b6c..471360c60 100644 --- a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java +++ b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java @@ -150,7 +150,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt // from its saved state, where the fragment manager has already // taken care of restoring the fragments we previously had instantiated. if (mFragments.size() > position) { - Fragment f = mFragments.get(position); + final Fragment f = mFragments.get(position); if (f != null) { return f; } @@ -160,12 +160,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt mCurTransaction = mFragmentManager.beginTransaction(); } - Fragment fragment = getItem(position); + final Fragment fragment = getItem(position); if (DEBUG) { Log.v(TAG, "Adding item #" + position + ": f=" + fragment); } if (mSavedState.size() > position) { - Fragment.SavedState fss = mSavedState.get(position); + final Fragment.SavedState fss = mSavedState.get(position); if (fss != null) { fragment.setInitialSavedState(fss); } @@ -191,7 +191,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt @Override public void destroyItem(@NonNull final ViewGroup container, final int position, @NonNull final Object object) { - Fragment fragment = (Fragment) object; + final Fragment fragment = (Fragment) object; if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); @@ -217,7 +217,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt @SuppressWarnings({"ReferenceEquality", "deprecation"}) public void setPrimaryItem(@NonNull final ViewGroup container, final int position, @NonNull final Object object) { - Fragment fragment = (Fragment) object; + final Fragment fragment = (Fragment) object; if (fragment != mCurrentPrimaryItem) { if (mCurrentPrimaryItem != null) { mCurrentPrimaryItem.setMenuVisibility(false); @@ -267,17 +267,17 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt Bundle state = null; if (mSavedState.size() > 0) { state = new Bundle(); - Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; + final Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; mSavedState.toArray(fss); state.putParcelableArray("states", fss); } for (int i = 0; i < mFragments.size(); i++) { - Fragment f = mFragments.get(i); + final Fragment f = mFragments.get(i); if (f != null && f.isAdded()) { if (state == null) { state = new Bundle(); } - String key = "f" + i; + final String key = "f" + i; mFragmentManager.putFragment(state, key, f); //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -294,21 +294,21 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt @Override public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) { if (state != null) { - Bundle bundle = (Bundle) state; + final Bundle bundle = (Bundle) state; bundle.setClassLoader(loader); - Parcelable[] fss = bundle.getParcelableArray("states"); + final Parcelable[] fss = bundle.getParcelableArray("states"); mSavedState.clear(); mFragments.clear(); if (fss != null) { - for (int i = 0; i < fss.length; i++) { - mSavedState.add((Fragment.SavedState) fss[i]); + for (final Parcelable parcelable : fss) { + mSavedState.add((Fragment.SavedState) parcelable); } } - Iterable keys = bundle.keySet(); - for (String key: keys) { + final Iterable keys = bundle.keySet(); + for (final String key : keys) { if (key.startsWith("f")) { - int index = Integer.parseInt(key.substring(1)); - Fragment f = mFragmentManager.getFragment(bundle, key); + final int index = Integer.parseInt(key.substring(1)); + final Fragment f = mFragmentManager.getFragment(bundle, key); if (f != null) { while (mFragments.size() <= index) { mFragments.add(null); diff --git a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java index 09f9aea58..b5c7fc564 100644 --- a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java +++ b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java @@ -4,11 +4,14 @@ import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; import android.widget.OverScroller; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; +import org.schabi.newpipe.R; import java.lang.reflect.Field; @@ -20,23 +23,25 @@ public final class FlingBehavior extends AppBarLayout.Behavior { super(context, attrs); } + private boolean allowScroll = true; + private final Rect globalRect = new Rect(); + @Override public boolean onRequestChildRectangleOnScreen( @NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child, @NonNull final Rect rectangle, final boolean immediate) { - focusScrollRect.set(rectangle); coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect); - int height = coordinatorLayout.getHeight(); + final int height = coordinatorLayout.getHeight(); if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) { // the child is too big to fit inside ourselves completely, ignore request return false; } - int dy; + final int dy; if (focusScrollRect.bottom > height) { dy = focusScrollRect.top; @@ -48,13 +53,30 @@ public final class FlingBehavior extends AppBarLayout.Behavior { return false; } - int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0); + final int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0); return consumed == dy; } public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child, final MotionEvent ev) { + final ViewGroup playQueue = child.findViewById(R.id.playQueuePanel); + if (playQueue != null) { + final boolean visible = playQueue.getGlobalVisibleRect(globalRect); + if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) { + allowScroll = false; + return false; + } + } + final View seekBar = child.findViewById(R.id.playbackSeekBar); + if (seekBar != null) { + final boolean visible = seekBar.getGlobalVisibleRect(globalRect); + if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) { + allowScroll = false; + return false; + } + } + allowScroll = true; switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // remove reference to old nested scrolling child @@ -68,17 +90,37 @@ public final class FlingBehavior extends AppBarLayout.Behavior { return super.onInterceptTouchEvent(parent, child, ev); } + @Override + public boolean onStartNestedScroll(@NonNull final CoordinatorLayout parent, + @NonNull final AppBarLayout child, + @NonNull final View directTargetChild, + final View target, + final int nestedScrollAxes, + final int type) { + return allowScroll && super.onStartNestedScroll( + parent, child, directTargetChild, target, nestedScrollAxes, type); + } + + @Override + public boolean onNestedFling(@NonNull final CoordinatorLayout coordinatorLayout, + @NonNull final AppBarLayout child, + @NonNull final View target, final float velocityX, + final float velocityY, final boolean consumed) { + return allowScroll && super.onNestedFling( + coordinatorLayout, child, target, velocityX, velocityY, consumed); + } + @Nullable private OverScroller getScrollerField() { try { - Class headerBehaviorType = this.getClass() + final Class headerBehaviorType = this.getClass() .getSuperclass().getSuperclass().getSuperclass(); if (headerBehaviorType != null) { - Field field = headerBehaviorType.getDeclaredField("scroller"); + final Field field = headerBehaviorType.getDeclaredField("scroller"); field.setAccessible(true); return ((OverScroller) field.get(this)); } - } catch (NoSuchFieldException | IllegalAccessException e) { + } catch (final NoSuchFieldException | IllegalAccessException e) { // ? } return null; @@ -87,34 +129,35 @@ public final class FlingBehavior extends AppBarLayout.Behavior { @Nullable private Field getLastNestedScrollingChildRefField() { try { - Class headerBehaviorType = this.getClass().getSuperclass().getSuperclass(); + final Class headerBehaviorType = this.getClass().getSuperclass().getSuperclass(); if (headerBehaviorType != null) { - Field field = headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef"); + final Field field + = headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef"); field.setAccessible(true); return field; } - } catch (NoSuchFieldException e) { + } catch (final NoSuchFieldException e) { // ? } return null; } private void resetNestedScrollingChild() { - Field field = getLastNestedScrollingChildRefField(); + final Field field = getLastNestedScrollingChildRefField(); if (field != null) { try { - Object value = field.get(this); + final Object value = field.get(this); if (value != null) { field.set(this, null); } - } catch (IllegalAccessException e) { + } catch (final IllegalAccessException e) { // ? } } } private void stopAppBarLayoutFling() { - OverScroller scroller = getScrollerField(); + final OverScroller scroller = getScrollerField(); if (scroller != null) { scroller.forceFinished(true); } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 531cb5a38..962679cd0 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -9,6 +9,7 @@ import android.content.SharedPreferences; import android.os.Build; import android.util.Log; +import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; @@ -19,10 +20,8 @@ import org.acra.ACRA; import org.acra.config.ACRAConfigurationException; import org.acra.config.CoreConfiguration; import org.acra.config.CoreConfigurationBuilder; -import org.acra.sender.ReportSenderFactory; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.downloader.Downloader; -import org.schabi.newpipe.report.AcraReportSenderFactory; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.settings.SettingsActivity; @@ -37,7 +36,6 @@ import java.net.SocketException; import java.util.Collections; import java.util.List; -import io.reactivex.annotations.NonNull; import io.reactivex.exceptions.CompositeException; import io.reactivex.exceptions.MissingBackpressureException; import io.reactivex.exceptions.OnErrorNotImplementedException; @@ -65,9 +63,6 @@ import io.reactivex.plugins.RxJavaPlugins; public class App extends Application { protected static final String TAG = App.class.toString(); - @SuppressWarnings("unchecked") - private static final Class[] - REPORT_SENDER_FACTORY_CLASSES = new Class[]{AcraReportSenderFactory.class}; private static App app; public static App getApp() { @@ -77,7 +72,6 @@ public class App extends Application { @Override protected void attachBaseContext(final Context base) { super.attachBaseContext(base); - initACRA(); } @@ -110,7 +104,7 @@ public class App extends Application { } protected Downloader getDownloader() { - DownloaderImpl downloader = DownloaderImpl.init(null); + final DownloaderImpl downloader = DownloaderImpl.init(null); setCookiesToDownloader(downloader); return downloader; } @@ -200,14 +194,21 @@ public class App extends Application { .build(); } - private void initACRA() { + /** + * Called in {@link #attachBaseContext(Context)} after calling the {@code super} method. + * Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA. + */ + protected void initACRA() { + if (ACRA.isACRASenderServiceProcess()) { + return; + } + try { final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this) - .setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES) .setBuildConfigClass(BuildConfig.class) .build(); ACRA.init(this, acraConfig); - } catch (ACRAConfigurationException ace) { + } catch (final ACRAConfigurationException ace) { ace.printStackTrace(); ErrorActivity.reportError(this, ace, @@ -219,7 +220,7 @@ public class App extends Application { } public void initNotificationChannel() { - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return; } @@ -230,10 +231,10 @@ public class App extends Application { // Keep this below DEFAULT to avoid making noise on every notification update final int importance = NotificationManager.IMPORTANCE_LOW; - NotificationChannel mChannel = new NotificationChannel(id, name, importance); + final NotificationChannel mChannel = new NotificationChannel(id, name, importance); mChannel.setDescription(description); - NotificationManager mNotificationManager = + final NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager.createNotificationChannel(mChannel); @@ -254,11 +255,11 @@ public class App extends Application { final String appUpdateDescription = getString(R.string.app_update_notification_channel_description); - NotificationChannel appUpdateChannel + final NotificationChannel appUpdateChannel = new NotificationChannel(appUpdateId, appUpdateName, importance); appUpdateChannel.setDescription(appUpdateDescription); - NotificationManager appUpdateNotificationManager + final NotificationManager appUpdateNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); appUpdateNotificationManager.createNotificationChannel(appUpdateChannel); } diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 625f514e9..d91230765 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -11,7 +11,7 @@ import android.content.pm.Signature; import android.net.ConnectivityManager; import android.net.Uri; import android.os.AsyncTask; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import androidx.core.app.NotificationCompat; @@ -62,7 +62,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { try { packageInfo = pm.getPackageInfo(packageName, flags); - } catch (PackageManager.NameNotFoundException e) { + } catch (final PackageManager.NameNotFoundException e) { ErrorActivity.reportError(APP, e, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", "Could not find package info", R.string.app_ui_crash)); @@ -77,7 +77,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { try { final CertificateFactory cf = CertificateFactory.getInstance("X509"); c = (X509Certificate) cf.generateCertificate(input); - } catch (CertificateException e) { + } catch (final CertificateException e) { ErrorActivity.reportError(APP, e, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", "Certificate error", R.string.app_ui_crash)); @@ -86,7 +86,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { String hexString = null; try { - MessageDigest md = MessageDigest.getInstance("SHA1"); + final MessageDigest md = MessageDigest.getInstance("SHA1"); final byte[] publicKey = md.digest(c.getEncoded()); hexString = byte2HexFormatted(publicKey); } catch (NoSuchAlgorithmException | CertificateEncodingException e) { @@ -167,7 +167,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode); - } catch (JsonParserException e) { + } catch (final JsonParserException e) { // connectivity problems, do not alarm user and fail silently if (DEBUG) { Log.w(TAG, Log.getStackTraceString(e)); @@ -187,7 +187,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { private void compareAppVersionAndShowNotification(final String versionName, final String apkLocationUrl, final int versionCode) { - int notificationId = 2000; + final int notificationId = 2000; if (BuildConfig.VERSION_CODE < versionCode) { diff --git a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java index 95d3c2b7c..69bfdfc2a 100644 --- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java +++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java @@ -2,7 +2,7 @@ package org.schabi.newpipe; import android.content.Context; import android.os.Build; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -94,18 +94,18 @@ public final class DownloaderImpl extends Downloader { private static void enableModernTLS(final OkHttpClient.Builder builder) { try { // get the default TrustManager - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init((KeyStore) null); - TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)); } - X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; + final X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; // insert our own TLSSocketFactory - SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance(); + final SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance(); builder.sslSocketFactory(sslSocketFactory, trustManager); @@ -114,16 +114,16 @@ public final class DownloaderImpl extends Downloader { // Necessary because some servers (e.g. Framatube.org) // don't support the old cipher suites. // https://github.com/square/okhttp/issues/4053#issuecomment-402579554 - List cipherSuites = new ArrayList<>(); - cipherSuites.addAll(ConnectionSpec.MODERN_TLS.cipherSuites()); + final List cipherSuites = + new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites()); cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA); cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA); - ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + final ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .cipherSuites(cipherSuites.toArray(new CipherSuite[0])) .build(); builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT)); - } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { + } catch (final KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { if (DEBUG) { e.printStackTrace(); } @@ -131,15 +131,15 @@ public final class DownloaderImpl extends Downloader { } public String getCookies(final String url) { - List resultCookies = new ArrayList<>(); + final List resultCookies = new ArrayList<>(); if (url.contains(YOUTUBE_DOMAIN)) { - String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY); + final String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY); if (youtubeCookie != null) { resultCookies.add(youtubeCookie); } } // Recaptcha cookie is always added TODO: not sure if this is necessary - String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY); + final String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY); if (recaptchaCookie != null) { resultCookies.add(recaptchaCookie); } @@ -159,9 +159,9 @@ public final class DownloaderImpl extends Downloader { } public void updateYoutubeRestrictedModeCookies(final Context context) { - String restrictedModeEnabledKey = + final String restrictedModeEnabledKey = context.getString(R.string.youtube_restricted_mode_enabled); - boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context) + final boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(restrictedModeEnabledKey, false); updateYoutubeRestrictedModeCookies(restrictedModeEnabled); } @@ -186,9 +186,9 @@ public final class DownloaderImpl extends Downloader { try { final Response response = head(url); return Long.parseLong(response.getHeader("Content-Length")); - } catch (NumberFormatException e) { + } catch (final NumberFormatException e) { throw new IOException("Invalid content length", e); - } catch (ReCaptchaException e) { + } catch (final ReCaptchaException e) { throw new IOException(e); } } @@ -199,7 +199,7 @@ public final class DownloaderImpl extends Downloader { .method("GET", null).url(siteUrl) .addHeader("User-Agent", USER_AGENT); - String cookies = getCookies(siteUrl); + final String cookies = getCookies(siteUrl); if (!cookies.isEmpty()) { requestBuilder.addHeader("Cookie", cookies); } @@ -218,7 +218,7 @@ public final class DownloaderImpl extends Downloader { } return body.byteStream(); - } catch (ReCaptchaException e) { + } catch (final ReCaptchaException e) { throw new IOException(e.getMessage(), e.getCause()); } } @@ -240,18 +240,18 @@ public final class DownloaderImpl extends Downloader { .method(httpMethod, requestBody).url(url) .addHeader("User-Agent", USER_AGENT); - String cookies = getCookies(url); + final String cookies = getCookies(url); if (!cookies.isEmpty()) { requestBuilder.addHeader("Cookie", cookies); } - for (Map.Entry> pair : headers.entrySet()) { + for (final Map.Entry> pair : headers.entrySet()) { final String headerName = pair.getKey(); final List headerValueList = pair.getValue(); if (headerValueList.size() > 1) { requestBuilder.removeHeader(headerName); - for (String headerValue : headerValueList) { + for (final String headerValue : headerValueList) { requestBuilder.addHeader(headerName, headerValue); } } else if (headerValueList.size() == 1) { diff --git a/app/src/main/java/org/schabi/newpipe/ExitActivity.java b/app/src/main/java/org/schabi/newpipe/ExitActivity.java index 94eff9560..d457500aa 100644 --- a/app/src/main/java/org/schabi/newpipe/ExitActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ExitActivity.java @@ -27,7 +27,7 @@ import android.os.Bundle; public class ExitActivity extends Activity { public static void exitAndRemoveFromRecentApps(final Activity activity) { - Intent intent = new Intent(activity, ExitActivity.class); + final Intent intent = new Intent(activity, ExitActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS @@ -42,7 +42,7 @@ public class ExitActivity extends Activity { protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { finishAndRemoveTask(); } else { finish(); diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java index ca61c9655..c2897cff1 100644 --- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java +++ b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java @@ -4,7 +4,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import com.nostra13.universalimageloader.core.download.BaseImageDownloader; diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 3fc3121b1..aedf64231 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -27,22 +27,22 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; + +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; @@ -53,6 +53,7 @@ import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.navigation.NavigationView; import org.schabi.newpipe.extractor.NewPipe; @@ -63,14 +64,18 @@ import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; +import org.schabi.newpipe.player.VideoPlayer; +import org.schabi.newpipe.player.event.OnKeyDownListener; +import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PeertubeHelper; 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; @@ -127,12 +132,6 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - Window w = getWindow(); - w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, - WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - } - if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) { initFragments(); @@ -141,11 +140,11 @@ public class MainActivity extends AppCompatActivity { setSupportActionBar(findViewById(R.id.toolbar)); try { setupDrawer(); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError(this, e); } - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(this); } } @@ -156,8 +155,8 @@ public class MainActivity extends AppCompatActivity { drawerItems = findViewById(R.id.navigation); //Tabs - int currentServiceId = ServiceHelper.getSelectedServiceId(this); - StreamingService service = NewPipe.getService(currentServiceId); + final int currentServiceId = ServiceHelper.getSelectedServiceId(this); + final StreamingService service = NewPipe.getService(currentServiceId); int kioskId = 0; @@ -229,7 +228,7 @@ public class MainActivity extends AppCompatActivity { case R.id.menu_tabs_group: try { tabSelected(item); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError(this, e); } break; @@ -270,8 +269,8 @@ public class MainActivity extends AppCompatActivity { NavigationHelper.openStatisticFragment(getSupportFragmentManager()); break; default: - int currentServiceId = ServiceHelper.getSelectedServiceId(this); - StreamingService service = NewPipe.getService(currentServiceId); + final int currentServiceId = ServiceHelper.getSelectedServiceId(this); + final StreamingService service = NewPipe.getService(currentServiceId); String serviceName = ""; int kioskId = 0; @@ -300,8 +299,8 @@ public class MainActivity extends AppCompatActivity { } private void setupDrawerHeader() { - NavigationView navigationView = findViewById(R.id.navigation); - View hView = navigationView.getHeaderView(0); + final NavigationView navigationView = findViewById(R.id.navigation); + final View hView = navigationView.getHeaderView(0); serviceArrow = hView.findViewById(R.id.drawer_arrow); headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon); @@ -336,7 +335,7 @@ public class MainActivity extends AppCompatActivity { } else { try { showTabs(); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError(this, e); } } @@ -345,11 +344,11 @@ public class MainActivity extends AppCompatActivity { private void showServices() { serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp); - for (StreamingService s : NewPipe.getServices()) { + for (final StreamingService s : NewPipe.getServices()) { final String title = s.getServiceInfo().getName() + (ServiceHelper.isBeta(s) ? " (beta)" : ""); - MenuItem menuItem = drawerItems.getMenu() + final MenuItem menuItem = drawerItems.getMenu() .add(R.id.menu_services_group, s.getServiceId(), ORDER, title) .setIcon(ServiceHelper.getIcon(s.getServiceId())); @@ -363,20 +362,20 @@ public class MainActivity extends AppCompatActivity { } private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) { - PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance(); + final PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance(); menuItem.setTitle(currentInstace.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : "")); - Spinner spinner = (Spinner) LayoutInflater.from(this) + final Spinner spinner = (Spinner) LayoutInflater.from(this) .inflate(R.layout.instance_spinner_layout, null); - List instances = PeertubeHelper.getInstanceList(this); - List items = new ArrayList<>(); + final List instances = PeertubeHelper.getInstanceList(this); + final List items = new ArrayList<>(); int defaultSelect = 0; - for (PeertubeInstance instance : instances) { + for (final PeertubeInstance instance : instances) { items.add(instance.getName()); if (instance.getUrl().equals(currentInstace.getUrl())) { defaultSelect = items.size() - 1; } } - ArrayAdapter adapter = new ArrayAdapter<>(this, + final ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); @@ -385,7 +384,7 @@ public class MainActivity extends AppCompatActivity { @Override public void onItemSelected(final AdapterView parent, final View view, final int position, final long id) { - PeertubeInstance newInstance = instances.get(position); + final PeertubeInstance newInstance = instances.get(position); if (newInstance.getUrl().equals(PeertubeHelper.getCurrentInstance().getUrl())) { return; } @@ -411,8 +410,8 @@ public class MainActivity extends AppCompatActivity { serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp); //Tabs - int currentServiceId = ServiceHelper.getSelectedServiceId(this); - StreamingService service = NewPipe.getService(currentServiceId); + final int currentServiceId = ServiceHelper.getSelectedServiceId(this); + final StreamingService service = NewPipe.getService(currentServiceId); int kioskId = 0; @@ -477,11 +476,12 @@ public class MainActivity extends AppCompatActivity { headerServiceView.post(() -> headerServiceView.setSelected(true)); toggleServiceButton.setContentDescription( getString(R.string.drawer_header_description) + selectedServiceName); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError(this, e); } - 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..."); @@ -514,7 +514,7 @@ public class MainActivity extends AppCompatActivity { if (intent != null) { // Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...) // to not destroy the already created backstack - String action = intent.getAction(); + final String action = intent.getAction(); if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { return; @@ -526,25 +526,60 @@ public class MainActivity extends AppCompatActivity { handleIntent(intent); } + @Override + public boolean onKeyDown(final int keyCode, final KeyEvent event) { + final Fragment fragment = getSupportFragmentManager() + .findFragmentById(R.id.fragment_player_holder); + if (fragment instanceof OnKeyDownListener + && !bottomSheetHiddenOrCollapsed()) { + // Provide keyDown event to fragment which then sends this event + // to the main player service + return ((OnKeyDownListener) fragment).onKeyDown(keyCode) + || super.onKeyDown(keyCode, event); + } + return super.onKeyDown(keyCode, event); + } + @Override public void onBackPressed() { if (DEBUG) { Log.d(TAG, "onBackPressed() called"); } - if (AndroidTvUtils.isTv(this)) { - View drawerPanel = findViewById(R.id.navigation); + if (DeviceUtils.isTv(this)) { + final View drawerPanel = findViewById(R.id.navigation); if (drawer.isDrawerOpen(drawerPanel)) { drawer.closeDrawers(); return; } } - Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); - // If current fragment implements BackPressable (i.e. can/wanna handle back press) - // delegate the back press to it - if (fragment instanceof BackPressable) { - if (((BackPressable) fragment).onBackPressed()) { + // In case bottomSheet is not visible on the screen or collapsed we can assume that the user + // interacts with a fragment inside fragment_holder so all back presses should be + // handled by it + if (bottomSheetHiddenOrCollapsed()) { + final Fragment fragment = getSupportFragmentManager() + .findFragmentById(R.id.fragment_holder); + // If current fragment implements BackPressable (i.e. can/wanna handle back press) + // delegate the back press to it + if (fragment instanceof BackPressable) { + if (((BackPressable) fragment).onBackPressed()) { + return; + } + } + + } else { + final Fragment fragmentPlayer = getSupportFragmentManager() + .findFragmentById(R.id.fragment_player_holder); + // If current fragment implements BackPressable (i.e. can/wanna handle back press) + // delegate the back press to it + if (fragmentPlayer instanceof BackPressable) { + if (!((BackPressable) fragmentPlayer).onBackPressed()) { + final FrameLayout bottomSheetLayout = + findViewById(R.id.fragment_player_holder); + BottomSheetBehavior.from(bottomSheetLayout) + .setState(BottomSheetBehavior.STATE_COLLAPSED); + } return; } } @@ -560,7 +595,7 @@ public class MainActivity extends AppCompatActivity { public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { - for (int i : grantResults) { + for (final int i : grantResults) { if (i == PackageManager.PERMISSION_DENIED) { return; } @@ -570,8 +605,8 @@ public class MainActivity extends AppCompatActivity { NavigationHelper.openDownloads(this); break; case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE: - Fragment fragment = getSupportFragmentManager() - .findFragmentById(R.id.fragment_holder); + final Fragment fragment = getSupportFragmentManager() + .findFragmentById(R.id.fragment_player_holder); if (fragment instanceof VideoDetailFragment) { ((VideoDetailFragment) fragment).openDownloadDialog(); } @@ -622,17 +657,14 @@ public class MainActivity extends AppCompatActivity { } super.onCreateOptionsMenu(menu); - Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); - if (!(fragment instanceof VideoDetailFragment)) { - findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner).setVisibility(View.GONE); - } - + final Fragment fragment + = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); if (!(fragment instanceof SearchFragment)) { findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container) .setVisibility(View.GONE); } - ActionBar actionBar = getSupportActionBar(); + final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); } @@ -647,7 +679,7 @@ public class MainActivity extends AppCompatActivity { if (DEBUG) { Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]"); } - int id = item.getItemId(); + final int id = item.getItemId(); switch (id) { case android.R.id.home: @@ -668,6 +700,13 @@ public class MainActivity extends AppCompatActivity { } StateSaver.clearStateFiles(); if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) { + // When user watch a video inside popup and then tries to open the video in main player + // while the app is closed he will see a blank fragment on place of kiosk. + // Let's open it first + if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + NavigationHelper.openMainFragment(getSupportFragmentManager()); + } + handleIntent(getIntent()); } else { NavigationHelper.gotoMainFragment(getSupportFragmentManager()); @@ -708,16 +747,22 @@ public class MainActivity extends AppCompatActivity { } if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { - String url = intent.getStringExtra(Constants.KEY_URL); - int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); - String title = intent.getStringExtra(Constants.KEY_TITLE); + final String url = intent.getStringExtra(Constants.KEY_URL); + final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); + final String title = intent.getStringExtra(Constants.KEY_TITLE); switch (((StreamingService.LinkType) intent .getSerializableExtra(Constants.KEY_LINK_TYPE))) { case STREAM: - boolean autoPlay = intent + final boolean autoPlay = intent .getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); + final String intentCacheKey = intent + .getStringExtra(VideoPlayer.PLAY_QUEUE_KEY); + final PlayQueue playQueue = intentCacheKey != null + ? SerializedCache.getInstance() + .take(intentCacheKey, PlayQueue.class) + : null; NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), - serviceId, url, title, autoPlay); + serviceId, url, title, autoPlay, playQueue); break; case CHANNEL: NavigationHelper.openChannelFragment(getSupportFragmentManager(), @@ -737,7 +782,7 @@ public class MainActivity extends AppCompatActivity { if (searchString == null) { searchString = ""; } - int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); + final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); NavigationHelper.openSearchFragment( getSupportFragmentManager(), serviceId, @@ -746,8 +791,21 @@ public class MainActivity extends AppCompatActivity { } else { NavigationHelper.gotoMainFragment(getSupportFragmentManager()); } - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError(this, e); } } + /* + * Utils + * */ + + private boolean bottomSheetHiddenOrCollapsed() { + final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder); + final BottomSheetBehavior bottomSheetBehavior = + BottomSheetBehavior.from(bottomSheetLayout); + + final int sheetState = bottomSheetBehavior.getState(); + return sheetState == BottomSheetBehavior.STATE_HIDDEN + || sheetState == BottomSheetBehavior.STATE_COLLAPSED; + } } diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index c59c48367..988a5ed98 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -46,7 +46,7 @@ public final class NewPipeDatabase { if (databaseInstance == null) { throw new IllegalStateException("database is not initialized"); } - Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null); + final Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null); if (c.moveToFirst() && c.getInt(0) == 1) { throw new RuntimeException("Checkpoint was blocked from completing"); } diff --git a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java index 2e1abd598..b4fbdfb28 100644 --- a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java +++ b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java @@ -31,7 +31,7 @@ public class PanicResponderActivity extends Activity { @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Intent intent = getIntent(); + final Intent intent = getIntent(); if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) { // TODO: Explicitly clear the search results // once they are restored when the app restarts @@ -40,7 +40,7 @@ public class PanicResponderActivity extends Activity { ExitActivity.exitAndRemoveFromRecentApps(this); } - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { finishAndRemoveTask(); } else { finish(); diff --git a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java index 40ea4fd58..c962ed99d 100644 --- a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java @@ -61,7 +61,7 @@ public class ReCaptchaActivity extends AppCompatActivity { ThemeHelper.setTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_recaptcha); - Toolbar toolbar = findViewById(R.id.toolbar); + final Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA); @@ -76,7 +76,7 @@ public class ReCaptchaActivity extends AppCompatActivity { webView = findViewById(R.id.reCaptchaWebView); // enable Javascript - WebSettings webSettings = webView.getSettings(); + final WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); webView.setWebViewClient(new WebViewClient() { @@ -84,7 +84,7 @@ public class ReCaptchaActivity extends AppCompatActivity { @Override public boolean shouldOverrideUrlLoading(final WebView view, final WebResourceRequest request) { - String url = request.getUrl().toString(); + final String url = request.getUrl().toString(); if (MainActivity.DEBUG) { Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url); } @@ -113,7 +113,7 @@ public class ReCaptchaActivity extends AppCompatActivity { // cleaning cache, history and cookies from webView webView.clearCache(true); webView.clearHistory(); - android.webkit.CookieManager cookieManager = CookieManager.getInstance(); + final android.webkit.CookieManager cookieManager = CookieManager.getInstance(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { cookieManager.removeAllCookies(aBoolean -> { }); @@ -128,7 +128,7 @@ public class ReCaptchaActivity extends AppCompatActivity { public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.menu_recaptcha, menu); - ActionBar actionBar = getSupportActionBar(); + final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(false); actionBar.setTitle(R.string.title_activity_recaptcha); @@ -145,7 +145,7 @@ public class ReCaptchaActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(final MenuItem item) { - int id = item.getItemId(); + final int id = item.getItemId(); switch (id) { case R.id.menu_item_done: saveCookiesAndFinish(); @@ -173,7 +173,7 @@ public class ReCaptchaActivity extends AppCompatActivity { setResult(RESULT_OK); } - Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class); + final Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); NavUtils.navigateUpTo(this, intent); } @@ -188,13 +188,13 @@ public class ReCaptchaActivity extends AppCompatActivity { return; } - String cookies = CookieManager.getInstance().getCookie(url); + final String cookies = CookieManager.getInstance().getCookie(url); handleCookies(cookies); // sometimes cookies are inside the url - int abuseStart = url.indexOf("google_abuse="); + final int abuseStart = url.indexOf("google_abuse="); if (abuseStart != -1) { - int abuseEnd = url.indexOf("+path"); + final int abuseEnd = url.indexOf("+path"); try { String abuseCookie = url.substring(abuseStart + 13, abuseEnd); diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 3d996f35c..12fdf8c78 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -8,7 +8,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.text.TextUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; @@ -44,7 +44,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ListHelper; @@ -320,7 +320,7 @@ public class RouterActivity extends AppCompatActivity { }; int id = 12345; - for (AdapterChoiceItem item : choices) { + for (final AdapterChoiceItem item : choices) { final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); radioButton.setText(item.description); @@ -340,7 +340,7 @@ public class RouterActivity extends AppCompatActivity { getString(R.string.preferred_open_action_last_selected_key), null); if (!TextUtils.isEmpty(lastSelectedPlayer)) { for (int i = 0; i < choices.size(); i++) { - AdapterChoiceItem c = choices.get(i); + final AdapterChoiceItem c = choices.get(i); if (lastSelectedPlayer.equals(c.key)) { selectedRadioPosition = i; break; @@ -357,7 +357,7 @@ public class RouterActivity extends AppCompatActivity { alertDialog.show(); - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(alertDialog); } } @@ -372,9 +372,9 @@ public class RouterActivity extends AppCompatActivity { final SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean( + final boolean isExtVideoEnabled = preferences.getBoolean( getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean( + final boolean isExtAudioEnabled = preferences.getBoolean( getString(R.string.use_external_audio_player_key), false); returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), @@ -420,9 +420,9 @@ public class RouterActivity extends AppCompatActivity { } private void handleText() { - String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT); - int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0); - Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class); + final String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT); + final int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0); + final Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); NavigationHelper.openSearch(getThemeWrapperContext(), serviceId, searchString); @@ -489,14 +489,14 @@ public class RouterActivity extends AppCompatActivity { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((@NonNull StreamInfo result) -> { - List sortedVideoStreams = ListHelper + final List sortedVideoStreams = ListHelper .getSortedStreamVideosList(this, result.getVideoStreams(), result.getVideoOnlyStreams(), false); - int selectedVideoStreamIndex = ListHelper + final int selectedVideoStreamIndex = ListHelper .getDefaultResolutionIndex(this, sortedVideoStreams); - FragmentManager fm = getSupportFragmentManager(); - DownloadDialog downloadDialog = DownloadDialog.newInstance(result); + final FragmentManager fm = getSupportFragmentManager(); + final DownloadDialog downloadDialog = DownloadDialog.newInstance(result); downloadDialog.setVideoStreams(sortedVideoStreams); downloadDialog.setAudioStreams(result.getAudioStreams()); downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); @@ -512,7 +512,7 @@ public class RouterActivity extends AppCompatActivity { public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { - for (int i : grantResults) { + for (final int i : grantResults) { if (i == PackageManager.PERMISSION_DENIED) { finish(); return; @@ -580,7 +580,7 @@ public class RouterActivity extends AppCompatActivity { } } } - return result.toArray(new String[result.size()]); + return result.toArray(new String[0]); } private static class AdapterChoiceItem { @@ -642,7 +642,7 @@ public class RouterActivity extends AppCompatActivity { if (!(serializable instanceof Choice)) { return; } - Choice playerChoice = (Choice) serializable; + final Choice playerChoice = (Choice) serializable; handleChoice(playerChoice); } @@ -690,13 +690,13 @@ public class RouterActivity extends AppCompatActivity { final SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(this); - boolean isExtVideoEnabled = preferences.getBoolean( + final boolean isExtVideoEnabled = preferences.getBoolean( getString(R.string.use_external_video_player_key), false); - boolean isExtAudioEnabled = preferences.getBoolean( + final boolean isExtAudioEnabled = preferences.getBoolean( getString(R.string.use_external_audio_player_key), false); PlayQueue playQueue; - String playerChoice = choice.playerChoice; + final String playerChoice = choice.playerChoice; if (info instanceof StreamInfo) { if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) { @@ -709,7 +709,7 @@ public class RouterActivity extends AppCompatActivity { playQueue = new SinglePlayQueue((StreamInfo) info); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue, true); + openMainPlayer(playQueue, choice); } else if (playerChoice.equals(backgroundPlayerKey)) { NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); } else if (playerChoice.equals(popupPlayerKey)) { @@ -724,7 +724,7 @@ public class RouterActivity extends AppCompatActivity { : new PlaylistPlayQueue((PlaylistInfo) info); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue, true); + openMainPlayer(playQueue, choice); } else if (playerChoice.equals(backgroundPlayerKey)) { NavigationHelper.playOnBackgroundPlayer(this, playQueue, true); } else if (playerChoice.equals(popupPlayerKey)) { @@ -734,6 +734,11 @@ public class RouterActivity extends AppCompatActivity { }; } + private void openMainPlayer(final PlayQueue playQueue, final Choice choice) { + NavigationHelper.playOnMainPlayer(this, playQueue, choice.linkType, + choice.url, "", true, true); + } + @Override public void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java index b5be2dde6..81ba78ba9 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java @@ -88,7 +88,7 @@ public class AboutActivity extends AppCompatActivity { setContentView(R.layout.activity_about); - Toolbar toolbar = findViewById(R.id.toolbar); + final Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Create the adapter that will return a fragment for each of the three @@ -99,13 +99,13 @@ public class AboutActivity extends AppCompatActivity { mViewPager = findViewById(R.id.container); mViewPager.setAdapter(mSectionsPagerAdapter); - TabLayout tabLayout = findViewById(R.id.tabs); + final TabLayout tabLayout = findViewById(R.id.tabs); tabLayout.setupWithViewPager(mViewPager); } @Override public boolean onOptionsItemSelected(final MenuItem item) { - int id = item.getItemId(); + final int id = item.getItemId(); switch (id) { case android.R.id.home: @@ -134,25 +134,25 @@ public class AboutActivity extends AppCompatActivity { @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_about, container, false); - Context context = this.getContext(); + final View rootView = inflater.inflate(R.layout.fragment_about, container, false); + final Context context = this.getContext(); - TextView version = rootView.findViewById(R.id.app_version); + final TextView version = rootView.findViewById(R.id.app_version); version.setText(BuildConfig.VERSION_NAME); - View githubLink = rootView.findViewById(R.id.github_link); + final View githubLink = rootView.findViewById(R.id.github_link); githubLink.setOnClickListener(nv -> openUrlInBrowser(context, context.getString(R.string.github_url))); - View donationLink = rootView.findViewById(R.id.donation_link); + final View donationLink = rootView.findViewById(R.id.donation_link); donationLink.setOnClickListener(v -> openUrlInBrowser(context, context.getString(R.string.donation_url))); - View websiteLink = rootView.findViewById(R.id.website_link); + final View websiteLink = rootView.findViewById(R.id.website_link); websiteLink.setOnClickListener(nv -> openUrlInBrowser(context, context.getString(R.string.website_url))); - View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link); + final View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link); privacyPolicyLink.setOnClickListener(v -> openUrlInBrowser(context, context.getString(R.string.privacy_policy_url))); @@ -167,7 +167,7 @@ public class AboutActivity extends AppCompatActivity { */ public class SectionsPagerAdapter extends FragmentPagerAdapter { public SectionsPagerAdapter(final FragmentManager fm) { - super(fm); + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/about/License.java b/app/src/main/java/org/schabi/newpipe/about/License.java index 370009860..877b51fd8 100644 --- a/app/src/main/java/org/schabi/newpipe/about/License.java +++ b/app/src/main/java/org/schabi/newpipe/about/License.java @@ -4,10 +4,12 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import java.io.Serializable; + /** * Class for storing information about a software license. */ -public class License implements Parcelable { +public class License implements Parcelable, Serializable { public static final Creator CREATOR = new Creator() { @Override public License createFromParcel(final Parcel source) { diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java index bc6310601..e869dbb14 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java @@ -1,7 +1,6 @@ package org.schabi.newpipe.about; import android.app.Activity; -import android.content.Context; import android.os.Bundle; import android.view.ContextMenu; import android.view.LayoutInflater; @@ -11,12 +10,14 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import org.schabi.newpipe.R; import org.schabi.newpipe.util.ShareUtils; +import java.io.Serializable; import java.util.Arrays; /** @@ -26,13 +27,15 @@ public class LicenseFragment extends Fragment { private static final String ARG_COMPONENTS = "components"; private SoftwareComponent[] softwareComponents; private SoftwareComponent componentForContextMenu; + private License activeLicense; + private static final String LICENSE_KEY = "ACTIVE_LICENSE"; public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) { if (softwareComponents == null) { throw new NullPointerException("softwareComponents is null"); } - LicenseFragment fragment = new LicenseFragment(); - Bundle bundle = new Bundle(); + final LicenseFragment fragment = new LicenseFragment(); + final Bundle bundle = new Bundle(); bundle.putParcelableArray(ARG_COMPONENTS, softwareComponents); fragment.setArguments(bundle); return fragment; @@ -44,8 +47,8 @@ public class LicenseFragment extends Fragment { * @param context the context to use * @param license the license to show */ - private static void showLicense(final Context context, final License license) { - new LicenseFragmentHelper((Activity) context).execute(license); + private static void showLicense(final Activity context, final License license) { + new LicenseFragmentHelper(context).execute(license); } @Override @@ -54,6 +57,12 @@ public class LicenseFragment extends Fragment { softwareComponents = (SoftwareComponent[]) getArguments() .getParcelableArray(ARG_COMPONENTS); + if (savedInstanceState != null) { + final Serializable license = savedInstanceState.getSerializable(LICENSE_KEY); + if (license != null) { + activeLicense = (License) license; + } + } // Sort components by name Arrays.sort(softwareComponents, (o1, o2) -> o1.getName().compareTo(o2.getName())); } @@ -66,8 +75,10 @@ public class LicenseFragment extends Fragment { final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components); final View licenseLink = rootView.findViewById(R.id.app_read_license); - licenseLink.setOnClickListener(v -> - showLicense(getActivity(), StandardLicenses.GPL3)); + licenseLink.setOnClickListener(v -> { + activeLicense = StandardLicenses.GPL3; + showLicense(getActivity(), StandardLicenses.GPL3); + }); for (final SoftwareComponent component : softwareComponents) { final View componentView = inflater @@ -81,11 +92,16 @@ public class LicenseFragment extends Fragment { component.getLicense().getAbbreviation())); componentView.setTag(component); - componentView.setOnClickListener(v -> - showLicense(getActivity(), component.getLicense())); + componentView.setOnClickListener(v -> { + activeLicense = component.getLicense(); + showLicense(getActivity(), component.getLicense()); + }); softwareComponentsView.addView(componentView); registerForContextMenu(componentView); } + if (activeLicense != null) { + showLicense(getActivity(), activeLicense); + } return rootView; } @@ -101,7 +117,7 @@ public class LicenseFragment extends Fragment { } @Override - public boolean onContextItemSelected(final MenuItem item) { + public boolean onContextItemSelected(@NonNull final MenuItem item) { // item.getMenuInfo() is null so we use the tag of the view final SoftwareComponent component = componentForContextMenu; if (component == null) { @@ -116,4 +132,12 @@ public class LicenseFragment extends Fragment { } return false; } + + @Override + public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + if (activeLicense != null) { + savedInstanceState.putSerializable(LICENSE_KEY, activeLicense); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java index 1c425567f..01a01bc88 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.java @@ -51,7 +51,7 @@ public class LicenseFragmentHelper extends AsyncTask { // split the HTML file and insert the stylesheet into the HEAD of the file webViewData = licenseContent.toString().replace("", ""); - } catch (IOException e) { + } catch (final IOException e) { throw new IllegalArgumentException( "Could not get license file: " + license.getFilename(), e); } diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java index bcb9ece10..dd526ecc8 100644 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java @@ -14,13 +14,13 @@ import io.reactivex.Flowable; @Dao public interface BasicDAO { /* Inserts */ - @Insert(onConflict = OnConflictStrategy.FAIL) + @Insert(onConflict = OnConflictStrategy.ABORT) long insert(Entity entity); - @Insert(onConflict = OnConflictStrategy.FAIL) + @Insert(onConflict = OnConflictStrategy.ABORT) List insertAll(Entity... entities); - @Insert(onConflict = OnConflictStrategy.FAIL) + @Insert(onConflict = OnConflictStrategy.ABORT) List insertAll(Collection entities); /* Searches */ diff --git a/app/src/main/java/org/schabi/newpipe/database/Converters.java b/app/src/main/java/org/schabi/newpipe/database/Converters.java index e1a2fe2f3..ca2d8d875 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Converters.java +++ b/app/src/main/java/org/schabi/newpipe/database/Converters.java @@ -49,7 +49,7 @@ public final class Converters { @TypeConverter public static FeedGroupIcon feedGroupIconOf(final Integer id) { - for (FeedGroupIcon icon : FeedGroupIcon.values()) { + for (final FeedGroupIcon icon : FeedGroupIcon.values()) { if (icon.getId() == id) { return icon; } diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index 088b9ed19..f5195ba8d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -6,7 +6,7 @@ import androidx.annotation.NonNull; import androidx.room.migration.Migration; import androidx.sqlite.db.SupportSQLiteDatabase; -import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.MainActivity; public final class Migrations { public static final int DB_VER_1 = 1; @@ -14,7 +14,7 @@ public final class Migrations { public static final int DB_VER_3 = 3; private static final String TAG = Migrations.class.getName(); - public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); + public static final boolean DEBUG = MainActivity.DEBUG; public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) { @Override diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index fd99f84a1..3ce95631c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -1,7 +1,33 @@ 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.List; public interface PlaylistLocalItem extends LocalItem { String getOrderingName(); + + static List merge( + final List localPlaylists, + final List remotePlaylists) { + final List items = new ArrayList<>( + localPlaylists.size() + remotePlaylists.size()); + items.addAll(localPlaylists); + items.addAll(remotePlaylists); + + Collections.sort(items, (left, right) -> { + final String on1 = left.getOrderingName(); + final String on2 = right.getOrderingName(); + if (on1 == null) { + return on2 == null ? 0 : 1; + } else { + return on2 == null ? -1 : on1.compareToIgnoreCase(on2); + } + }); + + return items; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 573fa4b90..60dd343b9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -20,6 +20,45 @@ abstract class SubscriptionDAO : BasicDAO { @Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC") abstract override fun getAll(): Flowable> + @Query(""" + SELECT * FROM subscriptions + + WHERE name LIKE '%' || :filter || '%' + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsFiltered(filter: String): Flowable> + + @Query(""" + SELECT * FROM subscriptions s + + LEFT JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + + WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsOnlyUngrouped( + currentGroupId: Long + ): Flowable> + + @Query(""" + SELECT * FROM subscriptions s + + LEFT JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + + WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) + AND s.name LIKE '%' || :filter || '%' + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsOnlyUngroupedFiltered( + currentGroupId: Long, + filter: String + ): Flowable> + @Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable> @@ -52,7 +91,7 @@ abstract class SubscriptionDAO : BasicDAO { entity.uid = uidFromInsert } else { val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url) - ?: throw IllegalStateException("Subscription cannot be null just after insertion.") + ?: throw IllegalStateException("Subscription cannot be null just after insertion.") entity.uid = subscriptionIdFromDb update(entity) diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index cc7219543..1cf38dbca 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -50,7 +50,7 @@ public class SubscriptionEntity { @Ignore public static SubscriptionEntity from(@NonNull final ChannelInfo info) { - SubscriptionEntity result = new SubscriptionEntity(); + final SubscriptionEntity result = new SubscriptionEntity(); result.setServiceId(info.getServiceId()); result.setUrl(info.getUrl()); result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), @@ -124,10 +124,61 @@ public class SubscriptionEntity { @Ignore public ChannelInfoItem toChannelInfoItem() { - ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName()); + final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName()); item.setThumbnailUrl(getAvatarUrl()); item.setSubscriberCount(getSubscriberCount()); item.setDescription(getDescription()); return item; } + + + // TODO: Remove these generated methods by migrating this class to a data class from Kotlin. + @Override + @SuppressWarnings("EqualsReplaceableByObjectsCall") + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final SubscriptionEntity that = (SubscriptionEntity) o; + + if (uid != that.uid) { + return false; + } + if (serviceId != that.serviceId) { + return false; + } + if (!url.equals(that.url)) { + return false; + } + if (name != null ? !name.equals(that.name) : that.name != null) { + return false; + } + if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) { + return false; + } + if (subscriberCount != null + ? !subscriberCount.equals(that.subscriberCount) + : that.subscriberCount != null) { + return false; + } + return description != null + ? description.equals(that.description) + : that.description == null; + } + + @Override + public int hashCode() { + int result = (int) (uid ^ (uid >>> 32)); + result = 31 * result + serviceId; + result = 31 * result + url.hashCode(); + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0); + result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + return result; + } } diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index e46ded40d..979f8be75 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -1,6 +1,5 @@ package org.schabi.newpipe.download; -import android.app.FragmentTransaction; import android.content.Intent; import android.os.Bundle; import android.view.Menu; @@ -11,9 +10,10 @@ import android.view.ViewTreeObserver; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.FragmentTransaction; import org.schabi.newpipe.R; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -29,7 +29,7 @@ public class DownloadActivity extends AppCompatActivity { @Override protected void onCreate(final Bundle savedInstanceState) { // Service - Intent i = new Intent(); + final Intent i = new Intent(); i.setClass(this, DownloadManagerService.class); startService(i); @@ -38,10 +38,10 @@ public class DownloadActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_downloader); - Toolbar toolbar = findViewById(R.id.toolbar); + final Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - ActionBar actionBar = getSupportActionBar(); + final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.downloads_title); @@ -57,13 +57,13 @@ public class DownloadActivity extends AppCompatActivity { } }); - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(this); } } private void updateFragments() { - MissionsFragment fragment = new MissionsFragment(); + final MissionsFragment fragment = new MissionsFragment(); getSupportFragmentManager().beginTransaction() .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) @@ -74,7 +74,7 @@ public class DownloadActivity extends AppCompatActivity { @Override public boolean onCreateOptionsMenu(final Menu menu) { super.onCreateOptionsMenu(menu); - MenuInflater inflater = getMenuInflater(); + final MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.download_menu, menu); diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index cad0258da..17d079d50 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -10,7 +10,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.IBinder; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; @@ -124,7 +124,7 @@ public class DownloadDialog extends DialogFragment private SharedPreferences prefs; public static DownloadDialog newInstance(final StreamInfo info) { - DownloadDialog dialog = new DownloadDialog(); + final DownloadDialog dialog = new DownloadDialog(); dialog.setInfo(info); return dialog; } @@ -208,14 +208,15 @@ public class DownloadDialog extends DialogFragment setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); Icepick.restoreInstanceState(this, savedInstanceState); - SparseArray> secondaryStreams = new SparseArray<>(4); - List videoStreams = wrappedVideoStreams.getStreamsList(); + final SparseArray> secondaryStreams + = new SparseArray<>(4); + final List videoStreams = wrappedVideoStreams.getStreamsList(); for (int i = 0; i < videoStreams.size(); i++) { if (!videoStreams.get(i).isVideoOnly()) { continue; } - AudioStream audioStream = SecondaryStreamHelper + final AudioStream audioStream = SecondaryStreamHelper .getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i)); if (audioStream != null) { @@ -232,13 +233,13 @@ public class DownloadDialog extends DialogFragment this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams); this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams); - Intent intent = new Intent(context, DownloadManagerService.class); + final Intent intent = new Intent(context, DownloadManagerService.class); context.startService(intent); context.bindService(intent, new ServiceConnection() { @Override public void onServiceConnected(final ComponentName cname, final IBinder service) { - DownloadManagerBinder mgr = (DownloadManagerBinder) service; + final DownloadManagerBinder mgr = (DownloadManagerBinder) service; mainStorageAudio = mgr.getMainStorageAudio(); mainStorageVideo = mgr.getMainStorageVideo(); @@ -294,9 +295,9 @@ public class DownloadDialog extends DialogFragment initToolbar(view.findViewById(R.id.toolbar)); setupDownloadOptions(); - prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - int threads = prefs.getInt(getString(R.string.default_download_threads), 3); + final int threads = prefs.getInt(getString(R.string.default_download_threads), 3); threadsCountTextView.setText(String.valueOf(threads)); threadsSeekBar.setProgress(threads - 1); threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @@ -373,13 +374,13 @@ public class DownloadDialog extends DialogFragment } if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) { - File file = Utils.getFileForUri(data.getData()); + final File file = Utils.getFileForUri(data.getData()); checkSelectedDownload(null, Uri.fromFile(file), file.getName(), StoredFileHelper.DEFAULT_MIME); return; } - DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData()); + final DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData()); if (docFile == null) { showFailedDialog(R.string.general_error); return; @@ -515,7 +516,23 @@ public class DownloadDialog extends DialogFragment videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE); - if (isVideoStreamsAvailable) { + prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type), + getString(R.string.last_download_type_video_key)); + + if (isVideoStreamsAvailable + && (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) { + videoButton.setChecked(true); + setupVideoSpinner(); + } else if (isAudioStreamsAvailable + && (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) { + audioButton.setChecked(true); + setupAudioSpinner(); + } else if (isSubtitleStreamsAvailable + && (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) { + subtitleButton.setChecked(true); + setupSubtitleSpinner(); + } else if (isVideoStreamsAvailable) { videoButton.setChecked(true); setupVideoSpinner(); } else if (isAudioStreamsAvailable) { @@ -564,7 +581,7 @@ public class DownloadDialog extends DialogFragment } private String getNameEditText() { - String str = nameEditText.getText().toString().trim(); + final String str = nameEditText.getText().toString().trim(); return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); } @@ -591,9 +608,10 @@ public class DownloadDialog extends DialogFragment } private void prepareSelectedDownload() { - StoredDirectoryHelper mainStorage; - MediaFormat format; - String mime; + final StoredDirectoryHelper mainStorage; + final MediaFormat format; + final String mime; + final String selectedMediaType; // first, build the filename and get the output folder (if possible) // later, run a very very very large file checking logic @@ -602,6 +620,7 @@ public class DownloadDialog extends DialogFragment switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: + selectedMediaType = getString(R.string.last_download_type_audio_key); mainStorage = mainStorageAudio; format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); switch (format) { @@ -616,12 +635,14 @@ public class DownloadDialog extends DialogFragment } break; case R.id.video_button: + selectedMediaType = getString(R.string.last_download_type_video_key); mainStorage = mainStorageVideo; format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); mime = format.mimeType; filename += format.suffix; break; case R.id.subtitle_button: + selectedMediaType = getString(R.string.last_download_type_subtitle_key); mainStorage = mainStorageVideo; // subtitle & video files go together format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); mime = format.mimeType; @@ -663,6 +684,11 @@ public class DownloadDialog extends DialogFragment // check for existing file with the same name checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime); + + // remember the last media type downloaded by the user + prefs.edit() + .putString(getString(R.string.last_used_download_type), selectedMediaType) + .apply(); } private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, @@ -683,15 +709,17 @@ public class DownloadDialog extends DialogFragment storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag()); } - } catch (Exception e) { + } catch (final Exception e) { showErrorActivity(e); return; } // check if is our file - MissionState state = downloadManager.checkForExistingMission(storage); - @StringRes int msgBtn; - @StringRes int msgBody; + final MissionState state = downloadManager.checkForExistingMission(storage); + @StringRes + final int msgBtn; + @StringRes + final int msgBody; switch (state) { case Finished: @@ -744,8 +772,7 @@ public class DownloadDialog extends DialogFragment return; } - - AlertDialog.Builder askDialog = new AlertDialog.Builder(context) + final AlertDialog.Builder askDialog = new AlertDialog.Builder(context) .setTitle(R.string.download_dialog_title) .setMessage(msgBody) .setNegativeButton(android.R.string.cancel, null); @@ -787,7 +814,7 @@ public class DownloadDialog extends DialogFragment // try take (or steal) the file storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag()); - } catch (IOException e) { + } catch (final IOException e) { Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString()); storageNew = null; @@ -825,18 +852,18 @@ public class DownloadDialog extends DialogFragment if (storage.length() > 0) { storage.truncate(); } - } catch (IOException e) { + } catch (final IOException e) { Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e); showFailedDialog(R.string.overwrite_failed); return; } - Stream selectedStream; + final Stream selectedStream; Stream secondaryStream = null; - char kind; + final char kind; int threads = threadsSeekBar.getProgress() + 1; - String[] urls; - MissionRecoveryInfo[] recoveryInfo; + final String[] urls; + final MissionRecoveryInfo[] recoveryInfo; String psName = null; String[] psArgs = null; long nearLength = 0; @@ -857,7 +884,7 @@ public class DownloadDialog extends DialogFragment kind = 'v'; selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex); - SecondaryStreamHelper secondary = videoStreamsAdapter + final SecondaryStreamHelper secondary = videoStreamsAdapter .getAllSecondary() .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); @@ -871,7 +898,7 @@ public class DownloadDialog extends DialogFragment } psArgs = null; - long videoSize = wrappedVideoStreams + final long videoSize = wrappedVideoStreams .getSizeInBytes((VideoStream) selectedStream); // set nearLength, only, if both sizes are fetched or known. This probably diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java index 255841857..c687c4a6a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -230,7 +230,7 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC } Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); // Starting ReCaptcha Challenge Activity - Intent intent = new Intent(activity, ReCaptchaActivity.class); + final Intent intent = new Intent(activity, ReCaptchaActivity.class); intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl()); startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 709dac368..d138a298c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.fragments; import android.content.Context; import android.content.res.ColorStateList; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -74,7 +74,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); previousYoutubeRestrictedModeEnabled = - PreferenceManager.getDefaultSharedPreferences(getContext()) + PreferenceManager.getDefaultSharedPreferences(requireContext()) .getBoolean(youtubeRestrictedModeEnabledKey, false); } @@ -104,8 +104,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte public void onResume() { super.onResume(); - boolean youtubeRestrictedModeEnabled = - PreferenceManager.getDefaultSharedPreferences(getContext()) + final boolean youtubeRestrictedModeEnabled = + PreferenceManager.getDefaultSharedPreferences(requireContext()) .getBoolean(youtubeRestrictedModeEnabledKey, false); if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) { previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled; @@ -137,7 +137,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte } inflater.inflate(R.menu.main_fragment_menu, menu); - ActionBar supportActionBar = activity.getSupportActionBar(); + final ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setDisplayHomeAsUpEnabled(false); } @@ -148,11 +148,9 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte switch (item.getItemId()) { case R.id.action_search: try { - NavigationHelper.openSearchFragment( - getFragmentManager(), - ServiceHelper.getSelectedServiceId(activity), - ""); - } catch (Exception e) { + NavigationHelper.openSearchFragment(getFM(), + ServiceHelper.getSelectedServiceId(activity), ""); + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } return true; @@ -239,7 +237,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte Fragment fragment = null; try { fragment = tab.getFragment(context); - } catch (ExtractionException e) { + } catch (final ExtractionException e) { throwable = e; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java index 28ce91f55..e93c333cb 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/OnScrollBelowItemsListener.java @@ -14,9 +14,9 @@ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollLi super.onScrolled(recyclerView, dx, dy); if (dy > 0) { int pastVisibleItems = 0; - int visibleItemCount; - int totalItemCount; - RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + final int visibleItemCount; + final int totalItemCount; + final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); visibleItemCount = layoutManager.getChildCount(); totalItemCount = layoutManager.getItemCount(); @@ -26,7 +26,7 @@ public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollLi pastVisibleItems = ((LinearLayoutManager) layoutManager) .findFirstVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { - int[] positions = ((StaggeredGridLayoutManager) layoutManager) + final int[] positions = ((StaggeredGridLayoutManager) layoutManager) .findFirstVisibleItemPositions(null); if (positions != null && positions.length > 0) { pastVisibleItems = positions[0]; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java index f966880b1..2fe615764 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java @@ -1,16 +1,29 @@ package org.schabi.newpipe.fragments.detail; +import org.schabi.newpipe.player.playqueue.PlayQueue; + import java.io.Serializable; class StackItem implements Serializable { private final int serviceId; - private final String url; + private String url; private String title; + private PlayQueue playQueue; - StackItem(final int serviceId, final String url, final String title) { + StackItem(final int serviceId, final String url, + final String title, final PlayQueue playQueue) { this.serviceId = serviceId; this.url = url; this.title = title; + this.playQueue = playQueue; + } + + public void setUrl(final String url) { + this.url = url; + } + + public void setPlayQueue(final PlayQueue queue) { + this.playQueue = queue; } public int getServiceId() { @@ -29,6 +42,10 @@ class StackItem implements Serializable { return url; } + public PlayQueue getPlayQueue() { + return playQueue; + } + @Override public String toString() { return getServiceId() + ":" + getUrl() + " > " + getTitle(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdapter.java similarity index 74% rename from app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java rename to app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdapter.java index 38f013200..1a11836d4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdapter.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.fragments.detail; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -10,16 +11,20 @@ import androidx.fragment.app.FragmentPagerAdapter; import java.util.ArrayList; import java.util.List; -public class TabAdaptor extends FragmentPagerAdapter { +public class TabAdapter extends FragmentPagerAdapter { private final List mFragmentList = new ArrayList<>(); private final List mFragmentTitleList = new ArrayList<>(); private final FragmentManager fragmentManager; - public TabAdaptor(final FragmentManager fm) { - super(fm); + public TabAdapter(final FragmentManager fm) { + // if changed to BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT => crash if enqueueing stream in + // the background and then clicking on it to open VideoDetailFragment: + // "Cannot setMaxLifecycle for Fragment not attached to FragmentManager" + super(fm, BEHAVIOR_SET_USER_VISIBLE_HINT); this.fragmentManager = fm; } + @NonNull @Override public Fragment getItem(final int position) { return mFragmentList.get(position); @@ -50,14 +55,14 @@ public class TabAdaptor extends FragmentPagerAdapter { } public void updateItem(final String title, final Fragment fragment) { - int index = mFragmentTitleList.indexOf(title); + final int index = mFragmentTitleList.indexOf(title); if (index != -1) { updateItem(index, fragment); } } @Override - public int getItemPosition(final Object object) { + public int getItemPosition(@NonNull final Object object) { if (mFragmentList.contains(object)) { return mFragmentList.indexOf(object); } else { @@ -82,7 +87,9 @@ public class TabAdaptor extends FragmentPagerAdapter { } @Override - public void destroyItem(final ViewGroup container, final int position, final Object object) { + public void destroyItem(@NonNull final ViewGroup container, + final int position, + @NonNull final Object object) { fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index fff689930..b731d0270 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1,47 +1,59 @@ package org.schabi.newpipe.fragments.detail; +import android.animation.ValueAnimator; import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.database.ContentObserver; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Html; +import android.os.Handler; +import android.os.IBinder; +import androidx.core.text.HtmlCompat; +import androidx.preference.PreferenceManager; +import android.provider.Settings; import android.text.Spanned; import android.text.TextUtils; import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; +import android.view.WindowManager; +import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; -import android.widget.Spinner; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.widget.Toolbar; +import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.viewpager.widget.ViewPager; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.tabs.TabLayout; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; @@ -68,31 +80,36 @@ import org.schabi.newpipe.fragments.list.comments.CommentsFragment; import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.MainVideoPlayer; -import org.schabi.newpipe.player.PopupVideoPlayer; +import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.VideoPlayer; +import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.event.OnKeyDownListener; +import org.schabi.newpipe.player.event.PlayerEventListener; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.helper.PlayerHelper; 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.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.InfoCache; -import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ShareUtils; -import org.schabi.newpipe.util.StreamItemAdapter; -import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.LargeTextMovementMethod; import java.io.Serializable; import java.util.Collection; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -108,30 +125,60 @@ import io.reactivex.schedulers.Schedulers; 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.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; +import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class VideoDetailFragment extends BaseStateFragment - implements BackPressable, SharedPreferences.OnSharedPreferenceChangeListener, - View.OnClickListener, View.OnLongClickListener { +public class VideoDetailFragment + extends BaseStateFragment + implements BackPressable, + SharedPreferences.OnSharedPreferenceChangeListener, + View.OnClickListener, + View.OnLongClickListener, + PlayerEventListener, + PlayerServiceEventListener, + OnKeyDownListener { public static final String AUTO_PLAY = "auto_play"; - private int updateFlags = 0; private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; - private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2; - private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4; - private static final int COMMENTS_UPDATE_FLAG = 0x8; + private static final int COMMENTS_UPDATE_FLAG = 0x2; + private static final float MAX_OVERLAY_ALPHA = 0.9f; + private static final float MAX_PLAYER_HEIGHT = 0.7f; + + public static final String ACTION_SHOW_MAIN_PLAYER = + "org.schabi.newpipe.VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER"; + public static final String ACTION_HIDE_MAIN_PLAYER = + "org.schabi.newpipe.VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER"; + public static final String ACTION_VIDEO_FRAGMENT_RESUMED = + "org.schabi.newpipe.VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED"; + public static final String ACTION_VIDEO_FRAGMENT_STOPPED = + "org.schabi.newpipe.VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED"; + + private static final String COMMENTS_TAB_TAG = "COMMENTS"; + private static final String RELATED_TAB_TAG = "NEXT VIDEO"; + private static final String EMPTY_TAB_TAG = "EMPTY TAB"; + + private static final String INFO_KEY = "info_key"; + private static final String STACK_KEY = "stack_key"; - private boolean autoPlayEnabled; private boolean showRelatedStreams; private boolean showComments; private String selectedTabTag; + private int updateFlags = 0; + @State protected int serviceId = Constants.NO_SERVICE_ID; @State protected String name; @State protected String url; + @State + protected PlayQueue playQueue; + @State + int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; + @State + protected boolean autoPlayEnabled = true; private StreamInfo currentInfo; private Disposable currentWorker; @@ -142,21 +189,20 @@ public class VideoDetailFragment extends BaseStateFragment private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; + private BottomSheetBehavior bottomSheetBehavior; + private BroadcastReceiver broadcastReceiver; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ - private Menu menu; - - private Spinner spinnerToolbar; - private LinearLayout contentRootLayoutHiding; private View thumbnailBackgroundButton; private ImageView thumbnailImageView; private ImageView thumbnailPlayButton; private AnimatedProgressBar positionView; + private ViewGroup playerPlaceholder; private View videoTitleRoot; private TextView videoTitleTextView; @@ -187,31 +233,148 @@ public class VideoDetailFragment extends BaseStateFragment private ImageView thumbsDownImageView; private TextView thumbsDisabledTextView; + private RelativeLayout overlay; + private LinearLayout overlayMetadata; + private ImageView overlayThumbnailImageView; + private TextView overlayTitleTextView; + private TextView overlayChannelTextView; + private LinearLayout overlayButtons; + private ImageButton overlayPlayPauseButton; + private ImageButton overlayCloseButton; + private AppBarLayout appBarLayout; private ViewPager viewPager; - private TabAdaptor pageAdapter; + private TabAdapter pageAdapter; private TabLayout tabLayout; private FrameLayout relatedStreamsLayout; + private ContentObserver settingsContentObserver; + private ServiceConnection serviceConnection; + private boolean bound; + private MainPlayer playerService; + private VideoPlayerImpl player; + + + /*////////////////////////////////////////////////////////////////////////// + // Service management + //////////////////////////////////////////////////////////////////////////*/ + + private ServiceConnection getServiceConnection(final Context context, + final boolean playAfterConnect) { + return new ServiceConnection() { + @Override + public void onServiceDisconnected(final ComponentName compName) { + if (DEBUG) { + Log.d(TAG, "Player service is disconnected"); + } + + unbind(context); + } + + @Override + public void onServiceConnected(final ComponentName compName, final IBinder service) { + if (DEBUG) { + Log.d(TAG, "Player service is connected"); + } + final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; + + playerService = localBinder.getService(); + player = localBinder.getPlayer(); + + startPlayerListener(); + + // It will do nothing if the player is not in fullscreen mode + hideSystemUiIfNeeded(); + + if (!player.videoPlayerSelected() && !playAfterConnect) { + return; + } + + if (playerIsNotStopped() && player.videoPlayerSelected()) { + addVideoPlayerView(); + } + + if (isLandscape()) { + // If the video is playing but orientation changed + // let's make the video in fullscreen again + checkLandscape(); + } else if (player.isFullscreen()) { + // Device is in portrait orientation after rotation but UI is in fullscreen. + // Return back to non-fullscreen state + player.toggleFullscreen(); + } + + if (playAfterConnect + || (currentInfo != null + && isAutoplayEnabled() + && player.getParentActivity() == null)) { + openVideoPlayer(); + } + } + }; + } + + private void bind(final Context context) { + if (DEBUG) { + Log.d(TAG, "bind() called"); + } + + final Intent serviceIntent = new Intent(context, MainPlayer.class); + bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); + if (!bound) { + context.unbindService(serviceConnection); + } + } + + private void unbind(final Context context) { + if (DEBUG) { + Log.d(TAG, "unbind() called"); + } + + if (bound) { + context.unbindService(serviceConnection); + bound = false; + stopPlayerListener(); + playerService = null; + player = null; + saveCurrentAndRestoreDefaultBrightness(); + } + } + + private void startPlayerListener() { + if (player != null) { + player.setFragmentListener(this); + } + } + + private void stopPlayerListener() { + if (player != null) { + player.removeFragmentListener(this); + } + } + + private void startService(final Context context, final boolean playAfterConnect) { + // startService() can be called concurrently and it will give a random crashes + // and NullPointerExceptions inside the service because the service will be + // bound twice. Prevent it with unbinding first + unbind(context); + context.startService(new Intent(context, MainPlayer.class)); + serviceConnection = getServiceConnection(context, playAfterConnect); + bind(context); + } + + private void stopService(final Context context) { + unbind(context); + context.stopService(new Intent(context, MainPlayer.class)); + } + + /*////////////////////////////////////////////////////////////////////////*/ - private static final String COMMENTS_TAB_TAG = "COMMENTS"; - private static final String RELATED_TAB_TAG = "NEXT VIDEO"; - private static final String EMPTY_TAB_TAG = "EMPTY TAB"; - - private static final String INFO_KEY = "info_key"; - private static final String STACK_KEY = "stack_key"; - - /** - * Stack that contains the "navigation history".
- * The peek is the current video. - */ - private final LinkedList stack = new LinkedList<>(); - public static VideoDetailFragment getInstance(final int serviceId, final String videoUrl, - final String name) { - VideoDetailFragment instance = new VideoDetailFragment(); - instance.setInitialData(serviceId, videoUrl, name); + final String name, final PlayQueue playQueue) { + final VideoDetailFragment instance = new VideoDetailFragment(); + instance.setInitialData(serviceId, videoUrl, name, playQueue); return instance; } @@ -223,7 +386,6 @@ public class VideoDetailFragment extends BaseStateFragment @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setHasOptionsMenu(true); showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(getString(R.string.show_next_video_key), true); @@ -236,6 +398,20 @@ public class VideoDetailFragment extends BaseStateFragment PreferenceManager.getDefaultSharedPreferences(activity) .registerOnSharedPreferenceChangeListener(this); + + setupBroadcastReceiver(); + + settingsContentObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(final boolean selfChange) { + if (activity != null && !PlayerHelper.globalScreenOrientationLocked(activity)) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + } + }; + activity.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, + settingsContentObserver); } @Override @@ -250,7 +426,8 @@ public class VideoDetailFragment extends BaseStateFragment if (currentWorker != null) { currentWorker.dispose(); } - PreferenceManager.getDefaultSharedPreferences(getContext()) + saveCurrentAndRestoreDefaultBrightness(); + PreferenceManager.getDefaultSharedPreferences(requireContext()) .edit() .putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem())) @@ -261,40 +438,54 @@ public class VideoDetailFragment extends BaseStateFragment public void onResume() { super.onResume(); + activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED)); + + setupBrightness(); + if (updateFlags != 0) { if (!isLoading.get() && currentInfo != null) { if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) { startLoading(false); } - if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) { - setupActionBar(currentInfo); - } if ((updateFlags & COMMENTS_UPDATE_FLAG) != 0) { startLoading(false); } } - if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0 - && menu != null) { - updateMenuItemVisibility(); - } - updateFlags = 0; } - // Check if it was loading when the fragment was stopped/paused, - if (wasLoading.getAndSet(false)) { - selectAndLoadVideo(serviceId, url, name); - } else if (currentInfo != null) { - updateProgressInfo(currentInfo); + // Check if it was loading when the fragment was stopped/paused + if (wasLoading.getAndSet(false) && !wasCleared()) { + startLoading(false); + } + } + + @Override + public void onStop() { + super.onStop(); + + if (!activity.isChangingConfigurations()) { + activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_STOPPED)); } } @Override public void onDestroy() { super.onDestroy(); + + // Stop the service when user leaves the app with double back press + // if video player is selected. Otherwise unbind + if (activity.isFinishing() && player != null && player.videoPlayerSelected()) { + stopService(requireContext()); + } else { + unbind(requireContext()); + } + PreferenceManager.getDefaultSharedPreferences(activity) .unregisterOnSharedPreferenceChangeListener(this); + activity.unregisterReceiver(broadcastReceiver); + activity.getContentResolver().unregisterContentObserver(settingsContentObserver); if (positionSubscriber != null) { positionSubscriber.dispose(); @@ -302,22 +493,10 @@ public class VideoDetailFragment extends BaseStateFragment if (currentWorker != null) { currentWorker.dispose(); } - if (disposables != null) { - disposables.clear(); - } + disposables.clear(); positionSubscriber = null; currentWorker = null; - disposables = null; - } - - @Override - public void onDestroyView() { - if (DEBUG) { - Log.d(TAG, "onDestroyView() called"); - } - spinnerToolbar.setOnItemSelectedListener(null); - spinnerToolbar.setAdapter(null); - super.onDestroyView(); + bottomSheetBehavior.setBottomSheetCallback(null); } @Override @@ -327,7 +506,7 @@ public class VideoDetailFragment extends BaseStateFragment case ReCaptchaActivity.RECAPTCHA_REQUEST: if (resultCode == Activity.RESULT_OK) { NavigationHelper - .openVideoDetailFragment(getFragmentManager(), serviceId, url, name); + .openVideoDetailFragment(getFM(), serviceId, url, name); } else { Log.e(TAG, "ReCaptcha failed"); } @@ -344,13 +523,6 @@ public class VideoDetailFragment extends BaseStateFragment if (key.equals(getString(R.string.show_next_video_key))) { showRelatedStreams = sharedPreferences.getBoolean(key, true); updateFlags |= RELATED_STREAMS_UPDATE_FLAG; - } else if (key.equals(getString(R.string.default_video_format_key)) - || key.equals(getString(R.string.default_resolution_key)) - || key.equals(getString(R.string.show_higher_resolutions_key)) - || key.equals(getString(R.string.use_external_video_player_key))) { - updateFlags |= RESOLUTIONS_MENU_UPDATE_FLAG; - } else if (key.equals(getString(R.string.show_play_with_kodi_key))) { - updateFlags |= TOOLBAR_ITEMS_UPDATE_FLAG; } else if (key.equals(getString(R.string.show_comments_key))) { showComments = sharedPreferences.getBoolean(key, true); updateFlags |= COMMENTS_UPDATE_FLAG; @@ -365,14 +537,13 @@ public class VideoDetailFragment extends BaseStateFragment public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); - // Check if the next video label and video is visible, - // if it is, include the two elements in the next check - int nextCount = currentInfo != null && currentInfo.getNextVideo() != null ? 2 : 0; - if (!isLoading.get() && currentInfo != null && isVisible()) { outState.putSerializable(INFO_KEY, currentInfo); } + if (playQueue != null) { + outState.putSerializable(VideoPlayer.PLAY_QUEUE_KEY, playQueue); + } outState.putSerializable(STACK_KEY, stack); } @@ -382,7 +553,6 @@ public class VideoDetailFragment extends BaseStateFragment Serializable serializable = savedState.getSerializable(INFO_KEY); if (serializable instanceof StreamInfo) { - //noinspection unchecked currentInfo = (StreamInfo) serializable; InfoCache.getInstance().putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM); } @@ -392,6 +562,7 @@ public class VideoDetailFragment extends BaseStateFragment //noinspection unchecked stack.addAll((Collection) serializable); } + playQueue = (PlayQueue) savedState.getSerializable(VideoPlayer.PLAY_QUEUE_KEY); } /*////////////////////////////////////////////////////////////////////////// @@ -400,10 +571,6 @@ public class VideoDetailFragment extends BaseStateFragment @Override public void onClick(final View v) { - if (isLoading.get() || currentInfo == null) { - return; - } - switch (v.getId()) { case R.id.detail_controls_background: openBackgroundPlayer(false); @@ -412,9 +579,9 @@ public class VideoDetailFragment extends BaseStateFragment openPopupPlayer(false); break; case R.id.detail_controls_playlist_append: - if (getFragmentManager() != null && currentInfo != null) { + if (getFM() != null && currentInfo != null) { PlaylistAppendDialog.fromStreamInfo(currentInfo) - .show(getFragmentManager(), TAG); + .show(getFM(), TAG); } break; case R.id.detail_controls_download: @@ -438,27 +605,38 @@ public class VideoDetailFragment extends BaseStateFragment } break; case R.id.detail_thumbnail_root_layout: - if (currentInfo.getVideoStreams().isEmpty() - && currentInfo.getVideoOnlyStreams().isEmpty()) { - openBackgroundPlayer(false); - } else { - openVideoPlayer(); - } + openVideoPlayer(); break; case R.id.detail_title_root_layout: toggleTitleAndDescription(); break; + case R.id.overlay_thumbnail: + case R.id.overlay_metadata_layout: + case R.id.overlay_buttons_layout: + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + break; + case R.id.overlay_play_pause_button: + if (playerIsNotStopped()) { + player.onPlayPause(); + player.hideControls(0, 0); + showSystemUi(); + } else { + openVideoPlayer(); + } + + setOverlayPlayPauseImage(); + break; + case R.id.overlay_close_button: + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + break; } } private void openChannel(final String subChannelUrl, final String subChannelName) { try { - NavigationHelper.openChannelFragment( - getFragmentManager(), - currentInfo.getServiceId(), - subChannelUrl, - subChannelName); - } catch (Exception e) { + NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(), + subChannelUrl, subChannelName); + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } } @@ -477,7 +655,13 @@ public class VideoDetailFragment extends BaseStateFragment openPopupPlayer(true); break; case R.id.detail_controls_download: - NavigationHelper.openDownloads(getActivity()); + NavigationHelper.openDownloads(activity); + break; + case R.id.overlay_thumbnail: + case R.id.overlay_metadata_layout: + if (currentInfo != null) { + openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName()); + } break; case R.id.detail_uploader_root_layout: if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { @@ -488,7 +672,8 @@ public class VideoDetailFragment extends BaseStateFragment } break; case R.id.detail_title_root_layout: - ShareUtils.copyToClipboard(getContext(), videoTitleTextView.getText().toString()); + ShareUtils.copyToClipboard(requireContext(), + videoTitleTextView.getText().toString()); break; } @@ -519,11 +704,10 @@ public class VideoDetailFragment extends BaseStateFragment @Override protected void initViews(final View rootView, final Bundle savedInstanceState) { super.initViews(rootView, savedInstanceState); - spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner); - thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout); thumbnailImageView = rootView.findViewById(R.id.detail_thumbnail_image_view); thumbnailPlayButton = rootView.findViewById(R.id.detail_thumbnail_play_button); + playerPlaceholder = rootView.findViewById(R.id.player_placeholder); contentRootLayoutHiding = rootView.findViewById(R.id.detail_content_root_hiding); @@ -557,9 +741,18 @@ public class VideoDetailFragment extends BaseStateFragment subChannelTextView = rootView.findViewById(R.id.detail_sub_channel_text_view); subChannelThumb = rootView.findViewById(R.id.detail_sub_channel_thumbnail_view); + overlay = rootView.findViewById(R.id.overlay_layout); + overlayMetadata = rootView.findViewById(R.id.overlay_metadata_layout); + overlayThumbnailImageView = rootView.findViewById(R.id.overlay_thumbnail); + overlayTitleTextView = rootView.findViewById(R.id.overlay_title_text_view); + overlayChannelTextView = rootView.findViewById(R.id.overlay_channel_text_view); + overlayButtons = rootView.findViewById(R.id.overlay_buttons_layout); + overlayPlayPauseButton = rootView.findViewById(R.id.overlay_play_pause_button); + overlayCloseButton = rootView.findViewById(R.id.overlay_close_button); + appBarLayout = rootView.findViewById(R.id.appbarlayout); viewPager = rootView.findViewById(R.id.viewpager); - pageAdapter = new TabAdaptor(getChildFragmentManager()); + pageAdapter = new TabAdapter(getChildFragmentManager()); viewPager.setAdapter(pageAdapter); tabLayout = rootView.findViewById(R.id.tablayout); tabLayout.setupWithViewPager(viewPager); @@ -570,7 +763,7 @@ public class VideoDetailFragment extends BaseStateFragment thumbnailBackgroundButton.requestFocus(); - if (AndroidTvUtils.isTv(getContext())) { + if (DeviceUtils.isTv(getContext())) { // remove ripple effects from detail controls final int transparent = getResources().getColor(R.color.transparent_background_color); detailControlsAddToPlaylist.setBackgroundColor(transparent); @@ -600,8 +793,20 @@ public class VideoDetailFragment extends BaseStateFragment detailControlsPopup.setLongClickable(true); detailControlsBackground.setOnLongClickListener(this); detailControlsPopup.setOnLongClickListener(this); + + overlayThumbnailImageView.setOnClickListener(this); + overlayThumbnailImageView.setOnLongClickListener(this); + overlayMetadata.setOnClickListener(this); + overlayMetadata.setOnLongClickListener(this); + overlayButtons.setOnClickListener(this); + overlayCloseButton.setOnClickListener(this); + overlayPlayPauseButton.setOnClickListener(this); + detailControlsBackground.setOnTouchListener(getOnControlsTouchListener()); detailControlsPopup.setOnTouchListener(getOnControlsTouchListener()); + + setupBottomPlayer(); + startService(requireContext(), false); } private View.OnTouchListener getOnControlsTouchListener() { @@ -621,6 +826,7 @@ public class VideoDetailFragment extends BaseStateFragment private void initThumbnailViews(@NonNull final StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); + if (!TextUtils.isEmpty(info.getThumbnailUrl())) { final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { @@ -647,144 +853,19 @@ public class VideoDetailFragment extends BaseStateFragment } } - /*////////////////////////////////////////////////////////////////////////// - // Menu - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreateOptionsMenu(final Menu m, final MenuInflater inflater) { - this.menu = m; - - // CAUTION set item properties programmatically otherwise it would not be accepted by - // appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu); - - inflater.inflate(R.menu.video_detail_menu, m); - - updateMenuItemVisibility(); - - ActionBar supportActionBar = activity.getSupportActionBar(); - if (supportActionBar != null) { - supportActionBar.setDisplayHomeAsUpEnabled(true); - supportActionBar.setDisplayShowTitleEnabled(false); - } - } - - private void updateMenuItemVisibility() { - // show kodi button if it supports the current service and it is enabled in settings - menu.findItem(R.id.action_play_with_kodi).setVisible( - KoreUtil.isServiceSupportedByKore(serviceId) - && PreferenceManager.getDefaultSharedPreferences(activity).getBoolean( - activity.getString(R.string.show_play_with_kodi_key), false)); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - int id = item.getItemId(); - if (id == R.id.action_settings) { - NavigationHelper.openSettings(requireContext()); - return true; - } - - if (isLoading.get()) { - // if still loading, block menu buttons related to video info - return true; - } - - switch (id) { - case R.id.menu_item_share: - if (currentInfo != null) { - ShareUtils.shareUrl(requireContext(), currentInfo.getName(), - currentInfo.getOriginalUrl()); - } - return true; - case R.id.menu_item_openInBrowser: - if (currentInfo != null) { - ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl()); - } - return true; - case R.id.action_play_with_kodi: - try { - NavigationHelper.playWithKore(activity, Uri.parse(currentInfo.getUrl())); - } catch (Exception e) { - if (DEBUG) { - Log.i(TAG, "Failed to start kore", e); - } - KoreUtil.showInstallKoreDialog(activity); - } - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void setupActionBarOnError(final String u) { - if (DEBUG) { - Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + u + "]"); - } - Log.e("-----", "missing code"); - } - - private void setupActionBar(final StreamInfo info) { - if (DEBUG) { - Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]"); - } - boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(activity.getString(R.string.use_external_video_player_key), false); - - sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), - info.getVideoOnlyStreams(), false); - selectedVideoStreamIndex = ListHelper - .getDefaultResolutionIndex(activity, sortedVideoStreams); - - final StreamItemAdapter streamsAdapter = new StreamItemAdapter<>( - activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), - isExternalPlayerEnabled); - spinnerToolbar.setAdapter(streamsAdapter); - spinnerToolbar.setSelection(selectedVideoStreamIndex); - spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(final AdapterView parent, final View view, - final int position, final long id) { - selectedVideoStreamIndex = position; - } - - @Override - public void onNothingSelected(final AdapterView parent) { } - }); - } - /*////////////////////////////////////////////////////////////////////////// // OwnStack //////////////////////////////////////////////////////////////////////////*/ - private void pushToStack(final int sid, final String videoUrl, final String title) { - if (DEBUG) { - Log.d(TAG, "pushToStack() called with: serviceId = [" - + sid + "], videoUrl = [" + videoUrl + "], title = [" + title + "]"); - } + /** + * Stack that contains the "navigation history".
+ * The peek is the current video. + */ + protected final LinkedList stack = new LinkedList<>(); - if (stack.size() > 0 - && stack.peek().getServiceId() == sid - && stack.peek().getUrl().equals(videoUrl)) { - Log.d(TAG, "pushToStack() called with: serviceId == peek.serviceId = [" - + sid + "], videoUrl == peek.getUrl = [" + videoUrl + "]"); - return; - } else { - Log.d(TAG, "pushToStack() wasn't equal"); - } - - stack.push(new StackItem(sid, videoUrl, title)); - } - - private void setTitleToUrl(final int sid, final String videoUrl, final String title) { - if (title != null && !title.isEmpty()) { - for (StackItem stackItem : stack) { - if (stack.peek().getServiceId() == sid - && stackItem.getUrl().equals(videoUrl)) { - stackItem.setTitle(title); - } - } - } + @Override + public boolean onKeyDown(final int keyCode) { + return player != null && player.onKeyDown(keyCode); } @Override @@ -792,27 +873,75 @@ public class VideoDetailFragment extends BaseStateFragment if (DEBUG) { Log.d(TAG, "onBackPressed() called"); } + + // If we are in fullscreen mode just exit from it via first back press + if (player != null && player.isFullscreen()) { + if (!DeviceUtils.isTablet(activity)) { + player.onPause(); + } + restoreDefaultOrientation(); + setAutoplay(false); + return true; + } + + // If we have something in history of played items we replay it here + if (player != null + && player.getPlayQueue() != null + && player.videoPlayerSelected() + && player.getPlayQueue().previous()) { + return true; + } // That means that we are on the start of the stack, // return false to let the MainActivity handle the onBack if (stack.size() <= 1) { + restoreDefaultOrientation(); + return false; } // Remove top stack.pop(); // Get stack item from the new top - StackItem peek = stack.peek(); + assert stack.peek() != null; + setupFromHistoryItem(stack.peek()); - selectAndLoadVideo(peek.getServiceId(), peek.getUrl(), - !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : ""); return true; } + private void setupFromHistoryItem(final StackItem item) { + setAutoplay(false); + hideMainPlayer(); + + setInitialData( + item.getServiceId(), + item.getUrl(), + !TextUtils.isEmpty(item.getTitle()) ? item.getTitle() : "", + item.getPlayQueue()); + startLoading(false); + + // Maybe an item was deleted in background activity + if (item.getPlayQueue().getItem() == null) { + return; + } + + final PlayQueueItem playQueueItem = item.getPlayQueue().getItem(); + // Update title, url, uploader from the last item in the stack (it's current now) + final boolean isPlayerStopped = player == null || player.isPlayerStopped(); + if (playQueueItem != null && isPlayerStopped) { + updateOverlayData(playQueueItem.getTitle(), + playQueueItem.getUploader(), playQueueItem.getThumbnailUrl()); + } + } + /*////////////////////////////////////////////////////////////////////////// // Info loading and handling //////////////////////////////////////////////////////////////////////////*/ @Override protected void doInitialLoadLogic() { + if (wasCleared()) { + return; + } + if (currentInfo == null) { prepareAndLoadInfo(); } else { @@ -820,9 +949,16 @@ public class VideoDetailFragment extends BaseStateFragment } } - public void selectAndLoadVideo(final int sid, final String videoUrl, final String title) { - setInitialData(sid, videoUrl, title); - prepareAndLoadInfo(); + public void selectAndLoadVideo(final int sid, final String videoUrl, final String title, + final PlayQueue queue) { + // Situation when user switches from players to main player. + // All needed data is here, we can start watching + if (this.playQueue != null && this.playQueue.equals(queue)) { + openVideoPlayer(); + return; + } + setInitialData(sid, videoUrl, title, queue); + startLoading(false, true); } private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) { @@ -831,22 +967,19 @@ public class VideoDetailFragment extends BaseStateFragment + "info = [" + info + "], scrollToTop = [" + scrollToTop + "]"); } - setInitialData(info.getServiceId(), info.getUrl(), info.getName()); - pushToStack(serviceId, url, name); showLoading(); initTabs(); if (scrollToTop) { - appBarLayout.setExpanded(true, true); + scrollToTop(); } handleResult(info); showContent(); } - private void prepareAndLoadInfo() { - appBarLayout.setExpanded(true, true); - pushToStack(serviceId, url, name); + protected void prepareAndLoadInfo() { + scrollToTop(); startLoading(false); } @@ -860,20 +993,46 @@ public class VideoDetailFragment extends BaseStateFragment currentWorker.dispose(); } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + runWorker(forceLoad, stack.isEmpty()); + } + private void startLoading(final boolean forceLoad, final boolean addToBackStack) { + super.startLoading(forceLoad); + + initTabs(); + currentInfo = null; + if (currentWorker != null) { + currentWorker.dispose(); + } + + runWorker(forceLoad, addToBackStack); + } + + private void runWorker(final boolean forceLoad, final boolean addToBackStack) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe((@NonNull final StreamInfo result) -> { isLoading.set(false); + hideMainPlayer(); if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean( getString(R.string.show_age_restricted_content), false)) { hideAgeRestrictedContent(); } else { - currentInfo = result; handleResult(result); showContent(); + if (addToBackStack) { + if (playQueue == null) { + playQueue = new SinglePlayQueue(result); + } + if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(playQueue)) { + stack.push(new StackItem(serviceId, url, name, playQueue)); + } + } + if (isAutoplayEnabled()) { + openVideoPlayer(); + } } }, (@NonNull final Throwable throwable) -> { isLoading.set(false); @@ -888,8 +1047,8 @@ public class VideoDetailFragment extends BaseStateFragment pageAdapter.clearAllItems(); if (shouldShowComments()) { - pageAdapter.addFragment(CommentsFragment.getInstance(serviceId, url, name), - COMMENTS_TAB_TAG); + pageAdapter.addFragment( + CommentsFragment.getInstance(serviceId, url, name), COMMENTS_TAB_TAG); } if (showRelatedStreams && null == relatedStreamsLayout) { @@ -906,7 +1065,7 @@ public class VideoDetailFragment extends BaseStateFragment if (pageAdapter.getCount() < 2) { tabLayout.setVisibility(View.GONE); } else { - int position = pageAdapter.getItemPositionByTitle(selectedTabTag); + final int position = pageAdapter.getItemPositionByTitle(selectedTabTag); if (position != -1) { viewPager.setCurrentItem(position); } @@ -920,23 +1079,34 @@ public class VideoDetailFragment extends BaseStateFragment .getServiceInfo() .getMediaCapabilities() .contains(COMMENTS); - } catch (ExtractionException e) { + } catch (final ExtractionException e) { return false; } } + public void scrollToTop() { + appBarLayout.setExpanded(true, true); + } + /*////////////////////////////////////////////////////////////////////////// // Play Utils //////////////////////////////////////////////////////////////////////////*/ private void openBackgroundPlayer(final boolean append) { - AudioStream audioStream = currentInfo.getAudioStreams() + final AudioStream audioStream = currentInfo.getAudioStreams() .get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); - boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) + final boolean useExternalAudioPlayer = PreferenceManager + .getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); - if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { + // If a user watched video inside fullscreen mode and than chose another player + // return to non-fullscreen mode + if (player != null && player.isFullscreen()) { + player.toggleFullscreen(); + } + + if (!useExternalAudioPlayer) { openNormalBackgroundPlayer(append); } else { startOnExternalPlayer(activity, currentInfo, audioStream); @@ -949,45 +1119,95 @@ public class VideoDetailFragment extends BaseStateFragment return; } - final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); + // See UI changes while remote playQueue changes + if (!bound) { + startService(requireContext(), false); + } + + // If a user watched video inside fullscreen mode and than chose another player + // return to non-fullscreen mode + if (player != null && player.isFullscreen()) { + player.toggleFullscreen(); + } + + final PlayQueue queue = setupPlayQueueForIntent(append); if (append) { - NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false); + NavigationHelper.enqueueOnPopupPlayer(activity, queue, false); } else { - Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - final Intent intent = NavigationHelper.getPlayerIntent(activity, - PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true); - activity.startService(intent); + replaceQueueIfUserConfirms(() -> NavigationHelper + .playOnPopupPlayer(activity, queue, true)); } } private void openVideoPlayer() { - VideoStream selectedVideoStream = getSelectedVideoStream(); - if (PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(this.getString(R.string.use_external_video_player_key), false)) { - startOnExternalPlayer(activity, currentInfo, selectedVideoStream); + showExternalPlaybackDialog(); } else { - openNormalPlayer(); + replaceQueueIfUserConfirms(this::openMainPlayer); } } private void openNormalBackgroundPlayer(final boolean append) { - final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); + // See UI changes while remote playQueue changes + if (!bound) { + startService(requireContext(), false); + } + + final PlayQueue queue = setupPlayQueueForIntent(append); if (append) { - NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue, false); + NavigationHelper.enqueueOnBackgroundPlayer(activity, queue, false); } else { - NavigationHelper.playOnBackgroundPlayer(activity, itemQueue, true); + replaceQueueIfUserConfirms(() -> NavigationHelper + .playOnBackgroundPlayer(activity, queue, true)); } } - private void openNormalPlayer() { - Intent mIntent; - final PlayQueue playQueue = new SinglePlayQueue(currentInfo); - mIntent = NavigationHelper.getPlayerIntent(activity, - MainVideoPlayer.class, - playQueue, - getSelectedVideoStream().getResolution(), true); - startActivity(mIntent); + private void openMainPlayer() { + if (playerService == null) { + startService(requireContext(), true); + return; + } + if (currentInfo == null) { + return; + } + + 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() + playerService.getView().setVisibility(View.GONE); + addVideoPlayerView(); + + final Intent playerIntent = NavigationHelper + .getPlayerIntent(requireContext(), MainPlayer.class, queue, null, true); + activity.startService(playerIntent); + } + + private void hideMainPlayer() { + if (playerService == null + || playerService.getView() == null + || !player.videoPlayerSelected()) { + return; + } + + removeVideoPlayerView(); + playerService.stop(isAutoplayEnabled()); + playerService.getView().setVisibility(View.GONE); + } + + private PlayQueue setupPlayQueueForIntent(final boolean append) { + if (append) { + return new SinglePlayQueue(currentInfo); + } + + PlayQueue queue = playQueue; + // Size can be 0 because queue removes bad stream automatically when error occurs + if (queue == null || queue.size() == 0) { + queue = new SinglePlayQueue(currentInfo); + } + + return queue; } /*////////////////////////////////////////////////////////////////////////// @@ -1012,37 +1232,88 @@ public class VideoDetailFragment extends BaseStateFragment )); } - @Nullable - private VideoStream getSelectedVideoStream() { - return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; + private boolean isExternalPlayerEnabled() { + return PreferenceManager.getDefaultSharedPreferences(getContext()) + .getBoolean(getString(R.string.use_external_video_player_key), false); + } + + // This method overrides default behaviour when setAutoplay() is called. + // Don't auto play if the user selected an external player or disabled it in settings + private boolean isAutoplayEnabled() { + return autoPlayEnabled + && !isExternalPlayerEnabled() + && (player == null || player.videoPlayerSelected()) + && bottomSheetState != BottomSheetBehavior.STATE_HIDDEN + && isAutoplayAllowedByUser(); + } + + private boolean isAutoplayAllowedByUser() { + if (activity == null) { + return false; + } + + switch (PlayerHelper.getAutoplayType(activity)) { + case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER: + return false; + case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI: + return !ListHelper.isMeteredNetwork(activity); + case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS: + default: + return true; + } + } + + private void addVideoPlayerView() { + if (player == null || getView() == null) { + return; + } + + // Check if viewHolder already contains a child + if (player.getRootView().getParent() != playerPlaceholder) { + removeVideoPlayerView(); + } + setHeightThumbnail(); + + // Prevent from re-adding a view multiple times + if (player.getRootView().getParent() == null) { + playerPlaceholder.addView(player.getRootView()); + } + } + + private void removeVideoPlayerView() { + makeDefaultHeightForVideoPlaceholder(); + + playerService.removeViewFromParent(); + } + + private void makeDefaultHeightForVideoPlaceholder() { + if (getView() == null) { + return; + } + + playerPlaceholder.getLayoutParams().height = FrameLayout.LayoutParams.MATCH_PARENT; + playerPlaceholder.requestLayout(); } private void prepareDescription(final Description description) { - if (TextUtils.isEmpty(description.getContent()) + if (description == null || TextUtils.isEmpty(description.getContent()) || description == Description.emptyDescription) { return; } if (description.getType() == Description.HTML) { disposables.add(Single.just(description.getContent()) - .map((@io.reactivex.annotations.NonNull String descriptionText) -> { - Spanned parsedDescription; - if (Build.VERSION.SDK_INT >= 24) { - parsedDescription = Html.fromHtml(descriptionText, 0); - } else { - //noinspection deprecation - parsedDescription = Html.fromHtml(descriptionText); - } - return parsedDescription; - }) + .map((@NonNull final String descriptionText) -> + HtmlCompat.fromHtml(descriptionText, + HtmlCompat.FROM_HTML_MODE_LEGACY)) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> { + .subscribe((@NonNull final Spanned spanned) -> { videoDescriptionView.setText(spanned); videoDescriptionView.setVisibility(View.VISIBLE); })); } else if (description.getType() == Description.MARKDOWN) { - final Markwon markwon = Markwon.builder(getContext()) + final Markwon markwon = Markwon.builder(requireContext()) .usePlugin(LinkifyPlugin.create()) .build(); markwon.setMarkdown(videoDescriptionView, description.getContent()); @@ -1055,25 +1326,47 @@ public class VideoDetailFragment extends BaseStateFragment } } + /** + * Method which controls the size of thumbnail and the size of main player inside + * a layout with thumbnail. It decides what height the player should have in both + * screen orientations. It knows about multiWindow feature + * and about videos with aspectRatio ZOOM (the height for them will be a bit higher, + * {@link #MAX_PLAYER_HEIGHT}) + */ private void setHeightThumbnail() { final DisplayMetrics metrics = getResources().getDisplayMetrics(); - boolean isPortrait = metrics.heightPixels > metrics.widthPixels; - int height = isPortrait - ? (int) (metrics.widthPixels / (16.0f / 9.0f)) - : (int) (metrics.heightPixels / 2f); + final boolean isPortrait = metrics.heightPixels > metrics.widthPixels; + + final int height; + if (player != null && player.isFullscreen()) { + height = isInMultiWindow() + ? requireView().getHeight() + : activity.getWindow().getDecorView().getHeight(); + } else { + height = isPortrait + ? (int) (metrics.widthPixels / (16.0f / 9.0f)) + : (int) (metrics.heightPixels / 2.0f); + } + thumbnailImageView.setLayoutParams( new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); thumbnailImageView.setMinimumHeight(height); + if (player != null) { + final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT); + player.getSurfaceView().setHeights(height, player.isFullscreen() ? height : maxHeight); + } } private void showContent() { contentRootLayoutHiding.setVisibility(View.VISIBLE); } - protected void setInitialData(final int sid, final String u, final String title) { + protected void setInitialData(final int sid, final String u, final String title, + final PlayQueue queue) { this.serviceId = sid; this.url = u; this.name = !TextUtils.isEmpty(title) ? title : ""; + this.playQueue = queue; } private void setErrorImage(final int imageResource) { @@ -1098,6 +1391,45 @@ public class VideoDetailFragment extends BaseStateFragment setErrorImage(imageError); } + private void setupBroadcastReceiver() { + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + if (intent.getAction().equals(ACTION_SHOW_MAIN_PLAYER)) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } else if (intent.getAction().equals(ACTION_HIDE_MAIN_PLAYER)) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + } + }; + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER); + intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER); + activity.registerReceiver(broadcastReceiver, intentFilter); + } + + + /*////////////////////////////////////////////////////////////////////////// + // Orientation listener + //////////////////////////////////////////////////////////////////////////*/ + + private void restoreDefaultOrientation() { + if (player == null || !player.videoPlayerSelected() || activity == null) { + return; + } + + if (player != null && player.isFullscreen()) { + player.toggleFullscreen(); + } + // This will show systemUI and pause the player. + // User can tap on Play button and video will be in fullscreen mode again + // Note for tablet: trying to avoid orientation changes since it's not easy + // to physically rotate the tablet every time + if (!DeviceUtils.isTablet(activity)) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + } + /*////////////////////////////////////////////////////////////////////////// // Contract //////////////////////////////////////////////////////////////////////////*/ @@ -1112,7 +1444,6 @@ public class VideoDetailFragment extends BaseStateFragment contentRootLayoutHiding.setVisibility(View.INVISIBLE); } - animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); animateView(detailDurationView, false, 100); animateView(detailPositionView, false, 100); @@ -1128,7 +1459,8 @@ public class VideoDetailFragment extends BaseStateFragment if (relatedStreamsLayout != null) { if (showRelatedStreams) { - relatedStreamsLayout.setVisibility(View.INVISIBLE); + relatedStreamsLayout.setVisibility( + player != null && player.isFullscreen() ? View.GONE : View.INVISIBLE); } else { relatedStreamsLayout.setVisibility(View.GONE); } @@ -1144,24 +1476,23 @@ public class VideoDetailFragment extends BaseStateFragment public void handleResult(@NonNull final StreamInfo info) { super.handleResult(info); - setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName()); + currentInfo = info; + setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName(), playQueue); if (showRelatedStreams) { if (null == relatedStreamsLayout) { //phone pageAdapter.updateItem(RELATED_TAB_TAG, - RelatedVideosFragment.getInstance(currentInfo)); + RelatedVideosFragment.getInstance(info)); pageAdapter.notifyDataSetUpdate(); } else { //tablet getChildFragmentManager().beginTransaction() .replace(R.id.relatedStreamsLayout, - RelatedVideosFragment.getInstance(currentInfo)) - .commitNow(); - relatedStreamsLayout.setVisibility(View.VISIBLE); + RelatedVideosFragment.getInstance(info)) + .commitAllowingStateLoss(); + relatedStreamsLayout.setVisibility( + player != null && player.isFullscreen() ? View.GONE : View.VISIBLE); } } - - //pushToStack(serviceId, url, name); - animateView(thumbnailPlayButton, true, 200); videoTitleTextView.setText(name); @@ -1174,7 +1505,7 @@ public class VideoDetailFragment extends BaseStateFragment uploaderThumb.setVisibility(View.GONE); } - Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy); + final Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy); subChannelThumb.setImageDrawable(buddyDrawable); uploaderThumb.setImageDrawable(buddyDrawable); @@ -1252,15 +1583,20 @@ public class VideoDetailFragment extends BaseStateFragment videoUploadDateView.setVisibility(View.GONE); } + sortedVideoStreams = ListHelper.getSortedStreamVideosList( + activity, + info.getVideoStreams(), + info.getVideoOnlyStreams(), + false); + selectedVideoStreamIndex = ListHelper + .getDefaultResolutionIndex(activity, sortedVideoStreams); prepareDescription(info.getDescription()); updateProgressInfo(info); - - animateView(spinnerToolbar, true, 500); - setupActionBar(info); initThumbnailViews(info); - setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName()); - setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName()); + if (player == null || player.isPlayerStopped()) { + updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); + } if (!info.getErrors().isEmpty()) { showSnackBarError(info.getErrors(), @@ -1274,7 +1610,6 @@ public class VideoDetailFragment extends BaseStateFragment case LIVE_STREAM: case AUDIO_LIVE_STREAM: detailControlsDownload.setVisibility(View.GONE); - spinnerToolbar.setVisibility(View.GONE); break; default: if (info.getAudioStreams().isEmpty()) { @@ -1283,18 +1618,10 @@ public class VideoDetailFragment extends BaseStateFragment if (!info.getVideoStreams().isEmpty() || !info.getVideoOnlyStreams().isEmpty()) { break; } - detailControlsPopup.setVisibility(View.GONE); - spinnerToolbar.setVisibility(View.GONE); thumbnailPlayButton.setImageResource(R.drawable.ic_headset_shadow); break; } - - if (autoPlayEnabled) { - openVideoPlayer(); - // Only auto play in the first open - autoPlayEnabled = false; - } } private void hideAgeRestrictedContent() { @@ -1335,15 +1662,15 @@ public class VideoDetailFragment extends BaseStateFragment public void openDownloadDialog() { try { - DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); + final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); downloadDialog.setVideoStreams(sortedVideoStreams); downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); - downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog"); - } catch (Exception e) { - ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, + downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); + } catch (final Exception e) { + final ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, ServiceList.all() .get(currentInfo .getServiceId()) @@ -1351,10 +1678,10 @@ public class VideoDetailFragment extends BaseStateFragment .getName(), "", R.string.could_not_setup_download_menu); - ErrorActivity.reportError(getActivity(), + ErrorActivity.reportError(activity, e, - getActivity().getClass(), - getActivity().findViewById(android.R.id.content), info); + activity.getClass(), + activity.findViewById(android.R.id.content), info); } } @@ -1368,11 +1695,11 @@ public class VideoDetailFragment extends BaseStateFragment return true; } - int errorId = exception instanceof YoutubeStreamExtractor.DecryptException + final int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error : exception instanceof ExtractionException - ? R.string.parsing_error - : R.string.general_error; + ? R.string.parsing_error + : R.string.general_error; onUnrecoverableError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, errorId); @@ -1388,17 +1715,28 @@ public class VideoDetailFragment extends BaseStateFragment final boolean playbackResumeEnabled = prefs .getBoolean(activity.getString(R.string.enable_watch_history_key), true) && prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true); - - if (!playbackResumeEnabled || info.getDuration() <= 0) { - positionView.setVisibility(View.INVISIBLE); - detailPositionView.setVisibility(View.GONE); - - // TODO: Remove this check when separation of concerns is done. - // (live streams weren't getting updated because they are mixed) - if (!info.getStreamType().equals(StreamType.LIVE_STREAM) - && !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { - return; + final boolean showPlaybackPosition = prefs.getBoolean( + activity.getString(R.string.enable_playback_state_lists_key), true); + if (!playbackResumeEnabled) { + if (playQueue == null || playQueue.getStreams().isEmpty() + || playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET + || !showPlaybackPosition) { + positionView.setVisibility(View.INVISIBLE); + detailPositionView.setVisibility(View.GONE); + // TODO: Remove this check when separation of concerns is done. + // (live streams weren't getting updated because they are mixed) + if (!info.getStreamType().equals(StreamType.LIVE_STREAM) + && !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { + return; + } + } else { + // Show saved position from backStack if user allows it + showPlaybackProgress(playQueue.getItem().getRecoveryPosition(), + playQueue.getItem().getDuration() * 1000); + animateView(positionView, true, 500); + animateView(detailPositionView, true, 500); } + return; } final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); @@ -1409,11 +1747,7 @@ public class VideoDetailFragment extends BaseStateFragment .onErrorComplete() .observeOn(AndroidSchedulers.mainThread()) .subscribe(state -> { - final int seconds - = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()); - positionView.setMax((int) info.getDuration()); - positionView.setProgressAnimated(seconds); - detailPositionView.setText(Localization.getDurationString(seconds)); + showPlaybackProgress(state.getProgressTime(), info.getDuration() * 1000); animateView(positionView, true, 500); animateView(detailPositionView, true, 500); }, e -> { @@ -1421,8 +1755,607 @@ public class VideoDetailFragment extends BaseStateFragment e.printStackTrace(); } }, () -> { - animateView(positionView, false, 500); - animateView(detailPositionView, false, 500); + positionView.setVisibility(View.GONE); + detailPositionView.setVisibility(View.GONE); }); } + + private void showPlaybackProgress(final long progress, final long duration) { + final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress); + final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration); + // If the old and the new progress values have a big difference then use + // animation. Otherwise don't because it affects CPU + final boolean shouldAnimate = Math.abs(positionView.getProgress() - progressSeconds) > 2; + positionView.setMax(durationSeconds); + if (shouldAnimate) { + positionView.setProgressAnimated(progressSeconds); + } else { + positionView.setProgress(progressSeconds); + } + final String position = Localization.getDurationString(progressSeconds); + if (position != detailPositionView.getText()) { + detailPositionView.setText(position); + } + if (positionView.getVisibility() != View.VISIBLE) { + animateView(positionView, true, 100); + animateView(detailPositionView, true, 100); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Player event listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onQueueUpdate(final PlayQueue queue) { + playQueue = queue; + // This should be the only place where we push data to stack. + // It will allow to have live instance of PlayQueue with actual information about + // deleted/added items inside Channel/Playlist queue and makes possible to have + // a history of played items + if (stack.isEmpty() || !stack.peek().getPlayQueue().equals(queue)) { + stack.push(new StackItem(serviceId, url, name, playQueue)); + } else { + final StackItem stackWithQueue = findQueueInStack(queue); + if (stackWithQueue != null) { + // On every MainPlayer service's destroy() playQueue gets disposed and + // no longer able to track progress. That's why we update our cached disposed + // queue with the new one that is active and have the same history. + // Without that the cached playQueue will have an old recovery position + stackWithQueue.setPlayQueue(queue); + } + } + + if (DEBUG) { + Log.d(TAG, "onQueueUpdate() called with: serviceId = [" + + serviceId + "], videoUrl = [" + url + "], name = [" + + name + "], playQueue = [" + playQueue + "]"); + } + } + + @Override + public void onPlaybackUpdate(final int state, + final int repeatMode, + final boolean shuffled, + final PlaybackParameters parameters) { + setOverlayPlayPauseImage(); + + switch (state) { + case BasePlayer.STATE_COMPLETED: + restoreDefaultOrientation(); + break; + case BasePlayer.STATE_PLAYING: + if (positionView.getAlpha() != 1.0f + && player.getPlayQueue() != null + && player.getPlayQueue().getItem() != null + && player.getPlayQueue().getItem().getUrl().equals(url)) { + animateView(positionView, true, 100); + animateView(detailPositionView, true, 100); + } + break; + } + } + + @Override + public void onProgressUpdate(final int currentProgress, + final int duration, + final int bufferPercent) { + // Progress updates every second even if media is paused. It's useless until playing + if (!player.getPlayer().isPlaying() || playQueue == null) { + return; + } + + if (player.getPlayQueue().getItem().getUrl().equals(url)) { + showPlaybackProgress(currentProgress, duration); + } + } + + @Override + public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { + final StackItem item = findQueueInStack(queue); + if (item != null) { + // When PlayQueue can have multiple streams (PlaylistPlayQueue or ChannelPlayQueue) + // every new played stream gives new title and url. + // StackItem contains information about first played stream. Let's update it here + item.setTitle(info.getName()); + item.setUrl(info.getUrl()); + } + // They are not equal when user watches something in popup while browsing in fragment and + // then changes screen orientation. In that case the fragment will set itself as + // a service listener and will receive initial call to onMetadataUpdate() + if (!queue.equals(playQueue)) { + return; + } + + updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); + if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) { + return; + } + + currentInfo = info; + setInitialData(info.getServiceId(), info.getUrl(), info.getName(), queue); + setAutoplay(false); + prepareAndHandleInfo(info, true); + } + + @Override + public void onPlayerError(final ExoPlaybackException error) { + if (error.type == ExoPlaybackException.TYPE_SOURCE + || error.type == ExoPlaybackException.TYPE_UNEXPECTED) { + hideMainPlayer(); + if (playerService != null && player.isFullscreen()) { + player.toggleFullscreen(); + } + } + } + + @Override + public void onServiceStopped() { + unbind(requireContext()); + setOverlayPlayPauseImage(); + if (currentInfo != null) { + updateOverlayData(currentInfo.getName(), + currentInfo.getUploaderName(), + currentInfo.getThumbnailUrl()); + } + } + + @Override + public void onFullscreenStateChanged(final boolean fullscreen) { + setupBrightness(); + if (playerService.getView() == null || player.getParentActivity() == null) { + return; + } + + final View view = playerService.getView(); + final ViewGroup parent = (ViewGroup) view.getParent(); + if (parent == null) { + return; + } + + if (fullscreen) { + hideSystemUiIfNeeded(); + } else { + showSystemUi(); + } + + if (relatedStreamsLayout != null) { + relatedStreamsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE); + } + scrollToTop(); + + addVideoPlayerView(); + } + + @Override + public void onScreenRotationButtonClicked() { + // In tablet user experience will be better if screen will not be rotated + // from landscape to portrait every time. + // Just turn on fullscreen mode in landscape orientation + if (isLandscape() && DeviceUtils.isTablet(activity)) { + player.toggleFullscreen(); + return; + } + + final int newOrientation = isLandscape() + ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + + activity.setRequestedOrientation(newOrientation); + } + + /* + * Will scroll down to description view after long click on moreOptionsButton + * */ + @Override + public void onMoreOptionsLongClicked() { + final CoordinatorLayout.LayoutParams params = + (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams(); + final AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior(); + final ValueAnimator valueAnimator = ValueAnimator + .ofInt(0, -playerPlaceholder.getHeight()); + valueAnimator.setInterpolator(new DecelerateInterpolator()); + valueAnimator.addUpdateListener(animation -> { + behavior.setTopAndBottomOffset((int) animation.getAnimatedValue()); + appBarLayout.requestLayout(); + }); + valueAnimator.setInterpolator(new DecelerateInterpolator()); + valueAnimator.setDuration(500); + valueAnimator.start(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Player related utils + //////////////////////////////////////////////////////////////////////////*/ + + private void showSystemUi() { + if (DEBUG) { + Log.d(TAG, "showSystemUi() called"); + } + + if (activity == null) { + return; + } + + // Prevent jumping of the player on devices with cutout + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + activity.getWindow().getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; + } + activity.getWindow().getDecorView().setSystemUiVisibility(0); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + private void hideSystemUi() { + if (DEBUG) { + Log.d(TAG, "hideSystemUi() called"); + } + + if (activity == null) { + return; + } + + // Prevent jumping of the player on devices with cutout + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + activity.getWindow().getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + } + final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + activity.getWindow().getDecorView().setSystemUiVisibility(visibility); + activity.getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + + // Listener implementation + public void hideSystemUiIfNeeded() { + if (player != null + && player.isFullscreen() + && bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { + hideSystemUi(); + } + } + + private boolean playerIsNotStopped() { + return player != null + && player.getPlayer() != null + && player.getPlayer().getPlaybackState() != Player.STATE_IDLE; + } + + private void saveCurrentAndRestoreDefaultBrightness() { + final WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); + if (lp.screenBrightness == -1) { + return; + } + // Save current brightness level + PlayerHelper.setScreenBrightness(activity, lp.screenBrightness); + + // Restore the old brightness when fragment.onPause() called or + // when a player is in portrait + lp.screenBrightness = -1; + activity.getWindow().setAttributes(lp); + } + + private void setupBrightness() { + if (activity == null) { + return; + } + + final WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); + if (player == null + || !player.videoPlayerSelected() + || !player.isFullscreen() + || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) { + // Apply system brightness when the player is not in fullscreen + saveCurrentAndRestoreDefaultBrightness(); + } else { + // Restore already saved brightness level + final float brightnessLevel = PlayerHelper.getScreenBrightness(activity); + if (brightnessLevel == lp.screenBrightness) { + return; + } + lp.screenBrightness = brightnessLevel; + activity.getWindow().setAttributes(lp); + } + } + + private void checkLandscape() { + if ((!player.isPlaying() && player.getPlayQueue() != playQueue) + || player.getPlayQueue() == null) { + setAutoplay(true); + } + + player.checkLandscape(); + final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(activity); + // Let's give a user time to look at video information page if video is not playing + if (orientationLocked && !player.isPlaying()) { + player.onPlay(); + player.showControlsThenHide(); + } + } + + private boolean isLandscape() { + return getResources().getDisplayMetrics().heightPixels < getResources() + .getDisplayMetrics().widthPixels; + } + + private boolean isInMultiWindow() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode(); + } + + /* + * Means that the player fragment was swiped away via BottomSheetLayout + * and is empty but ready for any new actions. See cleanUp() + * */ + private boolean wasCleared() { + return url == null; + } + + private StackItem findQueueInStack(final PlayQueue queue) { + StackItem item = null; + final Iterator iterator = stack.descendingIterator(); + while (iterator.hasNext()) { + final StackItem next = iterator.next(); + if (next.getPlayQueue().equals(queue)) { + item = next; + break; + } + } + return item; + } + + private void replaceQueueIfUserConfirms(final Runnable onAllow) { + @Nullable final PlayQueue activeQueue = player == null ? null : player.getPlayQueue(); + + // Player will have STATE_IDLE when a user pressed back button + if (isClearingQueueConfirmationRequired(activity) + && playerIsNotStopped() + && activeQueue != null + && !activeQueue.equals(playQueue) + && activeQueue.getStreams().size() > 1) { + showClearingQueueConfirmation(onAllow); + } else { + onAllow.run(); + } + } + + private void showClearingQueueConfirmation(final Runnable onAllow) { + new AlertDialog.Builder(activity) + .setTitle(R.string.clear_queue_confirmation_description) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + onAllow.run(); + dialog.dismiss(); + }).show(); + } + + private void showExternalPlaybackDialog() { + if (sortedVideoStreams == null) { + return; + } + final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()]; + for (int i = 0; i < sortedVideoStreams.size(); i++) { + resolutions[i] = sortedVideoStreams.get(i).getResolution(); + } + final AlertDialog.Builder builder = new AlertDialog.Builder(activity) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(R.string.open_in_browser, (dialog, i) -> + ShareUtils.openUrlInBrowser(requireActivity(), url) + ); + // Maybe there are no video streams available, show just `open in browser` button + if (resolutions.length > 0) { + builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> { + dialog.dismiss(); + startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i)); + } + ); + } + builder.show(); + } + + /* + * Remove unneeded information while waiting for a next task + * */ + private void cleanUp() { + // New beginning + stack.clear(); + if (currentWorker != null) { + currentWorker.dispose(); + } + stopService(requireContext()); + setInitialData(0, null, "", null); + currentInfo = null; + updateOverlayData(null, null, null); + } + + /*////////////////////////////////////////////////////////////////////////// + // Bottom mini player + //////////////////////////////////////////////////////////////////////////*/ + + /** + * That's for Android TV support. Move focus from main fragment to the player or back + * based on what is currently selected + * + * @param toMain if true than the main fragment will be focused or the player otherwise + */ + private void moveFocusToMainFragment(final boolean toMain) { + setupBrightness(); + final ViewGroup mainFragment = requireActivity().findViewById(R.id.fragment_holder); + // Hamburger button steels a focus even under bottomSheet + final Toolbar toolbar = requireActivity().findViewById(R.id.toolbar); + final int afterDescendants = ViewGroup.FOCUS_AFTER_DESCENDANTS; + final int blockDescendants = ViewGroup.FOCUS_BLOCK_DESCENDANTS; + if (toMain) { + mainFragment.setDescendantFocusability(afterDescendants); + toolbar.setDescendantFocusability(afterDescendants); + ((ViewGroup) requireView()).setDescendantFocusability(blockDescendants); + mainFragment.requestFocus(); + } else { + mainFragment.setDescendantFocusability(blockDescendants); + toolbar.setDescendantFocusability(blockDescendants); + ((ViewGroup) requireView()).setDescendantFocusability(afterDescendants); + thumbnailBackgroundButton.requestFocus(); + } + } + + /** + * When the mini player exists the view underneath it is not touchable. + * Bottom padding should be equal to the mini player's height in this case + * + * @param showMore whether main fragment should be expanded or not + */ + private void manageSpaceAtTheBottom(final boolean showMore) { + final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height); + final ViewGroup holder = requireActivity().findViewById(R.id.fragment_holder); + final int newBottomPadding; + if (showMore) { + newBottomPadding = 0; + } else { + newBottomPadding = peekHeight; + } + if (holder.getPaddingBottom() == newBottomPadding) { + return; + } + holder.setPadding(holder.getPaddingLeft(), + holder.getPaddingTop(), + holder.getPaddingRight(), + newBottomPadding); + } + + private void setupBottomPlayer() { + final CoordinatorLayout.LayoutParams params = + (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams(); + final AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior(); + + final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder); + bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout); + bottomSheetBehavior.setState(bottomSheetState); + final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height); + if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) { + manageSpaceAtTheBottom(false); + bottomSheetBehavior.setPeekHeight(peekHeight); + if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) { + overlay.setAlpha(MAX_OVERLAY_ALPHA); + } else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) { + overlay.setAlpha(0); + setOverlayElementsClickable(false); + } + } + + bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull final View bottomSheet, final int newState) { + bottomSheetState = newState; + + switch (newState) { + case BottomSheetBehavior.STATE_HIDDEN: + moveFocusToMainFragment(true); + manageSpaceAtTheBottom(true); + + bottomSheetBehavior.setPeekHeight(0); + cleanUp(); + break; + case BottomSheetBehavior.STATE_EXPANDED: + moveFocusToMainFragment(false); + manageSpaceAtTheBottom(false); + + bottomSheetBehavior.setPeekHeight(peekHeight); + // Disable click because overlay buttons located on top of buttons + // from the player + setOverlayElementsClickable(false); + hideSystemUiIfNeeded(); + // Conditions when the player should be expanded to fullscreen + if (isLandscape() + && player != null + && player.isPlaying() + && !player.isFullscreen() + && !DeviceUtils.isTablet(activity) + && player.videoPlayerSelected()) { + player.toggleFullscreen(); + } + break; + case BottomSheetBehavior.STATE_COLLAPSED: + moveFocusToMainFragment(true); + + // Re-enable clicks + setOverlayElementsClickable(true); + if (player != null) { + player.onQueueClosed(); + } + break; + case BottomSheetBehavior.STATE_DRAGGING: + case BottomSheetBehavior.STATE_SETTLING: + if (player != null && player.isFullscreen()) { + showSystemUi(); + } + if (player != null && player.isControlsVisible()) { + player.hideControls(0, 0); + } + break; + } + } + + @Override + public void onSlide(@NonNull final View bottomSheet, final float slideOffset) { + setOverlayLook(appBarLayout, behavior, slideOffset); + } + }); + + // User opened a new page and the player will hide itself + activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> { + if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + } + + private void updateOverlayData(@Nullable final String title, + @Nullable final String uploader, + @Nullable final String thumbnailUrl) { + overlayTitleTextView.setText(TextUtils.isEmpty(title) ? "" : title); + overlayChannelTextView.setText(TextUtils.isEmpty(uploader) ? "" : uploader); + overlayThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); + if (!TextUtils.isEmpty(thumbnailUrl)) { + IMAGE_LOADER.displayImage(thumbnailUrl, overlayThumbnailImageView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null); + } + } + + private void setOverlayPlayPauseImage() { + final int attr = player != null && player.isPlaying() + ? R.attr.ic_pause + : R.attr.ic_play_arrow; + overlayPlayPauseButton.setImageResource( + ThemeHelper.resolveResourceIdFromAttr(activity, attr)); + } + + private void setOverlayLook(final AppBarLayout appBar, + final AppBarLayout.Behavior behavior, + final float slideOffset) { + // SlideOffset < 0 when mini player is about to close via swipe. + // Stop animation in this case + if (behavior == null || slideOffset < 0) { + return; + } + overlay.setAlpha(Math.min(MAX_OVERLAY_ALPHA, 1 - slideOffset)); + // These numbers are not special. They just do a cool transition + behavior.setTopAndBottomOffset( + (int) (-thumbnailImageView.getHeight() * 2 * (1 - slideOffset) / 3)); + appBar.requestLayout(); + } + + private void setOverlayElementsClickable(final boolean enable) { + overlayThumbnailImageView.setClickable(enable); + overlayThumbnailImageView.setLongClickable(enable); + overlayMetadata.setClickable(enable); + overlayMetadata.setLongClickable(enable); + overlayButtons.setClickable(enable); + overlayPlayPauseButton.setClickable(enable); + overlayCloseButton.setClickable(enable); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 9ce62a0df..37598eb1a 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -6,7 +6,7 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -136,7 +136,7 @@ public abstract class BaseListFragment extends BaseStateFragment final RecyclerView.ViewHolder itemHolder = itemsList.findContainingViewHolder(focusedItem); return itemHolder.getAdapterPosition(); - } catch (NullPointerException e) { + } catch (final NullPointerException e) { return -1; } } @@ -169,7 +169,7 @@ public abstract class BaseListFragment extends BaseStateFragment } itemsList.post(() -> { - RecyclerView.ViewHolder focusedHolder = + final RecyclerView.ViewHolder focusedHolder = itemsList.findViewHolderForAdapterPosition(position); if (focusedHolder != null) { @@ -279,7 +279,7 @@ public abstract class BaseListFragment extends BaseStateFragment selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } } @@ -294,7 +294,7 @@ public abstract class BaseListFragment extends BaseStateFragment selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } } @@ -367,7 +367,7 @@ public abstract class BaseListFragment extends BaseStateFragment + "menu = [" + menu + "], inflater = [" + inflater + "]"); } super.onCreateOptionsMenu(menu, inflater); - ActionBar supportActionBar = activity.getSupportActionBar(); + final ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setDisplayShowTitleEnabled(true); if (useAsFrontPage) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index aed7c4795..f21eb5291 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.views.NewPipeRecyclerView; @@ -30,7 +31,7 @@ public abstract class BaseListInfoFragment protected String url; protected I currentInfo; - protected String currentNextPageUrl; + protected Page currentNextPage; protected Disposable currentWorker; @Override @@ -78,7 +79,7 @@ public abstract class BaseListInfoFragment public void writeTo(final Queue objectsToSave) { super.writeTo(objectsToSave); objectsToSave.add(currentInfo); - objectsToSave.add(currentNextPageUrl); + objectsToSave.add(currentNextPage); } @Override @@ -86,7 +87,7 @@ public abstract class BaseListInfoFragment public void readFrom(@NonNull final Queue savedObjects) throws Exception { super.readFrom(savedObjects); currentInfo = (I) savedObjects.poll(); - currentNextPageUrl = (String) savedObjects.poll(); + currentNextPage = (Page) savedObjects.poll(); } /*////////////////////////////////////////////////////////////////////////// @@ -130,9 +131,9 @@ public abstract class BaseListInfoFragment .subscribe((@NonNull I result) -> { isLoading.set(false); currentInfo = result; - currentNextPageUrl = result.getNextPageUrl(); + currentNextPage = result.getNextPage(); handleResult(result); - }, (@NonNull Throwable throwable) -> onError(throwable)); + }, this::onError); } /** @@ -157,11 +158,10 @@ public abstract class BaseListInfoFragment .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doFinally(this::allowDownwardFocusScroll) - .subscribe((@io.reactivex.annotations.NonNull - ListExtractor.InfoItemsPage InfoItemsPage) -> { + .subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> { isLoading.set(false); handleNextItems(InfoItemsPage); - }, (@io.reactivex.annotations.NonNull Throwable throwable) -> { + }, (@NonNull Throwable throwable) -> { isLoading.set(false); onError(throwable); }); @@ -182,7 +182,7 @@ public abstract class BaseListInfoFragment @Override public void handleNextItems(final ListExtractor.InfoItemsPage result) { super.handleNextItems(result); - currentNextPageUrl = result.getNextPageUrl(); + currentNextPage = result.getNextPage(); infoListAdapter.addInfoItemList(result.getItems()); showListFooter(hasMoreItems()); @@ -190,7 +190,7 @@ public abstract class BaseListInfoFragment @Override protected boolean hasMoreItems() { - return !TextUtils.isEmpty(currentNextPageUrl); + return Page.isValid(currentNextPage); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index b88d375ff..8902834e4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -98,7 +98,7 @@ public class ChannelFragment extends BaseListInfoFragment public static ChannelFragment getInstance(final int serviceId, final String url, final String name) { - ChannelFragment instance = new ChannelFragment(); + final ChannelFragment instance = new ChannelFragment(); instance.setInitialData(serviceId, url, name); return instance; } @@ -189,7 +189,7 @@ public class ChannelFragment extends BaseListInfoFragment @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - ActionBar supportActionBar = activity.getSupportActionBar(); + final ActionBar supportActionBar = activity.getSupportActionBar(); if (useAsFrontPage && supportActionBar != null) { supportActionBar.setDisplayHomeAsUpEnabled(false); } else { @@ -206,7 +206,7 @@ public class ChannelFragment extends BaseListInfoFragment private void openRssFeed() { final ChannelInfo info = currentInfo; if (info != null) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl())); + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl())); startActivity(intent); } } @@ -345,7 +345,7 @@ public class ChannelFragment extends BaseListInfoFragment if (DEBUG) { Log.d(TAG, "No subscription to this channel!"); } - SubscriptionEntity channel = new SubscriptionEntity(); + final SubscriptionEntity channel = new SubscriptionEntity(); channel.setServiceId(info.getServiceId()); channel.setUrl(info.getUrl()); channel.setData(info.getName(), @@ -371,16 +371,16 @@ public class ChannelFragment extends BaseListInfoFragment + "isSubscribed = [" + isSubscribed + "]"); } - boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE; - int backgroundDuration = isButtonVisible ? 300 : 0; - int textDuration = isButtonVisible ? 200 : 0; + final boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE; + final int backgroundDuration = isButtonVisible ? 300 : 0; + final int textDuration = isButtonVisible ? 200 : 0; - int subscribeBackground = ThemeHelper + final int subscribeBackground = ThemeHelper .resolveColorFromAttr(activity, R.attr.colorPrimary); - int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color); - int subscribedBackground = ContextCompat + final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color); + final int subscribedBackground = ContextCompat .getColor(activity, R.color.subscribed_background_color); - int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color); + final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color); if (!isSubscribed) { headerSubscribeButton.setText(R.string.subscribe_button_title); @@ -403,7 +403,7 @@ public class ChannelFragment extends BaseListInfoFragment @Override protected Single loadMoreItemsLogic() { - return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl); + return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage); } @Override @@ -426,10 +426,10 @@ public class ChannelFragment extends BaseListInfoFragment case R.id.sub_channel_title_view: if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) { try { - NavigationHelper.openChannelFragment(getFragmentManager(), - currentInfo.getServiceId(), currentInfo.getParentChannelUrl(), + NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(), + currentInfo.getParentChannelUrl(), currentInfo.getParentChannelName()); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } } else if (DEBUG) { @@ -490,13 +490,13 @@ public class ChannelFragment extends BaseListInfoFragment playlistCtrl.setVisibility(View.VISIBLE); - List errors = new ArrayList<>(result.getErrors()); + final List errors = new ArrayList<>(result.getErrors()); if (!errors.isEmpty()) { // handling ContentNotSupportedException not to show the error but an appropriate string // so that crashes won't be sent uselessly and the user will understand what happened for (Iterator it = errors.iterator(); it.hasNext();) { - Throwable throwable = it.next(); + final Throwable throwable = it.next(); if (throwable instanceof ContentNotSupportedException) { showContentNotSupported(); it.remove(); @@ -519,7 +519,7 @@ public class ChannelFragment extends BaseListInfoFragment monitorSubscription(result); headerPlayAllButton.setOnClickListener(view -> NavigationHelper - .playOnMainPlayer(activity, getPlayQueue(), false)); + .playOnMainPlayer(activity, getPlayQueue(), true)); headerPopupButton.setOnClickListener(view -> NavigationHelper .playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> NavigationHelper @@ -549,13 +549,13 @@ public class ChannelFragment extends BaseListInfoFragment private PlayQueue getPlayQueue(final int index) { final List streamItems = new ArrayList<>(); - for (InfoItem i : infoListAdapter.getItemsList()) { + for (final InfoItem i : infoListAdapter.getItemsList()) { if (i instanceof StreamInfoItem) { streamItems.add((StreamInfoItem) i); } } return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(), - currentInfo.getNextPageUrl(), streamItems, index); + currentInfo.getNextPage(), streamItems, index); } @Override @@ -581,7 +581,7 @@ public class ChannelFragment extends BaseListInfoFragment return true; } - int errorId = exception instanceof ExtractionException + final int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java index d23293c8a..fc61a4518 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java @@ -26,11 +26,9 @@ import io.reactivex.disposables.CompositeDisposable; public class CommentsFragment extends BaseListInfoFragment { private CompositeDisposable disposables = new CompositeDisposable(); - private boolean mIsVisibleToUser = false; - public static CommentsFragment getInstance(final int serviceId, final String url, final String name) { - CommentsFragment instance = new CommentsFragment(); + final CommentsFragment instance = new CommentsFragment(); instance.setInitialData(serviceId, url, name); return instance; } @@ -39,12 +37,6 @@ public class CommentsFragment extends BaseListInfoFragment { // LifeCycle //////////////////////////////////////////////////////////////////////////*/ - @Override - public void setUserVisibleHint(final boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - mIsVisibleToUser = isVisibleToUser; - } - @Override public void onAttach(final Context context) { super.onAttach(context); @@ -71,7 +63,7 @@ public class CommentsFragment extends BaseListInfoFragment { @Override protected Single loadMoreItemsLogic() { - return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl); + return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage); } @Override @@ -92,7 +84,7 @@ public class CommentsFragment extends BaseListInfoFragment { public void handleResult(@NonNull final CommentsInfo result) { super.handleResult(result); - AnimationUtils.slideUp(getView(), 120, 150, 0.06f); + AnimationUtils.slideUp(requireView(), 120, 150, 0.06f); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/DefaultKioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/DefaultKioskFragment.java index 0702553ad..1797191b6 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/DefaultKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/DefaultKioskFragment.java @@ -44,8 +44,8 @@ public class DefaultKioskFragment extends KioskFragment { name = kioskTranslatedName; currentInfo = null; - currentNextPageUrl = null; - } catch (ExtractionException e) { + currentNextPage = null; + } catch (final ExtractionException e) { onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java index 21a7944ee..68097e21e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java @@ -72,9 +72,9 @@ public class KioskFragment extends BaseListInfoFragment { public static KioskFragment getInstance(final int serviceId, final String kioskId) throws ExtractionException { - KioskFragment instance = new KioskFragment(); - StreamingService service = NewPipe.getService(serviceId); - ListLinkHandlerFactory kioskLinkHandlerFactory = service.getKioskList() + final KioskFragment instance = new KioskFragment(); + final StreamingService service = NewPipe.getService(serviceId); + final ListLinkHandlerFactory kioskLinkHandlerFactory = service.getKioskList() .getListLinkHandlerFactoryByType(kioskId); instance.setInitialData(serviceId, kioskLinkHandlerFactory.fromId(kioskId).getUrl(), kioskId); @@ -101,7 +101,7 @@ public class KioskFragment extends BaseListInfoFragment { if (useAsFrontPage && isVisibleToUser && activity != null) { try { setTitle(kioskTranslatedName); - } catch (Exception e) { + } catch (final Exception e) { onUnrecoverableError(e, UserAction.UI_ERROR, "none", "none", R.string.app_ui_crash); @@ -132,7 +132,7 @@ public class KioskFragment extends BaseListInfoFragment { @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - ActionBar supportActionBar = activity.getSupportActionBar(); + final ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null && useAsFrontPage) { supportActionBar.setDisplayHomeAsUpEnabled(false); } @@ -150,7 +150,7 @@ public class KioskFragment extends BaseListInfoFragment { @Override public Single loadMoreItemsLogic() { - return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPageUrl); + return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 93df98c97..38594553b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -85,7 +85,7 @@ public class PlaylistFragment extends BaseListInfoFragment { public static PlaylistFragment getInstance(final int serviceId, final String url, final String name) { - PlaylistFragment instance = new PlaylistFragment(); + final PlaylistFragment instance = new PlaylistFragment(); instance.setInitialData(serviceId, url, name); return instance; } @@ -229,7 +229,7 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override protected Single loadMoreItemsLogic() { - return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl); + return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage); } @Override @@ -286,11 +286,9 @@ public class PlaylistFragment extends BaseListInfoFragment { if (!TextUtils.isEmpty(result.getUploaderUrl())) { headerUploaderLayout.setOnClickListener(v -> { try { - NavigationHelper.openChannelFragment(getFragmentManager(), - result.getServiceId(), - result.getUploaderUrl(), - result.getUploaderName()); - } catch (Exception e) { + NavigationHelper.openChannelFragment(getFM(), result.getServiceId(), + result.getUploaderUrl(), result.getUploaderName()); + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } }); @@ -318,7 +316,7 @@ public class PlaylistFragment extends BaseListInfoFragment { .subscribe(getPlaylistBookmarkSubscriber()); headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true)); headerPopupButton.setOnClickListener(view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> @@ -341,7 +339,7 @@ public class PlaylistFragment extends BaseListInfoFragment { private PlayQueue getPlayQueue(final int index) { final List infoItems = new ArrayList<>(); - for (InfoItem i : infoListAdapter.getItemsList()) { + for (final InfoItem i : infoListAdapter.getItemsList()) { if (i instanceof StreamInfoItem) { infoItems.add((StreamInfoItem) i); } @@ -349,7 +347,7 @@ public class PlaylistFragment extends BaseListInfoFragment { return new PlaylistPlayQueue( currentInfo.getServiceId(), currentInfo.getUrl(), - currentInfo.getNextPageUrl(), + currentInfo.getNextPage(), infoItems, index ); @@ -375,7 +373,7 @@ public class PlaylistFragment extends BaseListInfoFragment { return true; } - int errorId = exception instanceof ExtractionException + final int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), url, errorId); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 060a5c9c9..6817e4595 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -5,8 +5,10 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.core.text.HtmlCompat; +import androidx.preference.PreferenceManager; import android.text.Editable; +import android.text.Html; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; @@ -37,6 +39,7 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.search.SearchExtractor; @@ -46,7 +49,7 @@ import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ExtractorHelper; @@ -118,13 +121,18 @@ public class SearchFragment extends BaseListFragment menuItemToFilterName; private StreamingService service; - private String currentPageUrl; - private String nextPageUrl; + private Page nextPage; private String contentCountry; private boolean isSuggestionsEnabled = true; @@ -143,6 +151,8 @@ public class SearchFragment extends BaseListFragment objectsToSave) { super.writeTo(objectsToSave); - objectsToSave.add(currentPageUrl); - objectsToSave.add(nextPageUrl); + objectsToSave.add(nextPage); } @Override public void readFrom(@NonNull final Queue savedObjects) throws Exception { super.readFrom(savedObjects); - currentPageUrl = (String) savedObjects.poll(); - nextPageUrl = (String) savedObjects.poll(); + nextPage = (Page) savedObjects.poll(); } @Override @@ -401,7 +415,7 @@ public class SearchFragment extends BaseListFragment cf = new ArrayList<>(1); + final List cf = new ArrayList<>(1); cf.add(menuItemToFilterName.get(item.getItemId())); changeContentFilter(item, cf); @@ -446,7 +460,7 @@ public class SearchFragment extends BaseListFragment()); showKeyboardSearch(); @@ -511,7 +527,7 @@ public class SearchFragment extends BaseListFragment> local = flowable.toObservable() .map(searchHistoryEntries -> { - List result = new ArrayList<>(); - for (SearchHistoryEntry entry : searchHistoryEntries) { + final List result = new ArrayList<>(); + for (final SearchHistoryEntry entry : searchHistoryEntries) { result.add(new SuggestionItem(true, entry.getSearch())); } return result; @@ -730,15 +744,15 @@ public class SearchFragment extends BaseListFragment { - List result = new ArrayList<>(); - for (String entry : strings) { + final List result = new ArrayList<>(); + for (final String entry : strings) { result.add(new SuggestionItem(false, entry)); } return result; }); return Observable.zip(local, network, (localResult, networkResult) -> { - List result = new ArrayList<>(); + final List result = new ArrayList<>(); if (localResult.size() > 0) { result.addAll(localResult); } @@ -747,7 +761,7 @@ public class SearchFragment extends BaseListFragment iterator = networkResult.iterator(); while (iterator.hasNext() && localResult.size() > 0) { final SuggestionItem next = iterator.next(); - for (SuggestionItem item : localResult) { + for (final SuggestionItem item : localResult) { if (item.query.equals(next.query)) { iterator.remove(); break; @@ -795,13 +809,13 @@ public class SearchFragment extends BaseListFragment { - getFragmentManager().popBackStackImmediate(); + getFM().popBackStackImmediate(); activity.startActivity(intent); }, throwable -> showError(getString(R.string.unsupported_url), false))); return; } - } catch (Exception ignored) { + } catch (final Exception ignored) { // Exception occurred, it's not a url } @@ -845,7 +859,7 @@ public class SearchFragment extends BaseListFragment isLoading.set(false)) @@ -923,7 +937,7 @@ public class SearchFragment extends BaseListFragment" + Html.escapeHtml(searchSuggestion) + ""; + final String text = String.format(helperText, highlightedSearchSuggestion); + correctSuggestion.setText(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY)); + + correctSuggestion.setOnClickListener(v -> { + correctSuggestion.setVisibility(View.GONE); + search(searchSuggestion, contentFilter, sortFilter); + searchEditText.setText(searchSuggestion); + }); + + correctSuggestion.setOnLongClickListener(v -> { + searchEditText.setText(searchSuggestion); + searchEditText.setSelection(searchSuggestion.length()); + showKeyboardSearch(); + return true; + }); + + correctSuggestion.setVisibility(View.VISIBLE); + } + } + @Override public void handleNextItems(final ListExtractor.InfoItemsPage result) { showListFooter(false); - currentPageUrl = result.getNextPageUrl(); infoListAdapter.addInfoItemList(result.getItems()); - nextPageUrl = result.getNextPageUrl(); + nextPage = result.getNextPage(); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), - "\"" + searchString + "\" → page: " + nextPageUrl, 0); + "\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", " + + "pageIds: " + nextPage.getIds() + ", " + + "pageCookies: " + nextPage.getCookies(), 0); } super.handleNextItems(result); } @@ -1003,7 +1052,7 @@ public class SearchFragment extends BaseListFragment + PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() + .putBoolean(getString(R.string.auto_queue_key), b).apply()); return headerRootLayout; } else { return null; @@ -105,7 +92,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment loadMoreItemsLogic() { - return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage()); + return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage); } /*////////////////////////////////////////////////////////////////////////// @@ -179,12 +166,10 @@ public class RelatedVideosFragment extends BaseListInfoFragment payloads) { if (!payloads.isEmpty() && holder instanceof InfoItemHolder) { - for (Object payload : payloads) { + for (final Object payload : payloads) { if (payload instanceof StreamStateEntity) { ((InfoItemHolder) holder).updateState(infoItemList .get(header == null ? position : position - 1), recordManager); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java index 28d447337..cf1ed255b 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelInfoItemHolder.java @@ -56,8 +56,8 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder { String details = super.getDetailLine(item); if (item.getStreamCount() >= 0) { - String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(), - item.getStreamCount()); + final String formattedVideoAmount = Localization.localizeStreamCount( + itemBuilder.getContext(), item.getStreamCount()); if (!details.isEmpty()) { details += " • " + formattedVideoAmount; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index 863273a88..944b578f5 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.CommentTextOnTouchListener; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; @@ -45,9 +45,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { @Override public String transformUrl(final Matcher match, final String url) { int timestamp = 0; - String hours = match.group(1); - String minutes = match.group(2); - String seconds = match.group(3); + final String hours = match.group(1); + final String minutes = match.group(2); + final String seconds = match.group(3); if (hours != null) { timestamp += (Integer.parseInt(hours.replace(":", "")) * 3600); } @@ -126,7 +126,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { itemView.setOnLongClickListener(view -> { - if (AndroidTvUtils.isTv(itemBuilder.getContext())) { + if (DeviceUtils.isTv(itemBuilder.getContext())) { openCommentAuthor(item); } else { ShareUtils.copyToClipboard(itemBuilder.getContext(), commentText); @@ -146,7 +146,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { item.getServiceId(), item.getUploaderUrl(), item.getUploaderName()); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e); } } @@ -164,7 +164,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { return false; } - URLSpan[] urls = itemContentView.getUrls(); + final URLSpan[] urls = itemContentView.getUrls(); return urls != null && urls.length != 0; } @@ -181,12 +181,13 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { boolean hasEllipsis = false; if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { - 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); } - String newVal = itemContentView.getText().subSequence(0, end) + " …"; + final String newVal = itemContentView.getText().subSequence(0, end) + " …"; itemContentView.setText(newVal); hasEllipsis = true; } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java index 5fa0904de..86fad3499 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java @@ -1,6 +1,6 @@ package org.schabi.newpipe.info_list.holder; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.text.TextUtils; import android.view.ViewGroup; import android.widget.TextView; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index da6c9e82f..c0096ed10 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -60,7 +60,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem) + final StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem) .blockingGet()[0]; if (state2 != null) { itemProgressView.setVisibility(View.VISIBLE); @@ -113,7 +113,8 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { final HistoryRecordManager historyRecordManager) { final StreamInfoItem item = (StreamInfoItem) infoItem; - StreamStateEntity state = historyRecordManager.loadStreamState(infoItem).blockingGet()[0]; + final StreamStateEntity state + = historyRecordManager.loadStreamState(infoItem).blockingGet()[0]; if (state != null && item.getDuration() > 0 && item.getStreamType() != StreamType.LIVE_STREAM) { itemProgressView.setMax((int) item.getDuration()); diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index 650953bea..8e88ceaed 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -4,7 +4,7 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index ad0524f92..5b67f51da 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -101,7 +101,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter> { + return when { + filterQuery.isNotEmpty() -> { + return if (showOnlyUngrouped) { + subscriptionTable.getSubscriptionsOnlyUngroupedFiltered( + currentGroupId, filterQuery) + } else { + subscriptionTable.getSubscriptionsFiltered(filterQuery) + } + } + showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) + else -> subscriptionTable.all + } + } + fun upsertAll(infoList: List): List { val listEntities = subscriptionTable.upsertAll( - infoList.map { SubscriptionEntity.from(it) }) + infoList.map { SubscriptionEntity.from(it) }) database.runInTransaction { infoList.forEachIndexed { index, info -> @@ -35,13 +56,13 @@ class SubscriptionManager(context: Context) { } fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url) - .flatMapCompletable { - Completable.fromRunnable { - it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) - subscriptionTable.update(it) - feedDatabaseManager.upsertAll(it.uid, info.relatedItems) - } + .flatMapCompletable { + Completable.fromRunnable { + it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) + subscriptionTable.update(it) + feedDatabaseManager.upsertAll(it.uid, info.relatedItems) } + } fun updateFromInfo(subscriptionId: Long, info: ListInfo) { val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId) @@ -57,8 +78,8 @@ class SubscriptionManager(context: Context) { fun deleteSubscription(serviceId: Int, url: String): Completable { return Completable.fromCallable { subscriptionTable.deleteSubscription(serviceId, url) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) { diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index d812a2a57..a2a848b4b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -64,7 +64,7 @@ public class SubscriptionsImportFragment extends BaseFragment { private Button inputButton; public static SubscriptionsImportFragment getInstance(final int serviceId) { - SubscriptionsImportFragment instance = new SubscriptionsImportFragment(); + final SubscriptionsImportFragment instance = new SubscriptionsImportFragment(); instance.setInitialData(serviceId); return instance; } @@ -140,7 +140,7 @@ public class SubscriptionsImportFragment extends BaseFragment { setInfoText(""); } - ActionBar supportActionBar = activity.getSupportActionBar(); + final ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setDisplayShowTitleEnabled(true); setTitle(getString(R.string.import_title)); @@ -206,7 +206,7 @@ public class SubscriptionsImportFragment extends BaseFragment { relatedUrl = extractor.getRelatedUrl(); instructionsString = ServiceHelper.getImportInstructions(currentServiceId); return; - } catch (ExtractionException ignored) { + } catch (final ExtractionException ignored) { } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt index e9d9ac5b3..5e9b6b6a4 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt @@ -5,6 +5,7 @@ import android.content.Context import android.os.Bundle import android.os.Parcelable import android.text.Editable +import android.text.TextUtils import android.text.TextWatcher import android.view.LayoutInflater import android.view.View @@ -13,34 +14,22 @@ import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.OnItemClickListener import com.xwray.groupie.Section import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import icepick.Icepick import icepick.State import java.io.Serializable -import kotlinx.android.synthetic.main.dialog_feed_group_create.cancel_button -import kotlinx.android.synthetic.main.dialog_feed_group_create.confirm_button -import kotlinx.android.synthetic.main.dialog_feed_group_create.delete_button -import kotlinx.android.synthetic.main.dialog_feed_group_create.delete_screen_message -import kotlinx.android.synthetic.main.dialog_feed_group_create.group_name_input -import kotlinx.android.synthetic.main.dialog_feed_group_create.group_name_input_container -import kotlinx.android.synthetic.main.dialog_feed_group_create.icon_preview -import kotlinx.android.synthetic.main.dialog_feed_group_create.icon_selector -import kotlinx.android.synthetic.main.dialog_feed_group_create.options_root -import kotlinx.android.synthetic.main.dialog_feed_group_create.select_channel_button -import kotlinx.android.synthetic.main.dialog_feed_group_create.selected_subscription_count_view -import kotlinx.android.synthetic.main.dialog_feed_group_create.separator -import kotlinx.android.synthetic.main.dialog_feed_group_create.subscriptions_selector -import kotlinx.android.synthetic.main.dialog_feed_group_create.subscriptions_selector_header_info -import kotlinx.android.synthetic.main.dialog_feed_group_create.subscriptions_selector_list +import kotlin.collections.contains +import kotlinx.android.synthetic.main.dialog_feed_group_create.* +import kotlinx.android.synthetic.main.toolbar_search_layout.* import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity -import org.schabi.newpipe.database.subscription.SubscriptionEntity +import org.schabi.newpipe.fragments.BackPressable import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.IconPickerScreen @@ -51,9 +40,10 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.Dia import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem import org.schabi.newpipe.local.subscription.item.PickerIconItem import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem +import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.ThemeHelper -class FeedGroupDialog : DialogFragment() { +class FeedGroupDialog : DialogFragment(), BackPressable { private lateinit var viewModel: FeedGroupDialogViewModel private var groupId: Long = NO_GROUP_SELECTED private var groupIcon: FeedGroupIcon? = null @@ -66,22 +56,20 @@ class FeedGroupDialog : DialogFragment() { object DeleteScreen : ScreenState() } - @State - @JvmField - var selectedIcon: FeedGroupIcon? = null - @State - @JvmField - var selectedSubscriptions: HashSet = HashSet() - @State - @JvmField - var currentScreen: ScreenState = InitialScreen + @State @JvmField var selectedIcon: FeedGroupIcon? = null + @State @JvmField var selectedSubscriptions: HashSet = HashSet() + @State @JvmField var wasSubscriptionSelectionChanged: Boolean = false + @State @JvmField var currentScreen: ScreenState = InitialScreen - @State - @JvmField - var subscriptionsListState: Parcelable? = null - @State - @JvmField - var iconsListState: Parcelable? = null + @State @JvmField var subscriptionsListState: Parcelable? = null + @State @JvmField var iconsListState: Parcelable? = null + @State @JvmField var wasSearchSubscriptionsVisible = false + @State @JvmField var subscriptionsCurrentSearchQuery = "" + @State @JvmField var subscriptionsShowOnlyUngrouped = false + + private val subscriptionMainSection = Section() + private val subscriptionEmptyFooter = Section() + private lateinit var subscriptionGroupAdapter: GroupAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -91,22 +79,30 @@ class FeedGroupDialog : DialogFragment() { groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { return inflater.inflate(R.layout.dialog_feed_group_create, container) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return object : Dialog(requireActivity(), theme) { override fun onBackPressed() { - if (currentScreen !is InitialScreen) { - showScreen(InitialScreen) - } else { + if (!this@FeedGroupDialog.onBackPressed()) { super.onBackPressed() } } } } + override fun onPause() { + super.onPause() + + wasSearchSubscriptionsVisible = isSearchVisible() + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) @@ -119,11 +115,15 @@ class FeedGroupDialog : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel = ViewModelProviders.of(this, FeedGroupDialogViewModel.Factory(requireContext(), groupId)) - .get(FeedGroupDialogViewModel::class.java) + viewModel = ViewModelProvider(this, + FeedGroupDialogViewModel.Factory(requireContext(), + groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped) + ).get(FeedGroupDialogViewModel::class.java) viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup)) - viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer { setupSubscriptionPicker(it.first, it.second) }) + viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer { + setupSubscriptionPicker(it.first, it.second) + }) viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer { when (it) { ProcessingEvent -> disableInput() @@ -131,15 +131,54 @@ class FeedGroupDialog : DialogFragment() { } }) + subscriptionGroupAdapter = GroupAdapter().apply { + add(subscriptionMainSection) + add(subscriptionEmptyFooter) + spanCount = 4 + } + subscriptions_selector_list.apply { + // Disable animations, too distracting. + itemAnimator = null + adapter = subscriptionGroupAdapter + layoutManager = GridLayoutManager(requireContext(), subscriptionGroupAdapter.spanCount, + RecyclerView.VERTICAL, false).apply { + spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup + } + } + setupIconPicker() setupListeners() showScreen(currentScreen) + + if (currentScreen == SubscriptionsPickerScreen && wasSearchSubscriptionsVisible) { + showSearch() + } else if (currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED) { + showKeyboard() + } } - // ///////////////////////////////////////////////////////////////////////// + override fun onDestroyView() { + super.onDestroyView() + subscriptions_selector_list?.adapter = null + icon_selector?.adapter = null + } + + /*/​////////////////////////////////////////////////////////////////////////// // Setup - // ///////////////////////////////////////////////////////////////////////// + //​//////////////////////////////////////////////////////////////////////// */ + + override fun onBackPressed(): Boolean { + if (currentScreen is SubscriptionsPickerScreen && isSearchVisible()) { + hideSearch() + return true + } else if (currentScreen !is InitialScreen) { + showScreen(InitialScreen) + return true + } + + return false + } private fun setupListeners() { delete_button.setOnClickListener { showScreen(DeleteScreen) } @@ -163,13 +202,64 @@ class FeedGroupDialog : DialogFragment() { } }) - confirm_button.setOnClickListener { - when (currentScreen) { - InitialScreen -> handlePositiveButtonInitialScreen() - DeleteScreen -> viewModel.deleteGroup() - else -> showScreen(InitialScreen) + confirm_button.setOnClickListener { handlePositiveButton() } + + select_channel_button.setOnClickListener { + subscriptions_selector_list.scrollToPosition(0) + showScreen(SubscriptionsPickerScreen) + } + + val headerMenu = subscriptions_header_toolbar.menu + requireActivity().menuInflater.inflate(R.menu.menu_feed_group_dialog, headerMenu) + + headerMenu.findItem(R.id.action_search).setOnMenuItemClickListener { + showSearch() + true + } + + headerMenu.findItem(R.id.feed_group_toggle_show_only_ungrouped_subscriptions).apply { + isChecked = subscriptionsShowOnlyUngrouped + setOnMenuItemClickListener { + subscriptionsShowOnlyUngrouped = !subscriptionsShowOnlyUngrouped + it.isChecked = subscriptionsShowOnlyUngrouped + viewModel.toggleShowOnlyUngrouped(subscriptionsShowOnlyUngrouped) + true } } + + toolbar_search_clear.setOnClickListener { + if (TextUtils.isEmpty(toolbar_search_edit_text.text)) { + hideSearch() + return@setOnClickListener + } + resetSearch() + showKeyboardSearch() + } + + toolbar_search_edit_text.setOnClickListener { + if (DeviceUtils.isTv(context)) { + showKeyboardSearch() + } + } + + toolbar_search_edit_text.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + override fun afterTextChanged(s: Editable) = Unit + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + val newQuery: String = toolbar_search_edit_text.text.toString() + subscriptionsCurrentSearchQuery = newQuery + viewModel.filterSubscriptionsBy(newQuery) + } + }) + + subscriptionGroupAdapter.setOnItemClickListener(subscriptionPickerItemListener) + } + + private fun handlePositiveButton() = when { + currentScreen is InitialScreen -> handlePositiveButtonInitialScreen() + currentScreen is DeleteScreen -> viewModel.deleteGroup() + currentScreen is SubscriptionsPickerScreen && isSearchVisible() -> hideSearch() + else -> showScreen(InitialScreen) } private fun handlePositiveButtonInitialScreen() { @@ -202,80 +292,73 @@ class FeedGroupDialog : DialogFragment() { groupIcon = feedGroupEntity?.icon groupSortOrder = feedGroupEntity?.sortOrder ?: -1 - icon_preview.setImageResource((if (selectedIcon == null) icon else selectedIcon!!).getDrawableRes(requireContext())) + val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!! + icon_preview.setImageResource(feedGroupIcon.getDrawableRes(requireContext())) if (group_name_input.text.isNullOrBlank()) { group_name_input.setText(name) } } - private fun setupSubscriptionPicker(subscriptions: List, selectedSubscriptions: Set) { - this.selectedSubscriptions.addAll(selectedSubscriptions) - val useGridLayout = subscriptions.isNotEmpty() + private val subscriptionPickerItemListener = OnItemClickListener { item, view -> + if (item is PickerSubscriptionItem) { + val subscriptionId = item.subscriptionEntity.uid + wasSubscriptionSelectionChanged = true - val groupAdapter = GroupAdapter() - groupAdapter.spanCount = if (useGridLayout) 4 else 1 - - val subscriptionsCount = this.selectedSubscriptions.size - val selectedCountText = resources.getQuantityString(R.plurals.feed_group_dialog_selection_count, subscriptionsCount, subscriptionsCount) - selected_subscription_count_view.text = selectedCountText - subscriptions_selector_header_info.text = selectedCountText - - Section().apply { - addAll(subscriptions.map { - val isSelected = this@FeedGroupDialog.selectedSubscriptions.contains(it.uid) - PickerSubscriptionItem(it, isSelected) - }) - setPlaceholder(EmptyPlaceholderItem()) - - groupAdapter.add(this) - } - - subscriptions_selector_list.apply { - layoutManager = if (useGridLayout) { - GridLayoutManager(requireContext(), groupAdapter.spanCount, RecyclerView.VERTICAL, false) + val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) { + this.selectedSubscriptions.remove(subscriptionId) + false } else { - LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + this.selectedSubscriptions.add(subscriptionId) + true } - adapter = groupAdapter + item.updateSelected(view, isSelected) + updateSubscriptionSelectedCount() + } + } - if (subscriptionsListState != null) { - layoutManager?.onRestoreInstanceState(subscriptionsListState) - subscriptionsListState = null - } + private fun setupSubscriptionPicker( + subscriptions: List, + selectedSubscriptions: Set + ) { + if (!wasSubscriptionSelectionChanged) { + this.selectedSubscriptions.addAll(selectedSubscriptions) } - groupAdapter.setOnItemClickListener { item, _ -> - when (item) { - is PickerSubscriptionItem -> { - val subscriptionId = item.subscriptionEntity.uid + updateSubscriptionSelectedCount() - val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) { - this.selectedSubscriptions.remove(subscriptionId) - false - } else { - this.selectedSubscriptions.add(subscriptionId) - true - } - - item.isSelected = isSelected - item.notifyChanged(PickerSubscriptionItem.UPDATE_SELECTED) - - val subscriptionsCount = this.selectedSubscriptions.size - val updateSelectedCountText = resources.getQuantityString(R.plurals.feed_group_dialog_selection_count, subscriptionsCount, subscriptionsCount) - selected_subscription_count_view.text = updateSelectedCountText - subscriptions_selector_header_info.text = updateSelectedCountText - } - } + if (subscriptions.isEmpty()) { + subscriptionEmptyFooter.clear() + subscriptionEmptyFooter.add(EmptyPlaceholderItem()) + } else { + subscriptionEmptyFooter.clear() } - select_channel_button.setOnClickListener { + subscriptions.forEach { + it.isSelected = this@FeedGroupDialog.selectedSubscriptions + .contains(it.subscriptionEntity.uid) + } + + subscriptionMainSection.update(subscriptions, false) + + if (subscriptionsListState != null) { + subscriptions_selector_list.layoutManager?.onRestoreInstanceState(subscriptionsListState) + subscriptionsListState = null + } else { subscriptions_selector_list.scrollToPosition(0) - showScreen(SubscriptionsPickerScreen) } } + private fun updateSubscriptionSelectedCount() { + val selectedCount = this.selectedSubscriptions.size + val selectedCountText = resources.getQuantityString( + R.plurals.feed_group_dialog_selection_count, + selectedCount, selectedCount) + selected_subscription_count_view.text = selectedCountText + subscriptions_header_info.text = selectedCountText + } + private fun setupIconPicker() { val groupAdapter = GroupAdapter() groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) }) @@ -311,9 +394,9 @@ class FeedGroupDialog : DialogFragment() { } } - // ///////////////////////////////////////////////////////////////////////// + /*/​////////////////////////////////////////////////////////////////////////// // Screen Selector - // ///////////////////////////////////////////////////////////////////////// + //​//////////////////////////////////////////////////////////////////////// */ private fun showScreen(screen: ScreenState) { currentScreen = screen @@ -337,7 +420,8 @@ class FeedGroupDialog : DialogFragment() { else -> View.VISIBLE } - if (currentScreen != InitialScreen) hideKeyboard() + hideKeyboard() + hideSearch() } private fun View.onlyVisibleIn(vararg screens: ScreenState) { @@ -347,13 +431,58 @@ class FeedGroupDialog : DialogFragment() { } } - // ///////////////////////////////////////////////////////////////////////// + /*/​////////////////////////////////////////////////////////////////////////// // Utils - // ///////////////////////////////////////////////////////////////////////// + //​//////////////////////////////////////////////////////////////////////// */ + + private fun isSearchVisible() = subscriptions_header_search_container?.visibility == View.VISIBLE + + private fun resetSearch() { + toolbar_search_edit_text.setText("") + subscriptionsCurrentSearchQuery = "" + viewModel.clearSubscriptionsFilter() + } + + private fun hideSearch() { + resetSearch() + subscriptions_header_search_container.visibility = View.GONE + subscriptions_header_info_container.visibility = View.VISIBLE + subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = true + hideKeyboardSearch() + } + + private fun showSearch() { + subscriptions_header_search_container.visibility = View.VISIBLE + subscriptions_header_info_container.visibility = View.GONE + subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = false + showKeyboardSearch() + } + + private val inputMethodManager by lazy { + requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + } + + private fun showKeyboardSearch() { + if (toolbar_search_edit_text.requestFocus()) { + inputMethodManager.showSoftInput(toolbar_search_edit_text, InputMethodManager.SHOW_IMPLICIT) + } + } + + private fun hideKeyboardSearch() { + inputMethodManager.hideSoftInputFromWindow(toolbar_search_edit_text.windowToken, + InputMethodManager.RESULT_UNCHANGED_SHOWN) + toolbar_search_edit_text.clearFocus() + } + + private fun showKeyboard() { + if (group_name_input.requestFocus()) { + inputMethodManager.showSoftInput(group_name_input, InputMethodManager.SHOW_IMPLICIT) + } + } private fun hideKeyboard() { - val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN) + inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, + InputMethodManager.RESULT_UNCHANGED_SHOWN) group_name_input.clearFocus() } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt index ac00245e6..e9a7e4eb7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt @@ -9,42 +9,56 @@ import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.disposables.Disposable import io.reactivex.functions.BiFunction +import io.reactivex.processors.BehaviorProcessor import io.reactivex.schedulers.Schedulers import org.schabi.newpipe.database.feed.model.FeedGroupEntity -import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.SubscriptionManager +import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem -class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() { - class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return FeedGroupDialogViewModel(context.applicationContext, groupId) as T - } - } +class FeedGroupDialogViewModel( + applicationContext: Context, + private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + initialQuery: String = "", + initialShowOnlyUngrouped: Boolean = false +) : ViewModel() { private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) private var subscriptionManager = SubscriptionManager(applicationContext) + private var filterSubscriptions = BehaviorProcessor.create() + private var toggleShowOnlyUngrouped = BehaviorProcessor.create() + + private var subscriptionsFlowable = Flowable + .combineLatest( + filterSubscriptions.startWith(initialQuery), + toggleShowOnlyUngrouped.startWith(initialShowOnlyUngrouped), + BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) } + ) + .distinctUntilChanged() + .switchMap { filter -> + subscriptionManager.getSubscriptions(groupId, filter.query, filter.showOnlyUngrouped) + }.map { list -> list.map { PickerSubscriptionItem(it) } } + private val mutableGroupLiveData = MutableLiveData() - private val mutableSubscriptionsLiveData = MutableLiveData, Set>>() + private val mutableSubscriptionsLiveData = MutableLiveData, Set>>() private val mutableDialogEventLiveData = MutableLiveData() val groupLiveData: LiveData = mutableGroupLiveData - val subscriptionsLiveData: LiveData, Set>> = mutableSubscriptionsLiveData + val subscriptionsLiveData: LiveData, Set>> = mutableSubscriptionsLiveData val dialogEventLiveData: LiveData = mutableDialogEventLiveData private var actionProcessingDisposable: Disposable? = null private var feedGroupDisposable = feedDatabaseManager.getGroup(groupId) - .subscribeOn(Schedulers.io()) - .subscribe(mutableGroupLiveData::postValue) + .subscribeOn(Schedulers.io()) + .subscribe(mutableGroupLiveData::postValue) private var subscriptionsDisposable = Flowable - .combineLatest(subscriptionManager.subscriptions(), feedDatabaseManager.subscriptionIdsForGroup(groupId), - BiFunction { t1: List, t2: List -> t1 to t2.toSet() }) - .subscribeOn(Schedulers.io()) - .subscribe(mutableSubscriptionsLiveData::postValue) + .combineLatest(subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId), + BiFunction { t1: List, t2: List -> t1 to t2.toSet() }) + .subscribeOn(Schedulers.io()) + .subscribe(mutableSubscriptionsLiveData::postValue) override fun onCleared() { super.onCleared() @@ -55,14 +69,14 @@ class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long = fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set) { doAction(feedDatabaseManager.createGroup(name, selectedIcon) - .flatMapCompletable { - feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList()) - }) + .flatMapCompletable { + feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList()) + }) } fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set, sortOrder: Long) { doAction(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList()) - .andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))) + .andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))) } fun deleteGroup() { @@ -74,13 +88,40 @@ class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long = mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent actionProcessingDisposable = completable - .subscribeOn(Schedulers.io()) - .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) } + .subscribeOn(Schedulers.io()) + .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) } } } + fun filterSubscriptionsBy(query: String) { + filterSubscriptions.onNext(query) + } + + fun clearSubscriptionsFilter() { + filterSubscriptions.onNext("") + } + + fun toggleShowOnlyUngrouped(showOnlyUngrouped: Boolean) { + toggleShowOnlyUngrouped.onNext(showOnlyUngrouped) + } + sealed class DialogEvent { object ProcessingEvent : DialogEvent() object SuccessEvent : DialogEvent() } + + data class Filter(val query: String, val showOnlyUngrouped: Boolean) + + class Factory( + private val context: Context, + private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + private val initialQuery: String = "", + private val initialShowOnlyUngrouped: Boolean = false + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return FeedGroupDialogViewModel(context.applicationContext, + groupId, initialQuery, initialShowOnlyUngrouped) as T + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt index 92c063b4b..48f40e219 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt @@ -6,7 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback import androidx.recyclerview.widget.LinearLayoutManager @@ -49,7 +49,7 @@ class FeedGroupReorderDialog : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel = ViewModelProviders.of(this).get(FeedGroupReorderDialogViewModel::class.java) + viewModel = ViewModelProvider(this).get(FeedGroupReorderDialogViewModel::class.java) viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups)) viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer { when (it) { diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt index c806277ee..ef7eb93cd 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt @@ -7,4 +7,5 @@ import org.schabi.newpipe.R class EmptyPlaceholderItem : Item() { override fun getLayout(): Int = R.layout.list_empty_view override fun bind(viewHolder: GroupieViewHolder, position: Int) {} + override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt index d90ac0d82..7d33da71f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt @@ -1,39 +1,28 @@ package org.schabi.newpipe.local.subscription.item import android.view.View -import com.nostra13.universalimageloader.core.DisplayImageOptions import com.nostra13.universalimageloader.core.ImageLoader import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import com.xwray.groupie.kotlinandroidextensions.Item -import kotlinx.android.synthetic.main.picker_subscription_item.selected_highlight -import kotlinx.android.synthetic.main.picker_subscription_item.thumbnail_view -import kotlinx.android.synthetic.main.picker_subscription_item.title_view +import kotlinx.android.synthetic.main.picker_subscription_item.* +import kotlinx.android.synthetic.main.picker_subscription_item.view.* import org.schabi.newpipe.R import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.util.AnimationUtils import org.schabi.newpipe.util.AnimationUtils.animateView import org.schabi.newpipe.util.ImageDisplayConstants -data class PickerSubscriptionItem(val subscriptionEntity: SubscriptionEntity, var isSelected: Boolean = false) : Item() { - companion object { - const val UPDATE_SELECTED = 123 - - val IMAGE_LOADING_OPTIONS: DisplayImageOptions = ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS - } - +data class PickerSubscriptionItem( + val subscriptionEntity: SubscriptionEntity, + var isSelected: Boolean = false +) : Item() { + override fun getId(): Long = subscriptionEntity.uid override fun getLayout(): Int = R.layout.picker_subscription_item - - override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList) { - if (payloads.contains(UPDATE_SELECTED)) { - animateView(viewHolder.selected_highlight, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150) - return - } - - super.bind(viewHolder, position, payloads) - } + override fun getSpanSize(spanCount: Int, position: Int): Int = 1 override fun bind(viewHolder: GroupieViewHolder, position: Int) { - ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl, viewHolder.thumbnail_view, IMAGE_LOADING_OPTIONS) + ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl, + viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS) viewHolder.title_view.text = subscriptionEntity.name viewHolder.selected_highlight.visibility = if (isSelected) View.VISIBLE else View.GONE @@ -47,7 +36,9 @@ data class PickerSubscriptionItem(val subscriptionEntity: SubscriptionEntity, va viewHolder.selected_highlight.alpha = 1F } - override fun getId(): Long { - return subscriptionEntity.uid + fun updateSelected(containerView: View, isSelected: Boolean) { + this.isSelected = isSelected + animateView(containerView.selected_highlight, + AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150) } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java index e6e081689..8e3aad893 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java @@ -86,12 +86,12 @@ public final class ImportExportJsonHelper { eventListener.onSizeReceived(channelsArray.size()); } - for (Object o : channelsArray) { + for (final Object o : channelsArray) { if (o instanceof JsonObject) { - JsonObject itemObject = (JsonObject) o; - int serviceId = itemObject.getInt(JSON_SERVICE_ID_KEY, 0); - String url = itemObject.getString(JSON_URL_KEY); - String name = itemObject.getString(JSON_NAME_KEY); + final JsonObject itemObject = (JsonObject) o; + final int serviceId = itemObject.getInt(JSON_SERVICE_ID_KEY, 0); + final String url = itemObject.getString(JSON_URL_KEY); + final String name = itemObject.getString(JSON_NAME_KEY); if (url != null && name != null && !url.isEmpty() && !name.isEmpty()) { channels.add(new SubscriptionItem(serviceId, url, name)); @@ -101,7 +101,7 @@ public final class ImportExportJsonHelper { } } } - } catch (Throwable e) { + } catch (final Throwable e) { throw new InvalidSourceException("Couldn't parse json", e); } @@ -117,7 +117,7 @@ public final class ImportExportJsonHelper { */ public static void writeTo(final List items, final OutputStream out, @Nullable final ImportExportEventListener eventListener) { - JsonAppendableWriter writer = JsonWriter.on(out); + final JsonAppendableWriter writer = JsonWriter.on(out); writeTo(items, writer, eventListener); writer.done(); } @@ -140,7 +140,7 @@ public final class ImportExportJsonHelper { writer.value(JSON_APP_VERSION_INT_KEY, BuildConfig.VERSION_CODE); writer.array(JSON_SUBSCRIPTIONS_ARRAY_KEY); - for (SubscriptionItem item : items) { + for (final SubscriptionItem item : items) { writer.object(); writer.value(JSON_SERVICE_ID_KEY, item.getServiceId()); writer.value(JSON_URL_KEY, item.getUrl()); diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java index 12b64d89d..f7d099712 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java @@ -74,7 +74,7 @@ public class SubscriptionsExportService extends BaseImportExportService { try { outFile = new File(path); outputStream = new FileOutputStream(outFile); - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException e) { handleError(e); return START_NOT_STICKY; } @@ -109,7 +109,7 @@ public class SubscriptionsExportService extends BaseImportExportService { .map(subscriptionEntities -> { final List result = new ArrayList<>(subscriptionEntities.size()); - for (SubscriptionEntity entity : subscriptionEntities) { + for (final SubscriptionEntity entity : subscriptionEntities) { result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(), entity.getName())); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index 06ba55106..51afc9fda 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -110,7 +110,7 @@ public class SubscriptionsImportService extends BaseImportExportService { try { inputStream = new FileInputStream(new File(filePath)); - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException e) { handleError(e); return START_NOT_STICKY; } @@ -187,7 +187,7 @@ public class SubscriptionsImportService extends BaseImportExportService { .getChannelInfo(subscriptionItem.getServiceId(), subscriptionItem.getUrl(), true) .blockingGet()); - } catch (Throwable e) { + } catch (final Throwable e) { return Notification.createOnError(e); } }) @@ -239,7 +239,7 @@ public class SubscriptionsImportService extends BaseImportExportService { private Consumer> getNotificationsConsumer() { return notification -> { if (notification.isOnNext()) { - String name = notification.getValue().getName(); + final String name = notification.getValue().getName(); eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : ""); } else if (notification.isOnError()) { final Throwable error = notification.getError(); @@ -260,7 +260,7 @@ public class SubscriptionsImportService extends BaseImportExportService { private Function>, List> upsertBatch() { return notificationList -> { final List infoList = new ArrayList<>(notificationList.size()); - for (Notification n : notificationList) { + for (final Notification n : notificationList) { if (n.isOnNext()) { infoList.add(n.getValue()); } diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java deleted file mode 100644 index 943d685b1..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ /dev/null @@ -1,684 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * BackgroundPlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.os.Build; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.View; -import android.widget.RemoteViews; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.core.app.NotificationCompat; - -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.source.MediaSource; -import com.nostra13.universalimageloader.core.assist.FailReason; - -import org.schabi.newpipe.BuildConfig; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.util.BitmapUtils; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ThemeHelper; - -import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - -/** - * Service Background Player implementing {@link VideoPlayer}. - * - * @author mauriciocolli - */ -public final class BackgroundPlayer extends Service { - public static final String ACTION_CLOSE - = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE"; - public static final String ACTION_PLAY_PAUSE - = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE"; - public static final String ACTION_REPEAT - = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT"; - public static final String ACTION_PLAY_NEXT - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT"; - public static final String ACTION_PLAY_PREVIOUS - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS"; - public static final String ACTION_FAST_REWIND - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND"; - public static final String ACTION_FAST_FORWARD - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD"; - - public static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource"; - private static final String TAG = "BackgroundPlayer"; - private static final boolean DEBUG = BasePlayer.DEBUG; - private static final int NOTIFICATION_ID = 123789; - private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60; - private BasePlayerImpl basePlayerImpl; - - /*////////////////////////////////////////////////////////////////////////// - // Service-Activity Binder - //////////////////////////////////////////////////////////////////////////*/ - private SharedPreferences sharedPreferences; - - /*////////////////////////////////////////////////////////////////////////// - // Notification - //////////////////////////////////////////////////////////////////////////*/ - private PlayerEventListener activityListener; - private IBinder mBinder; - private NotificationManager notificationManager; - private NotificationCompat.Builder notBuilder; - private RemoteViews notRemoteView; - private RemoteViews bigNotRemoteView; - private boolean shouldUpdateOnProgress; - private int timesNotificationUpdated; - - /*////////////////////////////////////////////////////////////////////////// - // Service's LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate() { - if (DEBUG) { - Log.d(TAG, "onCreate() called"); - } - notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - assureCorrectAppLanguage(this); - ThemeHelper.setTheme(this); - basePlayerImpl = new BasePlayerImpl(this); - basePlayerImpl.setup(); - - mBinder = new PlayerServiceBinder(basePlayerImpl); - shouldUpdateOnProgress = true; - } - - @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 + "]"); - } - basePlayerImpl.handleIntent(intent); - if (basePlayerImpl.mediaSessionManager != null) { - basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent); - } - return START_NOT_STICKY; - } - - @Override - public void onDestroy() { - if (DEBUG) { - Log.d(TAG, "destroy() called"); - } - onClose(); - } - - @Override - protected void attachBaseContext(final Context base) { - super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); - } - - @Override - public IBinder onBind(final Intent intent) { - return mBinder; - } - - /*////////////////////////////////////////////////////////////////////////// - // Actions - //////////////////////////////////////////////////////////////////////////*/ - private void onClose() { - if (DEBUG) { - Log.d(TAG, "onClose() called"); - } - - if (basePlayerImpl != null) { - basePlayerImpl.savePlaybackState(); - basePlayerImpl.stopActivityBinding(); - basePlayerImpl.destroy(); - } - if (notificationManager != null) { - notificationManager.cancel(NOTIFICATION_ID); - } - mBinder = null; - basePlayerImpl = null; - - stopForeground(true); - stopSelf(); - } - - private void onScreenOnOff(final boolean on) { - if (DEBUG) { - Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); - } - shouldUpdateOnProgress = on; - basePlayerImpl.triggerProgressUpdate(); - if (on) { - basePlayerImpl.startProgressLoop(); - } else { - basePlayerImpl.stopProgressLoop(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Notification - //////////////////////////////////////////////////////////////////////////*/ - - private void resetNotification() { - notBuilder = createNotification(); - timesNotificationUpdated = 0; - } - - private NotificationCompat.Builder createNotification() { - notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, - R.layout.player_background_notification); - bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, - R.layout.player_background_notification_expanded); - - setupNotification(notRemoteView); - setupNotification(bigNotRemoteView); - - NotificationCompat.Builder builder = new NotificationCompat - .Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setCustomContentView(notRemoteView) - .setCustomBigContentView(bigNotRemoteView); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setLockScreenThumbnail(builder); - } - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - builder.setPriority(NotificationCompat.PRIORITY_MAX); - } - return builder; - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private void setLockScreenThumbnail(final NotificationCompat.Builder builder) { - boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean( - getString(R.string.enable_lock_screen_video_thumbnail_key), true); - - if (isLockScreenThumbnailEnabled) { - basePlayerImpl.mediaSessionManager.setLockScreenArt( - builder, - getCenteredThumbnailBitmap() - ); - } else { - basePlayerImpl.mediaSessionManager.clearLockScreenArt(builder); - } - } - - @Nullable - private Bitmap getCenteredThumbnailBitmap() { - final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; - final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; - - return BitmapUtils.centerCrop(basePlayerImpl.getThumbnail(), screenWidth, screenHeight); - } - - private void setupNotification(final RemoteViews remoteViews) { - if (basePlayerImpl == null) { - return; - } - - remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle()); - remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName()); - - remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); - remoteViews.setOnClickPendingIntent(R.id.notificationStop, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); - remoteViews.setOnClickPendingIntent(R.id.notificationRepeat, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); - - // Starts background player activity -- attempts to unlock lockscreen - final Intent intent = NavigationHelper.getBackgroundPlayerActivityIntent(this); - remoteViews.setOnClickPendingIntent(R.id.notificationContent, - PendingIntent.getActivity(this, NOTIFICATION_ID, intent, - PendingIntent.FLAG_UPDATE_CURRENT)); - - if (basePlayerImpl.playQueue != null && basePlayerImpl.playQueue.size() > 1) { - remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_previous); - remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_next); - remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); - remoteViews.setOnClickPendingIntent(R.id.notificationFForward, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); - } else { - remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_rewind); - remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_fastforward); - remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); - remoteViews.setOnClickPendingIntent(R.id.notificationFForward, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, - new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); - } - - setRepeatModeIcon(remoteViews, basePlayerImpl.getRepeatMode()); - } - - /** - * Updates the notification, and the play/pause button in it. - * Used for changes on the remoteView - * - * @param drawableId if != -1, sets the drawable with that id on the play/pause button - */ - private synchronized void updateNotification(final int drawableId) { -// if (DEBUG) { -// Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); -// } - if (notBuilder == null) { - return; - } - if (drawableId != -1) { - if (notRemoteView != null) { - notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); - } - if (bigNotRemoteView != null) { - bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); - } - } - notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); - timesNotificationUpdated++; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) { - switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_repeat_off); - break; - case Player.REPEAT_MODE_ONE: - remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_repeat_one); - break; - case Player.REPEAT_MODE_ALL: - remoteViews.setInt(R.id.notificationRepeat, SET_IMAGE_RESOURCE_METHOD, - R.drawable.exo_controls_repeat_all); - break; - } - } - ////////////////////////////////////////////////////////////////////////// - - protected class BasePlayerImpl extends BasePlayer { - @NonNull - private final AudioPlaybackResolver resolver; - private int cachedDuration; - private String cachedDurationString; - - BasePlayerImpl(final Context context) { - super(context); - this.resolver = new AudioPlaybackResolver(context, dataSource); - } - - @Override - public void initPlayer(final boolean playOnReady) { - super.initPlayer(playOnReady); - } - - @Override - public void handleIntent(final Intent intent) { - super.handleIntent(intent); - - resetNotification(); - if (bigNotRemoteView != null) { - bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); - } - if (notRemoteView != null) { - notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); - } - startForeground(NOTIFICATION_ID, notBuilder.build()); - } - - /*////////////////////////////////////////////////////////////////////////// - // Thumbnail Loading - //////////////////////////////////////////////////////////////////////////*/ - - private void updateNotificationThumbnail() { - if (basePlayerImpl == null) { - return; - } - if (notRemoteView != null) { - notRemoteView.setImageViewBitmap(R.id.notificationCover, - basePlayerImpl.getThumbnail()); - } - if (bigNotRemoteView != null) { - bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, - basePlayerImpl.getThumbnail()); - } - } - - @Override - public void onLoadingComplete(final String imageUri, final View view, - final Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(-1); - } - - @Override - public void onLoadingFailed(final String imageUri, final View view, - final FailReason failReason) { - super.onLoadingFailed(imageUri, view, failReason); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(-1); - } - - /*////////////////////////////////////////////////////////////////////////// - // States Implementation - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onPrepared(final boolean playWhenReady) { - super.onPrepared(playWhenReady); - } - - @Override - public void onShuffleClicked() { - super.onShuffleClicked(); - updatePlayback(); - } - - @Override - public void onMuteUnmuteButtonClicked() { - super.onMuteUnmuteButtonClicked(); - updatePlayback(); - } - - @Override - public void onUpdateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - updateProgress(currentProgress, duration, bufferPercent); - - if (!shouldUpdateOnProgress) { - return; - } - if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) { - resetNotification(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*Oreo*/) { - updateNotificationThumbnail(); - } - } - if (bigNotRemoteView != null) { - if (cachedDuration != duration) { - cachedDuration = duration; - cachedDurationString = getTimeString(duration); - } - bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, - currentProgress, false); - bigNotRemoteView.setTextViewText(R.id.notificationTime, - getTimeString(currentProgress) + " / " + cachedDurationString); - } - if (notRemoteView != null) { - notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, - currentProgress, false); - } - updateNotification(-1); - } - - @Override - public void onPlayPrevious() { - super.onPlayPrevious(); - triggerProgressUpdate(); - } - - @Override - public void onPlayNext() { - super.onPlayNext(); - triggerProgressUpdate(); - } - - @Override - public void destroy() { - super.destroy(); - if (notRemoteView != null) { - notRemoteView.setImageViewBitmap(R.id.notificationCover, null); - } - if (bigNotRemoteView != null) { - bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { - super.onPlaybackParametersChanged(playbackParameters); - updatePlayback(); - } - - @Override - public void onLoadingChanged(final boolean isLoading) { - // Disable default behavior - } - - @Override - public void onRepeatModeChanged(final int i) { - resetNotification(); - updateNotification(-1); - updatePlayback(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(-1); - updateMetadata(); - } - - @Override - @Nullable - public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - return resolver.resolve(info); - } - - @Override - public void onPlaybackShutdown() { - super.onPlaybackShutdown(); - onClose(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Activity Event Listener - //////////////////////////////////////////////////////////////////////////*/ - - /*package-private*/ void setActivityListener(final PlayerEventListener listener) { - activityListener = listener; - updateMetadata(); - updatePlayback(); - triggerProgressUpdate(); - } - - /*package-private*/ void removeActivityListener(final PlayerEventListener listener) { - if (activityListener == listener) { - activityListener = null; - } - } - - private void updateMetadata() { - if (activityListener != null && getCurrentMetadata() != null) { - activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); - } - } - - private void updatePlayback() { - if (activityListener != null && simpleExoPlayer != null && playQueue != null) { - activityListener.onPlaybackUpdate(currentState, getRepeatMode(), - playQueue.isShuffled(), getPlaybackParameters()); - } - } - - private void updateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - if (activityListener != null) { - activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); - } - } - - private void stopActivityBinding() { - if (activityListener != null) { - activityListener.onServiceStopped(); - activityListener = null; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Broadcast Receiver - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void setupBroadcastReceiver(final IntentFilter intentFltr) { - super.setupBroadcastReceiver(intentFltr); - intentFltr.addAction(ACTION_CLOSE); - intentFltr.addAction(ACTION_PLAY_PAUSE); - intentFltr.addAction(ACTION_REPEAT); - intentFltr.addAction(ACTION_PLAY_PREVIOUS); - intentFltr.addAction(ACTION_PLAY_NEXT); - intentFltr.addAction(ACTION_FAST_REWIND); - intentFltr.addAction(ACTION_FAST_FORWARD); - - intentFltr.addAction(Intent.ACTION_SCREEN_ON); - intentFltr.addAction(Intent.ACTION_SCREEN_OFF); - - intentFltr.addAction(Intent.ACTION_HEADSET_PLUG); - } - - @Override - public void onBroadcastReceived(final Intent intent) { - super.onBroadcastReceived(intent); - if (intent == null || intent.getAction() == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); - } - switch (intent.getAction()) { - case ACTION_CLOSE: - onClose(); - break; - case ACTION_PLAY_PAUSE: - onPlayPause(); - break; - case ACTION_REPEAT: - onRepeatClicked(); - break; - case ACTION_PLAY_NEXT: - onPlayNext(); - break; - case ACTION_PLAY_PREVIOUS: - onPlayPrevious(); - break; - case ACTION_FAST_FORWARD: - onFastForward(); - break; - case ACTION_FAST_REWIND: - onFastRewind(); - break; - case Intent.ACTION_SCREEN_ON: - onScreenOnOff(true); - break; - case Intent.ACTION_SCREEN_OFF: - onScreenOnOff(false); - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // States - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void changeState(final int state) { - super.changeState(state); - updatePlayback(); - } - - @Override - public void onPlaying() { - super.onPlaying(); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(R.drawable.exo_controls_pause); - } - - @Override - public void onPaused() { - super.onPaused(); - resetNotification(); - updateNotificationThumbnail(); - updateNotification(R.drawable.exo_controls_play); - } - - @Override - public void onCompleted() { - super.onCompleted(); - resetNotification(); - if (bigNotRemoteView != null) { - bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); - } - if (notRemoteView != null) { - notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); - } - updateNotificationThumbnail(); - updateNotification(R.drawable.ic_replay_white_24dp); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java index 9da3c3c86..0e5222f5e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java @@ -1,13 +1,13 @@ package org.schabi.newpipe.player; import android.content.Intent; +import android.view.Menu; import android.view.MenuItem; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import static org.schabi.newpipe.player.BackgroundPlayer.ACTION_CLOSE; - public final class BackgroundPlayerActivity extends ServicePlayerActivity { private static final String TAG = "BackgroundPlayerActivity"; @@ -19,25 +19,25 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { @Override public String getSupportActionTitle() { - return getResources().getString(R.string.title_activity_background_player); + return getResources().getString(R.string.title_activity_play_queue); } @Override public Intent getBindIntent() { - return new Intent(this, BackgroundPlayer.class); + return new Intent(this, MainPlayer.class); } @Override public void startPlayerListener() { - if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) { - ((BackgroundPlayer.BasePlayerImpl) player).setActivityListener(this); + if (player instanceof VideoPlayerImpl) { + ((VideoPlayerImpl) player).setActivityListener(this); } } @Override public void stopPlayerListener() { - if (player != null && player instanceof BackgroundPlayer.BasePlayerImpl) { - ((BackgroundPlayer.BasePlayerImpl) player).removeActivityListener(this); + if (player instanceof VideoPlayerImpl) { + ((VideoPlayerImpl) player).removeActivityListener(this); } } @@ -56,18 +56,30 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { } this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startService( - getSwitchIntent(PopupVideoPlayer.class) - .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()) - ); + NavigationHelper.playOnPopupPlayer( + getApplicationContext(), player.playQueue, this.player.isPlaying()); return true; } + + if (item.getItemId() == R.id.action_switch_background) { + this.player.setRecovery(); + NavigationHelper.playOnBackgroundPlayer( + getApplicationContext(), player.playQueue, this.player.isPlaying()); + return true; + } + return false; } @Override - public Intent getPlayerShutdownIntent() { - return new Intent(ACTION_CLOSE); + public void setupMenu(final Menu menu) { + if (player == null) { + return; + } + + menu.findItem(R.id.action_switch_popup) + .setVisible(!((VideoPlayerImpl) player).popupPlayerSelected()); + menu.findItem(R.id.action_switch_background) + .setVisible(!((VideoPlayerImpl) player).audioPlayerSelected()); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 03faccd98..813a1cdf4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -27,7 +27,7 @@ import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.View; import android.widget.Toast; @@ -54,8 +54,8 @@ import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; -import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.DownloaderImpl; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -78,6 +78,7 @@ import org.schabi.newpipe.util.SerializedCache; import java.io.IOException; import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.disposables.SerialDisposable; @@ -97,7 +98,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; @SuppressWarnings({"WeakerAccess"}) public abstract class BasePlayer implements Player.EventListener, PlaybackListener, ImageLoadingListener { - public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); + public static final boolean DEBUG = MainActivity.DEBUG; @NonNull public static final String TAG = "BasePlayer"; @@ -128,13 +129,15 @@ public abstract class BasePlayer implements @NonNull public static final String SELECT_ON_APPEND = "select_on_append"; @NonNull + public static final String PLAYER_TYPE = "player_type"; + @NonNull public static final String IS_MUTED = "is_muted"; /*////////////////////////////////////////////////////////////////////////// // Playback //////////////////////////////////////////////////////////////////////////*/ - protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; + protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; protected PlayQueue playQueue; protected PlayQueueAdapter playQueueAdapter; @@ -159,6 +162,10 @@ public abstract class BasePlayer implements protected static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; + public static final int PLAYER_TYPE_VIDEO = 0; + public static final int PLAYER_TYPE_AUDIO = 1; + public static final int PLAYER_TYPE_POPUP = 2; + protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; protected MediaSessionManager mediaSessionManager; @@ -223,7 +230,7 @@ public abstract class BasePlayer implements public void setup() { if (simpleExoPlayer == null) { - initPlayer(/*playOnInit=*/true); + initPlayer(true); } initListeners(); } @@ -250,7 +257,8 @@ public abstract class BasePlayer implements registerBroadcastReceiver(); } - public void initListeners() { } + public void initListeners() { + } public void handleIntent(final Intent intent) { if (DEBUG) { @@ -272,7 +280,7 @@ public abstract class BasePlayer implements // Resolve append intents if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { - int sizeBeforeAppend = playQueue.size(); + final int sizeBeforeAppend = playQueue.size(); playQueue.append(queue.getStreams()); if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) @@ -288,34 +296,72 @@ public abstract class BasePlayer implements final float playbackPitch = savedParameters.pitch; final boolean playbackSkipSilence = savedParameters.skipSilence; + final boolean samePlayQueue = playQueue != null && playQueue.equals(queue); + final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); final boolean isMuted = intent .getBooleanExtra(IS_MUTED, simpleExoPlayer != null && isMuted()); + /* + * There are 3 situations when playback shouldn't be started from scratch (zero timestamp): + * 1. User pressed on a timestamp link and the same video should be rewound to the timestamp + * 2. User changed a player from, for example. main to popup, or from audio to main, etc + * 3. User chose to resume a video based on a saved timestamp from history of played videos + * In those cases time will be saved because re-init of the play queue is a not an instant + * task and requires network calls + * */ // seek to timestamp if stream is already playing if (simpleExoPlayer != null && queue.size() == 1 && playQueue != null + && playQueue.size() == 1 && playQueue.getItem() != null && queue.getItem().getUrl().equals(playQueue.getItem().getUrl()) - && queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET - ) { + && queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + // Player can have state = IDLE when playback is stopped or failed + // and we should retry() in this case + if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) { + simpleExoPlayer.retry(); + } simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition()); return; - } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && isPlaybackResumeEnabled()) { + + } else if (samePlayQueue && !playQueue.isDisposed() && simpleExoPlayer != null) { + // Do not re-init the same PlayQueue. Save time + // Player can have state = IDLE when playback is stopped or failed + // and we should retry() in this case + if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) { + simpleExoPlayer.retry(); + } + return; + } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) + && isPlaybackResumeEnabled() + && !samePlayQueue) { final PlayQueueItem item = queue.getItem(); if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { stateLoader = recordManager.loadStreamState(item) - .observeOn(mainThread()) - .doFinally(() -> initPlayback(queue, repeatMode, playbackSpeed, - playbackPitch, playbackSkipSilence, true, isMuted)) + .observeOn(AndroidSchedulers.mainThread()) + // Do not place initPlayback() in doFinally() because + // it restarts playback after destroy() + //.doFinally() .subscribe( - state -> queue - .setRecovery(queue.getIndex(), state.getProgressTime()), + state -> { + queue.setRecovery(queue.getIndex(), state.getProgressTime()); + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, true, isMuted); + }, error -> { if (DEBUG) { error.printStackTrace(); } + // In case any error we can start playback without history + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, true, isMuted); + }, + () -> { + // Completed but not found in history + initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, true, isMuted); } ); databaseUpdateReactor.add(stateLoader); @@ -323,21 +369,23 @@ public abstract class BasePlayer implements } } // Good to go... - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, - /*playOnInit=*/!intent.getBooleanExtra(START_PAUSED, false), isMuted); + // In a case of equal PlayQueues we can re-init old one but only when it is disposed + initPlayback(samePlayQueue ? playQueue : queue, repeatMode, + playbackSpeed, playbackPitch, playbackSkipSilence, + !intent.getBooleanExtra(START_PAUSED, false), + isMuted); } private PlaybackParameters retrievePlaybackParametersFromPreferences() { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - final float speed = preferences - .getFloat(context.getString(R.string.playback_speed_key), getPlaybackSpeed()); - final float pitch = preferences.getFloat(context.getString(R.string.playback_pitch_key), - getPlaybackPitch()); - final boolean skipSilence = preferences - .getBoolean(context.getString(R.string.playback_skip_silence_key), - getPlaybackSkipSilence()); + final float speed = preferences.getFloat( + context.getString(R.string.playback_speed_key), getPlaybackSpeed()); + final float pitch = preferences.getFloat( + context.getString(R.string.playback_pitch_key), getPlaybackPitch()); + final boolean skipSilence = preferences.getBoolean( + context.getString(R.string.playback_skip_silence_key), getPlaybackSkipSilence()); return new PlaybackParameters(speed, pitch, skipSilence); } @@ -411,6 +459,7 @@ public abstract class BasePlayer implements databaseUpdateReactor.clear(); progressUpdateReactor.set(null); + ImageLoader.getInstance().stop(); } /*////////////////////////////////////////////////////////////////////////// @@ -562,7 +611,8 @@ public abstract class BasePlayer implements } } - public void onPausedSeek() { } + public void onPausedSeek() { + } public void onCompleted() { if (DEBUG) { @@ -830,7 +880,6 @@ public abstract class BasePlayer implements } setRecovery(); - final Throwable cause = error.getCause(); if (error instanceof BehindLiveWindowException) { reload(); } else { @@ -1018,14 +1067,6 @@ public abstract class BasePlayer implements registerView(); } - @Override - public void onPlaybackShutdown() { - if (DEBUG) { - Log.d(TAG, "Shutting down..."); - } - destroy(); - } - /*////////////////////////////////////////////////////////////////////////// // General Player //////////////////////////////////////////////////////////////////////////*/ @@ -1090,6 +1131,7 @@ public abstract class BasePlayer implements } simpleExoPlayer.setPlayWhenReady(true); + savePlaybackState(); } public void onPause() { @@ -1102,6 +1144,7 @@ public abstract class BasePlayer implements audioReactor.abandonAudioFocus(); simpleExoPlayer.setPlayWhenReady(false); + savePlaybackState(); } public void onPlayPause() { @@ -1296,6 +1339,11 @@ public abstract class BasePlayer implements return; } final StreamInfo currentInfo = currentMetadata.getMetadata(); + if (playQueue != null) { + // Save current position. It will help to restore this position once a user + // wants to play prev or next stream from the queue + playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition()); + } savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); } @@ -1408,7 +1456,7 @@ public abstract class BasePlayer implements return false; } - Timeline.Window timelineWindow = new Timeline.Window(); + final Timeline.Window timelineWindow = new Timeline.Window(); currentTimeline.getWindow(currentWindowIndex, timelineWindow); return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); } @@ -1419,7 +1467,7 @@ public abstract class BasePlayer implements } try { return simpleExoPlayer.isCurrentWindowDynamic(); - } catch (@NonNull IndexOutOfBoundsException e) { + } catch (@NonNull final IndexOutOfBoundsException e) { // Why would this even happen =( // But lets log it anyway. Save is save if (DEBUG) { @@ -1434,6 +1482,10 @@ public abstract class BasePlayer implements return simpleExoPlayer != null && simpleExoPlayer.isPlaying(); } + public boolean isLoading() { + return simpleExoPlayer != null && simpleExoPlayer.isLoading(); + } + @Player.RepeatMode public int getRepeatMode() { return simpleExoPlayer == null @@ -1471,20 +1523,32 @@ public abstract class BasePlayer implements return parameters == null ? PlaybackParameters.DEFAULT : parameters; } + /** + * Sets the playback parameters of the player, and also saves them to shared preferences. + * Speed and pitch are rounded up to 2 decimal places before being used or saved. + * + * @param speed the playback speed, will be rounded to up to 2 decimal places + * @param pitch the playback pitch, will be rounded to up to 2 decimal places + * @param skipSilence skip silence during playback + */ public void setPlaybackParameters(final float speed, final float pitch, final boolean skipSilence) { - savePlaybackParametersToPreferences(speed, pitch, skipSilence); - simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch, skipSilence)); + final float roundedSpeed = Math.round(speed * 100.0f) / 100.0f; + final float roundedPitch = Math.round(pitch * 100.0f) / 100.0f; + + savePlaybackParametersToPreferences(roundedSpeed, roundedPitch, skipSilence); + simpleExoPlayer.setPlaybackParameters( + new PlaybackParameters(roundedSpeed, roundedPitch, skipSilence)); } private void savePlaybackParametersToPreferences(final float speed, final float pitch, final boolean skipSilence) { PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putFloat(context.getString(R.string.playback_speed_key), speed) - .putFloat(context.getString(R.string.playback_pitch_key), pitch) - .putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence) - .apply(); + .edit() + .putFloat(context.getString(R.string.playback_speed_key), speed) + .putFloat(context.getString(R.string.playback_pitch_key), pitch) + .putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence) + .apply(); } public PlayQueue getPlayQueue() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java new file mode 100644 index 000000000..273f37cc8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -0,0 +1,484 @@ +/* + * Copyright 2017 Mauricio Colli + * Part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.player; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import androidx.preference.PreferenceManager; +import android.util.DisplayMetrics; +import android.view.ViewGroup; +import android.view.WindowManager; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; + +import com.google.android.exoplayer2.Player; + +import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.BitmapUtils; +import org.schabi.newpipe.util.NavigationHelper; +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 = BasePlayer.DEBUG; + + private VideoPlayerImpl playerImpl; + private WindowManager windowManager; + private SharedPreferences sharedPreferences; + + private final IBinder mBinder = new MainPlayer.LocalBinder(); + + public enum PlayerType { + VIDEO, + AUDIO, + POPUP + } + + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ + + static final int NOTIFICATION_ID = 123789; + private NotificationManager notificationManager; + private NotificationCompat.Builder notBuilder; + private RemoteViews notRemoteView; + private RemoteViews bigNotRemoteView; + + static final String ACTION_CLOSE = + "org.schabi.newpipe.player.MainPlayer.CLOSE"; + static final String ACTION_PLAY_PAUSE = + "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE"; + static final String ACTION_OPEN_CONTROLS = + "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS"; + static final String ACTION_REPEAT = + "org.schabi.newpipe.player.MainPlayer.REPEAT"; + static final String ACTION_PLAY_NEXT = + "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT"; + static final String ACTION_PLAY_PREVIOUS = + "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS"; + static final String ACTION_FAST_REWIND = + "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND"; + static final String ACTION_FAST_FORWARD = + "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD"; + + private static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource"; + + /*////////////////////////////////////////////////////////////////////////// + // Service's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCreate() { + if (DEBUG) { + Log.d(TAG, "onCreate() called"); + } + assureCorrectAppLanguage(this); + notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); + windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + + ThemeHelper.setTheme(this); + createView(); + } + + private void createView() { + final View layout = View.inflate(this, R.layout.player, null); + + playerImpl = new VideoPlayerImpl(this); + playerImpl.setup(layout); + playerImpl.shouldUpdateOnProgress = true; + } + + @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()) + && playerImpl.playQueue == 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(VideoPlayer.PLAY_QUEUE_KEY) != null) { + showNotificationAndStartForeground(); + } + + playerImpl.handleIntent(intent); + if (playerImpl.mediaSessionManager != null) { + playerImpl.mediaSessionManager.handleMediaButtonIntent(intent); + } + return START_NOT_STICKY; + } + + public void stop(final boolean autoplayEnabled) { + if (DEBUG) { + Log.d(TAG, "stop() called"); + } + + if (playerImpl.getPlayer() != null) { + playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); + // Releases wifi & cpu, disables keepScreenOn, etc. + if (!autoplayEnabled) { + playerImpl.onPause(); + } + // We can't just pause the player here because it will make transition + // from one stream to a new stream not smooth + playerImpl.getPlayer().stop(false); + playerImpl.setRecovery(); + // Android TV will handle back button in case controls will be visible + // (one more additional unneeded click while the player is hidden) + playerImpl.hideControls(0, 0); + // 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 + if (!autoplayEnabled) { + stopForeground(true); + } + } + } + + @Override + public void onTaskRemoved(final Intent rootIntent) { + super.onTaskRemoved(rootIntent); + if (!playerImpl.videoPlayerSelected()) { + return; + } + onDestroy(); + // Unload from memory completely + Runtime.getRuntime().halt(0); + } + + @Override + public void onDestroy() { + if (DEBUG) { + Log.d(TAG, "destroy() called"); + } + onClose(); + } + + @Override + protected void attachBaseContext(final Context base) { + super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); + } + + @Override + public IBinder onBind(final Intent intent) { + return mBinder; + } + + /*////////////////////////////////////////////////////////////////////////// + // Actions + //////////////////////////////////////////////////////////////////////////*/ + private void onClose() { + if (DEBUG) { + Log.d(TAG, "onClose() called"); + } + + if (playerImpl != null) { + removeViewFromParent(); + + playerImpl.setRecovery(); + playerImpl.savePlaybackState(); + playerImpl.stopActivityBinding(); + playerImpl.removePopupFromView(); + playerImpl.destroy(); + } + if (notificationManager != null) { + notificationManager.cancel(NOTIFICATION_ID); + } + + stopForeground(true); + stopSelf(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + boolean isLandscape() { + // DisplayMetrics from activity context knows about MultiWindow feature + // while DisplayMetrics from app context doesn't + final DisplayMetrics metrics = (playerImpl != null + && playerImpl.getParentActivity() != null) + ? playerImpl.getParentActivity().getResources().getDisplayMetrics() + : getResources().getDisplayMetrics(); + return metrics.heightPixels < metrics.widthPixels; + } + + public View getView() { + if (playerImpl == null) { + return null; + } + + return playerImpl.getRootView(); + } + + public void removeViewFromParent() { + if (getView().getParent() != null) { + if (playerImpl.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()); + } + } + } + + private void showNotificationAndStartForeground() { + resetNotification(); + if (getBigNotRemoteView() != null) { + getBigNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false); + } + if (getNotRemoteView() != null) { + getNotRemoteView().setProgressBar(R.id.notificationProgressBar, 100, 0, false); + } + startForeground(NOTIFICATION_ID, getNotBuilder().build()); + } + + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ + + void resetNotification() { + notBuilder = createNotification(); + playerImpl.timesNotificationUpdated = 0; + } + + private NotificationCompat.Builder createNotification() { + notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, + R.layout.player_notification); + bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, + R.layout.player_notification_expanded); + + setupNotification(notRemoteView); + setupNotification(bigNotRemoteView); + + final NotificationCompat.Builder builder = new NotificationCompat + .Builder(this, getString(R.string.notification_channel_id)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCustomContentView(notRemoteView) + .setCustomBigContentView(bigNotRemoteView); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setLockScreenThumbnail(builder); + } + + builder.setPriority(NotificationCompat.PRIORITY_MAX); + return builder; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private void setLockScreenThumbnail(final NotificationCompat.Builder builder) { + final boolean isLockScreenThumbnailEnabled = sharedPreferences.getBoolean( + getString(R.string.enable_lock_screen_video_thumbnail_key), true); + + if (isLockScreenThumbnailEnabled) { + playerImpl.mediaSessionManager.setLockScreenArt( + builder, + getCenteredThumbnailBitmap() + ); + } else { + playerImpl.mediaSessionManager.clearLockScreenArt(builder); + } + } + + @Nullable + private Bitmap getCenteredThumbnailBitmap() { + final int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; + final int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + + return BitmapUtils.centerCrop(playerImpl.getThumbnail(), screenWidth, screenHeight); + } + + private void setupNotification(final RemoteViews remoteViews) { + // Don't show anything until player is playing + if (playerImpl == null) { + return; + } + + remoteViews.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); + remoteViews.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); + remoteViews.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); + + remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationStop, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); + // Starts VideoDetailFragment or opens BackgroundPlayerActivity. + remoteViews.setOnClickPendingIntent(R.id.notificationContent, + PendingIntent.getActivity(this, NOTIFICATION_ID, + getIntentForNotification(), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationRepeat, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); + + + if (playerImpl.playQueue != null && playerImpl.playQueue.size() > 1) { + remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_previous); + remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_next); + remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationFForward, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); + } else { + remoteViews.setInt(R.id.notificationFRewind, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_rewind); + remoteViews.setInt(R.id.notificationFForward, SET_IMAGE_RESOURCE_METHOD, + R.drawable.exo_controls_fastforward); + remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_FAST_REWIND), PendingIntent.FLAG_UPDATE_CURRENT)); + remoteViews.setOnClickPendingIntent(R.id.notificationFForward, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, + new Intent(ACTION_FAST_FORWARD), PendingIntent.FLAG_UPDATE_CURRENT)); + } + + setRepeatModeIcon(remoteViews, playerImpl.getRepeatMode()); + } + + /** + * Updates the notification, and the play/pause button in it. + * Used for changes on the remoteView + * + * @param drawableId if != -1, sets the drawable with that id on the play/pause button + */ + synchronized void updateNotification(final int drawableId) { + /*if (DEBUG) { + Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); + }*/ + if (notBuilder == null) { + return; + } + if (drawableId != -1) { + if (notRemoteView != null) { + notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + } + if (bigNotRemoteView != null) { + bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + } + } + notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); + playerImpl.timesNotificationUpdated++; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) { + if (remoteViews == null) { + return; + } + + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + remoteViews.setInt(R.id.notificationRepeat, + SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_off); + break; + case Player.REPEAT_MODE_ONE: + remoteViews.setInt(R.id.notificationRepeat, + SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_one); + break; + case Player.REPEAT_MODE_ALL: + remoteViews.setInt(R.id.notificationRepeat, + SET_IMAGE_RESOURCE_METHOD, R.drawable.exo_controls_repeat_all); + break; + } + } + + private Intent getIntentForNotification() { + final Intent intent; + if (playerImpl.audioPlayerSelected() || playerImpl.popupPlayerSelected()) { + // Means we play in popup or audio only. Let's show BackgroundPlayerActivity + intent = NavigationHelper.getBackgroundPlayerActivityIntent(getApplicationContext()); + } else { + // We are playing in fragment. Don't open another activity just show fragment. That's it + intent = NavigationHelper.getPlayerIntent(this, MainActivity.class, null, true); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + } + return intent; + } + + /*////////////////////////////////////////////////////////////////////////// + // Getters + //////////////////////////////////////////////////////////////////////////*/ + + NotificationCompat.Builder getNotBuilder() { + return notBuilder; + } + + RemoteViews getBigNotRemoteView() { + return bigNotRemoteView; + } + + RemoteViews getNotRemoteView() { + return notRemoteView; + } + + + public class LocalBinder extends Binder { + + public MainPlayer getService() { + return MainPlayer.this; + } + + public VideoPlayerImpl getPlayer() { + return MainPlayer.this.playerImpl; + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java deleted file mode 100644 index 56744d858..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ /dev/null @@ -1,1472 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * Copyright 2019 Eltex ltd - * MainVideoPlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.database.ContentObserver; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.media.AudioManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.provider.Settings; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; -import android.view.DisplayCutout; -import android.view.GestureDetector; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.PopupMenu; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.SeekBar; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.app.ActivityCompat; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.SubtitleView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.player.helper.PlaybackParameterDialog; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; -import org.schabi.newpipe.util.AndroidTvUtils; -import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.KoreUtil; -import org.schabi.newpipe.util.ListHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ShareUtils; -import org.schabi.newpipe.util.StateSaver; -import org.schabi.newpipe.util.ThemeHelper; -import org.schabi.newpipe.views.FocusOverlayView; - -import java.util.List; -import java.util.Queue; -import java.util.UUID; - -import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; -import static org.schabi.newpipe.player.VideoPlayer.DPAD_CONTROLS_HIDE_TIME; -import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; -import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; -import static org.schabi.newpipe.util.AnimationUtils.animateRotation; -import static org.schabi.newpipe.util.AnimationUtils.animateView; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; -import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; - -/** - * Activity Player implementing {@link VideoPlayer}. - * - * @author mauriciocolli - */ -public final class MainVideoPlayer extends AppCompatActivity - implements StateSaver.WriteRead, PlaybackParameterDialog.Callback { - private static final String TAG = ".MainVideoPlayer"; - private static final boolean DEBUG = BasePlayer.DEBUG; - - private GestureDetector gestureDetector; - - private VideoPlayerImpl playerImpl; - - private SharedPreferences defaultPreferences; - - @Nullable - private PlayerState playerState; - private boolean isInMultiWindow; - private boolean isBackPressed; - - private ContentObserver rotationObserver; - - /*////////////////////////////////////////////////////////////////////////// - // Activity LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void onCreate(@Nullable final Bundle savedInstanceState) { - assureCorrectAppLanguage(this); - super.onCreate(savedInstanceState); - if (DEBUG) { - Log.d(TAG, "onCreate() called with: " - + "savedInstanceState = [" + savedInstanceState + "]"); - } - defaultPreferences = PreferenceManager.getDefaultSharedPreferences(this); - ThemeHelper.setTheme(this); - getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - getWindow().setStatusBarColor(Color.BLACK); - } - setVolumeControlStream(AudioManager.STREAM_MUSIC); - - WindowManager.LayoutParams lp = getWindow().getAttributes(); - lp.screenBrightness = PlayerHelper.getScreenBrightness(getApplicationContext()); - getWindow().setAttributes(lp); - - hideSystemUi(); - setContentView(R.layout.activity_main_player); - - playerImpl = new VideoPlayerImpl(this); - playerImpl.setup(findViewById(android.R.id.content)); - - if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { - return; // We have saved states, stop here to restore it - } - - final Intent intent = getIntent(); - if (intent != null) { - playerImpl.handleIntent(intent); - } else { - Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show(); - finish(); - } - - rotationObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(final boolean selfChange) { - super.onChange(selfChange); - if (globalScreenOrientationLocked()) { - final String orientKey = getString(R.string.last_orientation_landscape_key); - - final boolean lastOrientationWasLandscape = defaultPreferences - .getBoolean(orientKey, AndroidTvUtils.isTv(getApplicationContext())); - setLandscape(lastOrientationWasLandscape); - } else { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - } - } - }; - - getContentResolver().registerContentObserver( - Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), - false, rotationObserver); - - if (AndroidTvUtils.isTv(this)) { - FocusOverlayView.setupFocusObserver(this); - } - } - - @Override - protected void onRestoreInstanceState(@NonNull final Bundle bundle) { - if (DEBUG) { - Log.d(TAG, "onRestoreInstanceState() called"); - } - super.onRestoreInstanceState(bundle); - StateSaver.tryToRestore(bundle, this); - } - - @Override - protected void onNewIntent(final Intent intent) { - if (DEBUG) { - Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); - } - super.onNewIntent(intent); - if (intent != null) { - playerState = null; - playerImpl.handleIntent(intent); - } - } - - @Override - public boolean onKeyDown(final int keyCode, final KeyEvent event) { - switch (event.getKeyCode()) { - default: - break; - case KeyEvent.KEYCODE_BACK: - if (AndroidTvUtils.isTv(getApplicationContext()) - && playerImpl.isControlsVisible()) { - playerImpl.hideControls(0, 0); - hideSystemUi(); - return true; - } - break; - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_CENTER: - View playerRoot = playerImpl.getRootView(); - View controls = playerImpl.getControlsRoot(); - if (playerRoot.hasFocus() && !controls.hasFocus()) { - // do not interfere with focus in playlist etc. - return super.onKeyDown(keyCode, event); - } - - if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { - return true; - } - - if (!playerImpl.isControlsVisible()) { - playerImpl.playPauseButton.requestFocus(); - playerImpl.showControlsThenHide(); - showSystemUi(); - return true; - } else { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); - } - break; - } - - return super.onKeyDown(keyCode, event); - } - - @Override - protected void onResume() { - if (DEBUG) { - Log.d(TAG, "onResume() called"); - } - assureCorrectAppLanguage(this); - super.onResume(); - - if (globalScreenOrientationLocked()) { - final String orientKey = getString(R.string.last_orientation_landscape_key); - - boolean lastOrientationWasLandscape = defaultPreferences - .getBoolean(orientKey, AndroidTvUtils.isTv(getApplicationContext())); - setLandscape(lastOrientationWasLandscape); - } - - final int lastResizeMode = defaultPreferences.getInt( - getString(R.string.last_resize_mode), AspectRatioFrameLayout.RESIZE_MODE_FIT); - playerImpl.setResizeMode(lastResizeMode); - - // Upon going in or out of multiwindow mode, isInMultiWindow will always be false, - // since the first onResume needs to restore the player. - // Subsequent onResume calls while multiwindow mode remains the same and the player is - // prepared should be ignored. - if (isInMultiWindow) { - return; - } - isInMultiWindow = isInMultiWindow(); - - if (playerState != null) { - playerImpl.setPlaybackQuality(playerState.getPlaybackQuality()); - playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(), - playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(), - playerState.isPlaybackSkipSilence(), playerState.wasPlaying(), - playerImpl.isMuted()); - } - } - - @Override - public void onConfigurationChanged(final Configuration newConfig) { - super.onConfigurationChanged(newConfig); - assureCorrectAppLanguage(this); - - if (playerImpl.isSomePopupMenuVisible()) { - playerImpl.getQualityPopupMenu().dismiss(); - playerImpl.getPlaybackSpeedPopupMenu().dismiss(); - } - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - isBackPressed = true; - } - - @Override - protected void onSaveInstanceState(final Bundle outState) { - if (DEBUG) { - Log.d(TAG, "onSaveInstanceState() called"); - } - super.onSaveInstanceState(outState); - if (playerImpl == null) { - return; - } - - playerImpl.setRecovery(); - if (!playerImpl.gotDestroyed()) { - playerState = createPlayerState(); - } - StateSaver.tryToSave(isChangingConfigurations(), null, outState, this); - } - - @Override - protected void onStop() { - if (DEBUG) { - Log.d(TAG, "onStop() called"); - } - super.onStop(); - PlayerHelper.setScreenBrightness(getApplicationContext(), - getWindow().getAttributes().screenBrightness); - - if (playerImpl == null) { - return; - } - if (!isBackPressed) { - playerImpl.minimize(); - } - playerState = createPlayerState(); - playerImpl.destroy(); - - if (rotationObserver != null) { - getContentResolver().unregisterContentObserver(rotationObserver); - } - - isInMultiWindow = false; - isBackPressed = false; - } - - @Override - protected void attachBaseContext(final Context newBase) { - super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase)); - } - - @Override - protected void onPause() { - playerImpl.savePlaybackState(); - super.onPause(); - } - - /*////////////////////////////////////////////////////////////////////////// - // State Saving - //////////////////////////////////////////////////////////////////////////*/ - - private PlayerState createPlayerState() { - return new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(), - playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(), - playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(), - playerImpl.isPlaying()); - } - - @Override - public String generateSuffix() { - return "." + UUID.randomUUID().toString() + ".player"; - } - - @Override - public void writeTo(final Queue objectsToSave) { - if (objectsToSave == null) { - return; - } - objectsToSave.add(playerState); - } - - @Override - @SuppressWarnings("unchecked") - public void readFrom(@NonNull final Queue savedObjects) { - playerState = (PlayerState) savedObjects.poll(); - } - - /*////////////////////////////////////////////////////////////////////////// - // View - //////////////////////////////////////////////////////////////////////////*/ - - private void showSystemUi() { - if (DEBUG) { - Log.d(TAG, "showSystemUi() called"); - } - if (playerImpl != null && playerImpl.queueVisible) { - return; - } - - final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - @ColorInt final int systenUiColor = - ActivityCompat.getColor(getApplicationContext(), R.color.video_overlay_color); - getWindow().setStatusBarColor(systenUiColor); - getWindow().setNavigationBarColor(systenUiColor); - } - - getWindow().getDecorView().setSystemUiVisibility(visibility); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - - private void hideSystemUi() { - if (DEBUG) { - Log.d(TAG, "hideSystemUi() called"); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - } - getWindow().getDecorView().setSystemUiVisibility(visibility); - } - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - - private void toggleOrientation() { - setLandscape(!isLandscape()); - defaultPreferences.edit() - .putBoolean(getString(R.string.last_orientation_landscape_key), !isLandscape()) - .apply(); - } - - private boolean isLandscape() { - return getResources().getDisplayMetrics().heightPixels - < getResources().getDisplayMetrics().widthPixels; - } - - private void setLandscape(final boolean v) { - setRequestedOrientation(v - ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); - } - - private boolean globalScreenOrientationLocked() { - // 1: Screen orientation changes using accelerometer - // 0: Screen orientation is locked - return !(android.provider.Settings.System - .getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1); - } - - protected void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { - switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - imageButton.setImageResource(R.drawable.exo_controls_repeat_off); - break; - case Player.REPEAT_MODE_ONE: - imageButton.setImageResource(R.drawable.exo_controls_repeat_one); - break; - case Player.REPEAT_MODE_ALL: - imageButton.setImageResource(R.drawable.exo_controls_repeat_all); - break; - } - } - - protected void setShuffleButton(final ImageButton shuffleButton, final boolean shuffled) { - final int shuffleAlpha = shuffled ? 255 : 77; - shuffleButton.setImageAlpha(shuffleAlpha); - } - - protected void setMuteButton(final ImageButton muteButton, final boolean isMuted) { - muteButton.setImageDrawable(AppCompatResources.getDrawable(getApplicationContext(), isMuted - ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); - } - - - private boolean isInMultiWindow() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode(); - } - - //////////////////////////////////////////////////////////////////////////// - // Playback Parameters Listener - //////////////////////////////////////////////////////////////////////////// - - @Override - public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, - final boolean playbackSkipSilence) { - if (playerImpl != null) { - playerImpl.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); - } - } - - /////////////////////////////////////////////////////////////////////////// - - @SuppressWarnings({"unused", "WeakerAccess"}) - private class VideoPlayerImpl extends VideoPlayer { - private static final float MAX_GESTURE_LENGTH = 0.75f; - - private TextView titleTextView; - private TextView channelTextView; - private RelativeLayout volumeRelativeLayout; - private ProgressBar volumeProgressBar; - private ImageView volumeImageView; - private RelativeLayout brightnessRelativeLayout; - private ProgressBar brightnessProgressBar; - private ImageView brightnessImageView; - private ImageButton queueButton; - private ImageButton repeatButton; - private ImageButton shuffleButton; - - private ImageButton playPauseButton; - private ImageButton playPreviousButton; - private ImageButton playNextButton; - private Button closeButton; - - private RelativeLayout queueLayout; - private ImageButton itemsListCloseButton; - private RecyclerView itemsList; - private ItemTouchHelper itemTouchHelper; - - private boolean queueVisible; - - private ImageButton moreOptionsButton; - private ImageButton kodiButton; - private ImageButton shareButton; - private ImageButton toggleOrientationButton; - private ImageButton switchPopupButton; - private ImageButton switchBackgroundButton; - private ImageButton muteButton; - - private RelativeLayout windowRootLayout; - private View secondaryControls; - - private int maxGestureLength; - - VideoPlayerImpl(final Context context) { - super("VideoPlayerImpl" + MainVideoPlayer.TAG, context); - } - - @Override - public void initViews(final View view) { - super.initViews(view); - this.titleTextView = view.findViewById(R.id.titleTextView); - this.channelTextView = view.findViewById(R.id.channelTextView); - this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout); - this.volumeProgressBar = view.findViewById(R.id.volumeProgressBar); - this.volumeImageView = view.findViewById(R.id.volumeImageView); - this.brightnessRelativeLayout = view.findViewById(R.id.brightnessRelativeLayout); - this.brightnessProgressBar = view.findViewById(R.id.brightnessProgressBar); - this.brightnessImageView = view.findViewById(R.id.brightnessImageView); - this.queueButton = view.findViewById(R.id.queueButton); - this.repeatButton = view.findViewById(R.id.repeatButton); - this.shuffleButton = view.findViewById(R.id.shuffleButton); - - this.playPauseButton = view.findViewById(R.id.playPauseButton); - this.playPreviousButton = view.findViewById(R.id.playPreviousButton); - this.playNextButton = view.findViewById(R.id.playNextButton); - this.closeButton = view.findViewById(R.id.closeButton); - - this.moreOptionsButton = view.findViewById(R.id.moreOptionsButton); - this.secondaryControls = view.findViewById(R.id.secondaryControls); - this.kodiButton = view.findViewById(R.id.kodi); - this.shareButton = view.findViewById(R.id.share); - this.toggleOrientationButton = view.findViewById(R.id.toggleOrientation); - this.switchBackgroundButton = view.findViewById(R.id.switchBackground); - this.muteButton = view.findViewById(R.id.switchMute); - this.switchPopupButton = view.findViewById(R.id.switchPopup); - - this.queueLayout = findViewById(R.id.playQueuePanel); - this.itemsListCloseButton = findViewById(R.id.playQueueClose); - this.itemsList = findViewById(R.id.playQueue); - - titleTextView.setSelected(true); - channelTextView.setSelected(true); - - getRootView().setKeepScreenOn(true); - } - - @Override - protected void setupSubtitleView(@NonNull final SubtitleView view, - final float captionScale, - @NonNull final CaptionStyleCompat captionStyle) { - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); - final float captionRatioInverse = 20f + 4f * (1f - captionScale); - view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - (float) minimumLength / captionRatioInverse); - view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); - view.setStyle(captionStyle); - } - - @Override - public void initListeners() { - super.initListeners(); - - PlayerGestureListener listener = new PlayerGestureListener(); - gestureDetector = new GestureDetector(context, listener); - gestureDetector.setIsLongpressEnabled(false); - getRootView().setOnTouchListener(listener); - - queueButton.setOnClickListener(this); - repeatButton.setOnClickListener(this); - shuffleButton.setOnClickListener(this); - - playPauseButton.setOnClickListener(this); - playPreviousButton.setOnClickListener(this); - playNextButton.setOnClickListener(this); - closeButton.setOnClickListener(this); - - moreOptionsButton.setOnClickListener(this); - kodiButton.setOnClickListener(this); - shareButton.setOnClickListener(this); - toggleOrientationButton.setOnClickListener(this); - switchBackgroundButton.setOnClickListener(this); - muteButton.setOnClickListener(this); - switchPopupButton.setOnClickListener(this); - - getRootView().addOnLayoutChangeListener((view, l, t, r, b, ol, ot, or, ob) -> { - if (l != ol || t != ot || r != or || b != ob) { - // Use smaller value to be consistent between screen orientations - // (and to make usage easier) - int width = r - l; - int height = b - t; - maxGestureLength = (int) (Math.min(width, height) * MAX_GESTURE_LENGTH); - - if (DEBUG) { - Log.d(TAG, "maxGestureLength = " + maxGestureLength); - } - - volumeProgressBar.setMax(maxGestureLength); - brightnessProgressBar.setMax(maxGestureLength); - - setInitialGestureValues(); - } - }); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - queueLayout.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { - @Override - public WindowInsets onApplyWindowInsets(final View view, - final WindowInsets windowInsets) { - final DisplayCutout cutout = windowInsets.getDisplayCutout(); - if (cutout != null) { - view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), - cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); - } - return windowInsets; - } - }); - } - } - - public void minimize() { - switch (PlayerHelper.getMinimizeOnExitAction(context)) { - case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND: - onPlayBackgroundButtonClicked(); - break; - case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP: - onFullScreenButtonClicked(); - break; - case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE: - default: - // No action - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Video Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onRepeatModeChanged(final int i) { - super.onRepeatModeChanged(i); - updatePlaybackButtons(); - } - - @Override - public void onShuffleClicked() { - super.onShuffleClicked(); - updatePlaybackButtons(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - - // show kodi button if it supports the current service and it is enabled in settings - final boolean showKodiButton = - KoreUtil.isServiceSupportedByKore(tag.getMetadata().getServiceId()) - && PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.show_play_with_kodi_key), false); - kodiButton.setVisibility(showKodiButton ? View.VISIBLE : View.GONE); - - titleTextView.setText(tag.getMetadata().getName()); - channelTextView.setText(tag.getMetadata().getUploaderName()); - } - - @Override - public void onPlaybackShutdown() { - super.onPlaybackShutdown(); - finish(); - } - - public void onKodiShare() { - onPause(); - try { - NavigationHelper.playWithKore(context, Uri.parse(playerImpl.getVideoUrl())); - } catch (Exception e) { - if (DEBUG) { - Log.i(TAG, "Failed to start kore", e); - } - KoreUtil.showInstallKoreDialog(context); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Player Overrides - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onFullScreenButtonClicked() { - super.onFullScreenButtonClicked(); - - if (DEBUG) { - Log.d(TAG, "onFullScreenButtonClicked() called"); - } - if (simpleExoPlayer == null) { - return; - } - - if (!PermissionHelper.isPopupEnabled(context)) { - PermissionHelper.showPopupEnablementToast(context); - return; - } - - setRecovery(); - final Intent intent = NavigationHelper.getPlayerIntent( - context, - PopupVideoPlayer.class, - this.getPlayQueue(), - this.getRepeatMode(), - this.getPlaybackSpeed(), - this.getPlaybackPitch(), - this.getPlaybackSkipSilence(), - this.getPlaybackQuality(), - false, - !isPlaying(), - isMuted() - ); - context.startService(intent); - - ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); - destroy(); - finish(); - } - - public void onPlayBackgroundButtonClicked() { - if (DEBUG) { - Log.d(TAG, "onPlayBackgroundButtonClicked() called"); - } - if (playerImpl.getPlayer() == null) { - return; - } - - setRecovery(); - final Intent intent = NavigationHelper.getPlayerIntent( - context, - BackgroundPlayer.class, - this.getPlayQueue(), - this.getRepeatMode(), - this.getPlaybackSpeed(), - this.getPlaybackPitch(), - this.getPlaybackSkipSilence(), - this.getPlaybackQuality(), - false, - !isPlaying(), - isMuted() - ); - context.startService(intent); - - ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); - destroy(); - finish(); - } - - @Override - public void onMuteUnmuteButtonClicked() { - super.onMuteUnmuteButtonClicked(); - setMuteButton(muteButton, playerImpl.isMuted()); - } - - - @Override - public void onClick(final View v) { - super.onClick(v); - if (v.getId() == playPauseButton.getId()) { - onPlayPause(); - } else if (v.getId() == playPreviousButton.getId()) { - onPlayPrevious(); - } else if (v.getId() == playNextButton.getId()) { - onPlayNext(); - } else if (v.getId() == queueButton.getId()) { - onQueueClicked(); - return; - } else if (v.getId() == repeatButton.getId()) { - onRepeatClicked(); - return; - } else if (v.getId() == shuffleButton.getId()) { - onShuffleClicked(); - return; - } else if (v.getId() == moreOptionsButton.getId()) { - onMoreOptionsClicked(); - } else if (v.getId() == shareButton.getId()) { - onShareClicked(); - } else if (v.getId() == toggleOrientationButton.getId()) { - onScreenRotationClicked(); - } else if (v.getId() == switchPopupButton.getId()) { - onFullScreenButtonClicked(); - } else if (v.getId() == switchBackgroundButton.getId()) { - onPlayBackgroundButtonClicked(); - } else if (v.getId() == muteButton.getId()) { - onMuteUnmuteButtonClicked(); - } else if (v.getId() == closeButton.getId()) { - onPlaybackShutdown(); - return; - } else if (v.getId() == kodiButton.getId()) { - onKodiShare(); - } - - if (getCurrentState() != STATE_COMPLETED) { - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { - if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { - safeHideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - }); - } - } - - private void onQueueClicked() { - queueVisible = true; - hideSystemUi(); - - buildQueue(); - updatePlaybackButtons(); - - getControlsRoot().setVisibility(View.INVISIBLE); - animateView(queueLayout, SLIDE_AND_ALPHA, true, DEFAULT_CONTROLS_DURATION); - - itemsList.scrollToPosition(playQueue.getIndex()); - } - - private void onQueueClosed() { - animateView(queueLayout, SLIDE_AND_ALPHA, false, DEFAULT_CONTROLS_DURATION); - queueVisible = false; - } - - private void onMoreOptionsClicked() { - if (DEBUG) { - Log.d(TAG, "onMoreOptionsClicked() called"); - } - - final boolean isMoreControlsVisible - = secondaryControls.getVisibility() == View.VISIBLE; - - animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, - isMoreControlsVisible ? 0 : 180); - animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, - DEFAULT_CONTROLS_DURATION); - showControls(DEFAULT_CONTROLS_DURATION); - setMuteButton(muteButton, playerImpl.isMuted()); - } - - private void onShareClicked() { - // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) - ShareUtils.shareUrl(MainVideoPlayer.this, playerImpl.getVideoTitle(), - playerImpl.getVideoUrl() - + "&t=" + playerImpl.getPlaybackSeekBar().getProgress() / 1000); - } - - private void onScreenRotationClicked() { - if (DEBUG) { - Log.d(TAG, "onScreenRotationClicked() called"); - } - toggleOrientation(); - showControlsThenHide(); - } - - @Override - public void onPlaybackSpeedClicked() { - PlaybackParameterDialog - .newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence()) - .show(getSupportFragmentManager(), TAG); - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - super.onStopTrackingTouch(seekBar); - if (wasPlaying()) { - showControlsThenHide(); - } - } - - @Override - public void onDismiss(final PopupMenu menu) { - super.onDismiss(menu); - if (isPlaying()) { - hideControls(DEFAULT_CONTROLS_DURATION, 0); - } - hideSystemUi(); - } - - @Override - protected int nextResizeMode(final int currentResizeMode) { - final int newResizeMode; - switch (currentResizeMode) { - case AspectRatioFrameLayout.RESIZE_MODE_FIT: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; - break; - case AspectRatioFrameLayout.RESIZE_MODE_FILL: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; - break; - default: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - break; - } - - storeResizeMode(newResizeMode); - return newResizeMode; - } - - private void storeResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { - defaultPreferences.edit() - .putInt(getString(R.string.last_resize_mode), resizeMode) - .apply(); - } - - @Override - protected VideoPlaybackResolver.QualityResolver getQualityResolver() { - return new VideoPlaybackResolver.QualityResolver() { - @Override - public int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getDefaultResolutionIndex(context, sortedVideos); - } - - @Override - public int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); - } - }; - } - - /*////////////////////////////////////////////////////////////////////////// - // States - //////////////////////////////////////////////////////////////////////////*/ - - private void animatePlayButtons(final boolean show, final int duration) { - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); - animateView(playPreviousButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); - animateView(playNextButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); - } - - @Override - public void onBlocked() { - super.onBlocked(); - playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); - animatePlayButtons(false, 100); - animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); - getRootView().setKeepScreenOn(true); - } - - @Override - public void onBuffering() { - super.onBuffering(); - getRootView().setKeepScreenOn(true); - } - - @Override - public void onPlaying() { - super.onPlaying(); - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); - animatePlayButtons(true, 200); - playPauseButton.requestFocus(); - animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); - }); - - getRootView().setKeepScreenOn(true); - } - - @Override - public void onPaused() { - super.onPaused(); - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); - animatePlayButtons(true, 200); - playPauseButton.requestFocus(); - animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); - }); - - showSystemUi(); - getRootView().setKeepScreenOn(false); - } - - @Override - public void onPausedSeek() { - super.onPausedSeek(); - animatePlayButtons(false, 100); - getRootView().setKeepScreenOn(true); - } - - - @Override - public void onCompleted() { - animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { - playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); - animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); - animateView(closeButton, true, DEFAULT_CONTROLS_DURATION); - }); - getRootView().setKeepScreenOn(false); - super.onCompleted(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void setInitialGestureValues() { - if (getAudioReactor() != null) { - final float currentVolumeNormalized - = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume(); - volumeProgressBar.setProgress( - (int) (volumeProgressBar.getMax() * currentVolumeNormalized)); - } - - float screenBrightness = getWindow().getAttributes().screenBrightness; - if (screenBrightness < 0) { - screenBrightness = Settings.System.getInt(getContentResolver(), - Settings.System.SCREEN_BRIGHTNESS, 0) / 255.0f; - } - - brightnessProgressBar.setProgress( - (int) (brightnessProgressBar.getMax() * screenBrightness)); - - if (DEBUG) { - Log.d(TAG, "setInitialGestureValues: volumeProgressBar.getProgress() [" - + volumeProgressBar.getProgress() + "] " - + "brightnessProgressBar.getProgress() [" - + brightnessProgressBar.getProgress() + "]"); - } - } - - @Override - public void showControlsThenHide() { - if (queueVisible) { - return; - } - - super.showControlsThenHide(); - } - - @Override - public void showControls(final long duration) { - if (queueVisible) { - return; - } - - super.showControls(duration); - } - - @Override - public void safeHideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); - } - - View controlsRoot = getControlsRoot(); - if (controlsRoot.isInTouchMode()) { - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - getControlsVisibilityHandler().postDelayed(() -> - animateView(controlsRoot, false, duration, 0, - MainVideoPlayer.this::hideSystemUi), delay); - } - } - - @Override - public void hideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); - } - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - getControlsVisibilityHandler().postDelayed(() -> - animateView(getControlsRoot(), false, duration, 0, - MainVideoPlayer.this::hideSystemUi), - /*delayMillis=*/delay - ); - } - - private void updatePlaybackButtons() { - if (repeatButton == null || shuffleButton == null - || simpleExoPlayer == null || playQueue == null) { - return; - } - - setRepeatModeButton(repeatButton, getRepeatMode()); - setShuffleButton(shuffleButton, playQueue.isShuffled()); - } - - private void buildQueue() { - itemsList.setAdapter(playQueueAdapter); - itemsList.setClickable(true); - itemsList.setLongClickable(true); - - itemsList.clearOnScrollListeners(); - itemsList.addOnScrollListener(getQueueScrollListener()); - - itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); - itemTouchHelper.attachToRecyclerView(itemsList); - - playQueueAdapter.setSelectedListener(getOnSelectedListener()); - - itemsListCloseButton.setOnClickListener(view -> onQueueClosed()); - } - - private OnScrollBelowItemsListener getQueueScrollListener() { - return new OnScrollBelowItemsListener() { - @Override - public void onScrolledDown(final RecyclerView recyclerView) { - if (playQueue != null && !playQueue.isComplete()) { - playQueue.fetch(); - } else if (itemsList != null) { - itemsList.clearOnScrollListeners(); - } - } - }; - } - - private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new PlayQueueItemTouchCallback() { - @Override - public void onMove(final int sourceIndex, final int targetIndex) { - if (playQueue != null) { - playQueue.move(sourceIndex, targetIndex); - } - } - - @Override - public void onSwiped(final int index) { - if (index != -1) { - playQueue.remove(index); - } - } - }; - } - - private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { - return new PlayQueueItemBuilder.OnSelectedListener() { - @Override - public void selected(final PlayQueueItem item, final View view) { - onSelected(item); - } - - @Override - public void held(final PlayQueueItem item, final View view) { - final int index = playQueue.indexOf(item); - if (index != -1) { - playQueue.remove(index); - } - } - - @Override - public void onStartDrag(final PlayQueueItemHolder viewHolder) { - if (itemTouchHelper != null) { - itemTouchHelper.startDrag(viewHolder); - } - } - }; - } - - /////////////////////////////////////////////////////////////////////////// - // Getters - /////////////////////////////////////////////////////////////////////////// - - public TextView getTitleTextView() { - return titleTextView; - } - - public TextView getChannelTextView() { - return channelTextView; - } - - public RelativeLayout getVolumeRelativeLayout() { - return volumeRelativeLayout; - } - - public ProgressBar getVolumeProgressBar() { - return volumeProgressBar; - } - - public ImageView getVolumeImageView() { - return volumeImageView; - } - - public RelativeLayout getBrightnessRelativeLayout() { - return brightnessRelativeLayout; - } - - public ProgressBar getBrightnessProgressBar() { - return brightnessProgressBar; - } - - public ImageView getBrightnessImageView() { - return brightnessImageView; - } - - public ImageButton getRepeatButton() { - return repeatButton; - } - - public ImageButton getMuteButton() { - return muteButton; - } - - public ImageButton getPlayPauseButton() { - return playPauseButton; - } - - public int getMaxGestureLength() { - return maxGestureLength; - } - } - - private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener - implements View.OnTouchListener { - private static final int MOVEMENT_THRESHOLD = 40; - - private final boolean isVolumeGestureEnabled = PlayerHelper - .isVolumeGestureEnabled(getApplicationContext()); - private final boolean isBrightnessGestureEnabled = PlayerHelper - .isBrightnessGestureEnabled(getApplicationContext()); - - private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume(); - - private boolean isMoving; - - @Override - public boolean onDoubleTap(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onDoubleTap() called with: " - + "e = [" + e + "], " - + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", " - + "xy = " + e.getX() + ", " + e.getY()); - } - - if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) { - playerImpl.onFastForward(); - } else if (e.getX() < playerImpl.getRootView().getWidth() / 3) { - playerImpl.onFastRewind(); - } else { - playerImpl.getPlayPauseButton().performClick(); - } - - return true; - } - - @Override - public boolean onSingleTapConfirmed(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - } - if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { - return true; - } - - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(150, 0); - } else { - playerImpl.playPauseButton.requestFocus(); - playerImpl.showControlsThenHide(); - showSystemUi(); - } - - return true; - } - - @Override - public boolean onDown(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onDown() called with: e = [" + e + "]"); - } - - return super.onDown(e); - } - - @Override - public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, - final float distanceX, final float distanceY) { - if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) { - return false; - } - - final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(); - final boolean isTouchingNavigationBar = initialEvent.getY() - > playerImpl.getRootView().getHeight() - getNavigationBarHeight(); - if (isTouchingStatusBar || isTouchingNavigationBar) { - return false; - } - -// if (DEBUG) { -// Log.d(TAG, "MainVideoPlayer.onScroll = " + -// "e1.getRaw = [" + initialEvent.getRawX() + ", " -// + initialEvent.getRawY() + "], " + -// "e2.getRaw = [" + movingEvent.getRawX() + ", " -// + movingEvent.getRawY() + "], " + -// "distanceXy = [" + distanceX + ", " + distanceY + "]"); -// } - - final boolean insideThreshold - = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; - if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) - || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { - return false; - } - - isMoving = true; - - boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled; - boolean acceptVolumeArea = acceptAnyArea - || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2; - boolean acceptBrightnessArea = acceptAnyArea || !acceptVolumeArea; - - if (isVolumeGestureEnabled && acceptVolumeArea) { - playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); - float currentProgressPercent = - (float) playerImpl.getVolumeProgressBar().getProgress() - / playerImpl.getMaxGestureLength(); - int currentVolume = (int) (maxVolume * currentProgressPercent); - playerImpl.getAudioReactor().setVolume(currentVolume); - - if (DEBUG) { - Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); - } - - final int resId = currentProgressPercent <= 0 - ? R.drawable.ic_volume_off_white_24dp - : currentProgressPercent < 0.25 - ? R.drawable.ic_volume_mute_white_24dp - : currentProgressPercent < 0.75 - ? R.drawable.ic_volume_down_white_24dp - : R.drawable.ic_volume_up_white_24dp; - - playerImpl.getVolumeImageView().setImageDrawable( - AppCompatResources.getDrawable(getApplicationContext(), resId) - ); - - if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); - } - if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); - } - } else if (isBrightnessGestureEnabled && acceptBrightnessArea) { - playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY); - float currentProgressPercent - = (float) playerImpl.getBrightnessProgressBar().getProgress() - / playerImpl.getMaxGestureLength(); - WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); - layoutParams.screenBrightness = currentProgressPercent; - getWindow().setAttributes(layoutParams); - - if (DEBUG) { - Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " - + currentProgressPercent); - } - - final int resId = currentProgressPercent < 0.25 - ? R.drawable.ic_brightness_low_white_24dp - : currentProgressPercent < 0.75 - ? R.drawable.ic_brightness_medium_white_24dp - : R.drawable.ic_brightness_high_white_24dp; - - playerImpl.getBrightnessImageView().setImageDrawable( - AppCompatResources.getDrawable(getApplicationContext(), resId) - ); - - if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, - 200); - } - if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); - } - } - return true; - } - - private int getNavigationBarHeight() { - int resId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); - if (resId > 0) { - return getResources().getDimensionPixelSize(resId); - } - return 0; - } - - private int getStatusBarHeight() { - int resId = getResources().getIdentifier("status_bar_height", "dimen", "android"); - if (resId > 0) { - return getResources().getDimensionPixelSize(resId); - } - return 0; - } - - private void onScrollEnd() { - if (DEBUG) { - Log.d(TAG, "onScrollEnd() called"); - } - - if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, - 200, 200); - } - if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, - 200, 200); - } - - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - } - - @Override - public boolean onTouch(final View v, final MotionEvent event) { -// if (DEBUG) { -// Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]"); -// } - gestureDetector.onTouchEvent(event); - if (event.getAction() == MotionEvent.ACTION_UP && isMoving) { - isMoving = false; - onScrollEnd(); - } - return true; - } - - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java deleted file mode 100644 index 0ccec3067..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ /dev/null @@ -1,1311 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * PopupVideoPlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.SuppressLint; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.PixelFormat; -import android.os.Build; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.GestureDetector; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.animation.AnticipateInterpolator; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.PopupMenu; -import android.widget.RemoteViews; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.SubtitleView; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.nostra13.universalimageloader.core.assist.FailReason; - -import org.schabi.newpipe.BuildConfig; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; -import org.schabi.newpipe.util.ListHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ThemeHelper; - -import java.util.List; - -import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; -import static org.schabi.newpipe.util.AnimationUtils.animateView; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - -/** - * Service Popup Player implementing {@link VideoPlayer}. - * - * @author mauriciocolli - */ -public final class PopupVideoPlayer extends Service { - public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; - public static final String ACTION_PLAY_PAUSE - = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE"; - public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT"; - private static final String TAG = ".PopupVideoPlayer"; - private static final boolean DEBUG = BasePlayer.DEBUG; - private static final int NOTIFICATION_ID = 40028922; - private static final String POPUP_SAVED_WIDTH = "popup_saved_width"; - private static final String POPUP_SAVED_X = "popup_saved_x"; - private static final String POPUP_SAVED_Y = "popup_saved_y"; - - private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; - - private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - - private WindowManager windowManager; - private WindowManager.LayoutParams popupLayoutParams; - private GestureDetector popupGestureDetector; - - private View closeOverlayView; - private FloatingActionButton closeOverlayButton; - - private int tossFlingVelocity; - - private float screenWidth; - private float screenHeight; - private float popupWidth; - private float popupHeight; - - private float minimumWidth; - private float minimumHeight; - private float maximumWidth; - private float maximumHeight; - - private NotificationManager notificationManager; - private NotificationCompat.Builder notBuilder; - private RemoteViews notRemoteView; - - private VideoPlayerImpl playerImpl; - private boolean isPopupClosing = false; - - /*////////////////////////////////////////////////////////////////////////// - // Service-Activity Binder - //////////////////////////////////////////////////////////////////////////*/ - - private PlayerEventListener activityListener; - private IBinder mBinder; - - /*////////////////////////////////////////////////////////////////////////// - // Service LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate() { - assureCorrectAppLanguage(this); - windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); - notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - - playerImpl = new VideoPlayerImpl(this); - ThemeHelper.setTheme(this); - - mBinder = new PlayerServiceBinder(playerImpl); - } - - @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 (playerImpl.getPlayer() == null) { - initPopup(); - initPopupCloseOverlay(); - } - - playerImpl.handleIntent(intent); - - return START_NOT_STICKY; - } - - @Override - public void onConfigurationChanged(final Configuration newConfig) { - assureCorrectAppLanguage(this); - if (DEBUG) { - Log.d(TAG, "onConfigurationChanged() called with: " - + "newConfig = [" + newConfig + "]"); - } - updateScreenSize(); - updatePopupSize(popupLayoutParams.width, -1); - checkPopupPositionBounds(); - } - - @Override - public void onDestroy() { - if (DEBUG) { - Log.d(TAG, "onDestroy() called"); - } - closePopup(); - } - - @Override - protected void attachBaseContext(final Context base) { - super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); - } - - @Override - public IBinder onBind(final Intent intent) { - return mBinder; - } - - /*////////////////////////////////////////////////////////////////////////// - // Init - //////////////////////////////////////////////////////////////////////////*/ - - @SuppressLint("RtlHardcoded") - private void initPopup() { - if (DEBUG) { - Log.d(TAG, "initPopup() called"); - } - View rootView = View.inflate(this, R.layout.player_popup, null); - playerImpl.setup(rootView); - - tossFlingVelocity = PlayerHelper.getTossFlingVelocity(this); - - updateScreenSize(); - - final boolean popupRememberSizeAndPos = PlayerHelper.isRememberingPopupDimensions(this); - final float defaultSize = getResources().getDimension(R.dimen.popup_default_width); - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - popupWidth = popupRememberSizeAndPos - ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize; - - final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O - ? WindowManager.LayoutParams.TYPE_PHONE - : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - - popupLayoutParams = new WindowManager.LayoutParams( - (int) popupWidth, (int) getMinimumVideoHeight(popupWidth), - layoutParamType, - IDLE_WINDOW_FLAGS, - PixelFormat.TRANSLUCENT); - popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - - int centerX = (int) (screenWidth / 2f - popupWidth / 2f); - int centerY = (int) (screenHeight / 2f - popupHeight / 2f); - popupLayoutParams.x = popupRememberSizeAndPos - ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; - popupLayoutParams.y = popupRememberSizeAndPos - ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; - - checkPopupPositionBounds(); - - PopupWindowGestureListener listener = new PopupWindowGestureListener(); - popupGestureDetector = new GestureDetector(this, listener); - rootView.setOnTouchListener(listener); - - playerImpl.getLoadingPanel().setMinimumWidth(popupLayoutParams.width); - playerImpl.getLoadingPanel().setMinimumHeight(popupLayoutParams.height); - windowManager.addView(rootView, popupLayoutParams); - } - - @SuppressLint("RtlHardcoded") - private void initPopupCloseOverlay() { - if (DEBUG) { - Log.d(TAG, "initPopupCloseOverlay() called"); - } - closeOverlayView = View.inflate(this, R.layout.player_popup_close_overlay, null); - closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton); - - final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O - ? WindowManager.LayoutParams.TYPE_PHONE - : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - - WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, - layoutParamType, - flags, - PixelFormat.TRANSLUCENT); - closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - closeOverlayLayoutParams.softInputMode = WindowManager - .LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - - closeOverlayButton.setVisibility(View.GONE); - windowManager.addView(closeOverlayView, closeOverlayLayoutParams); - } - - /*////////////////////////////////////////////////////////////////////////// - // Notification - //////////////////////////////////////////////////////////////////////////*/ - - private void resetNotification() { - notBuilder = createNotification(); - } - - private NotificationCompat.Builder createNotification() { - notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, - R.layout.player_popup_notification); - - notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); - notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName()); - notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail()); - - notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), - PendingIntent.FLAG_UPDATE_CURRENT)); - notRemoteView.setOnClickPendingIntent(R.id.notificationStop, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), - PendingIntent.FLAG_UPDATE_CURRENT)); - notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), - PendingIntent.FLAG_UPDATE_CURRENT)); - - // Starts popup player activity -- attempts to unlock lockscreen - final Intent intent = NavigationHelper.getPopupPlayerActivityIntent(this); - notRemoteView.setOnClickPendingIntent(R.id.notificationContent, - PendingIntent.getActivity(this, NOTIFICATION_ID, intent, - PendingIntent.FLAG_UPDATE_CURRENT)); - - setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode()); - - NotificationCompat.Builder builder = new NotificationCompat - .Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContent(notRemoteView); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - builder.setPriority(NotificationCompat.PRIORITY_MAX); - } - return builder; - } - - /** - * Updates the notification, and the play/pause button in it. - * Used for changes on the remoteView - * - * @param drawableId if != -1, sets the drawable with that id on the play/pause button - */ - private void updateNotification(final int drawableId) { - if (DEBUG) { - Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); - } - if (notBuilder == null || notRemoteView == null) { - return; - } - if (drawableId != -1) { - notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); - } - notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); - } - - /*////////////////////////////////////////////////////////////////////////// - // Misc - //////////////////////////////////////////////////////////////////////////*/ - - public void closePopup() { - if (DEBUG) { - Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); - } - if (isPopupClosing) { - return; - } - isPopupClosing = true; - - if (playerImpl != null) { - playerImpl.savePlaybackState(); - if (playerImpl.getRootView() != null) { - windowManager.removeView(playerImpl.getRootView()); - } - playerImpl.setRootView(null); - playerImpl.stopActivityBinding(); - playerImpl.destroy(); - playerImpl = null; - } - - mBinder = null; - if (notificationManager != null) { - notificationManager.cancel(NOTIFICATION_ID); - } - - animateOverlayAndFinishService(); - } - - private void animateOverlayAndFinishService() { - final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - - closeOverlayButton.getY()); - - closeOverlayButton.animate().setListener(null).cancel(); - closeOverlayButton.animate() - .setInterpolator(new AnticipateInterpolator()) - .translationY(targetTranslationY) - .setDuration(400) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(final Animator animation) { - end(); - } - - @Override - public void onAnimationEnd(final Animator animation) { - end(); - } - - private void end() { - windowManager.removeView(closeOverlayView); - - stopForeground(true); - stopSelf(); - } - }).start(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - /** - * @see #checkPopupPositionBounds(float, float) - * @return if the popup was out of bounds and have been moved back to it - */ - @SuppressWarnings("UnusedReturnValue") - private boolean checkPopupPositionBounds() { - return checkPopupPositionBounds(screenWidth, screenHeight); - } - - /** - * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary - * that goes from (0, 0) to (boundaryWidth, boundaryHeight). - *

- * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed - * and {@code true} is returned to represent this change. - *

- * - * @param boundaryWidth width of the boundary - * @param boundaryHeight height of the boundary - * @return if the popup was out of bounds and have been moved back to it - */ - private boolean checkPopupPositionBounds(final float boundaryWidth, - final float boundaryHeight) { - if (DEBUG) { - Log.d(TAG, "checkPopupPositionBounds() called with: " - + "boundaryWidth = [" + boundaryWidth + "], " - + "boundaryHeight = [" + boundaryHeight + "]"); - } - - if (popupLayoutParams.x < 0) { - popupLayoutParams.x = 0; - return true; - } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) { - popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width); - return true; - } - - if (popupLayoutParams.y < 0) { - popupLayoutParams.y = 0; - return true; - } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) { - popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height); - return true; - } - - return false; - } - - private void savePositionAndSize() { - SharedPreferences sharedPreferences = PreferenceManager - .getDefaultSharedPreferences(PopupVideoPlayer.this); - sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); - sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); - sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); - } - - private float getMinimumVideoHeight(final float width) { - final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have -// if (DEBUG) { -// Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], " -// + "returned: " + height); -// } - return height; - } - - private void updateScreenSize() { - DisplayMetrics metrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getMetrics(metrics); - - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - if (DEBUG) { - Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", " - + "screenHeight = " + screenHeight); - } - - popupWidth = getResources().getDimension(R.dimen.popup_default_width); - popupHeight = getMinimumVideoHeight(popupWidth); - - minimumWidth = getResources().getDimension(R.dimen.popup_minimum_width); - minimumHeight = getMinimumVideoHeight(minimumWidth); - - maximumWidth = screenWidth; - maximumHeight = screenHeight; - } - - private void updatePopupSize(final int width, final int height) { - if (playerImpl == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "updatePopupSize() called with: " - + "width = [" + width + "], height = [" + height + "]"); - } - - final int actualWidth = (int) (width > maximumWidth ? maximumWidth - : width < minimumWidth ? minimumWidth : width); - - final int actualHeight; - if (height == -1) { - actualHeight = (int) getMinimumVideoHeight(width); - } else { - actualHeight = (int) (height > maximumHeight ? maximumHeight - : height < minimumHeight ? minimumHeight : height); - } - - popupLayoutParams.width = actualWidth; - popupLayoutParams.height = actualHeight; - popupWidth = actualWidth; - popupHeight = actualHeight; - - if (DEBUG) { - Log.d(TAG, "updatePopupSize() updated values: " - + "width = [" + actualWidth + "], height = [" + actualHeight + "]"); - } - windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); - } - - protected void setRepeatModeRemote(final RemoteViews remoteViews, final int repeatMode) { - final String methodName = "setImageResource"; - - if (remoteViews == null) { - return; - } - - switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - remoteViews.setInt(R.id.notificationRepeat, methodName, - R.drawable.exo_controls_repeat_off); - break; - case Player.REPEAT_MODE_ONE: - remoteViews.setInt(R.id.notificationRepeat, methodName, - R.drawable.exo_controls_repeat_one); - break; - case Player.REPEAT_MODE_ALL: - remoteViews.setInt(R.id.notificationRepeat, methodName, - R.drawable.exo_controls_repeat_all); - break; - } - } - - private void updateWindowFlags(final int flags) { - if (popupLayoutParams == null || windowManager == null || playerImpl == null) { - return; - } - - popupLayoutParams.flags = flags; - windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); - } - /////////////////////////////////////////////////////////////////////////// - - protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener { - private TextView resizingIndicator; - private ImageButton fullScreenButton; - private ImageView videoPlayPause; - - private View extraOptionsView; - private View closingOverlayView; - - VideoPlayerImpl(final Context context) { - super("VideoPlayerImpl" + PopupVideoPlayer.TAG, context); - } - - @Override - public void handleIntent(final Intent intent) { - super.handleIntent(intent); - - resetNotification(); - startForeground(NOTIFICATION_ID, notBuilder.build()); - } - - @Override - public void initViews(final View view) { - super.initViews(view); - resizingIndicator = view.findViewById(R.id.resizing_indicator); - fullScreenButton = view.findViewById(R.id.fullScreenButton); - fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked()); - videoPlayPause = view.findViewById(R.id.videoPlayPause); - - extraOptionsView = view.findViewById(R.id.extraOptionsView); - closingOverlayView = view.findViewById(R.id.closingOverlay); - view.addOnLayoutChangeListener(this); - } - - @Override - public void initListeners() { - super.initListeners(); - videoPlayPause.setOnClickListener(v -> onPlayPause()); - } - - @Override - protected void setupSubtitleView(@NonNull final SubtitleView view, final float captionScale, - @NonNull final CaptionStyleCompat captionStyle) { - float captionRatio = (captionScale - 1f) / 5f + 1f; - view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); - view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); - view.setStyle(captionStyle); - } - - @Override - public void onLayoutChange(final View view, final int left, final int top, final int right, - final int bottom, final int oldLeft, final int oldTop, - final int oldRight, final int oldBottom) { - float widthDp = Math.abs(right - left) / getResources().getDisplayMetrics().density; - final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP ? View.VISIBLE : View.GONE; - extraOptionsView.setVisibility(visibility); - } - - @Override - public void destroy() { - if (notRemoteView != null) { - notRemoteView.setImageViewBitmap(R.id.notificationCover, null); - } - super.destroy(); - } - - @Override - public void onFullScreenButtonClicked() { - super.onFullScreenButtonClicked(); - - if (DEBUG) { - Log.d(TAG, "onFullScreenButtonClicked() called"); - } - - setRecovery(); - final Intent intent = NavigationHelper.getPlayerIntent( - context, - MainVideoPlayer.class, - this.getPlayQueue(), - this.getRepeatMode(), - this.getPlaybackSpeed(), - this.getPlaybackPitch(), - this.getPlaybackSkipSilence(), - this.getPlaybackQuality(), - false, - !isPlaying(), - isMuted() - ); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - closePopup(); - } - - @Override - public void onDismiss(final PopupMenu menu) { - super.onDismiss(menu); - if (isPlaying()) { - hideControls(500, 0); - } - } - - @Override - protected int nextResizeMode(final int resizeMode) { - if (resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) { - return AspectRatioFrameLayout.RESIZE_MODE_FIT; - } else { - return AspectRatioFrameLayout.RESIZE_MODE_FILL; - } - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - super.onStopTrackingTouch(seekBar); - if (wasPlaying()) { - hideControls(100, 0); - } - } - - @Override - public void onShuffleClicked() { - super.onShuffleClicked(); - updatePlayback(); - } - - @Override - public void onMuteUnmuteButtonClicked() { - super.onMuteUnmuteButtonClicked(); - updatePlayback(); - } - - @Override - public void onUpdateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - updateProgress(currentProgress, duration, bufferPercent); - super.onUpdateProgress(currentProgress, duration, bufferPercent); - } - - @Override - protected VideoPlaybackResolver.QualityResolver getQualityResolver() { - return new VideoPlaybackResolver.QualityResolver() { - @Override - public int getDefaultResolutionIndex(final List sortedVideos) { - return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); - } - - @Override - public int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return ListHelper.getPopupResolutionIndex(context, sortedVideos, - playbackQuality); - } - }; - } - - /*////////////////////////////////////////////////////////////////////////// - // Thumbnail Loading - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onLoadingComplete(final String imageUri, final View view, - final Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - if (playerImpl == null) { - return; - } - // rebuild notification here since remote view does not release bitmaps, - // causing memory leaks - resetNotification(); - updateNotification(-1); - } - - @Override - public void onLoadingFailed(final String imageUri, final View view, - final FailReason failReason) { - super.onLoadingFailed(imageUri, view, failReason); - resetNotification(); - updateNotification(-1); - } - - @Override - public void onLoadingCancelled(final String imageUri, final View view) { - super.onLoadingCancelled(imageUri, view); - resetNotification(); - updateNotification(-1); - } - - /*////////////////////////////////////////////////////////////////////////// - // Activity Event Listener - //////////////////////////////////////////////////////////////////////////*/ - - /*package-private*/ void setActivityListener(final PlayerEventListener listener) { - activityListener = listener; - updateMetadata(); - updatePlayback(); - triggerProgressUpdate(); - } - - /*package-private*/ void removeActivityListener(final PlayerEventListener listener) { - if (activityListener == listener) { - activityListener = null; - } - } - - private void updateMetadata() { - if (activityListener != null && getCurrentMetadata() != null) { - activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata()); - } - } - - private void updatePlayback() { - if (activityListener != null && simpleExoPlayer != null && playQueue != null) { - activityListener.onPlaybackUpdate(currentState, getRepeatMode(), - playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); - } - } - - private void updateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - if (activityListener != null) { - activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); - } - } - - private void stopActivityBinding() { - if (activityListener != null) { - activityListener.onServiceStopped(); - activityListener = null; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Video Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onRepeatModeChanged(final int i) { - super.onRepeatModeChanged(i); - setRepeatModeRemote(notRemoteView, i); - updatePlayback(); - resetNotification(); - updateNotification(-1); - } - - @Override - public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { - super.onPlaybackParametersChanged(playbackParameters); - updatePlayback(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - resetNotification(); - updateNotification(-1); - updateMetadata(); - } - - @Override - public void onPlaybackShutdown() { - super.onPlaybackShutdown(); - closePopup(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Broadcast Receiver - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void setupBroadcastReceiver(final IntentFilter intentFltr) { - super.setupBroadcastReceiver(intentFltr); - if (DEBUG) { - Log.d(TAG, "setupBroadcastReceiver() called with: " - + "intentFilter = [" + intentFltr + "]"); - } - intentFltr.addAction(ACTION_CLOSE); - intentFltr.addAction(ACTION_PLAY_PAUSE); - intentFltr.addAction(ACTION_REPEAT); - - intentFltr.addAction(Intent.ACTION_SCREEN_ON); - intentFltr.addAction(Intent.ACTION_SCREEN_OFF); - } - - @Override - public void onBroadcastReceived(final Intent intent) { - super.onBroadcastReceived(intent); - if (intent == null || intent.getAction() == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); - } - switch (intent.getAction()) { - case ACTION_CLOSE: - closePopup(); - break; - case ACTION_PLAY_PAUSE: - onPlayPause(); - break; - case ACTION_REPEAT: - onRepeatClicked(); - break; - case Intent.ACTION_SCREEN_ON: - enableVideoRenderer(true); - break; - case Intent.ACTION_SCREEN_OFF: - enableVideoRenderer(false); - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // States - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void changeState(final int state) { - super.changeState(state); - updatePlayback(); - } - - @Override - public void onBlocked() { - super.onBlocked(); - resetNotification(); - updateNotification(R.drawable.exo_controls_play); - } - - @Override - public void onPlaying() { - super.onPlaying(); - - updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); - - resetNotification(); - updateNotification(R.drawable.exo_controls_pause); - - videoPlayPause.setBackgroundResource(R.drawable.exo_controls_pause); - hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - - startForeground(NOTIFICATION_ID, notBuilder.build()); - } - - @Override - public void onBuffering() { - super.onBuffering(); - resetNotification(); - updateNotification(R.drawable.exo_controls_play); - } - - @Override - public void onPaused() { - super.onPaused(); - - updateWindowFlags(IDLE_WINDOW_FLAGS); - - resetNotification(); - updateNotification(R.drawable.exo_controls_play); - videoPlayPause.setBackgroundResource(R.drawable.exo_controls_play); - - stopForeground(false); - } - - @Override - public void onPausedSeek() { - super.onPausedSeek(); - resetNotification(); - updateNotification(R.drawable.exo_controls_play); - - videoPlayPause.setBackgroundResource(R.drawable.exo_controls_play); - } - - @Override - public void onCompleted() { - super.onCompleted(); - - updateWindowFlags(IDLE_WINDOW_FLAGS); - - resetNotification(); - updateNotification(R.drawable.ic_replay_white_24dp); - videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white_24dp); - - stopForeground(false); - } - - @Override - public void showControlsThenHide() { - videoPlayPause.setVisibility(View.VISIBLE); - super.showControlsThenHide(); - } - - public void showControls(final long duration) { - videoPlayPause.setVisibility(View.VISIBLE); - super.showControls(duration); - } - - public void hideControls(final long duration, final long delay) { - super.hideControlsAndButton(duration, delay, videoPlayPause); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - /*package-private*/ void enableVideoRenderer(final boolean enable) { - final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO); - if (videoRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(videoRendererIndex, !enable)); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Getters - //////////////////////////////////////////////////////////////////////////*/ - - @SuppressWarnings("WeakerAccess") - public TextView getResizingIndicator() { - return resizingIndicator; - } - - public View getClosingOverlayView() { - return closingOverlayView; - } - } - - private class PopupWindowGestureListener extends GestureDetector.SimpleOnGestureListener - implements View.OnTouchListener { - private int initialPopupX; - private int initialPopupY; - private boolean isMoving; - private boolean isResizing; - - //initial co-ordinates and distance between fingers - private double initPointerDistance = -1; - private float initFirstPointerX = -1; - private float initFirstPointerY = -1; - private float initSecPointerX = -1; - private float initSecPointerY = -1; - - - @Override - public boolean onDoubleTap(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onDoubleTap() called with: e = [" + e + "], " - + "rawXy = " + e.getRawX() + ", " + e.getRawY() - + ", xy = " + e.getX() + ", " + e.getY()); - } - if (playerImpl == null || !playerImpl.isPlaying()) { - return false; - } - - playerImpl.hideControls(0, 0); - - if (e.getX() > popupWidth / 2) { - playerImpl.onFastForward(); - } else { - playerImpl.onFastRewind(); - } - - return true; - } - - @Override - public boolean onSingleTapConfirmed(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - } - if (playerImpl == null || playerImpl.getPlayer() == null) { - return false; - } - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(100, 100); - } else { - playerImpl.showControlsThenHide(); - - } - return true; - } - - @Override - public boolean onDown(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onDown() called with: e = [" + e + "]"); - } - - // 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). - checkPopupPositionBounds(closeOverlayView.getWidth(), closeOverlayView.getHeight()); - - initialPopupX = popupLayoutParams.x; - initialPopupY = popupLayoutParams.y; - popupWidth = popupLayoutParams.width; - popupHeight = popupLayoutParams.height; - return super.onDown(e); - } - - @Override - public void onLongPress(final MotionEvent e) { - if (DEBUG) { - Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); - } - updateScreenSize(); - checkPopupPositionBounds(); - updatePopupSize((int) screenWidth, -1); - } - - @Override - public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, - final float distanceX, final float distanceY) { - if (isResizing || playerImpl == null) { - return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); - } - - if (!isMoving) { - animateView(closeOverlayButton, true, 200); - } - - isMoving = true; - - float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()); - float posX = (int) (initialPopupX + diffX); - float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()); - float posY = (int) (initialPopupY + diffY); - - if (posX > (screenWidth - popupWidth)) { - posX = (int) (screenWidth - popupWidth); - } else if (posX < 0) { - posX = 0; - } - - if (posY > (screenHeight - popupHeight)) { - posY = (int) (screenHeight - popupHeight); - } else if (posY < 0) { - posY = 0; - } - - popupLayoutParams.x = (int) posX; - popupLayoutParams.y = (int) posY; - - final View closingOverlayView = playerImpl.getClosingOverlayView(); - if (isInsideClosingRadius(movingEvent)) { - if (closingOverlayView.getVisibility() == View.GONE) { - animateView(closingOverlayView, true, 250); - } - } else { - if (closingOverlayView.getVisibility() == View.VISIBLE) { - animateView(closingOverlayView, false, 0); - } - } - -// if (DEBUG) { -// Log.d(TAG, "PopupVideoPlayer.onScroll = " -// + "e1.getRaw = [" + initialEvent.getRawX() + ", " -// + initialEvent.getRawY() + "], " -// + "e1.getX,Y = [" + initialEvent.getX() + ", " -// + initialEvent.getY() + "], " -// + "e2.getRaw = [" + movingEvent.getRawX() + ", " -// + movingEvent.getRawY() + "], " -// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], " -// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], " -// + "posX,Y = [" + posX + ", " + posY + "], " -// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]"); -// } - windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); - return true; - } - - private void onScrollEnd(final MotionEvent event) { - if (DEBUG) { - Log.d(TAG, "onScrollEnd() called"); - } - if (playerImpl == null) { - return; - } - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - - if (isInsideClosingRadius(event)) { - closePopup(); - } else { - animateView(playerImpl.getClosingOverlayView(), false, 0); - - if (!isPopupClosing) { - animateView(closeOverlayButton, false, 200); - } - } - } - - @Override - public boolean onFling(final MotionEvent e1, final MotionEvent e2, - final float velocityX, final float velocityY) { - if (DEBUG) { - Log.d(TAG, "Fling velocity: dX=[" + velocityX + "], dY=[" + velocityY + "]"); - } - if (playerImpl == null) { - return false; - } - - final float absVelocityX = Math.abs(velocityX); - final float absVelocityY = Math.abs(velocityY); - if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { - if (absVelocityX > tossFlingVelocity) { - popupLayoutParams.x = (int) velocityX; - } - if (absVelocityY > tossFlingVelocity) { - popupLayoutParams.y = (int) velocityY; - } - checkPopupPositionBounds(); - windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams); - return true; - } - return false; - } - - @Override - public boolean onTouch(final View v, final MotionEvent event) { - popupGestureDetector.onTouchEvent(event); - if (playerImpl == null) { - return false; - } - if (event.getPointerCount() == 2 && !isMoving && !isResizing) { - if (DEBUG) { - Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); - } - playerImpl.showAndAnimateControl(-1, true); - playerImpl.getLoadingPanel().setVisibility(View.GONE); - - playerImpl.hideControls(0, 0); - animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); - animateView(playerImpl.getResizingIndicator(), true, 200, 0); - - //record co-ordinates of fingers - initFirstPointerX = event.getX(0); - initFirstPointerY = event.getY(0); - initSecPointerX = event.getX(1); - initSecPointerY = event.getY(1); - //record distance between fingers - initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX, - initFirstPointerY - initSecPointerY); - - isResizing = true; - } - - if (event.getAction() == MotionEvent.ACTION_MOVE && !isMoving && isResizing) { - if (DEBUG) { - Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], " - + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); - } - return handleMultiDrag(event); - } - - if (event.getAction() == MotionEvent.ACTION_UP) { - if (DEBUG) { - Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], " - + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); - } - if (isMoving) { - isMoving = false; - onScrollEnd(event); - } - - if (isResizing) { - isResizing = false; - - initPointerDistance = -1; - initFirstPointerX = -1; - initFirstPointerY = -1; - initSecPointerX = -1; - initSecPointerY = -1; - - animateView(playerImpl.getResizingIndicator(), false, 100, 0); - playerImpl.changeState(playerImpl.getCurrentState()); - } - - if (!isPopupClosing) { - savePositionAndSize(); - } - } - - v.performClick(); - return true; - } - - private boolean handleMultiDrag(final MotionEvent event) { - if (initPointerDistance != -1 && event.getPointerCount() == 2) { - // get the movements of the fingers - double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX, - event.getY(0) - initFirstPointerY); - double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX, - event.getY(1) - initSecPointerY); - - // minimum threshold beyond which pinch gesture will work - int minimumMove = ViewConfiguration.get(PopupVideoPlayer.this).getScaledTouchSlop(); - - if (Math.max(firstPointerMove, secPointerMove) > minimumMove) { - // calculate current distance between the pointers - double currentPointerDistance = - Math.hypot(event.getX(0) - event.getX(1), - event.getY(0) - event.getY(1)); - - // change co-ordinates of popup so the center stays at the same position - double newWidth = (popupWidth * currentPointerDistance / initPointerDistance); - initPointerDistance = currentPointerDistance; - popupLayoutParams.x += (popupWidth - newWidth) / 2; - - checkPopupPositionBounds(); - updateScreenSize(); - - updatePopupSize((int) Math.min(screenWidth, newWidth), -1); - return true; - } - } - return false; - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { - final int closeOverlayButtonX = closeOverlayButton.getLeft() - + closeOverlayButton.getWidth() / 2; - final int closeOverlayButtonY = closeOverlayButton.getTop() - + closeOverlayButton.getHeight() / 2; - - float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); - float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); - - return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) - + Math.pow(closeOverlayButtonY - fingerY, 2)); - } - - private float getClosingRadius() { - final int buttonRadius = closeOverlayButton.getWidth() / 2; - // 20% wider than the button itself - return buttonRadius * 1.2f; - } - - private boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) { - return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java deleted file mode 100644 index efb4176a6..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.schabi.newpipe.player; - -import android.content.Intent; -import android.view.MenuItem; - -import org.schabi.newpipe.R; - -import static org.schabi.newpipe.player.PopupVideoPlayer.ACTION_CLOSE; - -public final class PopupVideoPlayerActivity extends ServicePlayerActivity { - - private static final String TAG = "PopupVideoPlayerActivity"; - - @Override - public String getTag() { - return TAG; - } - - @Override - public String getSupportActionTitle() { - return getResources().getString(R.string.title_activity_popup_player); - } - - @Override - public Intent getBindIntent() { - return new Intent(this, PopupVideoPlayer.class); - } - - @Override - public void startPlayerListener() { - if (player != null && player instanceof PopupVideoPlayer.VideoPlayerImpl) { - ((PopupVideoPlayer.VideoPlayerImpl) player).setActivityListener(this); - } - } - - @Override - public void stopPlayerListener() { - if (player != null && player instanceof PopupVideoPlayer.VideoPlayerImpl) { - ((PopupVideoPlayer.VideoPlayerImpl) player).removeActivityListener(this); - } - } - - @Override - public int getPlayerOptionMenuResource() { - return R.menu.menu_play_queue_popup; - } - - @Override - public boolean onPlayerOptionSelected(final MenuItem item) { - if (item.getItemId() == R.id.action_switch_background) { - this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); - getApplicationContext().startService( - getSwitchIntent(BackgroundPlayer.class) - .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()) - ); - return true; - } - return false; - } - - @Override - public Intent getPlayerShutdownIntent() { - return new Intent(ACTION_CLOSE); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 72becef8f..0ffd7f594 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -27,17 +27,21 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; +import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; +import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -110,7 +114,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity public abstract boolean onPlayerOptionSelected(MenuItem item); - public abstract Intent getPlayerShutdownIntent(); + public abstract void setupMenu(Menu m); //////////////////////////////////////////////////////////////////////////// // Activity Lifecycle //////////////////////////////////////////////////////////////////////////// @@ -152,6 +156,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return true; } + // Allow to setup visibility of menuItems + @Override + public boolean onPrepareOptionsMenu(final Menu m) { + setupMenu(m); + return super.onPrepareOptionsMenu(m); + } + @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { @@ -175,11 +186,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return true; case R.id.action_switch_main: this.player.setRecovery(); - getApplicationContext().sendBroadcast(getPlayerShutdownIntent()); getApplicationContext().startActivity( - getSwitchIntent(MainVideoPlayer.class) - .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()) - ); + getSwitchIntent(MainActivity.class, MainPlayer.PlayerType.VIDEO) + .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())); return true; } return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item); @@ -191,13 +200,22 @@ public abstract class ServicePlayerActivity extends AppCompatActivity unbind(); } - protected Intent getSwitchIntent(final Class clazz) { + protected Intent getSwitchIntent(final Class clazz, final MainPlayer.PlayerType playerType) { return NavigationHelper.getPlayerIntent(getApplicationContext(), clazz, this.player.getPlayQueue(), this.player.getRepeatMode(), this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(), - this.player.getPlaybackSkipSilence(), null, false, false, this.player.isMuted()) + this.player.getPlaybackSkipSilence(), + null, + true, + !this.player.isPlaying(), + this.player.isMuted()) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()); + .putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM) + .putExtra(Constants.KEY_URL, this.player.getVideoUrl()) + .putExtra(Constants.KEY_TITLE, this.player.getVideoTitle()) + .putExtra(Constants.KEY_SERVICE_ID, + this.player.getCurrentMetadata().getMetadata().getServiceId()) + .putExtra(VideoPlayer.PLAYER_TYPE, playerType); } //////////////////////////////////////////////////////////////////////////// @@ -247,6 +265,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (service instanceof PlayerServiceBinder) { player = ((PlayerServiceBinder) service).getPlayerInstance(); + } else if (service instanceof MainPlayer.LocalBinder) { + player = ((MainPlayer.LocalBinder) service).getPlayer(); } if (player == null || player.getPlayQueue() == null @@ -500,7 +520,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return; } PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), - player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag()); + player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag()); } @Override @@ -560,7 +580,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// private void shareUrl(final String subject, final String url) { - Intent intent = new Intent(Intent.ACTION_SEND); + final Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, subject); intent.putExtra(Intent.EXTRA_TEXT, url); @@ -571,6 +591,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity // Binding Service Listener //////////////////////////////////////////////////////////////////////////// + @Override + public void onQueueUpdate(final PlayQueue queue) { + } + @Override public void onPlaybackUpdate(final int state, final int repeatMode, final boolean shuffled, final PlaybackParameters parameters) { @@ -610,7 +634,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } @Override - public void onMetadataUpdate(final StreamInfo info) { + public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { if (info != null) { metadataTitle.setText(info.getName()); metadataArtist.setText(info.getUploaderName()); @@ -710,7 +734,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void onMaybeMuteChanged() { if (menu != null && player != null) { - MenuItem item = menu.findItem(R.id.action_mute); + final MenuItem item = menu.findItem(R.id.action_mute); //Change the mute-button item in ActionBar //1) Text change: diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 576d42a00..a5a53f7a9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -30,20 +30,21 @@ import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.os.Build; import android.os.Handler; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; + import android.view.Menu; import android.view.MenuItem; -import android.view.SurfaceView; import android.view.View; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; @@ -69,6 +70,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.resolver.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.views.ExpandableSurfaceView; import java.util.ArrayList; import java.util.List; @@ -117,8 +119,7 @@ public abstract class VideoPlayer extends BasePlayer private View rootView; - private AspectRatioFrameLayout aspectRatioFrameLayout; - private SurfaceView surfaceView; + private ExpandableSurfaceView surfaceView; private View surfaceForeground; private View loadingPanel; @@ -135,7 +136,7 @@ public abstract class VideoPlayer extends BasePlayer private TextView playbackLiveSync; private TextView playbackSpeedTextView; - private View topControlsRoot; + private LinearLayout topControlsRoot; private TextView qualityTextView; private SubtitleView subtitleView; @@ -167,7 +168,7 @@ public abstract class VideoPlayer extends BasePlayer // workaround to match normalized captions like english to English or deutsch to Deutsch private static boolean containsCaseInsensitive(final List list, final String toFind) { - for (String i : list) { + for (final String i : list) { if (i.equalsIgnoreCase(toFind)) { return true; } @@ -182,7 +183,6 @@ public abstract class VideoPlayer extends BasePlayer public void initViews(final View view) { this.rootView = view; - this.aspectRatioFrameLayout = view.findViewById(R.id.aspectRatioLayout); this.surfaceView = view.findViewById(R.id.surfaceView); this.surfaceForeground = view.findViewById(R.id.surfaceForeground); this.loadingPanel = view.findViewById(R.id.loading_panel); @@ -207,24 +207,22 @@ public abstract class VideoPlayer extends BasePlayer this.resizeView = view.findViewById(R.id.resizeTextView); resizeView.setText(PlayerHelper - .resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode())); + .resizeTypeOf(context, getSurfaceView().getResizeMode())); this.captionTextView = view.findViewById(R.id.captionTextView); - //this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); - } - this.playbackSeekBar.getProgressDrawable(). - setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); + playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + this.playbackSeekBar.getProgressDrawable() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)); this.qualityPopupMenu = new PopupMenu(context, qualityTextView); this.playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeedTextView); this.captionPopupMenu = new PopupMenu(context, captionTextView); ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)) - .getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); + .getIndeterminateDrawable() + .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY)); } protected abstract void setupSubtitleView(@NonNull SubtitleView view, float captionScale, @@ -252,7 +250,7 @@ public abstract class VideoPlayer extends BasePlayer simpleExoPlayer.addTextOutput(cues -> subtitleView.onCues(cues)); // Setup audio session with onboard equalizer - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { trackSelector.setParameters(trackSelector.buildUponParameters() .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); } @@ -282,7 +280,7 @@ public abstract class VideoPlayer extends BasePlayer qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); for (int i = 0; i < availableStreams.size(); i++) { - VideoStream videoStream = availableStreams.get(i); + final VideoStream videoStream = availableStreams.get(i); qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat .getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); } @@ -314,7 +312,7 @@ public abstract class VideoPlayer extends BasePlayer } captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); - String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context) + final String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context) .getString(context.getString(R.string.caption_user_set_key), null); /* * only search for autogenerated cc as fallback @@ -326,7 +324,7 @@ public abstract class VideoPlayer extends BasePlayer && !userPreferredLanguage.contains("("); // Add option for turning off caption - MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, + final MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, 0, Menu.NONE, R.string.caption_none); captionOffItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); @@ -342,7 +340,7 @@ public abstract class VideoPlayer extends BasePlayer // Add all available captions for (int i = 0; i < availableLanguages.size(); i++) { final String captionLanguage = availableLanguages.get(i); - MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, + final MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, i + 1, Menu.NONE, captionLanguage); captionItem.setOnMenuItemClickListener(menuItem -> { final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); @@ -459,11 +457,8 @@ public abstract class VideoPlayer extends BasePlayer animateView(controlsRoot, false, DEFAULT_CONTROLS_DURATION); playbackSeekBar.setEnabled(false); - // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, - // so sets the color again - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); - } + playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); loadingPanel.setBackgroundColor(Color.BLACK); animateView(loadingPanel, true, 0); @@ -479,11 +474,8 @@ public abstract class VideoPlayer extends BasePlayer showAndAnimateControl(-1, true); playbackSeekBar.setEnabled(true); - // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, - // so sets the color again - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); - } + playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); loadingPanel.setVisibility(View.GONE); @@ -520,7 +512,6 @@ public abstract class VideoPlayer extends BasePlayer super.onCompleted(); showControls(500); - animateView(endScreen, true, 800); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); loadingPanel.setVisibility(View.GONE); @@ -555,7 +546,7 @@ public abstract class VideoPlayer extends BasePlayer + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], " + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); } - aspectRatioFrameLayout.setAspectRatio(((float) width) / height); + getSurfaceView().setAspectRatio(((float) width) / height); } @Override @@ -583,7 +574,7 @@ public abstract class VideoPlayer extends BasePlayer .getTrackGroups(textRenderer); // Extract all loaded languages - List availableLanguages = new ArrayList<>(textTracks.length); + final List availableLanguages = new ArrayList<>(textTracks.length); for (int i = 0; i < textTracks.length; i++) { final TrackGroup textTrack = textTracks.get(i); if (textTrack.length > 0 && textTrack.getFormat(0) != null) { @@ -620,12 +611,6 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed())); super.onPrepared(playWhenReady); - - if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) { - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler - .postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION); - } } @Override @@ -675,7 +660,7 @@ public abstract class VideoPlayer extends BasePlayer } } - protected void onFullScreenButtonClicked() { + protected void toggleFullscreen() { changeState(STATE_BLOCKED); } @@ -739,8 +724,8 @@ public abstract class VideoPlayer extends BasePlayer qualityTextView.setText(menuItem.getTitle()); return true; } else if (playbackSpeedPopupMenuGroupId == menuItem.getGroupId()) { - int speedIndex = menuItem.getItemId(); - float speed = PLAYBACK_SPEEDS[speedIndex]; + final int speedIndex = menuItem.getItemId(); + final float speed = PLAYBACK_SPEEDS[speedIndex]; setPlaybackSpeed(speed); playbackSpeedTextView.setText(formatSpeed(speed)); @@ -799,16 +784,16 @@ public abstract class VideoPlayer extends BasePlayer showControls(DEFAULT_CONTROLS_DURATION); } - private void onResizeClicked() { - if (getAspectRatioFrameLayout() != null) { - final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode(); + void onResizeClicked() { + if (getSurfaceView() != null) { + final int currentResizeMode = getSurfaceView().getResizeMode(); final int newResizeMode = nextResizeMode(currentResizeMode); setResizeMode(newResizeMode); } } protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { - getAspectRatioFrameLayout().setResizeMode(resizeMode); + getSurfaceView().setResizeMode(resizeMode); getResizeView().setText(PlayerHelper.resizeTypeOf(context, resizeMode)); } @@ -916,9 +901,9 @@ public abstract class VideoPlayer extends BasePlayer if (drawableId == -1) { if (controlAnimationView.getVisibility() == View.VISIBLE) { controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, - PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), - PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), - PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) + PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f), + PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f), + PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f) ).setDuration(DEFAULT_CONTROLS_DURATION); controlViewAnimator.addListener(new AnimatorListenerAdapter() { @Override @@ -931,10 +916,10 @@ public abstract class VideoPlayer extends BasePlayer return; } - float scaleFrom = goneOnEnd ? 1f : 1f; - float scaleTo = goneOnEnd ? 1.8f : 1.4f; - float alphaFrom = goneOnEnd ? 1f : 0f; - float alphaTo = goneOnEnd ? 0f : 1f; + final float scaleFrom = goneOnEnd ? 1f : 1f; + final float scaleTo = goneOnEnd ? 1.8f : 1.4f; + final float alphaFrom = goneOnEnd ? 1f : 0f; + final float alphaTo = goneOnEnd ? 0f : 1f; controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, @@ -1020,6 +1005,9 @@ public abstract class VideoPlayer extends BasePlayer animateView(controlsRoot, false, duration); }; } + + public abstract void hideSystemUIIfNeeded(); + /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ @@ -1033,11 +1021,7 @@ public abstract class VideoPlayer extends BasePlayer this.resolver.setPlaybackQuality(quality); } - public AspectRatioFrameLayout getAspectRatioFrameLayout() { - return aspectRatioFrameLayout; - } - - public SurfaceView getSurfaceView() { + public ExpandableSurfaceView getSurfaceView() { return surfaceView; } @@ -1096,7 +1080,7 @@ public abstract class VideoPlayer extends BasePlayer return playbackEndTime; } - public View getTopControlsRoot() { + public LinearLayout getTopControlsRoot() { return topControlsRoot; } @@ -1108,6 +1092,10 @@ public abstract class VideoPlayer extends BasePlayer return qualityPopupMenu; } + public TextView getPlaybackSpeedTextView() { + return playbackSpeedTextView; + } + public PopupMenu getPlaybackSpeedPopupMenu() { return playbackSpeedPopupMenu; } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java new file mode 100644 index 000000000..a5758301c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -0,0 +1,2184 @@ +/* + * Copyright 2017 Mauricio Colli + * Part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.player; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import androidx.preference.PreferenceManager; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Display; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.AnticipateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.nostra13.universalimageloader.core.assist.FailReason; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; +import org.schabi.newpipe.player.event.PlayerEventListener; +import org.schabi.newpipe.player.event.PlayerGestureListener; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; +import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; +import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.KoreUtil; +import org.schabi.newpipe.util.ListHelper; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ShareUtils; + +import java.util.List; + +import static android.content.Context.WINDOW_SERVICE; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +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_OPEN_CONTROLS; +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.NOTIFICATION_ID; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; +import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; +import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; +import static org.schabi.newpipe.util.AnimationUtils.animateRotation; +import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; +import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; + +/** + * Unified UI for all players. + * + * @author mauriciocolli + */ + +public class VideoPlayerImpl extends VideoPlayer + implements View.OnLayoutChangeListener, + PlaybackParameterDialog.Callback, + View.OnLongClickListener { + private static final String TAG = ".VideoPlayerImpl"; + + static final String POPUP_SAVED_WIDTH = "popup_saved_width"; + static final String POPUP_SAVED_X = "popup_saved_x"; + static final String POPUP_SAVED_Y = "popup_saved_y"; + private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300; + private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + + private static final float MAX_GESTURE_LENGTH = 0.75f; + private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60; + + private TextView titleTextView; + private TextView channelTextView; + private RelativeLayout volumeRelativeLayout; + private ProgressBar volumeProgressBar; + private ImageView volumeImageView; + private RelativeLayout brightnessRelativeLayout; + private ProgressBar brightnessProgressBar; + private ImageView brightnessImageView; + private TextView resizingIndicator; + private ImageButton queueButton; + private ImageButton repeatButton; + private ImageButton shuffleButton; + private ImageButton playWithKodi; + private ImageButton openInBrowser; + private ImageButton fullscreenButton; + private ImageButton playerCloseButton; + private ImageButton screenRotationButton; + private ImageButton muteButton; + + private ImageButton playPauseButton; + private ImageButton playPreviousButton; + private ImageButton playNextButton; + + private RelativeLayout queueLayout; + private ImageButton itemsListCloseButton; + private RecyclerView itemsList; + private ItemTouchHelper itemTouchHelper; + + private boolean queueVisible; + private MainPlayer.PlayerType playerType = MainPlayer.PlayerType.VIDEO; + + private ImageButton moreOptionsButton; + private ImageButton shareButton; + + private View primaryControls; + private View secondaryControls; + + private int maxGestureLength; + + private boolean audioOnly = false; + private boolean isFullscreen = false; + private boolean isVerticalVideo = false; + private boolean fragmentIsVisible = false; + boolean shouldUpdateOnProgress; + int timesNotificationUpdated; + + private final MainPlayer service; + private PlayerServiceEventListener fragmentListener; + private PlayerEventListener activityListener; + private GestureDetector gestureDetector; + private final SharedPreferences defaultPreferences; + private ContentObserver settingsContentObserver; + @NonNull + private final AudioPlaybackResolver resolver; + + private int cachedDuration; + private String cachedDurationString; + + // Popup + private WindowManager.LayoutParams popupLayoutParams; + public WindowManager windowManager; + + private View closingOverlayView; + private View closeOverlayView; + private FloatingActionButton closeOverlayButton; + + public boolean isPopupClosing = false; + + private float screenWidth; + private float screenHeight; + private float popupWidth; + private float popupHeight; + private float minimumWidth; + private float minimumHeight; + private float maximumWidth; + private float maximumHeight; + // Popup end + + + @Override + public void handleIntent(final Intent intent) { + if (intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) == null) { + return; + } + + final MainPlayer.PlayerType oldPlayerType = playerType; + choosePlayerTypeFromIntent(intent); + audioOnly = audioPlayerSelected(); + + // We need to setup audioOnly before super(), see "sourceOf" + super.handleIntent(intent); + + if (oldPlayerType != playerType && playQueue != null) { + // If playerType changes from one to another we should reload the player + // (to disable/enable video stream or to set quality) + setRecovery(); + reload(); + } + + setupElementsVisibility(); + setupElementsSize(); + + if (audioPlayerSelected()) { + service.removeViewFromParent(); + } else if (popupPlayerSelected()) { + getRootView().setVisibility(View.VISIBLE); + initPopup(); + initPopupCloseOverlay(); + playPauseButton.requestFocus(); + } else { + getRootView().setVisibility(View.VISIBLE); + initVideoPlayer(); + // Android TV: without it focus will frame the whole player + playPauseButton.requestFocus(); + } + + onPlay(); + } + + VideoPlayerImpl(final MainPlayer service) { + super("MainPlayer" + TAG, service); + this.service = service; + this.shouldUpdateOnProgress = true; + this.windowManager = (WindowManager) service.getSystemService(WINDOW_SERVICE); + this.defaultPreferences = PreferenceManager.getDefaultSharedPreferences(service); + this.resolver = new AudioPlaybackResolver(context, dataSource); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public void initViews(final View view) { + super.initViews(view); + this.titleTextView = view.findViewById(R.id.titleTextView); + this.channelTextView = view.findViewById(R.id.channelTextView); + this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout); + this.volumeProgressBar = view.findViewById(R.id.volumeProgressBar); + this.volumeImageView = view.findViewById(R.id.volumeImageView); + this.brightnessRelativeLayout = view.findViewById(R.id.brightnessRelativeLayout); + this.brightnessProgressBar = view.findViewById(R.id.brightnessProgressBar); + this.brightnessImageView = view.findViewById(R.id.brightnessImageView); + this.resizingIndicator = view.findViewById(R.id.resizing_indicator); + this.queueButton = view.findViewById(R.id.queueButton); + this.repeatButton = view.findViewById(R.id.repeatButton); + this.shuffleButton = view.findViewById(R.id.shuffleButton); + this.playWithKodi = view.findViewById(R.id.playWithKodi); + this.openInBrowser = view.findViewById(R.id.openInBrowser); + this.fullscreenButton = view.findViewById(R.id.fullScreenButton); + this.screenRotationButton = view.findViewById(R.id.screenRotationButton); + this.playerCloseButton = view.findViewById(R.id.playerCloseButton); + this.muteButton = view.findViewById(R.id.switchMute); + + this.playPauseButton = view.findViewById(R.id.playPauseButton); + this.playPreviousButton = view.findViewById(R.id.playPreviousButton); + this.playNextButton = view.findViewById(R.id.playNextButton); + + this.moreOptionsButton = view.findViewById(R.id.moreOptionsButton); + this.primaryControls = view.findViewById(R.id.primaryControls); + this.secondaryControls = view.findViewById(R.id.secondaryControls); + this.shareButton = view.findViewById(R.id.share); + + this.queueLayout = view.findViewById(R.id.playQueuePanel); + this.itemsListCloseButton = view.findViewById(R.id.playQueueClose); + this.itemsList = view.findViewById(R.id.playQueue); + + closingOverlayView = view.findViewById(R.id.closingOverlay); + + titleTextView.setSelected(true); + channelTextView.setSelected(true); + } + + @Override + protected void setupSubtitleView(final @NonNull SubtitleView view, + final float captionScale, + @NonNull final CaptionStyleCompat captionStyle) { + if (popupPlayerSelected()) { + final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f; + view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); + } else { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + final float captionRatioInverse = 20f + 4f * (1.0f - captionScale); + view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, + (float) minimumLength / captionRatioInverse); + } + view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); + view.setStyle(captionStyle); + } + + /** + * This method ensures that popup and main players have different look. + * We use one layout for both players and need to decide what to show and what to hide. + * Additional measuring should be done inside {@link #setupElementsSize}. + * {@link #setControlsSize} is used to adapt the UI to fullscreen mode, multiWindow, navBar, etc + */ + private void setupElementsVisibility() { + if (popupPlayerSelected()) { + fullscreenButton.setVisibility(View.VISIBLE); + screenRotationButton.setVisibility(View.GONE); + getResizeView().setVisibility(View.GONE); + getRootView().findViewById(R.id.metadataView).setVisibility(View.GONE); + queueButton.setVisibility(View.GONE); + moreOptionsButton.setVisibility(View.GONE); + getTopControlsRoot().setOrientation(LinearLayout.HORIZONTAL); + primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; + secondaryControls.setAlpha(1.0f); + secondaryControls.setVisibility(View.VISIBLE); + secondaryControls.setTranslationY(0); + shareButton.setVisibility(View.GONE); + playWithKodi.setVisibility(View.GONE); + openInBrowser.setVisibility(View.GONE); + muteButton.setVisibility(View.GONE); + playerCloseButton.setVisibility(View.GONE); + getTopControlsRoot().bringToFront(); + getTopControlsRoot().setClickable(false); + getTopControlsRoot().setFocusable(false); + getBottomControlsRoot().bringToFront(); + onQueueClosed(); + } else { + fullscreenButton.setVisibility(View.GONE); + setupScreenRotationButton(); + getResizeView().setVisibility(View.VISIBLE); + getRootView().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); + moreOptionsButton.setVisibility(View.VISIBLE); + getTopControlsRoot().setOrientation(LinearLayout.VERTICAL); + primaryControls.getLayoutParams().width = LinearLayout.LayoutParams.MATCH_PARENT; + secondaryControls.setVisibility(View.INVISIBLE); + moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(service, + R.drawable.ic_expand_more_white_24dp)); + shareButton.setVisibility(View.VISIBLE); + showHideKodiButton(); + openInBrowser.setVisibility(View.VISIBLE); + muteButton.setVisibility(View.VISIBLE); + playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); + // Top controls have a large minHeight which is allows to drag the player + // down in fullscreen mode (just larger area to make easy to locate by finger) + getTopControlsRoot().setClickable(true); + getTopControlsRoot().setFocusable(true); + } + if (!isFullscreen()) { + titleTextView.setVisibility(View.GONE); + channelTextView.setVisibility(View.GONE); + } else { + titleTextView.setVisibility(View.VISIBLE); + channelTextView.setVisibility(View.VISIBLE); + } + setMuteButton(muteButton, isMuted()); + + animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0); + } + + /** + * Changes padding, size of elements based on player selected right now. + * Popup player has small padding in comparison with the main player + */ + private void setupElementsSize() { + if (popupPlayerSelected()) { + final int controlsPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_popup_controls_padding); + final int buttonsPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_popup_buttons_padding); + getTopControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, 0); + getBottomControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, 0); + getQualityTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getPlaybackSpeedTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getCaptionTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getPlaybackSpeedTextView().setMinimumWidth(0); + } else if (videoPlayerSelected()) { + final int buttonsMinWidth = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_buttons_min_width); + final int playerTopPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_top_padding); + final int controlsPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_controls_padding); + final int buttonsPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_buttons_padding); + getTopControlsRoot().setPaddingRelative( + controlsPadding, playerTopPadding, controlsPadding, 0); + getBottomControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, 0); + getQualityTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getPlaybackSpeedTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + getPlaybackSpeedTextView().setMinimumWidth(buttonsMinWidth); + getCaptionTextView().setPadding( + buttonsPadding, buttonsPadding, buttonsPadding, buttonsPadding); + } + } + + @Override + public void initListeners() { + super.initListeners(); + + final PlayerGestureListener listener = new PlayerGestureListener(this, service); + gestureDetector = new GestureDetector(context, listener); + getRootView().setOnTouchListener(listener); + + queueButton.setOnClickListener(this); + repeatButton.setOnClickListener(this); + shuffleButton.setOnClickListener(this); + + playPauseButton.setOnClickListener(this); + playPreviousButton.setOnClickListener(this); + playNextButton.setOnClickListener(this); + + moreOptionsButton.setOnClickListener(this); + moreOptionsButton.setOnLongClickListener(this); + shareButton.setOnClickListener(this); + fullscreenButton.setOnClickListener(this); + screenRotationButton.setOnClickListener(this); + playWithKodi.setOnClickListener(this); + openInBrowser.setOnClickListener(this); + playerCloseButton.setOnClickListener(this); + muteButton.setOnClickListener(this); + + settingsContentObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(final boolean selfChange) { + setupScreenRotationButton(); + } + }; + service.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, + settingsContentObserver); + getRootView().addOnLayoutChangeListener(this); + } + + public boolean onKeyDown(final int keyCode) { + switch (keyCode) { + default: + break; + case KeyEvent.KEYCODE_BACK: + if (DeviceUtils.isTv(service) && isControlsVisible()) { + hideControls(0, 0); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (getRootView().hasFocus() && !getControlsRoot().hasFocus()) { + // do not interfere with focus in playlist etc. + return false; + } + + if (getCurrentState() == BasePlayer.STATE_BLOCKED) { + return true; + } + + if (!isControlsVisible()) { + if (!queueVisible) { + playPauseButton.requestFocus(); + } + showControlsThenHide(); + showSystemUIPartially(); + return true; + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); + } + break; + } + + return false; + } + + public AppCompatActivity getParentActivity() { + // ! instanceof ViewGroup means that view was added via windowManager for Popup + if (getRootView() == null + || getRootView().getParent() == null + || !(getRootView().getParent() instanceof ViewGroup)) { + return null; + } + + final ViewGroup parent = (ViewGroup) getRootView().getParent(); + return (AppCompatActivity) parent.getContext(); + } + + /*////////////////////////////////////////////////////////////////////////// + // View + //////////////////////////////////////////////////////////////////////////*/ + + private void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + imageButton.setImageResource(R.drawable.exo_controls_repeat_off); + break; + case Player.REPEAT_MODE_ONE: + imageButton.setImageResource(R.drawable.exo_controls_repeat_one); + break; + case Player.REPEAT_MODE_ALL: + imageButton.setImageResource(R.drawable.exo_controls_repeat_all); + break; + } + } + + private void setShuffleButton(final ImageButton button, final boolean shuffled) { + final int shuffleAlpha = shuffled ? 255 : 77; + button.setImageAlpha(shuffleAlpha); + } + + //////////////////////////////////////////////////////////////////////////// + // Playback Parameters Listener + //////////////////////////////////////////////////////////////////////////// + + @Override + public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, + final boolean playbackSkipSilence) { + setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); + } + + @Override + public void onVideoSizeChanged(final int width, final int height, + final int unappliedRotationDegrees, + final float pixelWidthHeightRatio) { + super.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + isVerticalVideo = width < height; + prepareOrientation(); + setupScreenRotationButton(); + } + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Video Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onRepeatModeChanged(final int i) { + super.onRepeatModeChanged(i); + updatePlaybackButtons(); + updatePlayback(); + service.resetNotification(); + service.updateNotification(-1); + } + + @Override + public void onShuffleClicked() { + super.onShuffleClicked(); + updatePlaybackButtons(); + updatePlayback(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Playback Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onPlayerError(final ExoPlaybackException error) { + super.onPlayerError(error); + + if (fragmentListener != null) { + fragmentListener.onPlayerError(error); + } + } + + protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { + super.onMetadataChanged(tag); + + showHideKodiButton(); + + titleTextView.setText(tag.getMetadata().getName()); + channelTextView.setText(tag.getMetadata().getUploaderName()); + + service.resetNotification(); + service.updateNotification(-1); + updateMetadata(); + } + + @Override + public void onPlaybackShutdown() { + if (DEBUG) { + Log.d(TAG, "onPlaybackShutdown() called"); + } + service.onDestroy(); + } + + @Override + public void onMuteUnmuteButtonClicked() { + super.onMuteUnmuteButtonClicked(); + updatePlayback(); + setMuteButton(muteButton, isMuted()); + } + + @Override + public void onUpdateProgress(final int currentProgress, + final int duration, final int bufferPercent) { + super.onUpdateProgress(currentProgress, duration, bufferPercent); + + updateProgress(currentProgress, duration, bufferPercent); + + if (!shouldUpdateOnProgress || getCurrentState() == BasePlayer.STATE_COMPLETED + || getCurrentState() == BasePlayer.STATE_PAUSED || getPlayQueue() == null) { + return; + } + + if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) { + service.resetNotification(); + } + + if (service.getBigNotRemoteView() != null) { + if (cachedDuration != duration) { + cachedDuration = duration; + cachedDurationString = getTimeString(duration); + } + service.getBigNotRemoteView() + .setProgressBar(R.id.notificationProgressBar, + duration, currentProgress, false); + service.getBigNotRemoteView() + .setTextViewText(R.id.notificationTime, + getTimeString(currentProgress) + " / " + cachedDurationString); + } + if (service.getNotRemoteView() != null) { + service.getNotRemoteView() + .setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); + } + service.updateNotification(-1); + } + + @Override + @Nullable + public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { + // For LiveStream or video/popup players we can use super() method + // but not for audio player + if (!audioOnly) { + return super.sourceOf(item, info); + } else { + return resolver.resolve(info); + } + } + + @Override + public void onPlayPrevious() { + super.onPlayPrevious(); + triggerProgressUpdate(); + } + + @Override + public void onPlayNext() { + super.onPlayNext(); + triggerProgressUpdate(); + } + + @Override + protected void initPlayback(@NonNull final PlayQueue queue, final int repeatMode, + final float playbackSpeed, final float playbackPitch, + final boolean playbackSkipSilence, + final boolean playOnReady, final boolean isMuted) { + super.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, playOnReady, isMuted); + updateQueue(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Player Overrides + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void toggleFullscreen() { + if (DEBUG) { + Log.d(TAG, "toggleFullscreen() called"); + } + if (simpleExoPlayer == null || getCurrentMetadata() == null) { + return; + } + + if (popupPlayerSelected()) { + setRecovery(); + service.removeViewFromParent(); + final Intent intent = NavigationHelper.getPlayerIntent( + service, + MainActivity.class, + this.getPlayQueue(), + this.getRepeatMode(), + this.getPlaybackSpeed(), + this.getPlaybackPitch(), + this.getPlaybackSkipSilence(), + null, + true, + !isPlaying(), + isMuted() + ); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Constants.KEY_SERVICE_ID, + getCurrentMetadata().getMetadata().getServiceId()); + intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); + intent.putExtra(Constants.KEY_URL, getVideoUrl()); + intent.putExtra(Constants.KEY_TITLE, getVideoTitle()); + intent.putExtra(VideoDetailFragment.AUTO_PLAY, true); + service.onDestroy(); + context.startActivity(intent); + return; + } else { + if (fragmentListener == null) { + return; + } + + isFullscreen = !isFullscreen; + setControlsSize(); + fragmentListener.onFullscreenStateChanged(isFullscreen()); + } + + if (!isFullscreen()) { + titleTextView.setVisibility(View.GONE); + channelTextView.setVisibility(View.GONE); + playerCloseButton.setVisibility(videoPlayerSelected() ? View.VISIBLE : View.GONE); + } else { + titleTextView.setVisibility(View.VISIBLE); + channelTextView.setVisibility(View.VISIBLE); + playerCloseButton.setVisibility(View.GONE); + } + setupScreenRotationButton(); + } + + @Override + public void onClick(final View v) { + super.onClick(v); + if (v.getId() == playPauseButton.getId()) { + onPlayPause(); + } else if (v.getId() == playPreviousButton.getId()) { + onPlayPrevious(); + } else if (v.getId() == playNextButton.getId()) { + onPlayNext(); + } else if (v.getId() == queueButton.getId()) { + onQueueClicked(); + return; + } else if (v.getId() == repeatButton.getId()) { + onRepeatClicked(); + return; + } else if (v.getId() == shuffleButton.getId()) { + onShuffleClicked(); + return; + } else if (v.getId() == moreOptionsButton.getId()) { + onMoreOptionsClicked(); + } else if (v.getId() == shareButton.getId()) { + onShareClicked(); + } else if (v.getId() == playWithKodi.getId()) { + onPlayWithKodiClicked(); + } else if (v.getId() == openInBrowser.getId()) { + onOpenInBrowserClicked(); + } else if (v.getId() == fullscreenButton.getId()) { + toggleFullscreen(); + } else if (v.getId() == screenRotationButton.getId()) { + if (!isVerticalVideo) { + fragmentListener.onScreenRotationButtonClicked(); + } else { + toggleFullscreen(); + } + } else if (v.getId() == muteButton.getId()) { + onMuteUnmuteButtonClicked(); + } else if (v.getId() == playerCloseButton.getId()) { + service.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER)); + } + + if (getCurrentState() != STATE_COMPLETED) { + getControlsVisibilityHandler().removeCallbacksAndMessages(null); + animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { + if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { + if (v.getId() == playPauseButton.getId()) { + hideControls(0, 0); + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } + }); + } + } + + @Override + public boolean onLongClick(final View v) { + if (v.getId() == moreOptionsButton.getId() && isFullscreen()) { + fragmentListener.onMoreOptionsLongClicked(); + hideControls(0, 0); + hideSystemUIIfNeeded(); + } + return true; + } + + private void onQueueClicked() { + queueVisible = true; + + hideSystemUIIfNeeded(); + buildQueue(); + updatePlaybackButtons(); + + getControlsRoot().setVisibility(View.INVISIBLE); + queueLayout.requestFocus(); + animateView(queueLayout, SLIDE_AND_ALPHA, true, + DEFAULT_CONTROLS_DURATION); + + itemsList.scrollToPosition(playQueue.getIndex()); + } + + public void onQueueClosed() { + if (!queueVisible) { + return; + } + + animateView(queueLayout, SLIDE_AND_ALPHA, false, + DEFAULT_CONTROLS_DURATION, 0, () -> { + // Even when queueLayout is GONE it receives touch events + // and ruins normal behavior of the app. This line fixes it + queueLayout.setTranslationY(-queueLayout.getHeight() * 5); + }); + queueVisible = false; + playPauseButton.requestFocus(); + } + + private void onMoreOptionsClicked() { + if (DEBUG) { + Log.d(TAG, "onMoreOptionsClicked() called"); + } + + final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; + + animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, + isMoreControlsVisible ? 0 : 180); + animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, + DEFAULT_CONTROLS_DURATION, 0, + () -> { + // Fix for a ripple effect on background drawable. + // When view returns from GONE state it takes more milliseconds than returning + // from INVISIBLE state. And the delay makes ripple background end to fast + if (isMoreControlsVisible) { + secondaryControls.setVisibility(View.INVISIBLE); + } + }); + showControls(DEFAULT_CONTROLS_DURATION); + } + + private void onShareClicked() { + // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) + // Timestamp doesn't make sense in a live stream so drop it + final String ts = isLive() ? "" : ("&t=" + (getPlaybackSeekBar().getProgress() / 1000)); + ShareUtils.shareUrl(service, + getVideoTitle(), + getVideoUrl() + ts); + } + + private void onPlayWithKodiClicked() { + if (getCurrentMetadata() == null) { + return; + } + onPause(); + try { + NavigationHelper.playWithKore(getParentActivity(), Uri.parse(getVideoUrl())); + } catch (final Exception e) { + if (DEBUG) { + Log.i(TAG, "Failed to start kore", e); + } + KoreUtil.showInstallKoreDialog(getParentActivity()); + } + } + + private void onOpenInBrowserClicked() { + if (getCurrentMetadata() == null) { + return; + } + + ShareUtils.openUrlInBrowser(getParentActivity(), + getCurrentMetadata().getMetadata().getOriginalUrl()); + } + + private void showHideKodiButton() { + final boolean kodiEnabled = defaultPreferences.getBoolean( + service.getString(R.string.show_play_with_kodi_key), false); + // show kodi button if it supports the current service and it is enabled in settings + final boolean showKodiButton = playQueue != null && playQueue.getItem() != null + && KoreUtil.isServiceSupportedByKore(playQueue.getItem().getServiceId()) + && PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.show_play_with_kodi_key), false); + playWithKodi.setVisibility(videoPlayerSelected() && kodiEnabled && showKodiButton + ? View.VISIBLE : View.GONE); + } + + private void setupScreenRotationButton() { + final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); + final boolean tabletInLandscape = DeviceUtils.isTablet(service) && service.isLandscape(); + final boolean showButton = videoPlayerSelected() + && (orientationLocked || isVerticalVideo || tabletInLandscape); + screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE); + screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(service, isFullscreen() + ? R.drawable.ic_fullscreen_exit_white_24dp + : R.drawable.ic_fullscreen_white_24dp)); + } + + private void prepareOrientation() { + final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); + if (orientationLocked + && isFullscreen() + && service.isLandscape() == isVerticalVideo + && fragmentListener != null) { + fragmentListener.onScreenRotationButtonClicked(); + } + } + + @Override + public void onPlaybackSpeedClicked() { + if (videoPlayerSelected()) { + PlaybackParameterDialog + .newInstance( + getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence(), this) + .show(getParentActivity().getSupportFragmentManager(), null); + } else { + super.onPlaybackSpeedClicked(); + } + } + + @Override + public void onStopTrackingTouch(final SeekBar seekBar) { + super.onStopTrackingTouch(seekBar); + if (wasPlaying()) { + showControlsThenHide(); + } + } + + @Override + public void onDismiss(final PopupMenu menu) { + super.onDismiss(menu); + if (isPlaying()) { + hideControls(DEFAULT_CONTROLS_DURATION, 0); + } + } + + @Override + @SuppressWarnings("checkstyle:ParameterNumber") + public void onLayoutChange(final View view, final int l, final int t, final int r, final int b, + final int ol, final int ot, final int or, final int ob) { + if (l != ol || t != ot || r != or || b != ob) { + // Use smaller value to be consistent between screen orientations + // (and to make usage easier) + final int width = r - l; + final int height = b - t; + final int min = Math.min(width, height); + maxGestureLength = (int) (min * MAX_GESTURE_LENGTH); + + if (DEBUG) { + Log.d(TAG, "maxGestureLength = " + maxGestureLength); + } + + volumeProgressBar.setMax(maxGestureLength); + brightnessProgressBar.setMax(maxGestureLength); + + setInitialGestureValues(); + queueLayout.getLayoutParams().height = height - queueLayout.getTop(); + + if (popupPlayerSelected()) { + final float widthDp = Math.abs(r - l) / service.getResources() + .getDisplayMetrics().density; + final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP + ? View.VISIBLE + : View.GONE; + secondaryControls.setVisibility(visibility); + } + } + } + + @Override + protected int nextResizeMode(final int currentResizeMode) { + final int newResizeMode; + switch (currentResizeMode) { + case AspectRatioFrameLayout.RESIZE_MODE_FIT: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; + break; + case AspectRatioFrameLayout.RESIZE_MODE_FILL: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + break; + default: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + break; + } + + storeResizeMode(newResizeMode); + return newResizeMode; + } + + private void storeResizeMode(final @AspectRatioFrameLayout.ResizeMode int resizeMode) { + defaultPreferences.edit() + .putInt(service.getString(R.string.last_resize_mode), resizeMode) + .apply(); + } + + private void restoreResizeMode() { + setResizeMode(defaultPreferences.getInt( + service.getString(R.string.last_resize_mode), + AspectRatioFrameLayout.RESIZE_MODE_FIT)); + } + + @Override + protected VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(final List sortedVideos) { + return videoPlayerSelected() + ? ListHelper.getDefaultResolutionIndex(context, sortedVideos) + : ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + } + + @Override + public int getOverrideResolutionIndex(final List sortedVideos, + final String playbackQuality) { + return videoPlayerSelected() + ? getResolutionIndex(context, sortedVideos, playbackQuality) + : getPopupResolutionIndex(context, sortedVideos, playbackQuality); + } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ + + private void animatePlayButtons(final boolean show, final int duration) { + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + if (playQueue.getIndex() > 0 || !show) { + animateView(playPreviousButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + } + if (playQueue.getIndex() + 1 < playQueue.getStreams().size() || !show) { + animateView(playNextButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + } + + } + + @Override + public void changeState(final int state) { + super.changeState(state); + updatePlayback(); + } + + @Override + public void onBlocked() { + super.onBlocked(); + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); + animatePlayButtons(false, 100); + getRootView().setKeepScreenOn(false); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_play); + } + + @Override + public void onBuffering() { + super.onBuffering(); + getRootView().setKeepScreenOn(true); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_play); + } + + @Override + public void onPlaying() { + super.onPlaying(); + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { + playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); + animatePlayButtons(true, 200); + if (!queueVisible) { + playPauseButton.requestFocus(); + } + }); + + updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); + checkLandscape(); + getRootView().setKeepScreenOn(true); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_pause); + + service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build()); + } + + @Override + public void onPaused() { + super.onPaused(); + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); + animatePlayButtons(true, 200); + if (!queueVisible) { + playPauseButton.requestFocus(); + } + }); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_play); + + // Remove running notification when user don't want music (or video in popup) + // to be played in background + if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) { + service.stopForeground(true); + } + + getRootView().setKeepScreenOn(false); + } + + @Override + public void onPausedSeek() { + super.onPausedSeek(); + animatePlayButtons(false, 100); + getRootView().setKeepScreenOn(true); + + service.resetNotification(); + service.updateNotification(R.drawable.exo_controls_play); + } + + + @Override + public void onCompleted() { + animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { + playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); + animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); + }); + getRootView().setKeepScreenOn(false); + + updateWindowFlags(IDLE_WINDOW_FLAGS); + + service.resetNotification(); + service.updateNotification(R.drawable.ic_replay_white_24dp); + + super.onCompleted(); + } + + @Override + public void destroy() { + super.destroy(); + + service.getContentResolver().unregisterContentObserver(settingsContentObserver); + } + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast Receiver + //////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void setupBroadcastReceiver(final IntentFilter intentFilter) { + super.setupBroadcastReceiver(intentFilter); + if (DEBUG) { + Log.d(TAG, "setupBroadcastReceiver() called with: " + + "intentFilter = [" + intentFilter + "]"); + } + + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_PLAY_PAUSE); + intentFilter.addAction(ACTION_OPEN_CONTROLS); + intentFilter.addAction(ACTION_REPEAT); + intentFilter.addAction(ACTION_PLAY_PREVIOUS); + intentFilter.addAction(ACTION_PLAY_NEXT); + intentFilter.addAction(ACTION_FAST_REWIND); + intentFilter.addAction(ACTION_FAST_FORWARD); + + intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED); + intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED); + + intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + intentFilter.addAction(Intent.ACTION_SCREEN_ON); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + + intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); + } + + @Override + public void onBroadcastReceived(final Intent intent) { + super.onBroadcastReceived(intent); + if (intent == null || intent.getAction() == null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + } + + switch (intent.getAction()) { + case ACTION_CLOSE: + service.onDestroy(); + break; + case ACTION_PLAY_NEXT: + onPlayNext(); + break; + case ACTION_PLAY_PREVIOUS: + onPlayPrevious(); + break; + case ACTION_FAST_FORWARD: + onFastForward(); + break; + case ACTION_FAST_REWIND: + onFastRewind(); + break; + case ACTION_PLAY_PAUSE: + onPlayPause(); + if (!fragmentIsVisible) { + // Ensure that we have audio-only stream playing when a user + // started to play from notification's play button from outside of the app + onFragmentStopped(); + } + break; + case ACTION_REPEAT: + onRepeatClicked(); + break; + case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: + fragmentIsVisible = true; + useVideoSource(true); + break; + case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED: + fragmentIsVisible = false; + onFragmentStopped(); + break; + case Intent.ACTION_CONFIGURATION_CHANGED: + assureCorrectAppLanguage(service); + if (DEBUG) { + Log.d(TAG, "onConfigurationChanged() called"); + } + if (popupPlayerSelected()) { + updateScreenSize(); + updatePopupSize(getPopupLayoutParams().width, -1); + checkPopupPositionBounds(); + } + + // The only situation I need to re-calculate elements sizes is + // when a user rotates a device from landscape to landscape + // because in that case the controls should be aligned to another side of a screen. + // The problem is when user leaves the app and returns back + // (while the app in landscape) Android reports via DisplayMetrics that orientation + // is portrait and it gives wrong sizes calculations. + // Let's skip re-calculation in every case but landscape + final boolean reportedOrientationIsLandscape = service.isLandscape(); + final boolean actualOrientationIsLandscape = context.getResources() + .getConfiguration().orientation == ORIENTATION_LANDSCAPE; + if (reportedOrientationIsLandscape && actualOrientationIsLandscape) { + setControlsSize(); + } + // Close it because when changing orientation from portrait + // (in fullscreen mode) the size of queue layout can be larger than the screen size + onQueueClosed(); + break; + case Intent.ACTION_SCREEN_ON: + shouldUpdateOnProgress = true; + // Interrupt playback only when screen turns on + // and user is watching video in popup player. + // Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED + if (backgroundPlaybackEnabled() + && popupPlayerSelected() + && (isPlaying() || isLoading())) { + useVideoSource(true); + } + break; + case Intent.ACTION_SCREEN_OFF: + shouldUpdateOnProgress = false; + // Interrupt playback only when screen turns off with popup player working + if (backgroundPlaybackEnabled() + && popupPlayerSelected() + && (isPlaying() || isLoading())) { + useVideoSource(false); + } + break; + } + service.resetNotification(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail Loading + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onLoadingComplete(final String imageUri, + final View view, + final Bitmap loadedImage) { + super.onLoadingComplete(imageUri, view, loadedImage); + // rebuild notification here since remote view does not release bitmaps, + // causing memory leaks + service.resetNotification(); + service.updateNotification(-1); + } + + @Override + public void onLoadingFailed(final String imageUri, + final View view, + final FailReason failReason) { + super.onLoadingFailed(imageUri, view, failReason); + service.resetNotification(); + service.updateNotification(-1); + } + + @Override + public void onLoadingCancelled(final String imageUri, final View view) { + super.onLoadingCancelled(imageUri, view); + service.resetNotification(); + service.updateNotification(-1); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void setInitialGestureValues() { + if (getAudioReactor() != null) { + final float currentVolumeNormalized = (float) getAudioReactor() + .getVolume() / getAudioReactor().getMaxVolume(); + volumeProgressBar.setProgress( + (int) (volumeProgressBar.getMax() * currentVolumeNormalized)); + } + } + + private void choosePlayerTypeFromIntent(final Intent intent) { + // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra + if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_AUDIO) { + playerType = MainPlayer.PlayerType.AUDIO; + } else if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_POPUP) { + playerType = MainPlayer.PlayerType.POPUP; + } else { + playerType = MainPlayer.PlayerType.VIDEO; + } + } + + public boolean backgroundPlaybackEnabled() { + return PlayerHelper.getMinimizeOnExitAction(service) == MINIMIZE_ON_EXIT_MODE_BACKGROUND; + } + + public boolean minimizeOnPopupEnabled() { + return PlayerHelper.getMinimizeOnExitAction(service) + == PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; + } + + public boolean audioPlayerSelected() { + return playerType == MainPlayer.PlayerType.AUDIO; + } + + public boolean videoPlayerSelected() { + return playerType == MainPlayer.PlayerType.VIDEO; + } + + public boolean popupPlayerSelected() { + return playerType == MainPlayer.PlayerType.POPUP; + } + + public boolean isPlayerStopped() { + return getPlayer() == null || getPlayer().getPlaybackState() == SimpleExoPlayer.STATE_IDLE; + } + + private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { + final int closeOverlayButtonX = closeOverlayButton.getLeft() + + closeOverlayButton.getWidth() / 2; + final int closeOverlayButtonY = closeOverlayButton.getTop() + + closeOverlayButton.getHeight() / 2; + + final float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); + final float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); + + return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + + Math.pow(closeOverlayButtonY - fingerY, 2)); + } + + private float getClosingRadius() { + final int buttonRadius = closeOverlayButton.getWidth() / 2; + // 20% wider than the button itself + return buttonRadius * 1.2f; + } + + public boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) { + return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); + } + + public boolean isFullscreen() { + return isFullscreen; + } + + public void showControlsThenHide() { + if (DEBUG) { + Log.d(TAG, "showControlsThenHide() called"); + } + showOrHideButtons(); + showSystemUIPartially(); + super.showControlsThenHide(); + } + + @Override + public void showControls(final long duration) { + if (DEBUG) { + Log.d(TAG, "showControls() called with: duration = [" + duration + "]"); + } + showOrHideButtons(); + showSystemUIPartially(); + super.showControls(duration); + } + + @Override + public void hideControls(final long duration, final long delay) { + if (DEBUG) { + Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); + } + + showOrHideButtons(); + + getControlsVisibilityHandler().removeCallbacksAndMessages(null); + getControlsVisibilityHandler().postDelayed(() -> + animateView(getControlsRoot(), false, duration, 0, + this::hideSystemUIIfNeeded), delay + ); + } + + @Override + public void safeHideControls(final long duration, final long delay) { + if (getControlsRoot().isInTouchMode()) { + hideControls(duration, delay); + } + } + + private void showOrHideButtons() { + if (playQueue == null) { + return; + } + + playPreviousButton.setVisibility(playQueue.getIndex() == 0 + ? View.INVISIBLE + : View.VISIBLE); + playNextButton.setVisibility(playQueue.getIndex() + 1 == playQueue.getStreams().size() + ? View.INVISIBLE + : View.VISIBLE); + queueButton.setVisibility(playQueue.getStreams().size() <= 1 || popupPlayerSelected() + ? View.GONE + : View.VISIBLE); + } + + private void showSystemUIPartially() { + if (isFullscreen() && getParentActivity() != null) { + final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + getParentActivity().getWindow().getDecorView().setSystemUiVisibility(visibility); + } + } + + @Override + public void hideSystemUIIfNeeded() { + if (fragmentListener != null) { + fragmentListener.hideSystemUiIfNeeded(); + } + } + + /** + * Measures width and height of controls visible on screen. + * It ensures that controls will be side-by-side with NavigationBar and notches + * but not under them. Tablets have only bottom NavigationBar + */ + public void setControlsSize() { + final Point size = new Point(); + final Display display = getRootView().getDisplay(); + if (display == null || !videoPlayerSelected()) { + return; + } + // This method will give a correct size of a usable area of a window. + // It doesn't include NavigationBar, notches, etc. + display.getSize(size); + + final boolean isLandscape = service.isLandscape(); + final int width = isFullscreen + ? (isLandscape ? size.x : size.y) + : ViewGroup.LayoutParams.MATCH_PARENT; + final int gravity = isFullscreen + ? (display.getRotation() == Surface.ROTATION_90 + ? Gravity.START : Gravity.END) + : Gravity.TOP; + + getTopControlsRoot().getLayoutParams().width = width; + final RelativeLayout.LayoutParams topParams = + ((RelativeLayout.LayoutParams) getTopControlsRoot().getLayoutParams()); + topParams.removeRule(RelativeLayout.ALIGN_PARENT_START); + topParams.removeRule(RelativeLayout.ALIGN_PARENT_END); + topParams.addRule(gravity == Gravity.END + ? RelativeLayout.ALIGN_PARENT_END + : RelativeLayout.ALIGN_PARENT_START); + getTopControlsRoot().requestLayout(); + + getBottomControlsRoot().getLayoutParams().width = width; + final RelativeLayout.LayoutParams bottomParams = + ((RelativeLayout.LayoutParams) getBottomControlsRoot().getLayoutParams()); + bottomParams.removeRule(RelativeLayout.ALIGN_PARENT_START); + bottomParams.removeRule(RelativeLayout.ALIGN_PARENT_END); + bottomParams.addRule(gravity == Gravity.END + ? RelativeLayout.ALIGN_PARENT_END + : RelativeLayout.ALIGN_PARENT_START); + getBottomControlsRoot().requestLayout(); + + final ViewGroup controlsRoot = getRootView().findViewById(R.id.playbackWindowRoot); + // In tablet navigationBar located at the bottom of the screen. + // And the situations when we need to set custom height is + // in fullscreen mode in tablet in non-multiWindow mode or with vertical video. + // Other than that MATCH_PARENT is good + final boolean navBarAtTheBottom = DeviceUtils.isTablet(service) || !isLandscape; + controlsRoot.getLayoutParams().height = isFullscreen && !isInMultiWindow() + && navBarAtTheBottom ? size.y : ViewGroup.LayoutParams.MATCH_PARENT; + controlsRoot.requestLayout(); + + final DisplayMetrics metrics = getRootView().getResources().getDisplayMetrics(); + int topPadding = isFullscreen && !isInMultiWindow() ? getStatusBarHeight() : 0; + topPadding = !isLandscape && DeviceUtils.hasCutout(topPadding, metrics) ? 0 : topPadding; + getRootView().findViewById(R.id.playbackWindowRoot).setTranslationY(topPadding); + getBottomControlsRoot().setTranslationY(-topPadding); + } + + /** + * @return statusBar height that was found inside system resources + * or default value if no value was provided inside resources + */ + private int getStatusBarHeight() { + int statusBarHeight = 0; + final int resourceId = service.isLandscape() + ? service.getResources().getIdentifier( + "status_bar_height_landscape", "dimen", "android") + : service.getResources().getIdentifier( + "status_bar_height", "dimen", "android"); + + if (resourceId > 0) { + statusBarHeight = service.getResources().getDimensionPixelSize(resourceId); + } + if (statusBarHeight == 0) { + // Some devices provide wrong value for status bar height in landscape mode, + // this is workaround + final DisplayMetrics metrics = getRootView().getResources().getDisplayMetrics(); + statusBarHeight = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 24, metrics); + } + return statusBarHeight; + } + + protected void setMuteButton(final ImageButton button, final boolean isMuted) { + button.setImageDrawable(AppCompatResources.getDrawable(service, isMuted + ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); + } + + /** + * @return true if main player is attached to activity and activity inside multiWindow mode + */ + private boolean isInMultiWindow() { + final AppCompatActivity parent = getParentActivity(); + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && parent != null + && parent.isInMultiWindowMode(); + } + + private void updatePlaybackButtons() { + if (repeatButton == null + || shuffleButton == null + || simpleExoPlayer == null + || playQueue == null) { + return; + } + + setRepeatModeButton(repeatButton, getRepeatMode()); + setShuffleButton(shuffleButton, playQueue.isShuffled()); + } + + public void checkLandscape() { + final AppCompatActivity parent = getParentActivity(); + final boolean videoInLandscapeButNotInFullscreen = service.isLandscape() + && !isFullscreen() + && videoPlayerSelected() + && !audioOnly; + + final boolean playingState = getCurrentState() != STATE_COMPLETED + && getCurrentState() != STATE_PAUSED; + if (parent != null + && videoInLandscapeButNotInFullscreen + && playingState + && !DeviceUtils.isTablet(service)) { + toggleFullscreen(); + } + + setControlsSize(); + } + + private void buildQueue() { + itemsList.setAdapter(playQueueAdapter); + itemsList.setClickable(true); + itemsList.setLongClickable(true); + + itemsList.clearOnScrollListeners(); + itemsList.addOnScrollListener(getQueueScrollListener()); + + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(itemsList); + + playQueueAdapter.setSelectedListener(getOnSelectedListener()); + + itemsListCloseButton.setOnClickListener(view -> onQueueClosed()); + } + + public void useVideoSource(final boolean video) { + if (playQueue == null || audioOnly == !video || audioPlayerSelected()) { + return; + } + + audioOnly = !video; + // When a user returns from background controls could be hidden + // but systemUI will be shown 100%. Hide it + if (!audioOnly && !isControlsVisible()) { + hideSystemUIIfNeeded(); + } + setRecovery(); + reload(); + } + + private OnScrollBelowItemsListener getQueueScrollListener() { + return new OnScrollBelowItemsListener() { + @Override + public void onScrolledDown(final RecyclerView recyclerView) { + if (playQueue != null && !playQueue.isComplete()) { + playQueue.fetch(); + } else if (itemsList != null) { + itemsList.clearOnScrollListeners(); + } + } + }; + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new PlayQueueItemTouchCallback() { + @Override + public void onMove(final int sourceIndex, final int targetIndex) { + if (playQueue != null) { + playQueue.move(sourceIndex, targetIndex); + } + } + + @Override + public void onSwiped(final int index) { + if (index != -1) { + playQueue.remove(index); + } + } + }; + } + + private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { + return new PlayQueueItemBuilder.OnSelectedListener() { + @Override + public void selected(final PlayQueueItem item, final View view) { + onSelected(item); + } + + @Override + public void held(final PlayQueueItem item, final View view) { + final int index = playQueue.indexOf(item); + if (index != -1) { + playQueue.remove(index); + } + } + + @Override + public void onStartDrag(final PlayQueueItemHolder viewHolder) { + if (itemTouchHelper != null) { + itemTouchHelper.startDrag(viewHolder); + } + } + }; + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + @SuppressLint("RtlHardcoded") + private void initPopup() { + if (DEBUG) { + Log.d(TAG, "initPopup() called"); + } + + // Popup is already added to windowManager + if (popupHasParent()) { + return; + } + + updateScreenSize(); + + final boolean popupRememberSizeAndPos = PlayerHelper.isRememberingPopupDimensions(service); + final float defaultSize = service.getResources().getDimension(R.dimen.popup_default_width); + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(service); + popupWidth = popupRememberSizeAndPos + ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) + : defaultSize; + popupHeight = getMinimumVideoHeight(popupWidth); + + 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; + getSurfaceView().setHeights((int) popupHeight, (int) popupHeight); + + final int centerX = (int) (screenWidth / 2f - popupWidth / 2f); + final int centerY = (int) (screenHeight / 2f - popupHeight / 2f); + popupLayoutParams.x = popupRememberSizeAndPos + ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; + popupLayoutParams.y = popupRememberSizeAndPos + ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; + + checkPopupPositionBounds(); + + getLoadingPanel().setMinimumWidth(popupLayoutParams.width); + getLoadingPanel().setMinimumHeight(popupLayoutParams.height); + + service.removeViewFromParent(); + windowManager.addView(getRootView(), popupLayoutParams); + + // Popup doesn't have aspectRatio selector, using FIT automatically + setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); + } + + @SuppressLint("RtlHardcoded") + private void initPopupCloseOverlay() { + if (DEBUG) { + Log.d(TAG, "initPopupCloseOverlay() called"); + } + + // closeOverlayView is already added to windowManager + if (closeOverlayView != null) { + return; + } + + closeOverlayView = View.inflate(service, R.layout.player_popup_close_overlay, null); + closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton); + + 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); + closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; + closeOverlayLayoutParams.softInputMode = + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + + closeOverlayButton.setVisibility(View.GONE); + windowManager.addView(closeOverlayView, closeOverlayLayoutParams); + } + + private void initVideoPlayer() { + restoreResizeMode(); + getRootView().setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Popup utils + //////////////////////////////////////////////////////////////////////////*/ + + /** + * @return if the popup was out of bounds and have been moved back to it + * @see #checkPopupPositionBounds(float, float) + */ + @SuppressWarnings("UnusedReturnValue") + public boolean checkPopupPositionBounds() { + return checkPopupPositionBounds(screenWidth, screenHeight); + } + + /** + * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary + * that goes from (0, 0) to (boundaryWidth, boundaryHeight). + *

+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed + * and {@code true} is returned to represent this change. + *

+ * + * @param boundaryWidth width of the boundary + * @param boundaryHeight height of the boundary + * @return if the popup was out of bounds and have been moved back to it + */ + public boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) { + if (DEBUG) { + Log.d(TAG, "checkPopupPositionBounds() called with: " + + "boundaryWidth = [" + boundaryWidth + "], " + + "boundaryHeight = [" + boundaryHeight + "]"); + } + + if (popupLayoutParams.x < 0) { + popupLayoutParams.x = 0; + return true; + } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) { + popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width); + return true; + } + + if (popupLayoutParams.y < 0) { + popupLayoutParams.y = 0; + return true; + } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) { + popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height); + return true; + } + + return false; + } + + public void savePositionAndSize() { + final SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(service); + sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); + sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); + sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); + } + + private float getMinimumVideoHeight(final float width) { + final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have + /*if (DEBUG) { + Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + + width + "], returned: " + height); + }*/ + return height; + } + + public void updateScreenSize() { + final DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(metrics); + + screenWidth = metrics.widthPixels; + screenHeight = metrics.heightPixels; + if (DEBUG) { + Log.d(TAG, "updateScreenSize() called > screenWidth = " + + screenWidth + ", screenHeight = " + screenHeight); + } + + popupWidth = service.getResources().getDimension(R.dimen.popup_default_width); + popupHeight = getMinimumVideoHeight(popupWidth); + + minimumWidth = service.getResources().getDimension(R.dimen.popup_minimum_width); + minimumHeight = getMinimumVideoHeight(minimumWidth); + + maximumWidth = screenWidth; + maximumHeight = screenHeight; + } + + public void updatePopupSize(final int width, final int height) { + if (DEBUG) { + Log.d(TAG, "updatePopupSize() called with: width = [" + + width + "], height = [" + height + "]"); + } + + if (popupLayoutParams == null + || windowManager == null + || getParentActivity() != null + || getRootView().getParent() == null) { + return; + } + + final int actualWidth = (int) (width > maximumWidth + ? maximumWidth : width < minimumWidth ? minimumWidth : width); + final int actualHeight; + if (height == -1) { + actualHeight = (int) getMinimumVideoHeight(width); + } else { + actualHeight = (int) (height > maximumHeight + ? maximumHeight : height < minimumHeight + ? minimumHeight : height); + } + + popupLayoutParams.width = actualWidth; + popupLayoutParams.height = actualHeight; + popupWidth = actualWidth; + popupHeight = actualHeight; + getSurfaceView().setHeights((int) popupHeight, (int) popupHeight); + + if (DEBUG) { + Log.d(TAG, "updatePopupSize() updated values:" + + " width = [" + actualWidth + "], height = [" + actualHeight + "]"); + } + windowManager.updateViewLayout(getRootView(), popupLayoutParams); + } + + private void updateWindowFlags(final int flags) { + if (popupLayoutParams == null + || windowManager == null + || getParentActivity() != null + || getRootView().getParent() == null) { + return; + } + + popupLayoutParams.flags = flags; + windowManager.updateViewLayout(getRootView(), popupLayoutParams); + } + + private int popupLayoutParamType() { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.O + ? WindowManager.LayoutParams.TYPE_PHONE + : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } + + /*////////////////////////////////////////////////////////////////////////// + // Misc + //////////////////////////////////////////////////////////////////////////*/ + + public void closePopup() { + if (DEBUG) { + Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); + } + if (isPopupClosing) { + return; + } + isPopupClosing = true; + + savePlaybackState(); + windowManager.removeView(getRootView()); + + animateOverlayAndFinishService(); + } + + public void removePopupFromView() { + final boolean isCloseOverlayHasParent = closeOverlayView != null + && closeOverlayView.getParent() != null; + if (popupHasParent()) { + windowManager.removeView(getRootView()); + } + if (isCloseOverlayHasParent) { + windowManager.removeView(closeOverlayView); + } + } + + private void animateOverlayAndFinishService() { + final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() + - closeOverlayButton.getY()); + + closeOverlayButton.animate().setListener(null).cancel(); + closeOverlayButton.animate() + .setInterpolator(new AnticipateInterpolator()) + .translationY(targetTranslationY) + .setDuration(400) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(final Animator animation) { + end(); + } + + @Override + public void onAnimationEnd(final Animator animation) { + end(); + } + + private void end() { + windowManager.removeView(closeOverlayView); + closeOverlayView = null; + + service.onDestroy(); + } + }).start(); + } + + private boolean popupHasParent() { + final View root = getRootView(); + return root != null + && root.getLayoutParams() instanceof WindowManager.LayoutParams + && root.getParent() != null; + } + + /////////////////////////////////////////////////////////////////////////// + // Manipulations with listener + /////////////////////////////////////////////////////////////////////////// + + public void setFragmentListener(final PlayerServiceEventListener listener) { + fragmentListener = listener; + fragmentIsVisible = true; + updateMetadata(); + updatePlayback(); + triggerProgressUpdate(); + } + + public void removeFragmentListener(final PlayerServiceEventListener listener) { + if (fragmentListener == listener) { + fragmentListener = null; + } + } + + void setActivityListener(final PlayerEventListener listener) { + activityListener = listener; + updateMetadata(); + updatePlayback(); + triggerProgressUpdate(); + } + + void removeActivityListener(final PlayerEventListener listener) { + if (activityListener == listener) { + activityListener = null; + } + } + + private void updateQueue() { + if (fragmentListener != null && playQueue != null) { + fragmentListener.onQueueUpdate(playQueue); + } + if (activityListener != null && playQueue != null) { + activityListener.onQueueUpdate(playQueue); + } + } + + private void updateMetadata() { + if (fragmentListener != null && getCurrentMetadata() != null) { + fragmentListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); + } + if (activityListener != null && getCurrentMetadata() != null) { + activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); + } + } + + private void updatePlayback() { + if (fragmentListener != null && simpleExoPlayer != null && playQueue != null) { + fragmentListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); + } + if (activityListener != null && simpleExoPlayer != null && playQueue != null) { + activityListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), getPlaybackParameters()); + } + } + + private void updateProgress(final int currentProgress, final int duration, + final int bufferPercent) { + if (fragmentListener != null) { + fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + if (activityListener != null) { + activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + } + + void stopActivityBinding() { + if (fragmentListener != null) { + fragmentListener.onServiceStopped(); + fragmentListener = null; + } + if (activityListener != null) { + activityListener.onServiceStopped(); + activityListener = null; + } + } + + /** + * This will be called when a user goes to another app/activity, turns off a screen. + * We don't want to interrupt playback and don't want to see notification so + * next lines of code will enable audio-only playback only if needed + */ + private void onFragmentStopped() { + if (videoPlayerSelected() && (isPlaying() || isLoading())) { + if (backgroundPlaybackEnabled()) { + useVideoSource(false); + } else if (minimizeOnPopupEnabled()) { + setRecovery(); + NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true); + } else { + onPause(); + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////// + + public RelativeLayout getVolumeRelativeLayout() { + return volumeRelativeLayout; + } + + public ProgressBar getVolumeProgressBar() { + return volumeProgressBar; + } + + public ImageView getVolumeImageView() { + return volumeImageView; + } + + public RelativeLayout getBrightnessRelativeLayout() { + return brightnessRelativeLayout; + } + + public ProgressBar getBrightnessProgressBar() { + return brightnessProgressBar; + } + + public ImageView getBrightnessImageView() { + return brightnessImageView; + } + + public ImageButton getPlayPauseButton() { + return playPauseButton; + } + + public int getMaxGestureLength() { + return maxGestureLength; + } + + public TextView getResizingIndicator() { + return resizingIndicator; + } + + public GestureDetector getGestureDetector() { + return gestureDetector; + } + + public WindowManager.LayoutParams getPopupLayoutParams() { + return popupLayoutParams; + } + + public float getScreenWidth() { + return screenWidth; + } + + public float getScreenHeight() { + return screenHeight; + } + + public float getPopupWidth() { + return popupWidth; + } + + public float getPopupHeight() { + return popupHeight; + } + + public void setPopupWidth(final float width) { + popupWidth = width; + } + + public void setPopupHeight(final float height) { + popupHeight = height; + } + + public View getCloseOverlayButton() { + return closeOverlayButton; + } + + public View getClosingOverlayView() { + return closingOverlayView; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java new file mode 100644 index 000000000..19c621221 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java @@ -0,0 +1,72 @@ +package org.schabi.newpipe.player.event; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import org.schabi.newpipe.R; + +import java.util.Arrays; +import java.util.List; + +public class CustomBottomSheetBehavior extends BottomSheetBehavior { + + public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + boolean visible; + Rect globalRect = new Rect(); + private boolean skippingInterception = false; + private final List skipInterceptionOfElements = Arrays.asList( + R.id.detail_content_root_layout, R.id.relatedStreamsLayout, + R.id.playQueuePanel, R.id.viewpager, R.id.bottomControls); + + @Override + public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent, + @NonNull final FrameLayout child, + final MotionEvent event) { + // Drop following when action ends + if (event.getAction() == MotionEvent.ACTION_CANCEL + || event.getAction() == MotionEvent.ACTION_UP) { + skippingInterception = false; + } + + // Found that user still swiping, continue following + if (skippingInterception) { + return false; + } + + // Don't need to do anything if bottomSheet isn't expanded + if (getState() == BottomSheetBehavior.STATE_EXPANDED) { + // Without overriding scrolling will not work when user touches these elements + for (final Integer element : skipInterceptionOfElements) { + final ViewGroup viewGroup = child.findViewById(element); + if (viewGroup != null) { + visible = viewGroup.getGlobalVisibleRect(globalRect); + if (visible + && globalRect.contains((int) event.getRawX(), (int) event.getRawY())) { + // Makes bottom part of the player draggable in portrait when + // playbackControlRoot is hidden + if (element == R.id.bottomControls + && child.findViewById(R.id.playbackControlRoot) + .getVisibility() != View.VISIBLE) { + return super.onInterceptTouchEvent(parent, child, event); + } + skippingInterception = true; + return false; + } + } + } + } + + return super.onInterceptTouchEvent(parent, child, event); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/OnKeyDownListener.java b/app/src/main/java/org/schabi/newpipe/player/event/OnKeyDownListener.java new file mode 100644 index 000000000..fc1f9d80d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/OnKeyDownListener.java @@ -0,0 +1,5 @@ +package org.schabi.newpipe.player.event; + +public interface OnKeyDownListener { + boolean onKeyDown(int keyCode); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java index 0809fa0f5..b5520e8be 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java @@ -4,14 +4,13 @@ package org.schabi.newpipe.player.event; import com.google.android.exoplayer2.PlaybackParameters; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.playqueue.PlayQueue; public interface PlayerEventListener { + void onQueueUpdate(PlayQueue queue); void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, PlaybackParameters parameters); - void onProgressUpdate(int currentProgress, int duration, int bufferPercent); - - void onMetadataUpdate(StreamInfo info); - + void onMetadataUpdate(StreamInfo info, PlayQueue queue); void onServiceStopped(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java new file mode 100644 index 000000000..4aa6070eb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -0,0 +1,623 @@ +package org.schabi.newpipe.player.event; + +import android.app.Activity; +import android.content.Context; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.Window; +import android.view.WindowManager; +import androidx.appcompat.content.res.AppCompatResources; +import org.schabi.newpipe.R; +import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.helper.PlayerHelper; + +import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; +import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; +import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; +import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; +import static org.schabi.newpipe.util.AnimationUtils.animateView; + +public class PlayerGestureListener + extends GestureDetector.SimpleOnGestureListener + implements View.OnTouchListener { + private static final String TAG = ".PlayerGestureListener"; + private static final boolean DEBUG = BasePlayer.DEBUG; + + private final VideoPlayerImpl playerImpl; + private final MainPlayer service; + + private int initialPopupX; + private int initialPopupY; + + private boolean isMovingInMain; + private boolean isMovingInPopup; + + private boolean isResizing; + + private final int tossFlingVelocity; + + private final boolean isVolumeGestureEnabled; + private final boolean isBrightnessGestureEnabled; + private final int maxVolume; + private static final int MOVEMENT_THRESHOLD = 40; + + // [popup] initial coordinates and distance between fingers + private double initPointerDistance = -1; + private float initFirstPointerX = -1; + private float initFirstPointerY = -1; + private float initSecPointerX = -1; + private float initSecPointerY = -1; + + + public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) { + this.playerImpl = playerImpl; + this.service = service; + this.tossFlingVelocity = PlayerHelper.getTossFlingVelocity(service); + + isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service); + isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(service); + maxVolume = playerImpl.getAudioReactor().getMaxVolume(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Helpers + //////////////////////////////////////////////////////////////////////////*/ + + /* + * Main and popup players' gesture listeners is too different. + * So it will be better to have different implementations of them + * */ + @Override + public boolean onDoubleTap(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); + } + + if (playerImpl.popupPlayerSelected()) { + return onDoubleTapInPopup(e); + } else { + return onDoubleTapInMain(e); + } + } + + @Override + public boolean onSingleTapConfirmed(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + } + + if (playerImpl.popupPlayerSelected()) { + return onSingleTapConfirmedInPopup(e); + } else { + return onSingleTapConfirmedInMain(e); + } + } + + @Override + public boolean onDown(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onDown() called with: e = [" + e + "]"); + } + + if (playerImpl.popupPlayerSelected()) { + return onDownInPopup(e); + } else { + return true; + } + } + + @Override + public void onLongPress(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onLongPress() called with: e = [" + e + "]"); + } + + if (playerImpl.popupPlayerSelected()) { + onLongPressInPopup(e); + } + } + + @Override + public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, + final float distanceX, final float distanceY) { + if (playerImpl.popupPlayerSelected()) { + return onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY); + } else { + return onScrollInMain(initialEvent, movingEvent, distanceX, distanceY); + } + } + + @Override + public boolean onFling(final MotionEvent e1, final MotionEvent e2, + final float velocityX, final float velocityY) { + if (DEBUG) { + Log.d(TAG, "onFling() called with velocity: dX=[" + + velocityX + "], dY=[" + velocityY + "]"); + } + + if (playerImpl.popupPlayerSelected()) { + return onFlingInPopup(e1, e2, velocityX, velocityY); + } else { + return true; + } + } + + @Override + public boolean onTouch(final View v, final MotionEvent event) { + /*if (DEBUG && false) { + Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]"); + }*/ + + if (playerImpl.popupPlayerSelected()) { + return onTouchInPopup(v, event); + } else { + return onTouchInMain(v, event); + } + } + + + /*////////////////////////////////////////////////////////////////////////// + // Main player listener + //////////////////////////////////////////////////////////////////////////*/ + + private boolean onDoubleTapInMain(final MotionEvent e) { + if (e.getX() > playerImpl.getRootView().getWidth() * 2.0 / 3.0) { + playerImpl.onFastForward(); + } else if (e.getX() < playerImpl.getRootView().getWidth() / 3.0) { + playerImpl.onFastRewind(); + } else { + playerImpl.getPlayPauseButton().performClick(); + } + + return true; + } + + + private boolean onSingleTapConfirmedInMain(final MotionEvent e) { + if (DEBUG) { + Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + } + + if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { + return true; + } + + if (playerImpl.isControlsVisible()) { + playerImpl.hideControls(150, 0); + } else { + if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { + playerImpl.showControls(0); + } else { + playerImpl.showControlsThenHide(); + } + } + return true; + } + + private boolean onScrollInMain(final MotionEvent initialEvent, final MotionEvent movingEvent, + final float distanceX, final float distanceY) { + if ((!isVolumeGestureEnabled && !isBrightnessGestureEnabled) + || !playerImpl.isFullscreen()) { + return false; + } + + final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(service); + final boolean isTouchingNavigationBar = initialEvent.getY() + > playerImpl.getRootView().getHeight() - getNavigationBarHeight(service); + if (isTouchingStatusBar || isTouchingNavigationBar) { + return false; + } + + /*if (DEBUG && false) Log.d(TAG, "onScrollInMain = " + + ", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + + ", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + + ", distanceXy = [" + distanceX + ", " + distanceY + "]");*/ + + final boolean insideThreshold = + Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; + if (!isMovingInMain && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) + || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { + return false; + } + + isMovingInMain = true; + + final boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled; + final boolean acceptVolumeArea = acceptAnyArea + || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2.0; + + if (isVolumeGestureEnabled && acceptVolumeArea) { + playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); + final float currentProgressPercent = (float) playerImpl + .getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); + final int currentVolume = (int) (maxVolume * currentProgressPercent); + playerImpl.getAudioReactor().setVolume(currentVolume); + + if (DEBUG) { + Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); + } + + playerImpl.getVolumeImageView().setImageDrawable( + AppCompatResources.getDrawable(service, currentProgressPercent <= 0 + ? R.drawable.ic_volume_off_white_24dp + : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp + : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_24dp + : R.drawable.ic_volume_up_white_24dp) + ); + + if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); + } + if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); + } + } else { + final Activity parent = playerImpl.getParentActivity(); + if (parent == null) { + return true; + } + + final Window window = parent.getWindow(); + + playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY); + final float currentProgressPercent = (float) playerImpl.getBrightnessProgressBar() + .getProgress() / playerImpl.getMaxGestureLength(); + final WindowManager.LayoutParams layoutParams = window.getAttributes(); + layoutParams.screenBrightness = currentProgressPercent; + window.setAttributes(layoutParams); + + if (DEBUG) { + Log.d(TAG, "onScroll().brightnessControl, " + + "currentBrightness = " + currentProgressPercent); + } + + playerImpl.getBrightnessImageView().setImageDrawable( + AppCompatResources.getDrawable(service, + currentProgressPercent < 0.25 + ? R.drawable.ic_brightness_low_white_24dp + : currentProgressPercent < 0.75 + ? R.drawable.ic_brightness_medium_white_24dp + : R.drawable.ic_brightness_high_white_24dp) + ); + + if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); + } + if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); + } + } + return true; + } + + private void onScrollEndInMain() { + if (DEBUG) { + Log.d(TAG, "onScrollEnd() called"); + } + + if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); + } + if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); + } + + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } + + private boolean onTouchInMain(final View v, final MotionEvent event) { + playerImpl.getGestureDetector().onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP && isMovingInMain) { + isMovingInMain = false; + onScrollEndInMain(); + } + // This hack allows to stop receiving touch events on appbar + // while touching video player's view + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + v.getParent().requestDisallowInterceptTouchEvent(playerImpl.isFullscreen()); + return true; + case MotionEvent.ACTION_UP: + v.getParent().requestDisallowInterceptTouchEvent(false); + return false; + default: + return true; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Popup player listener + //////////////////////////////////////////////////////////////////////////*/ + + private boolean onDoubleTapInPopup(final MotionEvent e) { + if (playerImpl == null || !playerImpl.isPlaying()) { + return false; + } + + playerImpl.hideControls(0, 0); + + if (e.getX() > playerImpl.getPopupWidth() / 2) { + playerImpl.onFastForward(); + } else { + playerImpl.onFastRewind(); + } + + return true; + } + + private boolean onSingleTapConfirmedInPopup(final MotionEvent e) { + if (playerImpl == null || playerImpl.getPlayer() == null) { + return false; + } + if (playerImpl.isControlsVisible()) { + playerImpl.hideControls(100, 100); + } else { + playerImpl.getPlayPauseButton().requestFocus(); + playerImpl.showControlsThenHide(); + } + return true; + } + + private boolean onDownInPopup(final MotionEvent e) { + // 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). + playerImpl.updateScreenSize(); + playerImpl.checkPopupPositionBounds(); + + initialPopupX = playerImpl.getPopupLayoutParams().x; + initialPopupY = playerImpl.getPopupLayoutParams().y; + playerImpl.setPopupWidth(playerImpl.getPopupLayoutParams().width); + playerImpl.setPopupHeight(playerImpl.getPopupLayoutParams().height); + return super.onDown(e); + } + + private void onLongPressInPopup(final MotionEvent e) { + playerImpl.updateScreenSize(); + playerImpl.checkPopupPositionBounds(); + playerImpl.updatePopupSize((int) playerImpl.getScreenWidth(), -1); + } + + private boolean onScrollInPopup(final MotionEvent initialEvent, + final MotionEvent movingEvent, + final float distanceX, + final float distanceY) { + if (isResizing || playerImpl == null) { + return super.onScroll(initialEvent, movingEvent, distanceX, distanceY); + } + + if (!isMovingInPopup) { + animateView(playerImpl.getCloseOverlayButton(), true, 200); + } + + isMovingInPopup = true; + + final float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()); + float posX = (int) (initialPopupX + diffX); + final float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()); + float posY = (int) (initialPopupY + diffY); + + if (posX > (playerImpl.getScreenWidth() - playerImpl.getPopupWidth())) { + posX = (int) (playerImpl.getScreenWidth() - playerImpl.getPopupWidth()); + } else if (posX < 0) { + posX = 0; + } + + if (posY > (playerImpl.getScreenHeight() - playerImpl.getPopupHeight())) { + posY = (int) (playerImpl.getScreenHeight() - playerImpl.getPopupHeight()); + } else if (posY < 0) { + posY = 0; + } + + playerImpl.getPopupLayoutParams().x = (int) posX; + playerImpl.getPopupLayoutParams().y = (int) posY; + + final View closingOverlayView = playerImpl.getClosingOverlayView(); + if (playerImpl.isInsideClosingRadius(movingEvent)) { + if (closingOverlayView.getVisibility() == View.GONE) { + animateView(closingOverlayView, true, 250); + } + } else { + if (closingOverlayView.getVisibility() == View.VISIBLE) { + animateView(closingOverlayView, false, 0); + } + } + +// if (DEBUG) { +// Log.d(TAG, "onScrollInPopup = " +// + "e1.getRaw = [" + initialEvent.getRawX() + ", " +// + initialEvent.getRawY() + "], " +// + "e1.getX,Y = [" + initialEvent.getX() + ", " +// + initialEvent.getY() + "], " +// + "e2.getRaw = [" + movingEvent.getRawX() + ", " +// + movingEvent.getRawY() + "], " +// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], " +// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], " +// + "posX,Y = [" + posX + ", " + posY + "], " +// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]"); +// } + playerImpl.windowManager + .updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams()); + return true; + } + + private void onScrollEndInPopup(final MotionEvent event) { + if (playerImpl == null) { + return; + } + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + + if (playerImpl.isInsideClosingRadius(event)) { + playerImpl.closePopup(); + } else { + animateView(playerImpl.getClosingOverlayView(), false, 0); + + if (!playerImpl.isPopupClosing) { + animateView(playerImpl.getCloseOverlayButton(), false, 200); + } + } + } + + private boolean onFlingInPopup(final MotionEvent e1, + final MotionEvent e2, + final float velocityX, + final float velocityY) { + if (playerImpl == null) { + return false; + } + + final float absVelocityX = Math.abs(velocityX); + final float absVelocityY = Math.abs(velocityY); + if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) { + if (absVelocityX > tossFlingVelocity) { + playerImpl.getPopupLayoutParams().x = (int) velocityX; + } + if (absVelocityY > tossFlingVelocity) { + playerImpl.getPopupLayoutParams().y = (int) velocityY; + } + playerImpl.checkPopupPositionBounds(); + playerImpl.windowManager + .updateViewLayout(playerImpl.getRootView(), playerImpl.getPopupLayoutParams()); + return true; + } + return false; + } + + private boolean onTouchInPopup(final View v, final MotionEvent event) { + if (playerImpl == null) { + return false; + } + playerImpl.getGestureDetector().onTouchEvent(event); + + if (event.getPointerCount() == 2 && !isMovingInPopup && !isResizing) { + if (DEBUG) { + Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing."); + } + playerImpl.showAndAnimateControl(-1, true); + playerImpl.getLoadingPanel().setVisibility(View.GONE); + + playerImpl.hideControls(0, 0); + animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); + animateView(playerImpl.getResizingIndicator(), true, 200, 0); + //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 = Math.hypot(initFirstPointerX - initSecPointerX, + initFirstPointerY - initSecPointerY); + + isResizing = true; + } + + if (event.getAction() == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) { + if (DEBUG) { + Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], " + + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + } + return handleMultiDrag(event); + } + + if (event.getAction() == MotionEvent.ACTION_UP) { + if (DEBUG) { + Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], " + + "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]"); + } + if (isMovingInPopup) { + isMovingInPopup = false; + onScrollEndInPopup(event); + } + + if (isResizing) { + isResizing = false; + + initPointerDistance = -1; + initFirstPointerX = -1; + initFirstPointerY = -1; + initSecPointerX = -1; + initSecPointerY = -1; + + animateView(playerImpl.getResizingIndicator(), false, 100, 0); + playerImpl.changeState(playerImpl.getCurrentState()); + } + + if (!playerImpl.isPopupClosing) { + playerImpl.savePositionAndSize(); + } + } + + v.performClick(); + return true; + } + + private boolean handleMultiDrag(final MotionEvent event) { + if (initPointerDistance != -1 && event.getPointerCount() == 2) { + // get the movements of the fingers + final double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX, + event.getY(0) - initFirstPointerY); + final double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX, + event.getY(1) - initSecPointerY); + + // minimum threshold beyond which pinch gesture will work + final int minimumMove = ViewConfiguration.get(service).getScaledTouchSlop(); + + if (Math.max(firstPointerMove, secPointerMove) > minimumMove) { + // calculate current distance between the pointers + final double currentPointerDistance = + Math.hypot(event.getX(0) - event.getX(1), + event.getY(0) - event.getY(1)); + + final double popupWidth = playerImpl.getPopupWidth(); + // change co-ordinates of popup so the center stays at the same position + final double newWidth = (popupWidth * currentPointerDistance / initPointerDistance); + initPointerDistance = currentPointerDistance; + playerImpl.getPopupLayoutParams().x += (popupWidth - newWidth) / 2; + + playerImpl.checkPopupPositionBounds(); + playerImpl.updateScreenSize(); + + playerImpl.updatePopupSize( + (int) Math.min(playerImpl.getScreenWidth(), newWidth), + -1); + return true; + } + } + return false; + } + + + /* + * Utils + * */ + + private int getNavigationBarHeight(final Context context) { + final int resId = context.getResources() + .getIdentifier("navigation_bar_height", "dimen", "android"); + if (resId > 0) { + return context.getResources().getDimensionPixelSize(resId); + } + return 0; + } + + private int getStatusBarHeight(final Context context) { + final int resId = context.getResources() + .getIdentifier("status_bar_height", "dimen", "android"); + if (resId > 0) { + return context.getResources().getDimensionPixelSize(resId); + } + return 0; + } +} + + diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java new file mode 100644 index 000000000..f8d03087e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java @@ -0,0 +1,15 @@ +package org.schabi.newpipe.player.event; + +import com.google.android.exoplayer2.ExoPlaybackException; + +public interface PlayerServiceEventListener extends PlayerEventListener { + void onFullscreenStateChanged(boolean fullscreen); + + void onScreenRotationButtonClicked(); + + void onMoreOptionsLongClicked(); + + void onPlayerError(ExoPlaybackException error); + + void hideSystemUiIfNeeded(); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 369e3236e..f434b0621 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -114,7 +114,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An private void onAudioFocusGain() { Log.d(TAG, "onAudioFocusGain() called"); player.setVolume(DUCK_AUDIO_TO); - animateAudio(DUCK_AUDIO_TO, 1f); + animateAudio(DUCK_AUDIO_TO, 1.0f); if (PlayerHelper.isResumeAfterAudioFocusGain(context)) { player.setPlayWhenReady(true); @@ -133,7 +133,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An } private void animateAudio(final float from, final float to) { - ValueAnimator valueAnimator = new ValueAnimator(); + final ValueAnimator valueAnimator = new ValueAnimator(); valueAnimator.setFloatValues(from, to); valueAnimator.setDuration(AudioReactor.DUCK_DURATION); valueAnimator.addListener(new AnimatorListenerAdapter() { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index 2ef22f2eb..9703a3588 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -80,13 +80,13 @@ import java.io.File; } try { - for (File file : cacheDir.listFiles()) { + for (final File file : cacheDir.listFiles()) { final String filePath = file.getAbsolutePath(); final boolean deleteSuccessful = file.delete(); Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful); } - } catch (Exception ignored) { + } catch (final Exception ignored) { Log.e(TAG, "Failed to delete file.", ignored); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index 92ae009f6..e164e0563 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -29,7 +29,7 @@ public class LoadController implements LoadControl { final int optimalPlaybackBufferMs) { this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000; - DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder(); + final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder(); builder.setBufferDurationsMs(minimumPlaybackbufferMs, optimalPlaybackBufferMs, initialPlaybackBufferMs, initialPlaybackBufferMs); internalLoadControl = builder.createDefaultLoadControl(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index e101e2185..849593e89 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -62,7 +62,7 @@ public class MediaSessionManager { .build() ); - MediaStyle mediaStyle = new MediaStyle() + final MediaStyle mediaStyle = new MediaStyle() .setMediaSession(mediaSession.getSessionToken()); builder.setStyle(mediaStyle); @@ -76,7 +76,7 @@ public class MediaSessionManager { .build() ); - MediaStyle mediaStyle = new MediaStyle() + final MediaStyle mediaStyle = new MediaStyle() .setMediaSession(mediaSession.getSessionToken()); builder.setStyle(mediaStyle); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 0d511d565..8b2c0e925 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.player.helper; import android.app.Dialog; import android.content.Context; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.View; import android.widget.CheckBox; @@ -92,8 +92,10 @@ public class PlaybackParameterDialog extends DialogFragment { public static PlaybackParameterDialog newInstance(final double playbackTempo, final double playbackPitch, - final boolean playbackSkipSilence) { - PlaybackParameterDialog dialog = new PlaybackParameterDialog(); + final boolean playbackSkipSilence, + final Callback callback) { + final PlaybackParameterDialog dialog = new PlaybackParameterDialog(); + dialog.callback = callback; dialog.initialTempo = playbackTempo; dialog.initialPitch = playbackPitch; @@ -111,9 +113,9 @@ public class PlaybackParameterDialog extends DialogFragment { @Override public void onAttach(final Context context) { super.onAttach(context); - if (context != null && context instanceof Callback) { + if (context instanceof Callback) { callback = (Callback) context; - } else { + } else if (callback == null) { dismiss(); } } @@ -185,8 +187,8 @@ public class PlaybackParameterDialog extends DialogFragment { private void setupTempoControl(@NonNull final View rootView) { tempoSlider = rootView.findViewById(R.id.tempoSeekbar); - TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText); - TextView tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText); + final TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText); + final TextView tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText); tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText); tempoStepUpText = rootView.findViewById(R.id.tempoStepUp); tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); @@ -210,8 +212,8 @@ public class PlaybackParameterDialog extends DialogFragment { private void setupPitchControl(@NonNull final View rootView) { pitchSlider = rootView.findViewById(R.id.pitchSeekbar); - TextView pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText); - TextView pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText); + final TextView pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText); + final TextView pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText); pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText); pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); @@ -237,12 +239,13 @@ public class PlaybackParameterDialog extends DialogFragment { unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); if (unhookingCheckbox != null) { // restore whether pitch and tempo are unhooked or not - unhookingCheckbox.setChecked(PreferenceManager.getDefaultSharedPreferences(getContext()) + unhookingCheckbox.setChecked(PreferenceManager + .getDefaultSharedPreferences(requireContext()) .getBoolean(getString(R.string.playback_unhook_key), true)); unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { // save whether pitch and tempo are unhooked or not - PreferenceManager.getDefaultSharedPreferences(getContext()) + PreferenceManager.getDefaultSharedPreferences(requireContext()) .edit() .putBoolean(getString(R.string.playback_unhook_key), isChecked) .apply(); @@ -267,12 +270,12 @@ public class PlaybackParameterDialog extends DialogFragment { } private void setupStepSizeSelector(@NonNull final View rootView) { - TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent); - TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent); - TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent); - TextView stepSizeTwentyFivePercentText = rootView + final TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent); + final TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent); + final TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent); + final TextView stepSizeTwentyFivePercentText = rootView .findViewById(R.id.stepSizeTwentyFivePercent); - TextView stepSizeOneHundredPercentText = rootView + final TextView stepSizeOneHundredPercentText = rootView .findViewById(R.id.stepSizeOneHundredPercent); if (stepSizeOnePercentText != null) { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index db98ee6d3..6667d0ecb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -2,8 +2,8 @@ package org.schabi.newpipe.player.helper; import android.content.Context; import android.content.SharedPreferences; -import android.os.Build; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; +import android.provider.Settings; import android.view.accessibility.CaptioningManager; import androidx.annotation.IntDef; @@ -45,6 +45,9 @@ import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MOD import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; import static java.lang.annotation.RetentionPolicy.SOURCE; +import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; +import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI; +import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER; import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; @@ -56,6 +59,15 @@ public final class PlayerHelper { private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x"); private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%"); + @Retention(SOURCE) + @IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI, + AUTOPLAY_TYPE_NEVER}) + public @interface AutoplayType { + int AUTOPLAY_TYPE_ALWAYS = 0; + int AUTOPLAY_TYPE_WIFI = 1; + int AUTOPLAY_TYPE_NEVER = 2; + } + private PlayerHelper() { } //////////////////////////////////////////////////////////////////////////// @@ -63,10 +75,10 @@ public final class PlayerHelper { //////////////////////////////////////////////////////////////////////////// public static String getTimeString(final int milliSeconds) { - int seconds = (milliSeconds % 60000) / 1000; - int minutes = (milliSeconds % 3600000) / 60000; - int hours = (milliSeconds % 86400000) / 3600000; - int days = (milliSeconds % (86400000 * 7)) / 86400000; + final int seconds = (milliSeconds % 60000) / 1000; + final int minutes = (milliSeconds % 3600000) / 60000; + final int hours = (milliSeconds % 86400000) / 3600000; + final int days = (milliSeconds % (86400000 * 7)) / 86400000; STRING_BUILDER.setLength(0); return days > 0 @@ -132,17 +144,17 @@ public final class PlayerHelper { } /** - * Given a {@link StreamInfo} and the existing queue items, provide the - * {@link SinglePlayQueue} consisting of the next video for auto queuing. + * Given a {@link StreamInfo} and the existing queue items, + * provide the {@link SinglePlayQueue} consisting of the next video for auto queueing. *

- * This method detects and prevents cycle by naively checking if a - * candidate next video's url already exists in the existing items. + * This method detects and prevents cycles by naively checking + * if a candidate next video's url already exists in the existing items. *

*

- * To select the next video, {@link StreamInfo#getNextVideo()} is first - * checked. If it is nonnull and is not part of the existing items, then - * it will be used as the next video. Otherwise, an random item with - * non-repeating url will be selected from the {@link StreamInfo#getRelatedStreams()}. + * The first item in {@link StreamInfo#getRelatedStreams()} is checked first. + * If it is non-null and is not part of the existing items, it will be used as the next stream. + * Otherwise, a random item with non-repeating url will be selected + * from the {@link StreamInfo#getRelatedStreams()}. *

* * @param info currently playing stream @@ -152,27 +164,28 @@ public final class PlayerHelper { @Nullable public static PlayQueue autoQueueOf(@NonNull final StreamInfo info, @NonNull final List existingItems) { - Set urls = new HashSet<>(existingItems.size()); + final Set urls = new HashSet<>(existingItems.size()); for (final PlayQueueItem item : existingItems) { urls.add(item.getUrl()); } - final StreamInfoItem nextVideo = info.getNextVideo(); - if (nextVideo != null && !urls.contains(nextVideo.getUrl())) { - return getAutoQueuedSinglePlayQueue(nextVideo); - } - final List relatedItems = info.getRelatedStreams(); if (relatedItems == null) { return null; } - List autoQueueItems = new ArrayList<>(); - for (final InfoItem item : info.getRelatedStreams()) { + if (relatedItems.get(0) != null && relatedItems.get(0) instanceof StreamInfoItem + && !urls.contains(relatedItems.get(0).getUrl())) { + return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0)); + } + + final List autoQueueItems = new ArrayList<>(); + for (final InfoItem item : relatedItems) { if (item instanceof StreamInfoItem && !urls.contains(item.getUrl())) { autoQueueItems.add((StreamInfoItem) item); } } + Collections.shuffle(autoQueueItems); return autoQueueItems.isEmpty() ? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0)); @@ -202,6 +215,11 @@ public final class PlayerHelper { return isAutoQueueEnabled(context, false); } + public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) { + return getPreferences(context) + .getBoolean(context.getString(R.string.clear_queue_confirmation_key), false); + } + @MinimizeMode public static int getMinimizeOnExitAction(@NonNull final Context context) { final String defaultAction = context.getString(R.string.minimize_on_exit_none_key); @@ -218,6 +236,18 @@ public final class PlayerHelper { } } + @AutoplayType + public static int getAutoplayType(@NonNull final Context context) { + final String type = getAutoplayType(context, context.getString(R.string.autoplay_wifi_key)); + if (type.equals(context.getString(R.string.autoplay_always_key))) { + return AUTOPLAY_TYPE_ALWAYS; + } else if (type.equals(context.getString(R.string.autoplay_never_key))) { + return AUTOPLAY_TYPE_NEVER; + } else { + return AUTOPLAY_TYPE_WIFI; + } + } + @NonNull public static SeekParameters getSeekParameters(@NonNull final Context context) { return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT; @@ -272,10 +302,6 @@ public final class PlayerHelper { @NonNull public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - return CaptionStyleCompat.DEFAULT; - } - final CaptioningManager captioningManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); if (captioningManager == null || !captioningManager.isEnabled()) { @@ -300,14 +326,10 @@ public final class PlayerHelper { * @return caption scaling */ public static float getCaptionScale(@NonNull final Context context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - return 1f; - } - final CaptioningManager captioningManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); if (captioningManager == null || !captioningManager.isEnabled()) { - return 1f; + return 1.0f; } return captioningManager.getFontScale(); @@ -323,6 +345,13 @@ public final class PlayerHelper { setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis()); } + public static boolean globalScreenOrientationLocked(final Context context) { + // 1: Screen orientation changes using accelerometer + // 0: Screen orientation is locked + return android.provider.Settings.System.getInt( + context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 0; + } + //////////////////////////////////////////////////////////////////////////// // Private helpers //////////////////////////////////////////////////////////////////////////// @@ -367,7 +396,7 @@ public final class PlayerHelper { private static void setScreenBrightness(@NonNull final Context context, final float screenBrightness, final long timestamp) { - SharedPreferences.Editor editor = getPreferences(context).edit(); + final SharedPreferences.Editor editor = getPreferences(context).edit(); editor.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness); editor.putLong(context.getString(R.string.screen_brightness_timestamp_key), timestamp); editor.apply(); @@ -375,8 +404,8 @@ public final class PlayerHelper { private static float getScreenBrightness(@NonNull final Context context, final float screenBrightness) { - SharedPreferences sp = getPreferences(context); - long timestamp = sp + final SharedPreferences sp = getPreferences(context); + final long timestamp = sp .getLong(context.getString(R.string.screen_brightness_timestamp_key), 0); // Hypothesis: 4h covers a viewing block, e.g. evening. // External lightning conditions will change in the next @@ -395,9 +424,15 @@ public final class PlayerHelper { .getString(context.getString(R.string.minimize_on_exit_key), key); } + private static String getAutoplayType(@NonNull final Context context, + final String key) { + return getPreferences(context).getString(context.getString(R.string.autoplay_key), + key); + } + private static SinglePlayQueue getAutoQueuedSinglePlayQueue( final StreamInfoItem streamInfoItem) { - SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem); + final SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem); singlePlayQueue.getItem().setAutoQueued(true); return singlePlayQueue; } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java index 1f1152b62..764c375af 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -87,13 +87,13 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator } // Yes this is almost a copypasta, got a problem with that? =\ - int windowCount = callback.getQueueSize(); - int currentWindowIndex = callback.getCurrentPlayingIndex(); - int queueSize = Math.min(maxQueueSize, windowCount); - int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, + final int windowCount = callback.getQueueSize(); + final int currentWindowIndex = callback.getCurrentPlayingIndex(); + final int queueSize = Math.min(maxQueueSize, windowCount); + final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, windowCount - queueSize); - List queue = new ArrayList<>(); + final List queue = new ArrayList<>(); for (int i = startIndex; i < startIndex + queueSize; i++) { queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i)); } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java index 0154716e0..5b20077c3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java @@ -57,13 +57,14 @@ public class BasePlayerMediaSession implements MediaSessionCallback { } final PlayQueueItem item = player.getPlayQueue().getItem(index); - MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder() + final MediaDescriptionCompat.Builder descriptionBuilder + = new MediaDescriptionCompat.Builder() .setMediaId(String.valueOf(index)) .setTitle(item.getTitle()) .setSubtitle(item.getUploader()); // set additional metadata for A2DP/AVRCP - Bundle additionalMetadata = new Bundle(); + final Bundle additionalMetadata = new Bundle(); additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle()); additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader()); additionalMetadata diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java index e554059d9..d70707fdb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java @@ -60,14 +60,14 @@ public class CustomTrackSelector extends DefaultTrackSelector { TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { - TrackGroup trackGroup = groups.get(groupIndex); - @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; + final TrackGroup trackGroup = groups.get(groupIndex); + @Capabilities final int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { - Format format = trackGroup.getFormat(trackIndex); - TextTrackScore trackScore = new TextTrackScore(format, params, + final Format format = trackGroup.getFormat(trackIndex); + final TextTrackScore trackScore = new TextTrackScore(format, params, trackFormatSupport[trackIndex], selectedAudioLanguage); if (formatHasLanguage(format, preferredTextLanguage)) { diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java index f0d6dc6ec..ec364c4df 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java @@ -2,9 +2,12 @@ package org.schabi.newpipe.player.playqueue; import android.util.Log; +import androidx.annotation.NonNull; + import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import java.util.ArrayList; @@ -12,7 +15,6 @@ import java.util.Collections; import java.util.List; import io.reactivex.SingleObserver; -import io.reactivex.annotations.NonNull; import io.reactivex.disposables.Disposable; abstract class AbstractInfoPlayQueue extends PlayQueue { @@ -21,7 +23,7 @@ abstract class AbstractInfoPlayQueue ext final int serviceId; final String baseUrl; - String nextUrl; + Page nextPage; private transient Disposable fetchReactor; @@ -29,16 +31,16 @@ abstract class AbstractInfoPlayQueue ext this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0); } - AbstractInfoPlayQueue(final int serviceId, final String url, final String nextPageUrl, + AbstractInfoPlayQueue(final int serviceId, final String url, final Page nextPage, final List streams, final int index) { super(index, extractListItems(streams)); this.baseUrl = url; - this.nextUrl = nextPageUrl; + this.nextPage = nextPage; this.serviceId = serviceId; this.isInitial = streams.isEmpty(); - this.isComplete = !isInitial && (nextPageUrl == null || nextPageUrl.isEmpty()); + this.isComplete = !isInitial && !Page.isValid(nextPage); } protected abstract String getTag(); @@ -66,7 +68,7 @@ abstract class AbstractInfoPlayQueue ext if (!result.hasNextPage()) { isComplete = true; } - nextUrl = result.getNextPageUrl(); + nextPage = result.getNextPage(); append(extractListItems(result.getRelatedItems())); @@ -100,7 +102,7 @@ abstract class AbstractInfoPlayQueue ext if (!result.hasNextPage()) { isComplete = true; } - nextUrl = result.getNextPageUrl(); + nextPage = result.getNextPage(); append(extractListItems(result.getItems())); @@ -127,7 +129,7 @@ abstract class AbstractInfoPlayQueue ext } private static List extractListItems(final List infos) { - List result = new ArrayList<>(); + final List result = new ArrayList<>(); for (final InfoItem stream : infos) { if (stream instanceof StreamInfoItem) { result.add(new PlayQueueItem((StreamInfoItem) stream)); diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java index 5a2e34d31..9e0d2b694 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.playqueue; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; @@ -17,15 +18,15 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue streams, final int index) { - super(serviceId, url, nextPageUrl, streams, index); + super(serviceId, url, nextPage, streams, index); } @Override @@ -41,7 +42,7 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue backup; private ArrayList streams; @NonNull private final AtomicInteger queueIndex; + private final ArrayList history; private transient BehaviorSubject eventBroadcast; private transient Flowable broadcastReceiver; private transient Subscription reportingReactor; + private transient boolean disposed; + PlayQueue(final int index, final List startWith) { streams = new ArrayList<>(); streams.addAll(startWith); + history = new ArrayList<>(); + if (streams.size() > index) { + history.add(streams.get(index)); + } queueIndex = new AtomicInteger(index); + disposed = false; } /*////////////////////////////////////////////////////////////////////////// @@ -99,6 +107,7 @@ public abstract class PlayQueue implements Serializable { eventBroadcast = null; broadcastReceiver = null; reportingReactor = null; + disposed = true; } /** @@ -149,6 +158,9 @@ public abstract class PlayQueue implements Serializable { if (index >= streams.size()) { newIndex = isComplete() ? index % streams.size() : streams.size() - 1; } + if (oldIndex != newIndex) { + history.add(streams.get(newIndex)); + } queueIndex.set(newIndex); broadcast(new SelectEvent(oldIndex, newIndex)); @@ -269,7 +281,7 @@ public abstract class PlayQueue implements Serializable { * @param items {@link PlayQueueItem}s to append */ public synchronized void append(@NonNull final List items) { - List itemList = new ArrayList<>(items); + final List itemList = new ArrayList<>(items); if (isShuffled()) { backup.addAll(itemList); @@ -314,6 +326,9 @@ public abstract class PlayQueue implements Serializable { public synchronized void error() { final int oldIndex = getIndex(); queueIndex.incrementAndGet(); + if (streams.size() > queueIndex.get()) { + history.add(streams.get(queueIndex.get())); + } broadcast(new ErrorEvent(oldIndex, getIndex())); } @@ -334,7 +349,11 @@ public abstract class PlayQueue implements Serializable { if (backup != null) { backup.remove(getItem(removeIndex)); } - streams.remove(removeIndex); + + history.remove(streams.remove(removeIndex)); + if (streams.size() > queueIndex.get()) { + history.add(streams.get(queueIndex.get())); + } } /** @@ -367,7 +386,7 @@ public abstract class PlayQueue implements Serializable { queueIndex.incrementAndGet(); } - PlayQueueItem playQueueItem = streams.remove(source); + final PlayQueueItem playQueueItem = streams.remove(source); playQueueItem.setAutoQueued(false); streams.add(target, playQueueItem); broadcast(new MoveEvent(source, target)); @@ -427,6 +446,9 @@ public abstract class PlayQueue implements Serializable { streams.add(0, streams.remove(newIndex)); } queueIndex.set(0); + if (streams.size() > 0) { + history.add(streams.get(0)); + } broadcast(new ReorderEvent(originIndex, queueIndex.get())); } @@ -458,10 +480,60 @@ public abstract class PlayQueue implements Serializable { } else { queueIndex.set(0); } + if (streams.size() > queueIndex.get()) { + history.add(streams.get(queueIndex.get())); + } broadcast(new ReorderEvent(originIndex, queueIndex.get())); } + /** + * Selects previous played item. + * + * This method removes currently playing item from history and + * starts playing the last item from history if it exists + * + * @return true if history is not empty and the item can be played + * */ + public synchronized boolean previous() { + if (history.size() <= 1) { + return false; + } + + history.remove(history.size() - 1); + + final PlayQueueItem last = history.remove(history.size() - 1); + setIndex(indexOf(last)); + + return true; + } + + /* + * Compares two PlayQueues. Useful when a user switches players but queue is the same so + * we don't have to do anything with new queue. + * This method also gives a chance to track history of items in a queue in + * VideoDetailFragment without duplicating items from two identical queues + * */ + @Override + public boolean equals(@Nullable final Object obj) { + if (!(obj instanceof PlayQueue) + || getStreams().size() != ((PlayQueue) obj).getStreams().size()) { + return false; + } + + final PlayQueue other = (PlayQueue) obj; + for (int i = 0; i < getStreams().size(); i++) { + if (!getItem(i).getUrl().equals(other.getItem(i).getUrl())) { + return false; + } + } + + return true; + } + + public boolean isDisposed() { + return disposed; + } /*////////////////////////////////////////////////////////////////////////// // Rx Broadcast //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java index f8777597a..3b42f2745 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java @@ -6,6 +6,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import org.schabi.newpipe.R; @@ -20,7 +21,6 @@ import org.schabi.newpipe.util.FallbackViewHolder; import java.util.List; import io.reactivex.Observer; -import io.reactivex.annotations.NonNull; import io.reactivex.disposables.Disposable; /** diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java index fcb7080c5..077702747 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlaylistPlayQueue.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.player.playqueue; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; @@ -16,15 +17,15 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue streams, final int index) { - super(serviceId, url, nextPageUrl, streams, index); + super(serviceId, url, nextPage, streams, index); } @Override @@ -40,7 +41,7 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue playQueueItemsOf(final List items) { - List playQueueItems = new ArrayList<>(items.size()); + final List playQueueItems = new ArrayList<>(items.size()); for (final StreamInfoItem item : items) { playQueueItems.add(new PlayQueueItem(item)); } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index 2eb766769..a2b3a1d3d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -52,7 +52,7 @@ public class VideoPlaybackResolver implements PlaybackResolver { return liveSource; } - List mediaSources = new ArrayList<>(); + final List mediaSources = new ArrayList<>(); // Create video stream source final List videos = ListHelper.getSortedStreamVideosList(context, @@ -106,7 +106,7 @@ public class VideoPlaybackResolver implements PlaybackResolver { SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); final MediaSource textSource = dataSource.getSampleMediaSourceFactory() - .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); + .createMediaSource(Uri.parse(subtitle.getUrl()), textFormat, TIME_UNSET); mediaSources.add(textSource); } } @@ -115,7 +115,7 @@ public class VideoPlaybackResolver implements PlaybackResolver { return mediaSources.get(0); } else { return new MergingMediaSource(mediaSources.toArray( - new MediaSource[mediaSources.size()])); + new MediaSource[0])); } } diff --git a/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java b/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java index f4c1c4ac8..2655ea672 100644 --- a/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java +++ b/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java @@ -4,9 +4,12 @@ import android.content.Context; import androidx.annotation.NonNull; +import com.google.auto.service.AutoService; + import org.acra.config.CoreConfiguration; import org.acra.sender.ReportSender; import org.acra.sender.ReportSenderFactory; +import org.schabi.newpipe.App; /* * Created by Christian Schabesberger on 13.09.16. @@ -28,6 +31,10 @@ import org.acra.sender.ReportSenderFactory; * along with NewPipe. If not, see . */ +/** + * Used by ACRA in {@link App}.initAcra() as the factory for report senders. + */ +@AutoService(ReportSenderFactory.class) public class AcraReportSenderFactory implements ReportSenderFactory { @NonNull public ReportSender create(@NonNull final Context context, diff --git a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java index 20724c02b..b83f9fd97 100644 --- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java @@ -11,7 +11,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; -import android.preference.PreferenceManager; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -20,6 +19,7 @@ import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -37,6 +37,8 @@ import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import java.io.PrintWriter; @@ -45,7 +47,6 @@ import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.List; -import java.util.Locale; import java.util.TimeZone; import java.util.Vector; @@ -81,6 +82,10 @@ public class ErrorActivity extends AppCompatActivity { public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME; + + public static final String ERROR_GITHUB_ISSUE_URL + = "https://github.com/TeamNewPipe/NewPipe/issues"; + private String[] errorList; private ErrorInfo errorInfo; private Class returnActivity; @@ -107,9 +112,9 @@ public class ErrorActivity extends AppCompatActivity { private static void startErrorActivity(final Class returnActivity, final Context context, final ErrorInfo errorInfo, final List el) { - ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); + final ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); ac.setReturnActivity(returnActivity); - Intent intent = new Intent(context, ErrorActivity.class); + final Intent intent = new Intent(context, ErrorActivity.class); intent.putExtra(ERROR_INFO, errorInfo); intent.putExtra(ERROR_LIST, elToSl(el)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -149,9 +154,9 @@ public class ErrorActivity extends AppCompatActivity { public static void reportError(final Context context, final CrashReportData report, final ErrorInfo errorInfo) { - String[] el = new String[]{report.getString(ReportField.STACK_TRACE)}; + final String[] el = new String[]{report.getString(ReportField.STACK_TRACE)}; - Intent intent = new Intent(context, ErrorActivity.class); + final Intent intent = new Intent(context, ErrorActivity.class); intent.putExtra(ERROR_INFO, errorInfo); intent.putExtra(ERROR_LIST, el); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -167,7 +172,7 @@ public class ErrorActivity extends AppCompatActivity { // errorList to StringList private static String[] elToSl(final List stackTraces) { - String[] out = new String[stackTraces.size()]; + final String[] out = new String[stackTraces.size()]; for (int i = 0; i < stackTraces.size(); i++) { out[i] = getStackTrace(stackTraces.get(i)); } @@ -181,64 +186,48 @@ public class ErrorActivity extends AppCompatActivity { ThemeHelper.setTheme(this); setContentView(R.layout.activity_error); - Intent intent = getIntent(); + final Intent intent = getIntent(); - Toolbar toolbar = findViewById(R.id.toolbar); + final Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - ActionBar actionBar = getSupportActionBar(); + final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.error_report_title); actionBar.setDisplayShowTitleEnabled(true); } - Button reportButton = findViewById(R.id.errorReportButton); - userCommentBox = findViewById(R.id.errorCommentBox); - TextView errorView = findViewById(R.id.errorView); - TextView infoView = findViewById(R.id.errorInfosView); - TextView errorMessageView = findViewById(R.id.errorMessageView); + final Button reportEmailButton = findViewById(R.id.errorReportEmailButton); + final Button copyButton = findViewById(R.id.errorReportCopyButton); + final Button reportGithubButton = findViewById(R.id.errorReportGitHubButton); - ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); + userCommentBox = findViewById(R.id.errorCommentBox); + final TextView errorView = findViewById(R.id.errorView); + final TextView infoView = findViewById(R.id.errorInfosView); + final TextView errorMessageView = findViewById(R.id.errorMessageView); + + final ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); returnActivity = ac.getReturnActivity(); errorInfo = intent.getParcelableExtra(ERROR_INFO); errorList = intent.getStringArrayExtra(ERROR_LIST); // important add guru meditation - addGuruMeditaion(); + addGuruMeditation(); currentTimeStamp = getCurrentTimeStamp(); - reportButton.setOnClickListener((View v) -> { - Context context = this; - new AlertDialog.Builder(context) - .setIcon(android.R.drawable.ic_dialog_alert) - .setTitle(R.string.privacy_policy_title) - .setMessage(R.string.start_accept_privacy_policy) - .setCancelable(false) - .setNeutralButton(R.string.read_privacy_policy, (dialog, which) -> { - Intent webIntent = new Intent(Intent.ACTION_VIEW, - Uri.parse(context.getString(R.string.privacy_policy_url)) - ); - context.startActivity(webIntent); - }) - .setPositiveButton(R.string.accept, (dialog, which) -> { - final Intent i = new Intent(Intent.ACTION_SENDTO) - .setData(Uri.parse("mailto:")) // only email apps should handle this - .putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS}) - .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) - .putExtra(Intent.EXTRA_TEXT, buildJson()); - if (i.resolveActivity(getPackageManager()) != null) { - startActivity(i); - } - - }) - .setNegativeButton(R.string.decline, (dialog, which) -> { - // do nothing - }) - .show(); + reportEmailButton.setOnClickListener((View v) -> + openPrivacyPolicyDialog(this, "EMAIL")); + copyButton.setOnClickListener((View v) -> { + ShareUtils.copyToClipboard(this, buildMarkdown()); + Toast.makeText(this, R.string.msg_copied, Toast.LENGTH_SHORT).show(); }); + reportGithubButton.setOnClickListener((View v) -> + openPrivacyPolicyDialog(this, "GITHUB")); + + // normal bugreport buildInfo(errorInfo); if (errorInfo.message != 0) { @@ -250,28 +239,28 @@ public class ErrorActivity extends AppCompatActivity { errorView.setText(formErrorText(errorList)); - //print stack trace once again for debugging: - for (String e : errorList) { + // print stack trace once again for debugging: + for (final String e : errorList) { Log.e(TAG, e); } } @Override public boolean onCreateOptionsMenu(final Menu menu) { - MenuInflater inflater = getMenuInflater(); + final MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.error_menu, menu); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { - int id = item.getItemId(); + final int id = item.getItemId(); switch (id) { case android.R.id.home: goToReturnActivity(); break; case R.id.menu_item_share_error: - Intent intent = new Intent(); + final Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_TEXT, buildJson()); intent.setType("text/plain"); @@ -281,10 +270,40 @@ public class ErrorActivity extends AppCompatActivity { return false; } + private void openPrivacyPolicyDialog(final Context context, final String action) { + new AlertDialog.Builder(context) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.privacy_policy_title) + .setMessage(R.string.start_accept_privacy_policy) + .setCancelable(false) + .setNeutralButton(R.string.read_privacy_policy, (dialog, which) -> + ShareUtils.openUrlInBrowser(context, + context.getString(R.string.privacy_policy_url))) + .setPositiveButton(R.string.accept, (dialog, which) -> { + if (action.equals("EMAIL")) { // send on email + final Intent i = new Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse("mailto:")) // only email apps should handle this + .putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS}) + .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) + .putExtra(Intent.EXTRA_TEXT, buildJson()); + if (i.resolveActivity(getPackageManager()) != null) { + startActivity(i); + } + } else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub + ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL); + } + + }) + .setNegativeButton(R.string.decline, (dialog, which) -> { + // do nothing + }) + .show(); + } + private String formErrorText(final String[] el) { - StringBuilder text = new StringBuilder(); + final StringBuilder text = new StringBuilder(); if (el != null) { - for (String e : el) { + for (final String e : el) { text.append("-------------------------------------\n").append(e); } } @@ -312,26 +331,28 @@ public class ErrorActivity extends AppCompatActivity { } private void goToReturnActivity() { - Class checkedReturnActivity = getReturnActivity(returnActivity); + final Class checkedReturnActivity = getReturnActivity(returnActivity); if (checkedReturnActivity == null) { super.onBackPressed(); } else { - Intent intent = new Intent(this, checkedReturnActivity); + final Intent intent = new Intent(this, checkedReturnActivity); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); NavUtils.navigateUpTo(this, intent); } } private void buildInfo(final ErrorInfo info) { - TextView infoLabelView = findViewById(R.id.errorInfoLabelsView); - TextView infoView = findViewById(R.id.errorInfosView); + final TextView infoLabelView = findViewById(R.id.errorInfoLabelsView); + final TextView infoView = findViewById(R.id.errorInfosView); String text = ""; infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n")); text += getUserActionString(info.userAction) + "\n" + info.request + "\n" - + getContentLangString() + "\n" + + getContentLanguageString() + "\n" + + getContentCountryString() + "\n" + + getAppLanguage() + "\n" + info.serviceName + "\n" + currentTimeStamp + "\n" + getPackageName() + "\n" @@ -347,7 +368,9 @@ public class ErrorActivity extends AppCompatActivity { .object() .value("user_action", getUserActionString(errorInfo.userAction)) .value("request", errorInfo.request) - .value("content_language", getContentLangString()) + .value("content_language", getContentLanguageString()) + .value("content_country", getContentCountryString()) + .value("app_language", getAppLanguage()) .value("service", errorInfo.serviceName) .value("package", getPackageName()) .value("version", BuildConfig.VERSION_NAME) @@ -357,7 +380,7 @@ public class ErrorActivity extends AppCompatActivity { .value("user_comment", userCommentBox.getText().toString()) .end() .done(); - } catch (Throwable e) { + } catch (final Throwable e) { Log.e(TAG, "Error while erroring: Could not build json"); e.printStackTrace(); } @@ -365,6 +388,63 @@ public class ErrorActivity extends AppCompatActivity { return ""; } + private String buildMarkdown() { + try { + final StringBuilder htmlErrorReport = new StringBuilder(); + + final String userComment = userCommentBox.getText().toString(); + if (!userComment.isEmpty()) { + htmlErrorReport.append(userComment).append("\n"); + } + + // basic error info + htmlErrorReport + .append("## Exception") + .append("\n* __User Action:__ ") + .append(getUserActionString(errorInfo.userAction)) + .append("\n* __Request:__ ").append(errorInfo.request) + .append("\n* __Content Country:__ ").append(getContentCountryString()) + .append("\n* __Content Language:__ ").append(getContentLanguageString()) + .append("\n* __App Language:__ ").append(getAppLanguage()) + .append("\n* __Service:__ ").append(errorInfo.serviceName) + .append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME) + .append("\n* __OS:__ ").append(getOsString()).append("\n"); + + + // Collapse all logs to a single paragraph when there are more than one + // to keep the GitHub issue clean. + if (errorList.length > 1) { + htmlErrorReport + .append("
Exceptions (") + .append(errorList.length) + .append(")

\n"); + } + + // add the logs + for (int i = 0; i < errorList.length; i++) { + htmlErrorReport.append("

Crash log "); + if (errorList.length > 1) { + htmlErrorReport.append(i + 1); + } + htmlErrorReport.append("") + .append("

\n") + .append("\n```\n").append(errorList[i]).append("\n```\n") + .append("

\n"); + } + + // make sure to close everything + if (errorList.length > 1) { + htmlErrorReport.append("

\n"); + } + htmlErrorReport.append("
\n"); + return htmlErrorReport.toString(); + } catch (final Throwable e) { + Log.e(TAG, "Error while erroring: Could not build markdown"); + e.printStackTrace(); + return ""; + } + } + private String getUserActionString(final UserAction userAction) { if (userAction == null) { return "Your description is in another castle."; @@ -373,26 +453,30 @@ public class ErrorActivity extends AppCompatActivity { } } - private String getContentLangString() { - String contentLanguage = PreferenceManager.getDefaultSharedPreferences(this) - .getString(this.getString(R.string.content_country_key), "none"); - if (contentLanguage.equals(getString(R.string.default_localization_key))) { - contentLanguage = Locale.getDefault().toString(); - } - return contentLanguage; + private String getContentCountryString() { + return Localization.getPreferredContentCountry(this).getCountryCode(); + } + + private String getContentLanguageString() { + return Localization.getPreferredLocalization(this).getLocalizationCode(); + } + + private String getAppLanguage() { + return Localization.getAppLocale(getApplicationContext()).toString(); } private String getOsString() { - String osBase = Build.VERSION.SDK_INT >= 23 ? Build.VERSION.BASE_OS : "Android"; + final String osBase = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + ? Build.VERSION.BASE_OS : "Android"; return System.getProperty("os.name") + " " + (osBase.isEmpty() ? "Android" : osBase) + " " + Build.VERSION.RELEASE + " - " + Build.VERSION.SDK_INT; } - private void addGuruMeditaion() { + private void addGuruMeditation() { //just an easter egg - TextView sorryView = findViewById(R.id.errorSorryView); + final TextView sorryView = findViewById(R.id.errorSorryView); String text = sorryView.getText().toString(); text += "\n" + getString(R.string.guru_meditation); sorryView.setText(text); @@ -405,7 +489,7 @@ public class ErrorActivity extends AppCompatActivity { } public String getCurrentTimeStamp() { - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); df.setTimeZone(TimeZone.getTimeZone("GMT")); return df.format(new Date()); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index a9531693c..ab875ed5d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -42,7 +42,7 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment { @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - String themeKey = getString(R.string.theme_key); + final String themeKey = getString(R.string.theme_key); startThemeKey = defaultPreferences .getString(themeKey, getString(R.string.default_theme_value)); findPreference(themeKey).setOnPreferenceChangeListener(themePreferenceChange); @@ -64,7 +64,7 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment { if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) { try { startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS)); - } catch (ActivityNotFoundException e) { + } catch (final ActivityNotFoundException e) { Toast.makeText(getActivity(), R.string.general_error, Toast.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index 125931ee1..c49be8c56 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -2,7 +2,7 @@ package org.schabi.newpipe.settings; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.view.View; import androidx.annotation.Nullable; @@ -20,7 +20,7 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { @Override public void onCreate(@Nullable final Bundle savedInstanceState) { - defaultPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + defaultPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); super.onCreate(savedInstanceState); } @@ -39,7 +39,7 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { private void updateTitle() { if (getActivity() instanceof AppCompatActivity) { - ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(getPreferenceScreen().getTitle()); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index b0bb30aa7..df30a21f3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -7,7 +7,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.widget.Toast; @@ -32,7 +32,6 @@ import org.schabi.newpipe.util.ZipHelper; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; @@ -75,7 +74,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { initialSelectedContentCountry = org.schabi.newpipe.util.Localization .getPreferredContentCountry(requireContext()); initialLanguage = PreferenceManager - .getDefaultSharedPreferences(getContext()).getString("app_language_key", "en"); + .getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en"); } @Override @@ -91,7 +90,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } if (preference.getKey().equals(youtubeRestrictedModeEnabledKey)) { - Context context = getContext(); + final Context context = getContext(); if (context != null) { DownloaderImpl.getInstance().updateYoutubeRestrictedModeCookies(context); } else { @@ -105,7 +104,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - String homeDir = getActivity().getApplicationInfo().dataDir; + final String homeDir = getActivity().getApplicationInfo().dataDir; databasesDir = new File(homeDir + "/databases"); newpipeDb = new File(homeDir + "/databases/newpipe.db"); newpipeDbJournal = new File(homeDir + "/databases/newpipe.db-journal"); @@ -117,9 +116,9 @@ public class ContentSettingsFragment extends BasePreferenceFragment { addPreferencesFromResource(R.xml.content_settings); - Preference importDataPreference = findPreference(getString(R.string.import_data)); + final Preference importDataPreference = findPreference(getString(R.string.import_data)); importDataPreference.setOnPreferenceClickListener((Preference p) -> { - Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) + final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) .putExtra(FilePickerActivityHelper.EXTRA_MODE, @@ -128,9 +127,9 @@ public class ContentSettingsFragment extends BasePreferenceFragment { return true; }); - Preference exportDataPreference = findPreference(getString(R.string.export_data)); + final Preference exportDataPreference = findPreference(getString(R.string.export_data)); exportDataPreference.setOnPreferenceClickListener((Preference p) -> { - Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) + final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) .putExtra(FilePickerActivityHelper.EXTRA_MODE, @@ -149,7 +148,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization .getPreferredContentCountry(requireContext()); final String selectedLanguage = PreferenceManager - .getDefaultSharedPreferences(getContext()).getString("app_language_key", "en"); + .getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en"); if (!selectedLocalization.equals(initialSelectedLocalization) || !selectedContentCountry.equals(initialSelectedContentCountry) @@ -175,12 +174,12 @@ public class ContentSettingsFragment extends BasePreferenceFragment { if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH) && resultCode == Activity.RESULT_OK && data.getData() != null) { - String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); + final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); if (requestCode == REQUEST_EXPORT_PATH) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); exportDatabase(path + "/NewPipeData-" + sdf.format(new Date()) + ".zip"); } else { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(R.string.override_current_data) .setPositiveButton(getString(R.string.finish), (DialogInterface d, int id) -> importDatabase(path)) @@ -196,7 +195,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { //checkpoint before export NewPipeDatabase.checkpoint(); - ZipOutputStream outZip = new ZipOutputStream( + final ZipOutputStream outZip = new ZipOutputStream( new BufferedOutputStream( new FileOutputStream(path))); ZipHelper.addFileToZip(outZip, newpipeDb.getPath(), "newpipe.db"); @@ -208,7 +207,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT) .show(); - } catch (Exception e) { + } catch (final Exception e) { onError(e); } } @@ -217,12 +216,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment { ObjectOutputStream output = null; try { output = new ObjectOutputStream(new FileOutputStream(dst)); - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext()); + final SharedPreferences pref + = PreferenceManager.getDefaultSharedPreferences(requireContext()); output.writeObject(pref.getAll()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { + } catch (final IOException e) { e.printStackTrace(); } finally { try { @@ -230,7 +228,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { output.flush(); output.close(); } - } catch (IOException ex) { + } catch (final IOException ex) { ex.printStackTrace(); } } @@ -241,14 +239,14 @@ public class ContentSettingsFragment extends BasePreferenceFragment { ZipFile zipFile = null; try { zipFile = new ZipFile(filePath); - } catch (IOException ioe) { + } catch (final IOException ioe) { Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) .show(); return; } finally { try { zipFile.close(); - } catch (Exception ignored) { + } catch (final Exception ignored) { } } @@ -272,7 +270,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { //If settings file exist, ask if it should be imported. if (ZipHelper.extractFileFromZip(filePath, newpipeSettings.getPath(), "newpipe.settings")) { - AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); + final AlertDialog.Builder alert = new AlertDialog.Builder(getContext()); alert.setTitle(R.string.import_settings); alert.setNegativeButton(android.R.string.no, (dialog, which) -> { @@ -291,7 +289,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { // restart app to properly load db System.exit(0); } - } catch (Exception e) { + } catch (final Exception e) { onError(e); } } @@ -300,13 +298,13 @@ public class ContentSettingsFragment extends BasePreferenceFragment { ObjectInputStream input = null; try { input = new ObjectInputStream(new FileInputStream(src)); - SharedPreferences.Editor prefEdit = PreferenceManager - .getDefaultSharedPreferences(getContext()).edit(); + final SharedPreferences.Editor prefEdit = PreferenceManager + .getDefaultSharedPreferences(requireContext()).edit(); prefEdit.clear(); - Map entries = (Map) input.readObject(); - for (Map.Entry entry : entries.entrySet()) { - Object v = entry.getValue(); - String key = entry.getKey(); + final Map entries = (Map) input.readObject(); + for (final Map.Entry entry : entries.entrySet()) { + final Object v = entry.getValue(); + final String key = entry.getKey(); if (v instanceof Boolean) { prefEdit.putBoolean(key, (Boolean) v); @@ -321,18 +319,14 @@ public class ContentSettingsFragment extends BasePreferenceFragment { } } prefEdit.commit(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { + } catch (final IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { try { if (input != null) { input.close(); } - } catch (IOException ex) { + } catch (final IOException ex) { ex.printStackTrace(); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index aaa572eab..a4b29fc49 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -120,7 +120,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { try { rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { + } catch (final UnsupportedEncodingException e) { // nothing to do } @@ -132,7 +132,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } private boolean hasInvalidPath(final String prefKey) { - String value = defaultPreferences.getString(prefKey, null); + final String value = defaultPreferences.getString(prefKey, null); return value == null || value.isEmpty(); } @@ -152,20 +152,20 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } try { - Uri uri = Uri.parse(oldPath); + final Uri uri = Uri.parse(oldPath); context.getContentResolver() .releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); context.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); Log.i(TAG, "Revoke old path permissions success on " + oldPath); - } catch (Exception err) { + } catch (final Exception err) { Log.e(TAG, "Error revoking old path permissions on " + oldPath, err); } } private void showMessageDialog(@StringRes final int title, @StringRes final int message) { - AlertDialog.Builder msg = new AlertDialog.Builder(ctx); + final AlertDialog.Builder msg = new AlertDialog.Builder(ctx); msg.setTitle(title); msg.setMessage(message); msg.setPositiveButton(getString(R.string.finish), null); @@ -179,8 +179,8 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { + "preference = [" + preference + "]"); } - String key = preference.getKey(); - int request; + final String key = preference.getKey(); + final int request; if (key.equals(storageUseSafPreference)) { Toast.makeText(getContext(), R.string.download_choose_new_path, @@ -194,7 +194,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { return super.onPreferenceTreeClick(preference); } - Intent i; + final Intent i; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && NewPipeSettings.useStorageAccessFramework(ctx)) { i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) @@ -229,7 +229,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { return; } - String key; + final String key; if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH) { key = downloadPathVideoPreference; } else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) { @@ -262,19 +262,20 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { context.grantUriPermission(context.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS); - StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, null); + final StoredDirectoryHelper mainStorage + = new StoredDirectoryHelper(context, uri, null); Log.i(TAG, "Acquiring tree success from " + uri.toString()); if (!mainStorage.canWrite()) { throw new IOException("No write permissions on " + uri.toString()); } - } catch (IOException err) { + } catch (final IOException err) { Log.e(TAG, "Error acquiring tree from " + uri.toString(), err); showMessageDialog(R.string.general_error, R.string.no_available_dir); return; } } else { - File target = Utils.getFileForUri(uri); + final File target = Utils.getFileForUri(uri); if (!target.canWrite()) { showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 159625c92..5502a06eb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -4,12 +4,12 @@ import android.os.Bundle; import androidx.preference.Preference; -import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.CheckForNewAppVersionTask; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; public class MainSettingsFragment extends BasePreferenceFragment { - public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release"); + public static final boolean DEBUG = MainActivity.DEBUG; @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 47a16f6f3..8ce5fe4c2 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -60,14 +60,14 @@ public final class NewPipeSettings { private static void getDir(final Context context, final int keyID, final String defaultDirectoryName) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final String key = context.getString(keyID); - String downloadPath = prefs.getString(key, null); + final String downloadPath = prefs.getString(key, null); if ((downloadPath != null) && (!downloadPath.isEmpty())) { return; } - SharedPreferences.Editor spEditor = prefs.edit(); + final SharedPreferences.Editor spEditor = prefs.edit(); spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); spEditor.apply(); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index 03e246533..61cee7b20 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -4,7 +4,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.text.InputType; import android.view.LayoutInflater; import android.view.Menu; @@ -96,16 +96,16 @@ public class PeertubeInstanceListFragment extends Fragment { } private void initViews(@NonNull final View rootView) { - TextView instanceHelpTV = rootView.findViewById(R.id.instanceHelpTV); + final TextView instanceHelpTV = rootView.findViewById(R.id.instanceHelpTV); instanceHelpTV.setText(getString(R.string.peertube_instance_url_help, getString(R.string.peertube_instance_list_url))); initButton(rootView); - RecyclerView listInstances = rootView.findViewById(R.id.instances); + final RecyclerView listInstances = rootView.findViewById(R.id.instances); listInstances.setLayoutManager(new LinearLayoutManager(requireContext())); - ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(listInstances); instanceListAdapter = new InstanceListAdapter(requireContext(), itemTouchHelper); @@ -178,7 +178,7 @@ public class PeertubeInstanceListFragment extends Fragment { private void updateTitle() { if (getActivity() instanceof AppCompatActivity) { - ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(R.string.peertube_instance_url_title); } @@ -186,14 +186,14 @@ public class PeertubeInstanceListFragment extends Fragment { } private void saveChanges() { - JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances"); - for (PeertubeInstance instance : instanceList) { + final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances"); + for (final PeertubeInstance instance : instanceList) { jsonWriter.object(); jsonWriter.value("name", instance.getName()); jsonWriter.value("url", instance.getUrl()); jsonWriter.end(); } - String jsonToSave = jsonWriter.end().end().done(); + final String jsonToSave = jsonWriter.end().end().done(); sharedPreferences.edit().putString(savedInstanceListKey, jsonToSave).apply(); } @@ -213,21 +213,20 @@ public class PeertubeInstanceListFragment extends Fragment { private void initButton(final View rootView) { final FloatingActionButton fab = rootView.findViewById(R.id.addInstanceButton); - fab.setOnClickListener(v -> { - showAddItemDialog(requireContext()); - }); + fab.setOnClickListener(v -> + showAddItemDialog(requireContext())); } private void showAddItemDialog(final Context c) { final EditText urlET = new EditText(c); urlET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); urlET.setHint(R.string.peertube_instance_add_help); - AlertDialog dialog = new AlertDialog.Builder(c) + final AlertDialog dialog = new AlertDialog.Builder(c) .setTitle(R.string.peertube_instance_add_title) .setIcon(R.drawable.place_holder_peertube) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.finish, (dialog1, which) -> { - String url = urlET.getText().toString(); + final String url = urlET.getText().toString(); addInstance(url); }) .create(); @@ -236,13 +235,13 @@ public class PeertubeInstanceListFragment extends Fragment { } private void addInstance(final String url) { - String cleanUrl = cleanUrl(url); + final String cleanUrl = cleanUrl(url); if (cleanUrl == null) { return; } progressBar.setVisibility(View.VISIBLE); - Disposable disposable = Single.fromCallable(() -> { - PeertubeInstance instance = new PeertubeInstance(cleanUrl); + final Disposable disposable = Single.fromCallable(() -> { + final PeertubeInstance instance = new PeertubeInstance(cleanUrl); instance.fetchInstanceMetaData(); return instance; }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) @@ -273,7 +272,7 @@ public class PeertubeInstanceListFragment extends Fragment { return null; } // only allow if not already exists - for (PeertubeInstance instance : instanceList) { + for (final PeertubeInstance instance : instanceList) { if (instance.getUrl().equals(cleanUrl)) { Toast.makeText(getActivity(), R.string.peertube_instance_add_exists, Toast.LENGTH_SHORT).show(); @@ -331,7 +330,7 @@ public class PeertubeInstanceListFragment extends Fragment { @Override public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { - int position = viewHolder.getAdapterPosition(); + final int position = viewHolder.getAdapterPosition(); // do not allow swiping the selected instance if (instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) { instanceListAdapter.notifyItemChanged(position); @@ -372,7 +371,7 @@ public class PeertubeInstanceListFragment extends Fragment { @Override public InstanceListAdapter.TabViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { - View view = inflater.inflate(R.layout.item_instance, parent, false); + final View view = inflater.inflate(R.layout.item_instance, parent, false); return new InstanceListAdapter.TabViewHolder(view); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java index 5b452430b..96e2781f5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -64,7 +64,7 @@ public class SelectChannelFragment extends DialogFragment { private final ImageLoader imageLoader = ImageLoader.getInstance(); - private OnSelectedLisener onSelectedLisener = null; + private OnSelectedListener onSelectedListener = null; private OnCancelListener onCancelListener = null; private ProgressBar progressBar; @@ -73,8 +73,8 @@ public class SelectChannelFragment extends DialogFragment { private List subscriptions = new Vector<>(); - public void setOnSelectedLisener(final OnSelectedLisener listener) { - onSelectedLisener = listener; + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; } public void setOnCancelListener(final OnCancelListener listener) { @@ -94,10 +94,10 @@ public class SelectChannelFragment extends DialogFragment { @Override public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.select_channel_fragment, container, false); + final View v = inflater.inflate(R.layout.select_channel_fragment, container, false); recyclerView = v.findViewById(R.id.items_list); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - SelectChannelAdapter channelAdapter = new SelectChannelAdapter(); + final SelectChannelAdapter channelAdapter = new SelectChannelAdapter(); recyclerView.setAdapter(channelAdapter); progressBar = v.findViewById(R.id.progressBar); @@ -107,7 +107,7 @@ public class SelectChannelFragment extends DialogFragment { emptyView.setVisibility(View.GONE); - SubscriptionManager subscriptionManager = new SubscriptionManager(getContext()); + final SubscriptionManager subscriptionManager = new SubscriptionManager(getContext()); subscriptionManager.subscriptions().toObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -129,9 +129,9 @@ public class SelectChannelFragment extends DialogFragment { } private void clickedItem(final int position) { - if (onSelectedLisener != null) { - SubscriptionEntity entry = subscriptions.get(position); - onSelectedLisener + if (onSelectedListener != null) { + final SubscriptionEntity entry = subscriptions.get(position); + onSelectedListener .onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName()); } dismiss(); @@ -186,7 +186,7 @@ public class SelectChannelFragment extends DialogFragment { // Interfaces //////////////////////////////////////////////////////////////////////////*/ - public interface OnSelectedLisener { + public interface OnSelectedListener { void onChannelSelected(int serviceId, String url, String name); } @@ -199,21 +199,16 @@ public class SelectChannelFragment extends DialogFragment { @Override public SelectChannelItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { - View item = LayoutInflater.from(parent.getContext()) + final View item = LayoutInflater.from(parent.getContext()) .inflate(R.layout.select_channel_item, parent, false); return new SelectChannelItemHolder(item); } @Override public void onBindViewHolder(final SelectChannelItemHolder holder, final int position) { - SubscriptionEntity entry = subscriptions.get(position); + final SubscriptionEntity entry = subscriptions.get(position); holder.titleView.setText(entry.getName()); - holder.view.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View view) { - clickedItem(position); - } - }); + holder.view.setOnClickListener(view -> clickedItem(position)); imageLoader.displayImage(entry.getAvatarUrl(), holder.thumbnailView, DISPLAY_IMAGE_OPTIONS); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java index 4df70ccec..9d0fece4f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -52,11 +52,11 @@ public class SelectKioskFragment extends DialogFragment { private RecyclerView recyclerView = null; private SelectKioskAdapter selectKioskAdapter = null; - private OnSelectedLisener onSelectedLisener = null; + private OnSelectedListener onSelectedListener = null; private OnCancelListener onCancelListener = null; - public void setOnSelectedLisener(final OnSelectedLisener listener) { - onSelectedLisener = listener; + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; } public void setOnCancelListener(final OnCancelListener listener) { @@ -76,12 +76,12 @@ public class SelectKioskFragment extends DialogFragment { @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { - View v = inflater.inflate(R.layout.select_kiosk_fragment, container, false); + final View v = inflater.inflate(R.layout.select_kiosk_fragment, container, false); recyclerView = v.findViewById(R.id.items_list); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); try { selectKioskAdapter = new SelectKioskAdapter(); - } catch (Exception e) { + } catch (final Exception e) { onError(e); } recyclerView.setAdapter(selectKioskAdapter); @@ -102,8 +102,8 @@ public class SelectKioskFragment extends DialogFragment { } private void clickedItem(final SelectKioskAdapter.Entry entry) { - if (onSelectedLisener != null) { - onSelectedLisener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); + if (onSelectedListener != null) { + onSelectedListener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); } dismiss(); } @@ -122,7 +122,7 @@ public class SelectKioskFragment extends DialogFragment { // Interfaces //////////////////////////////////////////////////////////////////////////*/ - public interface OnSelectedLisener { + public interface OnSelectedListener { void onKioskSelected(int serviceId, String kioskId, String kioskName); } @@ -135,9 +135,9 @@ public class SelectKioskFragment extends DialogFragment { private final List kioskList = new Vector<>(); SelectKioskAdapter() throws Exception { - for (StreamingService service : NewPipe.getServices()) { - for (String kioskId : service.getKioskList().getAvailableKiosks()) { - String name = String.format(getString(R.string.service_kiosk_string), + for (final StreamingService service : NewPipe.getServices()) { + for (final String kioskId : service.getKioskList().getAvailableKiosks()) { + final String name = String.format(getString(R.string.service_kiosk_string), service.getServiceInfo().getName(), KioskTranslator.getTranslatedKioskName(kioskId, getContext())); kioskList.add(new Entry(ServiceHelper.getIcon(service.getServiceId()), @@ -151,7 +151,7 @@ public class SelectKioskFragment extends DialogFragment { } public SelectKioskItemHolder onCreateViewHolder(final ViewGroup parent, final int type) { - View item = LayoutInflater.from(parent.getContext()) + final View item = LayoutInflater.from(parent.getContext()) .inflate(R.layout.select_kiosk_item, parent, false); return new SelectKioskItemHolder(item); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java new file mode 100644 index 000000000..c858c7f77 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -0,0 +1,225 @@ +package org.schabi.newpipe.settings; + +import android.app.Activity; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; + +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.playlist.PlaylistLocalItem; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.local.playlist.LocalPlaylistManager; +import org.schabi.newpipe.local.playlist.RemotePlaylistManager; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; + +import java.util.List; +import java.util.Vector; + +import io.reactivex.Flowable; +import io.reactivex.disposables.Disposable; + +public class SelectPlaylistFragment extends DialogFragment { + /** + * This contains the base display options for images. + */ + private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS + = new DisplayImageOptions.Builder().cacheInMemory(true).build(); + + private final ImageLoader imageLoader = ImageLoader.getInstance(); + + private OnSelectedListener onSelectedListener = null; + private OnCancelListener onCancelListener = null; + + private ProgressBar progressBar; + private TextView emptyView; + private RecyclerView recyclerView; + private Disposable playlistsSubscriber; + + private List playlists = new Vector<>(); + + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; + } + + public void setOnCancelListener(final OnCancelListener listener) { + onCancelListener = listener; + } + + /*////////////////////////////////////////////////////////////////////////// + // Fragment's Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + final View v = + inflater.inflate(R.layout.select_playlist_fragment, container, false); + recyclerView = v.findViewById(R.id.items_list); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter(); + recyclerView.setAdapter(playlistAdapter); + + progressBar = v.findViewById(R.id.progressBar); + emptyView = v.findViewById(R.id.empty_state_view); + progressBar.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + emptyView.setVisibility(View.GONE); + + final AppDatabase database = NewPipeDatabase.getInstance(requireContext()); + final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database); + final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database); + + playlistsSubscriber = Flowable.combineLatest(localPlaylistManager.getPlaylists(), + remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) + .subscribe(this::displayPlaylists, this::onError); + + return v; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (playlistsSubscriber != null) { + playlistsSubscriber.dispose(); + playlistsSubscriber = null; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Handle actions + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCancel(final DialogInterface dialogInterface) { + super.onCancel(dialogInterface); + if (onCancelListener != null) { + onCancelListener.onCancel(); + } + } + + private void clickedItem(final int position) { + if (onSelectedListener != null) { + final LocalItem selectedItem = playlists.get(position); + + if (selectedItem instanceof PlaylistMetadataEntry) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); + onSelectedListener + .onLocalPlaylistSelected(entry.uid, entry.name); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + onSelectedListener.onRemotePlaylistSelected( + entry.getServiceId(), entry.getUrl(), entry.getName()); + } + } + dismiss(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Item handling + //////////////////////////////////////////////////////////////////////////*/ + + private void displayPlaylists(final List newPlaylists) { + this.playlists = newPlaylists; + progressBar.setVisibility(View.GONE); + if (newPlaylists.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + return; + } + recyclerView.setVisibility(View.VISIBLE); + + } + + /*////////////////////////////////////////////////////////////////////////// + // Error + //////////////////////////////////////////////////////////////////////////*/ + + protected void onError(final Throwable e) { + final Activity activity = getActivity(); + ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo + .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Interfaces + //////////////////////////////////////////////////////////////////////////*/ + + public interface OnSelectedListener { + void onLocalPlaylistSelected(long id, String name); + void onRemotePlaylistSelected(int serviceId, String url, String name); + } + + public interface OnCancelListener { + void onCancel(); + } + + private class SelectPlaylistAdapter + extends RecyclerView.Adapter { + @Override + public SelectPlaylistItemHolder onCreateViewHolder(final ViewGroup parent, + final int viewType) { + final View item = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_playlist_mini_item, parent, false); + return new SelectPlaylistItemHolder(item); + } + + @Override + public void onBindViewHolder(final SelectPlaylistItemHolder holder, final int position) { + final PlaylistLocalItem selectedItem = playlists.get(position); + + if (selectedItem instanceof PlaylistMetadataEntry) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); + + holder.titleView.setText(entry.name); + holder.view.setOnClickListener(view -> clickedItem(position)); + imageLoader.displayImage(entry.thumbnailUrl, holder.thumbnailView, + DISPLAY_IMAGE_OPTIONS); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + + holder.titleView.setText(entry.getName()); + holder.view.setOnClickListener(view -> clickedItem(position)); + imageLoader.displayImage(entry.getThumbnailUrl(), holder.thumbnailView, + DISPLAY_IMAGE_OPTIONS); + } + } + + @Override + public int getItemCount() { + return playlists.size(); + } + + public class SelectPlaylistItemHolder extends RecyclerView.ViewHolder { + public final View view; + final ImageView thumbnailView; + final TextView titleView; + + SelectPlaylistItemHolder(final View v) { + super(v); + this.view = v; + thumbnailView = v.findViewById(R.id.itemThumbnailView); + titleView = v.findViewById(R.id.itemTitleView); + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 18cbece6f..d2d4c2404 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -13,7 +13,7 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import org.schabi.newpipe.R; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; @@ -53,7 +53,7 @@ public class SettingsActivity extends AppCompatActivity super.onCreate(savedInstanceBundle); setContentView(R.layout.settings_layout); - Toolbar toolbar = findViewById(R.id.toolbar); + final Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); if (savedInstanceBundle == null) { @@ -62,14 +62,14 @@ public class SettingsActivity extends AppCompatActivity .commit(); } - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(this); } } @Override public boolean onCreateOptionsMenu(final Menu menu) { - ActionBar actionBar = getSupportActionBar(); + final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); @@ -80,7 +80,7 @@ public class SettingsActivity extends AppCompatActivity @Override public boolean onOptionsItemSelected(final MenuItem item) { - int id = item.getItemId(); + final int id = item.getItemId(); if (id == android.R.id.home) { if (getSupportFragmentManager().getBackStackEntryCount() == 0) { finish(); @@ -95,7 +95,7 @@ public class SettingsActivity extends AppCompatActivity @Override public boolean onPreferenceStartFragment(final PreferenceFragmentCompat caller, final Preference preference) { - Fragment fragment = Fragment + final Fragment fragment = Fragment .instantiate(this, preference.getFragment(), preference.getExtras()); getSupportFragmentManager().beginTransaction() .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index 2b103e794..476cf97ab 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -19,7 +19,7 @@ public class UpdateSettingsFragment extends BasePreferenceFragment { public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - String updateToggleKey = getString(R.string.update_app_key); + final String updateToggleKey = getString(R.string.update_app_key); findPreference(updateToggleKey).setOnPreferenceChangeListener(updatePreferenceChange); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java index bef9a7b56..5eca99822 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java @@ -35,7 +35,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { // show a snackbar to let the user give permission if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && s.equals(getString(R.string.minimize_on_exit_key))) { - String newSetting = sharedPreferences.getString(s, null); + final String newSetting = sharedPreferences.getString(s, null); if (newSetting != null && newSetting.equals(getString(R.string.minimize_on_exit_popup_key)) && !Settings.canDrawOverlays(getContext())) { @@ -68,7 +68,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { final boolean inexactSeek = getPreferenceManager().getSharedPreferences() .getBoolean(res.getString(R.string.use_inexact_seek_key), false); - for (String durationsValue : durationsValues) { + for (final String durationsValue : durationsValues) { currentDurationValue = Integer.parseInt(durationsValue) / (int) DateUtils.SECOND_IN_MILLIS; if (inexactSeek && currentDurationValue % 10 == 5) { @@ -81,13 +81,13 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { res.getQuantityString(R.plurals.seconds, currentDurationValue), currentDurationValue)); - } catch (Resources.NotFoundException ignored) { + } catch (final Resources.NotFoundException ignored) { // if this happens, the translation is missing, // and the english string will be displayed instead } } - final ListPreference durations = (ListPreference) findPreference( + final ListPreference durations = findPreference( getString(R.string.seek_duration_key)); durations.setEntryValues(displayedDurationValues.toArray(new CharSequence[0])); durations.setEntries(displayedDescriptionValues.toArray(new CharSequence[0])); @@ -96,7 +96,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { final int newDuration = selectedDuration / (int) DateUtils.SECOND_IN_MILLIS + 5; durations.setValue(Integer.toString(newDuration * (int) DateUtils.SECOND_IN_MILLIS)); - Toast toast = Toast + final Toast toast = Toast .makeText(getContext(), getString(R.string.new_seek_duration_toast, newDuration), Toast.LENGTH_LONG); diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java index 6ebfbd73c..44fe987ee 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -34,6 +34,7 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.settings.SelectChannelFragment; import org.schabi.newpipe.settings.SelectKioskFragment; +import org.schabi.newpipe.settings.SelectPlaylistFragment; import org.schabi.newpipe.settings.tabs.AddTabDialog.ChooseTabListItem; import org.schabi.newpipe.util.ThemeHelper; @@ -48,7 +49,7 @@ public class ChooseTabsFragment extends Fragment { private TabsManager tabsManager; - private List tabList = new ArrayList<>(); + private final List tabList = new ArrayList<>(); private ChooseTabsFragment.SelectedTabsAdapter selectedTabsAdapter; /*////////////////////////////////////////////////////////////////////////// @@ -78,10 +79,10 @@ public class ChooseTabsFragment extends Fragment { initButton(rootView); - RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs); + final RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs); listSelectedTabs.setLayoutManager(new LinearLayoutManager(requireContext())); - ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(listSelectedTabs); selectedTabsAdapter = new SelectedTabsAdapter(requireContext(), itemTouchHelper); @@ -138,7 +139,7 @@ public class ChooseTabsFragment extends Fragment { private void updateTitle() { if (getActivity() instanceof AppCompatActivity) { - ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(R.string.main_page_content); } @@ -172,7 +173,7 @@ public class ChooseTabsFragment extends Fragment { return; } - Dialog.OnClickListener actionListener = (dialog, which) -> { + final Dialog.OnClickListener actionListener = (dialog, which) -> { final ChooseTabListItem selected = availableTabs[which]; addTab(selected.tabId); }; @@ -200,17 +201,34 @@ public class ChooseTabsFragment extends Fragment { switch (type) { case KIOSK: - SelectKioskFragment selectKioskFragment = new SelectKioskFragment(); - selectKioskFragment.setOnSelectedLisener((serviceId, kioskId, kioskName) -> + final SelectKioskFragment selectKioskFragment = new SelectKioskFragment(); + selectKioskFragment.setOnSelectedListener((serviceId, kioskId, kioskName) -> addTab(new Tab.KioskTab(serviceId, kioskId))); selectKioskFragment.show(requireFragmentManager(), "select_kiosk"); return; case CHANNEL: - SelectChannelFragment selectChannelFragment = new SelectChannelFragment(); - selectChannelFragment.setOnSelectedLisener((serviceId, url, name) -> + final SelectChannelFragment selectChannelFragment = new SelectChannelFragment(); + selectChannelFragment.setOnSelectedListener((serviceId, url, name) -> addTab(new Tab.ChannelTab(serviceId, url, name))); selectChannelFragment.show(requireFragmentManager(), "select_channel"); return; + case PLAYLIST: + final SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment(); + selectPlaylistFragment.setOnSelectedListener( + new SelectPlaylistFragment.OnSelectedListener() { + @Override + public void onLocalPlaylistSelected(final long id, final String name) { + addTab(new Tab.PlaylistTab(id, name)); + } + + @Override + public void onRemotePlaylistSelected( + final int serviceId, final String url, final String name) { + addTab(new Tab.PlaylistTab(serviceId, url, name)); + } + }); + selectPlaylistFragment.show(requireFragmentManager(), "select_playlist"); + return; default: addTab(type.getTab()); break; @@ -220,7 +238,7 @@ public class ChooseTabsFragment extends Fragment { private ChooseTabListItem[] getAvailableTabs(final Context context) { final ArrayList returnList = new ArrayList<>(); - for (Tab.Type type : Tab.Type.values()) { + for (final Tab.Type type : Tab.Type.values()) { final Tab tab = type.getTab(); switch (type) { case BLANK: @@ -248,6 +266,11 @@ public class ChooseTabsFragment extends Fragment { R.attr.ic_kiosk_hot))); } break; + case PLAYLIST: + returnList.add(new ChooseTabListItem(tab.getTabId(), + getString(R.string.playlist_page_summary), + tab.getTabIconRes(context))); + break; default: if (!tabList.contains(tab)) { returnList.add(new ChooseTabListItem(context, tab)); @@ -306,7 +329,7 @@ public class ChooseTabsFragment extends Fragment { @Override public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) { - int position = viewHolder.getAdapterPosition(); + final int position = viewHolder.getAdapterPosition(); tabList.remove(position); selectedTabsAdapter.notifyItemRemoved(position); @@ -337,7 +360,7 @@ public class ChooseTabsFragment extends Fragment { @Override public ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder onCreateViewHolder( @NonNull final ViewGroup parent, final int viewType) { - View view = inflater.inflate(R.layout.list_choose_tabs, parent, false); + final View view = inflater.inflate(R.layout.list_choose_tabs, parent, false); return new ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder(view); } @@ -393,6 +416,13 @@ public class ChooseTabsFragment extends Fragment { tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab) .getChannelServiceId()) + "/" + tab.getTabName(requireContext()); break; + case PLAYLIST: + final int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId(); + final String serviceName = serviceId == -1 + ? getString(R.string.local) + : NewPipe.getNameOfService(serviceId); + tabName = serviceName + "/" + tab.getTabName(requireContext()); + break; default: tabName = tab.getTabName(requireContext()); break; diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index d06b4b14e..8e440c93d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -11,6 +11,7 @@ import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonSink; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem.LocalItemType; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -18,9 +19,11 @@ import org.schabi.newpipe.fragments.BlankFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; +import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; +import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -33,7 +36,8 @@ import java.util.Objects; public abstract class Tab { private static final String JSON_TAB_ID_KEY = "tab_id"; - Tab() { } + Tab() { + } Tab(@NonNull final JsonObject jsonObject) { readDataFromJson(jsonObject); @@ -61,7 +65,7 @@ public abstract class Tab { @Nullable public static Type typeFrom(final int tabId) { - for (Type available : Type.values()) { + for (final Type available : Type.values()) { if (available.getTabId() == tabId) { return available; } @@ -83,6 +87,8 @@ public abstract class Tab { return new KioskTab(jsonObject); case CHANNEL: return new ChannelTab(jsonObject); + case PLAYLIST: + return new PlaylistTab(jsonObject); } } @@ -147,7 +153,8 @@ public abstract class Tab { BOOKMARKS(new BookmarksTab()), HISTORY(new HistoryTab()), KIOSK(new KioskTab()), - CHANNEL(new ChannelTab()); + CHANNEL(new ChannelTab()), + PLAYLIST(new PlaylistTab()); private Tab tab; @@ -474,7 +481,7 @@ public abstract class Tab { try { final StreamingService service = NewPipe.getService(kioskServiceId); kioskId = service.getKioskList().getDefaultKioskId(); - } catch (ExtractionException e) { + } catch (final ExtractionException e) { ErrorActivity.reportError(context, e, null, null, ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0)); @@ -482,4 +489,123 @@ public abstract class Tab { return kioskId; } } + + public static class PlaylistTab extends Tab { + public static final int ID = 8; + private static final String JSON_PLAYLIST_SERVICE_ID_KEY = "playlist_service_id"; + private static final String JSON_PLAYLIST_URL_KEY = "playlist_url"; + private static final String JSON_PLAYLIST_NAME_KEY = "playlist_name"; + private static final String JSON_PLAYLIST_ID_KEY = "playlist_id"; + private static final String JSON_PLAYLIST_TYPE_KEY = "playlist_type"; + private int playlistServiceId; + private String playlistUrl; + private String playlistName; + private long playlistId; + private LocalItemType playlistType; + + private PlaylistTab() { + this(-1, ""); + } + + public PlaylistTab(final long playlistId, final String playlistName) { + this.playlistName = playlistName; + this.playlistId = playlistId; + this.playlistType = LocalItemType.PLAYLIST_LOCAL_ITEM; + this.playlistServiceId = -1; + this.playlistUrl = ""; + } + + public PlaylistTab(final int playlistServiceId, final String playlistUrl, + final String playlistName) { + this.playlistServiceId = playlistServiceId; + this.playlistUrl = playlistUrl; + this.playlistName = playlistName; + this.playlistType = LocalItemType.PLAYLIST_REMOTE_ITEM; + this.playlistId = -1; + } + + public PlaylistTab(final JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(final Context context) { + return playlistName; + } + + @DrawableRes + @Override + public int getTabIconRes(final Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_bookmark); + } + + @Override + public Fragment getFragment(final Context context) { + if (playlistType == LocalItemType.PLAYLIST_LOCAL_ITEM) { + return LocalPlaylistFragment.getInstance(playlistId, playlistName); + + } else { // playlistType == LocalItemType.PLAYLIST_REMOTE_ITEM + return PlaylistFragment.getInstance(playlistServiceId, playlistUrl, playlistName); + } + } + + @Override + protected void writeDataToJson(final JsonSink writerSink) { + writerSink.value(JSON_PLAYLIST_SERVICE_ID_KEY, playlistServiceId) + .value(JSON_PLAYLIST_URL_KEY, playlistUrl) + .value(JSON_PLAYLIST_NAME_KEY, playlistName) + .value(JSON_PLAYLIST_ID_KEY, playlistId) + .value(JSON_PLAYLIST_TYPE_KEY, playlistType.toString()); + } + + @Override + protected void readDataFromJson(final JsonObject jsonObject) { + playlistServiceId = jsonObject.getInt(JSON_PLAYLIST_SERVICE_ID_KEY, -1); + playlistUrl = jsonObject.getString(JSON_PLAYLIST_URL_KEY, ""); + playlistName = jsonObject.getString(JSON_PLAYLIST_NAME_KEY, ""); + playlistId = jsonObject.getInt(JSON_PLAYLIST_ID_KEY, -1); + playlistType = LocalItemType.valueOf( + jsonObject.getString(JSON_PLAYLIST_TYPE_KEY, + LocalItemType.PLAYLIST_LOCAL_ITEM.toString()) + ); + } + + @Override + public boolean equals(final Object obj) { + if (!(super.equals(obj) + && Objects.equals(playlistType, ((PlaylistTab) obj).playlistType) + && Objects.equals(playlistName, ((PlaylistTab) obj).playlistName))) { + return false; // base objects are different + } + + return (playlistId == ((PlaylistTab) obj).playlistId) // local + || (playlistServiceId == ((PlaylistTab) obj).playlistServiceId // remote + && Objects.equals(playlistUrl, ((PlaylistTab) obj).playlistUrl)); + } + + public int getPlaylistServiceId() { + return playlistServiceId; + } + + public String getPlaylistUrl() { + return playlistUrl; + } + + public String getPlaylistName() { + return playlistName; + } + + public long getPlaylistId() { + return playlistId; + } + + public LocalItemType getPlaylistType() { + return playlistType; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java index d18aad9d3..057ca50f0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsJsonHelper.java @@ -59,7 +59,7 @@ public final class TabsJsonHelper { final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY); - for (Object o : tabsArray) { + for (final Object o : tabsArray) { if (!(o instanceof JsonObject)) { continue; } @@ -70,7 +70,7 @@ public final class TabsJsonHelper { returnTabs.add(tab); } } - } catch (JsonParserException e) { + } catch (final JsonParserException e) { throw new InvalidJsonException(e); } @@ -93,7 +93,7 @@ public final class TabsJsonHelper { jsonWriter.array(JSON_TABS_ARRAY_KEY); if (tabList != null) { - for (Tab tab : tabList) { + for (final Tab tab : tabList) { tab.writeJsonOn(jsonWriter); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java index c76df7047..02b0c3df5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java @@ -2,7 +2,7 @@ package org.schabi.newpipe.settings.tabs; import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.widget.Toast; import org.schabi.newpipe.R; @@ -30,7 +30,7 @@ public final class TabsManager { final String savedJson = sharedPreferences.getString(savedTabsKey, null); try { return TabsJsonHelper.getTabsFromJson(savedJson); - } catch (TabsJsonHelper.InvalidJsonException e) { + } catch (final TabsJsonHelper.InvalidJsonException e) { Toast.makeText(context, R.string.saved_tabs_invalid_json, Toast.LENGTH_SHORT).show(); return getDefaultTabs(); } diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index 96f78ac0e..dc6e29d7d 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -43,7 +43,8 @@ public class DataReader { return readBuffer[readOffset++] & 0xFF; } - public long skipBytes(long amount) throws IOException { + public long skipBytes(final long byteAmount) throws IOException { + long amount = byteAmount; if (readCount < 0) { return 0; } else if (readCount == 0) { @@ -69,7 +70,7 @@ public class DataReader { } public long readUnsignedInt() throws IOException { - long value = readInt(); + final long value = readInt(); return value & 0xffffffffL; } @@ -81,8 +82,9 @@ public class DataReader { public long readLong() throws IOException { primitiveRead(LONG_SIZE); - long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; - long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7]; + final long high + = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; + final long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7]; return high << 32 | low; } @@ -90,7 +92,10 @@ public class DataReader { return read(buffer, 0, buffer.length); } - public int read(final byte[] buffer, int offset, int count) throws IOException { + public int read(final byte[] buffer, final int off, final int c) throws IOException { + int offset = off; + int count = c; + if (readCount < 0) { return -1; } @@ -110,7 +115,7 @@ public class DataReader { total += Math.max(stream.read(buffer, offset, count), 0); } else { while (count > 0 && !fillBuffer()) { - int read = Math.min(readCount, count); + final int read = Math.min(readCount, count); System.arraycopy(readBuffer, readOffset, buffer, offset, read); readOffset += read; @@ -165,7 +170,7 @@ public class DataReader { if (viewSize < 1) { return -1; } - int res = DataReader.this.read(); + final int res = DataReader.this.read(); if (res > 0) { viewSize--; } @@ -184,7 +189,7 @@ public class DataReader { return -1; } - int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count)); + final int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count)); viewSize -= res; return res; @@ -195,7 +200,7 @@ public class DataReader { if (viewSize < 1) { return 0; } - int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize)); + final int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize)); viewSize -= res; return res; @@ -226,12 +231,12 @@ public class DataReader { private final short[] primitive = new short[LONG_SIZE]; private void primitiveRead(final int amount) throws IOException { - byte[] buffer = new byte[amount]; - int read = read(buffer, 0, amount); + final byte[] buffer = new byte[amount]; + final int read = read(buffer, 0, amount); if (read != amount) { throw new EOFException("Truncated stream, missing " - + String.valueOf(amount - read) + " bytes"); + + (amount - read) + " bytes"); } for (int i = 0; i < amount; i++) { diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java index ff3aabd78..e1eb0e9e5 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java @@ -5,8 +5,8 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.NoSuchElementException; @@ -116,7 +116,7 @@ public class Mp4DashReader { tracks[i].trak = moov.trak[i]; if (moov.mvexTrex != null) { - for (Trex mvexTrex : moov.mvexTrex) { + for (final Trex mvexTrex : moov.mvexTrex) { if (tracks[i].trak.tkhd.trackId == mvexTrex.trackId) { tracks[i].trex = mvexTrex; } @@ -174,7 +174,7 @@ public class Mp4DashReader { } public Mp4DashChunk getNextChunk(final boolean infoOnly) throws IOException { - Mp4Track track = tracks[selectedTrack]; + final Mp4Track track = tracks[selectedTrack]; while (stream.available()) { @@ -233,7 +233,7 @@ public class Mp4DashReader { continue; // find another chunk } - Mp4DashChunk chunk = new Mp4DashChunk(); + final Mp4DashChunk chunk = new Mp4DashChunk(); chunk.moof = moof; if (!infoOnly) { chunk.data = stream.getView(moof.traf.trun.chunkSize); @@ -259,15 +259,11 @@ public class Mp4DashReader { } private String boxName(final int type) { - try { - return new String(ByteBuffer.allocate(4).putInt(type).array(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - return "0x" + Integer.toHexString(type); - } + return new String(ByteBuffer.allocate(4).putInt(type).array(), StandardCharsets.UTF_8); } private Box readBox() throws IOException { - Box b = new Box(); + final Box b = new Box(); b.offset = stream.position(); b.size = stream.readUnsignedInt(); b.type = stream.readInt(); @@ -280,7 +276,7 @@ public class Mp4DashReader { } private Box readBox(final int expected) throws IOException { - Box b = readBox(); + final Box b = readBox(); if (b.type != expected) { throw new NoSuchElementException("expected " + boxName(expected) + " found " + boxName(b)); @@ -290,13 +286,13 @@ public class Mp4DashReader { private byte[] readFullBox(final Box ref) throws IOException { // full box reading is limited to 2 GiB, and should be enough - int size = (int) ref.size; + final int size = (int) ref.size; - ByteBuffer buffer = ByteBuffer.allocate(size); + final ByteBuffer buffer = ByteBuffer.allocate(size); buffer.putInt(size); buffer.putInt(ref.type); - int read = size - 8; + final int read = size - 8; if (stream.read(buffer.array(), 8, read) != read) { throw new EOFException(String.format("EOF reached in box: type=%s offset=%s size=%s", @@ -307,7 +303,7 @@ public class Mp4DashReader { } private void ensure(final Box ref) throws IOException { - long skip = ref.offset + ref.size - stream.position(); + final long skip = ref.offset + ref.size - stream.position(); if (skip == 0) { return; @@ -325,7 +321,7 @@ public class Mp4DashReader { Box b; while (stream.position() < (ref.offset + ref.size)) { b = readBox(); - for (int type : expected) { + for (final int type : expected) { if (b.type == type) { return b; } @@ -345,7 +341,7 @@ public class Mp4DashReader { } private Moof parseMoof(final Box ref, final int trackId) throws IOException { - Moof obj = new Moof(); + final Moof obj = new Moof(); Box b = readBox(ATOM_MFHD); obj.mfhdSequenceNumber = parseMfhd(); @@ -372,7 +368,7 @@ public class Mp4DashReader { } private Traf parseTraf(final Box ref, final int trackId) throws IOException { - Traf traf = new Traf(); + final Traf traf = new Traf(); Box b = readBox(ATOM_TFHD); traf.tfhd = parseTfhd(trackId); @@ -397,7 +393,7 @@ public class Mp4DashReader { } private Tfhd parseTfhd(final int trackId) throws IOException { - Tfhd obj = new Tfhd(); + final Tfhd obj = new Tfhd(); obj.bFlags = stream.readInt(); obj.trackId = stream.readInt(); @@ -426,13 +422,13 @@ public class Mp4DashReader { } private long parseTfdt() throws IOException { - int version = stream.read(); + final int version = stream.read(); stream.skipBytes(3); // flags return version == 0 ? stream.readUnsignedInt() : stream.readLong(); } private Trun parseTrun() throws IOException { - Trun obj = new Trun(); + final Trun obj = new Trun(); obj.bFlags = stream.readInt(); obj.entryCount = stream.readInt(); // unsigned int @@ -461,7 +457,7 @@ public class Mp4DashReader { stream.read(obj.bEntries); for (int i = 0; i < obj.entryCount; i++) { - TrunEntry entry = obj.getEntry(i); + final TrunEntry entry = obj.getEntry(i); if (hasFlag(obj.bFlags, 0x0100)) { obj.chunkDuration += entry.sampleDuration; } @@ -480,7 +476,7 @@ public class Mp4DashReader { private int[] parseFtyp(final Box ref) throws IOException { int i = 0; - int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)]; + final int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)]; list[i++] = stream.readInt(); // major brand @@ -494,14 +490,14 @@ public class Mp4DashReader { } private Mvhd parseMvhd() throws IOException { - int version = stream.read(); + final int version = stream.read(); stream.skipBytes(3); // flags // creation entries_time // modification entries_time stream.skipBytes(2 * (version == 0 ? 4 : 8)); - Mvhd obj = new Mvhd(); + final Mvhd obj = new Mvhd(); obj.timeScale = stream.readUnsignedInt(); // chunkDuration @@ -520,9 +516,9 @@ public class Mp4DashReader { } private Tkhd parseTkhd() throws IOException { - int version = stream.read(); + final int version = stream.read(); - Tkhd obj = new Tkhd(); + final Tkhd obj = new Tkhd(); // flags // creation entries_time @@ -553,7 +549,7 @@ public class Mp4DashReader { } private Trak parseTrak(final Box ref) throws IOException { - Trak trak = new Trak(); + final Trak trak = new Trak(); Box b = readBox(ATOM_TKHD); trak.tkhd = parseTkhd(); @@ -576,7 +572,7 @@ public class Mp4DashReader { } private Mdia parseMdia(final Box ref) throws IOException { - Mdia obj = new Mdia(); + final Mdia obj = new Mdia(); Box b; while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) { @@ -585,8 +581,8 @@ public class Mp4DashReader { obj.mdhd = readFullBox(b); // read time scale - ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd); - byte version = buffer.get(8); + final ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd); + final byte version = buffer.get(8); buffer.position(12 + ((version == 0 ? 4 : 8) * 2)); obj.mdhdTimeScale = buffer.getInt(); break; @@ -608,7 +604,7 @@ public class Mp4DashReader { // flags stream.skipBytes(4); - Hdlr obj = new Hdlr(); + final Hdlr obj = new Hdlr(); obj.bReserved = new byte[12]; obj.type = stream.readInt(); @@ -623,11 +619,11 @@ public class Mp4DashReader { private Moov parseMoov(final Box ref) throws IOException { Box b = readBox(ATOM_MVHD); - Moov moov = new Moov(); + final Moov moov = new Moov(); moov.mvhd = parseMvhd(); ensure(b); - ArrayList tmp = new ArrayList<>((int) moov.mvhd.nextTrackId); + final ArrayList tmp = new ArrayList<>((int) moov.mvhd.nextTrackId); while ((b = untilBox(ref, ATOM_TRAK, ATOM_MVEX)) != null) { switch (b.type) { @@ -648,7 +644,7 @@ public class Mp4DashReader { } private Trex[] parseMvex(final Box ref, final int possibleTrackCount) throws IOException { - ArrayList tmp = new ArrayList<>(possibleTrackCount); + final ArrayList tmp = new ArrayList<>(possibleTrackCount); Box b; while ((b = untilBox(ref, ATOM_TREX)) != null) { @@ -664,7 +660,7 @@ public class Mp4DashReader { // flags stream.skipBytes(4); - Trex obj = new Trex(); + final Trex obj = new Trex(); obj.trackId = stream.readInt(); obj.defaultSampleDescriptionIndex = stream.readInt(); obj.defaultSampleDuration = stream.readInt(); @@ -675,17 +671,17 @@ public class Mp4DashReader { } private Elst parseEdts(final Box ref) throws IOException { - Box b = untilBox(ref, ATOM_ELST); + final Box b = untilBox(ref, ATOM_ELST); if (b == null) { return null; } - Elst obj = new Elst(); + final Elst obj = new Elst(); - boolean v1 = stream.read() == 1; + final boolean v1 = stream.read() == 1; stream.skipBytes(3); // flags - int entryCount = stream.readInt(); + final int entryCount = stream.readInt(); if (entryCount < 1) { obj.bMediaRate = 0x00010000; // default media rate (1.0) return obj; @@ -707,7 +703,7 @@ public class Mp4DashReader { } private Minf parseMinf(final Box ref) throws IOException { - Minf obj = new Minf(); + final Minf obj = new Minf(); Box b; while ((b = untilAnyBox(ref)) != null) { @@ -738,7 +734,7 @@ public class Mp4DashReader { * @return stsd box inside */ private byte[] parseStbl(final Box ref) throws IOException { - Box b = untilBox(ref, ATOM_STSD); + final Box b = untilBox(ref, ATOM_STSD); if (b == null) { return new byte[0]; // this never should happens (missing codec startup data) @@ -796,8 +792,8 @@ public class Mp4DashReader { int entriesRowSize; public TrunEntry getEntry(final int i) { - ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entriesRowSize, entriesRowSize); - TrunEntry entry = new TrunEntry(); + final ByteBuffer buffer = ByteBuffer.wrap(bEntries, i * entriesRowSize, entriesRowSize); + final TrunEntry entry = new TrunEntry(); if (hasFlag(bFlags, 0x0100)) { entry.sampleDuration = buffer.getInt(); @@ -819,7 +815,7 @@ public class Mp4DashReader { } public TrunEntry getAbsoluteEntry(final int i, final Tfhd header) { - TrunEntry entry = getEntry(i); + final TrunEntry entry = getEntry(i); if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) { entry.sampleFlags = header.defaultSampleFlags; @@ -928,7 +924,7 @@ public class Mp4DashReader { return null; } - Mp4DashSample sample = new Mp4DashSample(); + final Mp4DashSample sample = new Mp4DashSample(); sample.info = moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd); sample.data = new byte[sample.info.sampleSize]; diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index eb208280e..8f71c6934 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -51,7 +51,7 @@ public class Mp4FromDashWriter { private final ArrayList compatibleBrands = new ArrayList<>(5); public Mp4FromDashWriter(final SharpStream... sources) throws IOException { - for (SharpStream src : sources) { + for (final SharpStream src : sources) { if (!src.canRewind() && !src.canRead()) { throw new IOException("All sources must be readable and allow rewind"); } @@ -128,7 +128,7 @@ public class Mp4FromDashWriter { done = true; parsed = true; - for (SharpStream src : sourceTracks) { + for (final SharpStream src : sourceTracks) { src.close(); } @@ -157,17 +157,17 @@ public class Mp4FromDashWriter { outStream = output; long read = 8; // mdat box header size long totalSampleSize = 0; - int[] sampleExtra = new int[readers.length]; - int[] defaultMediaTime = new int[readers.length]; - int[] defaultSampleDuration = new int[readers.length]; - int[] sampleCount = new int[readers.length]; + final int[] sampleExtra = new int[readers.length]; + final int[] defaultMediaTime = new int[readers.length]; + final int[] defaultSampleDuration = new int[readers.length]; + final int[] sampleCount = new int[readers.length]; - TablesInfo[] tablesInfo = new TablesInfo[tracks.length]; + final TablesInfo[] tablesInfo = new TablesInfo[tracks.length]; for (int i = 0; i < tablesInfo.length; i++) { tablesInfo[i] = new TablesInfo(); } - int singleSampleBuffer; + final int singleSampleBuffer; if (tracks.length == 1 && tracks[0].kind == TrackKind.Audio) { // near 1 second of audio data per chunk, avoid split the audio stream in large chunks singleSampleBuffer = tracks[0].trak.mdia.mdhdTimeScale / 1000; @@ -250,10 +250,10 @@ public class Mp4FromDashWriter { } - boolean is64 = read > THRESHOLD_FOR_CO64; + final boolean is64 = read > THRESHOLD_FOR_CO64; // calculate the moov size - int auxSize = makeMoov(defaultMediaTime, tablesInfo, is64); + final int auxSize = makeMoov(defaultMediaTime, tablesInfo, is64); if (auxSize < THRESHOLD_MOOV_LENGTH) { auxBuffer = ByteBuffer.allocate(auxSize); // cache moov in the memory @@ -267,9 +267,9 @@ public class Mp4FromDashWriter { // reserve moov space in the output stream if (auxSize > 0) { int length = auxSize; - byte[] buffer = new byte[64 * 1024]; // 64 KiB + final byte[] buffer = new byte[64 * 1024]; // 64 KiB while (length > 0) { - int count = Math.min(length, buffer.length); + final int count = Math.min(length, buffer.length); outWrite(buffer, count); length -= count; } @@ -305,9 +305,10 @@ public class Mp4FromDashWriter { outWrite(makeMdat(totalSampleSize, is64)); - int[] sampleIndex = new int[readers.length]; - int[] sizes = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; - int[] sync = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; + final int[] sampleIndex = new int[readers.length]; + final int[] sizes + = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; + final int[] sync = new int[singleSampleBuffer > 0 ? singleSampleBuffer : SAMPLES_PER_CHUNK]; int written = readers.length; while (written > 0) { @@ -318,9 +319,9 @@ public class Mp4FromDashWriter { continue; // track is done } - long chunkOffset = writeOffset; + final long chunkOffset = writeOffset; int syncCount = 0; - int limit; + final int limit; if (singleSampleBuffer > 0) { limit = singleSampleBuffer; } else { @@ -329,7 +330,7 @@ public class Mp4FromDashWriter { int j = 0; for (; j < limit; j++) { - Mp4DashSample sample = getNextSample(i); + final Mp4DashSample sample = getNextSample(i); if (sample == null) { if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) { @@ -409,7 +410,7 @@ public class Mp4FromDashWriter { } } - Mp4DashSample sample = readersChunks[track].getNextSample(); + final Mp4DashSample sample = readersChunks[track].getNextSample(); if (sample == null) { readersChunks[track] = null; return getNextSample(track); @@ -434,8 +435,8 @@ public class Mp4FromDashWriter { auxSeek(offset); - int size = count * 4; - ByteBuffer buffer = ByteBuffer.allocate(size); + final int size = count * 4; + final ByteBuffer buffer = ByteBuffer.allocate(size); for (int i = 0; i < count; i++) { buffer.putInt(values[i]); @@ -464,16 +465,16 @@ public class Mp4FromDashWriter { } private void initChunkTables(final TablesInfo tables, final int firstCount, - final int succesiveCount) { + final int successiveCount) { // tables.stsz holds amount of samples of the track (total) - int totalSamples = (tables.stsz - firstCount); - float chunkAmount = totalSamples / (float) succesiveCount; - int remainChunkOffset = (int) Math.ceil(chunkAmount); - boolean remain = remainChunkOffset != (int) chunkAmount; + final int totalSamples = (tables.stsz - firstCount); + final float chunkAmount = totalSamples / (float) successiveCount; + final int remainChunkOffset = (int) Math.ceil(chunkAmount); + final boolean remain = remainChunkOffset != (int) chunkAmount; int index = 0; tables.stsc = 1; - if (firstCount != succesiveCount) { + if (firstCount != successiveCount) { tables.stsc++; } if (remain) { @@ -488,15 +489,15 @@ public class Mp4FromDashWriter { tables.stscBEntries[index++] = firstCount; tables.stscBEntries[index++] = 1; - if (firstCount != succesiveCount) { + if (firstCount != successiveCount) { tables.stscBEntries[index++] = 2; - tables.stscBEntries[index++] = succesiveCount; + tables.stscBEntries[index++] = successiveCount; tables.stscBEntries[index++] = 1; } if (remain) { tables.stscBEntries[index++] = remainChunkOffset + 1; - tables.stscBEntries[index++] = totalSamples % succesiveCount; + tables.stscBEntries[index++] = totalSamples % successiveCount; tables.stscBEntries[index] = 1; } } @@ -529,7 +530,7 @@ public class Mp4FromDashWriter { } private int lengthFor(final int offset) throws IOException { - int size = auxOffset() - offset; + final int size = auxOffset() - offset; if (moovSimulation) { return size; @@ -545,7 +546,7 @@ public class Mp4FromDashWriter { private int make(final int type, final int extra, final int columns, final int rows) throws IOException { final byte base = 16; - int size = columns * rows * 4; + final int size = columns * rows * 4; int total = size + base; int offset = auxOffset(); @@ -618,7 +619,7 @@ public class Mp4FromDashWriter { size += 4; } - ByteBuffer buffer = ByteBuffer.allocate(size); + final ByteBuffer buffer = ByteBuffer.allocate(size); buffer.putInt(size); buffer.putInt(0x66747970); // "ftyp" @@ -631,7 +632,7 @@ public class Mp4FromDashWriter { buffer.putInt(0x6D703432); // "mp42" compatible brand } - for (Integer brand : compatibleBrands) { + for (final Integer brand : compatibleBrands) { buffer.putInt(brand); // compatible brand } @@ -640,19 +641,20 @@ public class Mp4FromDashWriter { return size; } - private byte[] makeMdat(long refSize, final boolean is64) { + private byte[] makeMdat(final long refSize, final boolean is64) { + long size = refSize; if (is64) { - refSize += 16; + size += 16; } else { - refSize += 8; + size += 8; } - ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8) - .putInt(is64 ? 0x01 : (int) refSize) + final ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8) + .putInt(is64 ? 0x01 : (int) size) .putInt(0x6D646174); // mdat if (is64) { - buffer.putLong(refSize); + buffer.putLong(size); } return buffer.array(); @@ -688,14 +690,14 @@ public class Mp4FromDashWriter { private int makeMoov(final int[] defaultMediaTime, final TablesInfo[] tablesInfo, final boolean is64) throws RuntimeException, IOException { - int start = auxOffset(); + final int start = auxOffset(); auxWrite(new byte[]{ 0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76 }); long longestTrack = 0; - long[] durations = new long[tracks.length]; + final long[] durations = new long[tracks.length]; for (int i = 0; i < durations.length; i++) { durations[i] = (long) Math.ceil( @@ -717,24 +719,12 @@ public class Mp4FromDashWriter { makeTrak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64); } - // udta/meta/ilst/©too - auxWrite(new byte[]{ - 0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, - 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, - 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, - 0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65 // "NewPipe" binary string - }); - return lengthFor(start); } private void makeTrak(final int index, final long duration, final int defaultMediaTime, final TablesInfo tables, final boolean is64) throws IOException { - int start = auxOffset(); + final int start = auxOffset(); auxWrite(new byte[]{ // trak header @@ -743,7 +733,7 @@ public class Mp4FromDashWriter { 0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 }); - ByteBuffer buffer = ByteBuffer.allocate(48); + final ByteBuffer buffer = ByteBuffer.allocate(48); buffer.putLong(time); buffer.putLong(time); buffer.putInt(index + 1); @@ -768,8 +758,8 @@ public class Mp4FromDashWriter { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 // elst header }); - int bMediaRate; - int mediaTime; + final int bMediaRate; + final int mediaTime; if (tracks[index].trak.edstElst == null) { // is a audio track ¿is edst/elst optional for audio tracks? @@ -795,17 +785,17 @@ public class Mp4FromDashWriter { private void makeMdia(final Mdia mdia, final TablesInfo tablesInfo, final boolean is64, final boolean isAudio) throws IOException { - int startMdia = auxOffset(); + final int startMdia = auxOffset(); auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61}); // mdia auxWrite(mdia.mdhd); auxWrite(makeHdlr(mdia.hdlr)); - int startMinf = auxOffset(); + final int startMinf = auxOffset(); auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66}); // minf auxWrite(mdia.minf.mhd); auxWrite(mdia.minf.dinf); - int startStbl = auxOffset(); + final int startStbl = auxOffset(); auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C}); // stbl auxWrite(mdia.minf.stblStsd); @@ -849,21 +839,11 @@ public class Mp4FromDashWriter { } private byte[] makeHdlr(final Hdlr hdlr) { - ByteBuffer buffer = ByteBuffer.wrap(new byte[]{ - 0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72, // hdlr + final ByteBuffer buffer = ByteBuffer.wrap(new byte[]{ + 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, // hdlr 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // binary string - // "ISO Media file created in NewPipe ( - // A libre lightweight streaming frontend for Android)." - 0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, - 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, - 0x77, 0x50, 0x69, 0x70, 0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, - 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, - 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67, - 0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, - 0x41, 0x6E, - 0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E + 0x00// null string character }); buffer.position(12); @@ -875,7 +855,7 @@ public class Mp4FromDashWriter { } private int makeSbgp() throws IOException { - int offset = auxOffset(); + final int offset = auxOffset(); auxWrite(new byte[] { 0x00, 0x00, 0x00, 0x1C, // box size @@ -899,12 +879,12 @@ public class Mp4FromDashWriter { * characteristics of sample groups. The descriptive information is any other * information needed to define or characterize the sample group. * - * ¿is replicabled this box? + * ¿is replicable this box? * NO due lacks of documentation about this box but... * most of m4a encoders and ffmpeg uses this box with dummy values (same values) */ - ByteBuffer buffer = ByteBuffer.wrap(new byte[] { + final ByteBuffer buffer = ByteBuffer.wrap(new byte[] { 0x00, 0x00, 0x00, 0x1A, // box size 0x73, 0x67, 0x70, 0x64, // "sgpd" 0x01, 0x00, 0x00, 0x00, // box flags (unknown flag sets) diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java index e24464dc0..44104f133 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -1,430 +1,416 @@ -package org.schabi.newpipe.streams; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.schabi.newpipe.streams.WebMReader.Cluster; -import org.schabi.newpipe.streams.WebMReader.Segment; -import org.schabi.newpipe.streams.WebMReader.SimpleBlock; -import org.schabi.newpipe.streams.WebMReader.WebMTrack; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * @author kapodamy - */ -public class OggFromWebMWriter implements Closeable { - private static final byte FLAG_UNSET = 0x00; - //private static final byte FLAG_CONTINUED = 0x01; - private static final byte FLAG_FIRST = 0x02; - private static final byte FLAG_LAST = 0x04; - - private static final byte HEADER_CHECKSUM_OFFSET = 22; - private static final byte HEADER_SIZE = 27; - - private static final int TIME_SCALE_NS = 1000000000; - - private boolean done = false; - private boolean parsed = false; - - private SharpStream source; - private SharpStream output; - - private int sequenceCount = 0; - private final int streamId; - private byte packetFlag = FLAG_FIRST; - - private WebMReader webm = null; - private WebMTrack webmTrack = null; - private Segment webmSegment = null; - private Cluster webmCluster = null; - private SimpleBlock webmBlock = null; - - private long webmBlockLastTimecode = 0; - private long webmBlockNearDuration = 0; - - private short segmentTableSize = 0; - private final byte[] segmentTable = new byte[255]; - private long segmentTableNextTimestamp = TIME_SCALE_NS; - - private final int[] crc32Table = new int[256]; - - public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { - if (!source.canRead() || !source.canRewind()) { - throw new IllegalArgumentException("source stream must be readable and allows seeking"); - } - if (!target.canWrite() || !target.canRewind()) { - throw new IllegalArgumentException("output stream must be writable and allows seeking"); - } - - this.source = source; - this.output = target; - - this.streamId = (int) System.currentTimeMillis(); - - populateCrc32Table(); - } - - public boolean isDone() { - return done; - } - - public boolean isParsed() { - return parsed; - } - - public WebMTrack[] getTracksFromSource() throws IllegalStateException { - if (!parsed) { - throw new IllegalStateException("source must be parsed first"); - } - - return webm.getAvailableTracks(); - } - - public void parseSource() throws IOException, IllegalStateException { - if (done) { - throw new IllegalStateException("already done"); - } - if (parsed) { - throw new IllegalStateException("already parsed"); - } - - try { - webm = new WebMReader(source); - webm.parse(); - webmSegment = webm.getNextSegment(); - } finally { - parsed = true; - } - } - - public void selectTrack(final int trackIndex) throws IOException { - if (!parsed) { - throw new IllegalStateException("source must be parsed first"); - } - if (done) { - throw new IOException("already done"); - } - if (webmTrack != null) { - throw new IOException("tracks already selected"); - } - - switch (webm.getAvailableTracks()[trackIndex].kind) { - case Audio: - case Video: - break; - default: - throw new UnsupportedOperationException("the track must an audio or video stream"); - } - - try { - webmTrack = webm.selectTrack(trackIndex); - } finally { - parsed = true; - } - } - - @Override - public void close() throws IOException { - done = true; - parsed = true; - - webmTrack = null; - webm = null; - - if (!output.isClosed()) { - output.flush(); - } - - source.close(); - output.close(); - } - - public void build() throws IOException { - float resolution; - SimpleBlock bloq; - ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255)); - ByteBuffer page = ByteBuffer.allocate(64 * 1024); - - header.order(ByteOrder.LITTLE_ENDIAN); - - /* step 1: get the amount of frames per seconds */ - switch (webmTrack.kind) { - case Audio: - resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata); - if (resolution == 0f) { - throw new RuntimeException("cannot get the audio sample rate"); - } - break; - case Video: - // WARNING: untested - if (webmTrack.defaultDuration == 0) { - throw new RuntimeException("missing default frame time"); - } - resolution = 1000f / ((float) webmTrack.defaultDuration - / webmSegment.info.timecodeScale); - break; - default: - throw new RuntimeException("not implemented"); - } - - /* step 2: create packet with code init data */ - if (webmTrack.codecPrivate != null) { - addPacketSegment(webmTrack.codecPrivate.length); - makePacketheader(0x00, header, webmTrack.codecPrivate); - write(header); - output.write(webmTrack.codecPrivate); - } - - /* step 3: create packet with metadata */ - byte[] buffer = makeMetadata(); - if (buffer != null) { - addPacketSegment(buffer.length); - makePacketheader(0x00, header, buffer); - write(header); - output.write(buffer); - } - - /* step 4: calculate amount of packets */ - while (webmSegment != null) { - bloq = getNextBlock(); - - if (bloq != null && addPacketSegment(bloq)) { - int pos = page.position(); - //noinspection ResultOfMethodCallIgnored - bloq.data.read(page.array(), pos, bloq.dataSize); - page.position(pos + bloq.dataSize); - continue; - } - - // calculate the current packet duration using the next block - double elapsedNs = webmTrack.codecDelay; - - if (bloq == null) { - packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed - elapsedNs += webmBlockLastTimecode; - - if (webmTrack.defaultDuration > 0) { - elapsedNs += webmTrack.defaultDuration; - } else { - // hardcoded way, guess the sample duration - elapsedNs += webmBlockNearDuration; - } - } else { - elapsedNs += bloq.absoluteTimeCodeNs; - } - - // get the sample count in the page - elapsedNs = elapsedNs / TIME_SCALE_NS; - elapsedNs = Math.ceil(elapsedNs * resolution); - - // create header and calculate page checksum - int checksum = makePacketheader((long) elapsedNs, header, null); - checksum = calcCrc32(checksum, page.array(), page.position()); - - header.putInt(HEADER_CHECKSUM_OFFSET, checksum); - - // dump data - write(header); - write(page); - - webmBlock = bloq; - } - } - - private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer, - final byte[] immediatePage) { - short length = HEADER_SIZE; - - buffer.putInt(0x5367674f); // "OggS" binary string in little-endian - buffer.put((byte) 0x00); // version - buffer.put(packetFlag); // type - - buffer.putLong(granPos); // granulate position - - buffer.putInt(streamId); // bitstream serial number - buffer.putInt(sequenceCount++); // page sequence number - - buffer.putInt(0x00); // page checksum - - buffer.put((byte) segmentTableSize); // segment table - buffer.put(segmentTable, 0, segmentTableSize); // segment size - - length += segmentTableSize; - - clearSegmentTable(); // clear segment table for next header - - int checksumCrc32 = calcCrc32(0x00, buffer.array(), length); - - if (immediatePage != null) { - checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length); - buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32); - segmentTableNextTimestamp -= TIME_SCALE_NS; - } - - return checksumCrc32; - } - - @Nullable - private byte[] makeMetadata() { - if ("A_OPUS".equals(webmTrack.codecId)) { - return new byte[]{ - 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string - 0x07, 0x00, 0x00, 0x00, // writing application string size - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) - }; - } else if ("A_VORBIS".equals(webmTrack.codecId)) { - return new byte[]{ - 0x03, // ???????? - 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string - 0x07, 0x00, 0x00, 0x00, // writting application string size - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - 0x01, 0x00, 0x00, 0x00, // additional tags count (zero means no tags) - - /* - // whole file duration (not implemented) - 0x44,// tag string size - 0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, - 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x2E, 0x30, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 - */ - 0x0F, // tag string size - 0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, - 0x44, 0x45, 0x52, 0x3D, // "ENCODER=" binary string - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ???????? - }; - } - - // not implemented for the desired codec - return null; - } - - private void write(final ByteBuffer buffer) throws IOException { - output.write(buffer.array(), 0, buffer.position()); - buffer.position(0); - } - - @Nullable - private SimpleBlock getNextBlock() throws IOException { - SimpleBlock res; - - if (webmBlock != null) { - res = webmBlock; - webmBlock = null; - return res; - } - - if (webmSegment == null) { - webmSegment = webm.getNextSegment(); - if (webmSegment == null) { - return null; // no more blocks in the selected track - } - } - - if (webmCluster == null) { - webmCluster = webmSegment.getNextCluster(); - if (webmCluster == null) { - webmSegment = null; - return getNextBlock(); - } - } - - res = webmCluster.getNextSimpleBlock(); - if (res == null) { - webmCluster = null; - return getNextBlock(); - } - - webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode; - webmBlockLastTimecode = res.absoluteTimeCodeNs; - - return res; - } - - private float getSampleFrequencyFromTrack(final byte[] bMetadata) { - // hardcoded way - ByteBuffer buffer = ByteBuffer.wrap(bMetadata); - - while (buffer.remaining() >= 6) { - int id = buffer.getShort() & 0xFFFF; - if (id == 0x0000B584) { - return buffer.getFloat(); - } - } - - return 0f; - } - - private void clearSegmentTable() { - segmentTableNextTimestamp += TIME_SCALE_NS; - packetFlag = FLAG_UNSET; - segmentTableSize = 0; - } - - private boolean addPacketSegment(final SimpleBlock block) { - long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay; - - if (timestamp >= segmentTableNextTimestamp) { - return false; - } - - return addPacketSegment(block.dataSize); - } - - private boolean addPacketSegment(int size) { - if (size > 65025) { - throw new UnsupportedOperationException("page size cannot be larger than 65025"); - } - - int available = (segmentTable.length - segmentTableSize) * 255; - boolean extra = (size % 255) == 0; - - if (extra) { - // add a zero byte entry in the table - // required to indicate the sample size is multiple of 255 - available -= 255; - } - - // check if possible add the segment, without overflow the table - if (available < size) { - return false; // not enough space on the page - } - - for (; size > 0; size -= 255) { - segmentTable[segmentTableSize++] = (byte) Math.min(size, 255); - } - - if (extra) { - segmentTable[segmentTableSize++] = 0x00; - } - - return true; - } - - private void populateCrc32Table() { - for (int i = 0; i < 0x100; i++) { - int crc = i << 24; - for (int j = 0; j < 8; j++) { - long b = crc >>> 31; - crc <<= 1; - crc ^= (int) (0x100000000L - b) & 0x04c11db7; - } - crc32Table[i] = crc; - } - } - - private int calcCrc32(int initialCrc, final byte[] buffer, final int size) { - for (int i = 0; i < size; i++) { - int reg = (initialCrc >>> 24) & 0xff; - initialCrc = (initialCrc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; - } - - return initialCrc; - } -} +package org.schabi.newpipe.streams; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.schabi.newpipe.streams.WebMReader.Cluster; +import org.schabi.newpipe.streams.WebMReader.Segment; +import org.schabi.newpipe.streams.WebMReader.SimpleBlock; +import org.schabi.newpipe.streams.WebMReader.WebMTrack; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * @author kapodamy + */ +public class OggFromWebMWriter implements Closeable { + private static final byte FLAG_UNSET = 0x00; + //private static final byte FLAG_CONTINUED = 0x01; + private static final byte FLAG_FIRST = 0x02; + private static final byte FLAG_LAST = 0x04; + + private static final byte HEADER_CHECKSUM_OFFSET = 22; + private static final byte HEADER_SIZE = 27; + + private static final int TIME_SCALE_NS = 1000000000; + + private boolean done = false; + private boolean parsed = false; + + private SharpStream source; + private SharpStream output; + + private int sequenceCount = 0; + private final int streamId; + private byte packetFlag = FLAG_FIRST; + + private WebMReader webm = null; + private WebMTrack webmTrack = null; + private Segment webmSegment = null; + private Cluster webmCluster = null; + private SimpleBlock webmBlock = null; + + private long webmBlockLastTimecode = 0; + private long webmBlockNearDuration = 0; + + private short segmentTableSize = 0; + private final byte[] segmentTable = new byte[255]; + private long segmentTableNextTimestamp = TIME_SCALE_NS; + + private final int[] crc32Table = new int[256]; + + public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { + if (!source.canRead() || !source.canRewind()) { + throw new IllegalArgumentException("source stream must be readable and allows seeking"); + } + if (!target.canWrite() || !target.canRewind()) { + throw new IllegalArgumentException("output stream must be writable and allows seeking"); + } + + this.source = source; + this.output = target; + + this.streamId = (int) System.currentTimeMillis(); + + populateCrc32Table(); + } + + public boolean isDone() { + return done; + } + + public boolean isParsed() { + return parsed; + } + + public WebMTrack[] getTracksFromSource() throws IllegalStateException { + if (!parsed) { + throw new IllegalStateException("source must be parsed first"); + } + + return webm.getAvailableTracks(); + } + + public void parseSource() throws IOException, IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (parsed) { + throw new IllegalStateException("already parsed"); + } + + try { + webm = new WebMReader(source); + webm.parse(); + webmSegment = webm.getNextSegment(); + } finally { + parsed = true; + } + } + + public void selectTrack(final int trackIndex) throws IOException { + if (!parsed) { + throw new IllegalStateException("source must be parsed first"); + } + if (done) { + throw new IOException("already done"); + } + if (webmTrack != null) { + throw new IOException("tracks already selected"); + } + + switch (webm.getAvailableTracks()[trackIndex].kind) { + case Audio: + case Video: + break; + default: + throw new UnsupportedOperationException("the track must an audio or video stream"); + } + + try { + webmTrack = webm.selectTrack(trackIndex); + } finally { + parsed = true; + } + } + + @Override + public void close() throws IOException { + done = true; + parsed = true; + + webmTrack = null; + webm = null; + + if (!output.isClosed()) { + output.flush(); + } + + source.close(); + output.close(); + } + + public void build() throws IOException { + final float resolution; + SimpleBlock bloq; + final ByteBuffer header = ByteBuffer.allocate(27 + (255 * 255)); + final ByteBuffer page = ByteBuffer.allocate(64 * 1024); + + header.order(ByteOrder.LITTLE_ENDIAN); + + /* step 1: get the amount of frames per seconds */ + switch (webmTrack.kind) { + case Audio: + resolution = getSampleFrequencyFromTrack(webmTrack.bMetadata); + if (resolution == 0f) { + throw new RuntimeException("cannot get the audio sample rate"); + } + break; + case Video: + // WARNING: untested + if (webmTrack.defaultDuration == 0) { + throw new RuntimeException("missing default frame time"); + } + resolution = 1000f / ((float) webmTrack.defaultDuration + / webmSegment.info.timecodeScale); + break; + default: + throw new RuntimeException("not implemented"); + } + + /* step 2: create packet with code init data */ + if (webmTrack.codecPrivate != null) { + addPacketSegment(webmTrack.codecPrivate.length); + makePacketheader(0x00, header, webmTrack.codecPrivate); + write(header); + output.write(webmTrack.codecPrivate); + } + + /* step 3: create packet with metadata */ + final byte[] buffer = makeMetadata(); + if (buffer != null) { + addPacketSegment(buffer.length); + makePacketheader(0x00, header, buffer); + write(header); + output.write(buffer); + } + + /* step 4: calculate amount of packets */ + while (webmSegment != null) { + bloq = getNextBlock(); + + if (bloq != null && addPacketSegment(bloq)) { + final int pos = page.position(); + //noinspection ResultOfMethodCallIgnored + bloq.data.read(page.array(), pos, bloq.dataSize); + page.position(pos + bloq.dataSize); + continue; + } + + // calculate the current packet duration using the next block + double elapsedNs = webmTrack.codecDelay; + + if (bloq == null) { + packetFlag = FLAG_LAST; // note: if the flag is FLAG_CONTINUED, is changed + elapsedNs += webmBlockLastTimecode; + + if (webmTrack.defaultDuration > 0) { + elapsedNs += webmTrack.defaultDuration; + } else { + // hardcoded way, guess the sample duration + elapsedNs += webmBlockNearDuration; + } + } else { + elapsedNs += bloq.absoluteTimeCodeNs; + } + + // get the sample count in the page + elapsedNs = elapsedNs / TIME_SCALE_NS; + elapsedNs = Math.ceil(elapsedNs * resolution); + + // create header and calculate page checksum + int checksum = makePacketheader((long) elapsedNs, header, null); + checksum = calcCrc32(checksum, page.array(), page.position()); + + header.putInt(HEADER_CHECKSUM_OFFSET, checksum); + + // dump data + write(header); + write(page); + + webmBlock = bloq; + } + } + + private int makePacketheader(final long granPos, @NonNull final ByteBuffer buffer, + final byte[] immediatePage) { + short length = HEADER_SIZE; + + buffer.putInt(0x5367674f); // "OggS" binary string in little-endian + buffer.put((byte) 0x00); // version + buffer.put(packetFlag); // type + + buffer.putLong(granPos); // granulate position + + buffer.putInt(streamId); // bitstream serial number + buffer.putInt(sequenceCount++); // page sequence number + + buffer.putInt(0x00); // page checksum + + buffer.put((byte) segmentTableSize); // segment table + buffer.put(segmentTable, 0, segmentTableSize); // segment size + + length += segmentTableSize; + + clearSegmentTable(); // clear segment table for next header + + int checksumCrc32 = calcCrc32(0x00, buffer.array(), length); + + if (immediatePage != null) { + checksumCrc32 = calcCrc32(checksumCrc32, immediatePage, immediatePage.length); + buffer.putInt(HEADER_CHECKSUM_OFFSET, checksumCrc32); + segmentTableNextTimestamp -= TIME_SCALE_NS; + } + + return checksumCrc32; + } + + @Nullable + private byte[] makeMetadata() { + if ("A_OPUS".equals(webmTrack.codecId)) { + return new byte[]{ + 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) + }; + } else if ("A_VORBIS".equals(webmTrack.codecId)) { + return new byte[]{ + 0x03, // ¿¿¿??? + 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) + }; + } + + // not implemented for the desired codec + return null; + } + + private void write(final ByteBuffer buffer) throws IOException { + output.write(buffer.array(), 0, buffer.position()); + buffer.position(0); + } + + @Nullable + private SimpleBlock getNextBlock() throws IOException { + SimpleBlock res; + + if (webmBlock != null) { + res = webmBlock; + webmBlock = null; + return res; + } + + if (webmSegment == null) { + webmSegment = webm.getNextSegment(); + if (webmSegment == null) { + return null; // no more blocks in the selected track + } + } + + if (webmCluster == null) { + webmCluster = webmSegment.getNextCluster(); + if (webmCluster == null) { + webmSegment = null; + return getNextBlock(); + } + } + + res = webmCluster.getNextSimpleBlock(); + if (res == null) { + webmCluster = null; + return getNextBlock(); + } + + webmBlockNearDuration = res.absoluteTimeCodeNs - webmBlockLastTimecode; + webmBlockLastTimecode = res.absoluteTimeCodeNs; + + return res; + } + + private float getSampleFrequencyFromTrack(final byte[] bMetadata) { + // hardcoded way + final ByteBuffer buffer = ByteBuffer.wrap(bMetadata); + + while (buffer.remaining() >= 6) { + final int id = buffer.getShort() & 0xFFFF; + if (id == 0x0000B584) { + return buffer.getFloat(); + } + } + + return 0.0f; + } + + private void clearSegmentTable() { + segmentTableNextTimestamp += TIME_SCALE_NS; + packetFlag = FLAG_UNSET; + segmentTableSize = 0; + } + + private boolean addPacketSegment(final SimpleBlock block) { + final long timestamp = block.absoluteTimeCodeNs + webmTrack.codecDelay; + + if (timestamp >= segmentTableNextTimestamp) { + return false; + } + + return addPacketSegment(block.dataSize); + } + + private boolean addPacketSegment(final int size) { + if (size > 65025) { + throw new UnsupportedOperationException("page size cannot be larger than 65025"); + } + + int available = (segmentTable.length - segmentTableSize) * 255; + final boolean extra = (size % 255) == 0; + + if (extra) { + // add a zero byte entry in the table + // required to indicate the sample size is multiple of 255 + available -= 255; + } + + // check if possible add the segment, without overflow the table + if (available < size) { + return false; // not enough space on the page + } + + for (int seg = size; seg > 0; seg -= 255) { + segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255); + } + + if (extra) { + segmentTable[segmentTableSize++] = 0x00; + } + + return true; + } + + private void populateCrc32Table() { + for (int i = 0; i < 0x100; i++) { + int crc = i << 24; + for (int j = 0; j < 8; j++) { + final long b = crc >>> 31; + crc <<= 1; + crc ^= (int) (0x100000000L - b) & 0x04c11db7; + } + crc32Table[i] = crc; + } + } + + private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) { + int crc = initialCrc; + for (int i = 0; i < size; i++) { + final int reg = (crc >>> 24) & 0xff; + crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; + } + + return crc; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java index eddb951e5..8cb31141b 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java @@ -65,23 +65,23 @@ public class SrtFromTtmlWriter { */ // parse XML - byte[] buffer = new byte[(int) ttml.available()]; + final byte[] buffer = new byte[(int) ttml.available()]; ttml.read(buffer); - Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "", + final Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "", Parser.xmlParser()); - StringBuilder text = new StringBuilder(128); - Elements paragraphList = doc.select("body > div > p"); + final StringBuilder text = new StringBuilder(128); + final Elements paragraphList = doc.select("body > div > p"); // check if has frames if (paragraphList.size() < 1) { return; } - for (Element paragraph : paragraphList) { + for (final Element paragraph : paragraphList) { text.setLength(0); - for (Node children : paragraph.childNodes()) { + for (final Node children : paragraph.childNodes()) { if (children instanceof TextNode) { text.append(((TextNode) children).text()); } else if (children instanceof Element @@ -94,8 +94,8 @@ public class SrtFromTtmlWriter { continue; } - String begin = getTimestamp(paragraph, "begin"); - String end = getTimestamp(paragraph, "end"); + final String begin = getTimestamp(paragraph, "begin"); + final String end = getTimestamp(paragraph, "end"); writeFrame(begin, end, text); } diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java index 56cea9f2d..193f89996 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java @@ -99,7 +99,7 @@ public class WebMReader { ensure(segment.ref); // WARNING: track cannot be the same or have different index in new segments - Element elem = untilElement(null, ID_SEGMENT); + final Element elem = untilElement(null, ID_SEGMENT); if (elem == null) { done = true; return null; @@ -113,7 +113,7 @@ public class WebMReader { int length = (int) parent.contentSize; long value = 0; while (length-- > 0) { - int read = stream.read(); + final int read = stream.read(); if (read == -1) { throw new EOFException(); } @@ -127,9 +127,9 @@ public class WebMReader { } private byte[] readBlob(final Element parent) throws IOException { - long length = parent.contentSize; - byte[] buffer = new byte[(int) length]; - int read = stream.read(buffer); + final long length = parent.contentSize; + final byte[] buffer = new byte[(int) length]; + final int read = stream.read(buffer); if (read < length) { throw new EOFException(); } @@ -168,7 +168,7 @@ public class WebMReader { } private Element readElement() throws IOException { - Element elem = new Element(); + final Element elem = new Element(); elem.offset = stream.position(); elem.type = (int) readEncodedNumber(); elem.contentSize = readEncodedNumber(); @@ -178,7 +178,7 @@ public class WebMReader { } private Element readElement(final int expected) throws IOException { - Element elem = readElement(); + final Element elem = readElement(); if (expected != 0 && elem.type != expected) { throw new NoSuchElementException("expected " + elementID(expected) + " found " + elementID(elem.type)); @@ -194,7 +194,7 @@ public class WebMReader { if (expected.length < 1) { return elem; } - for (int type : expected) { + for (final int type : expected) { if (elem.type == type) { return elem; } @@ -211,7 +211,7 @@ public class WebMReader { } private void ensure(final Element ref) throws IOException { - long skip = (ref.offset + ref.size) - stream.position(); + final long skip = (ref.offset + ref.size) - stream.position(); if (skip == 0) { return; @@ -249,7 +249,7 @@ public class WebMReader { private Info readInfo(final Element ref) throws IOException { Element elem; - Info info = new Info(); + final Info info = new Info(); while ((elem = untilElement(ref, ID_TIMECODE_SCALE, ID_DURATION)) != null) { switch (elem.type) { @@ -272,7 +272,7 @@ public class WebMReader { private Segment readSegment(final Element ref, final int trackLacingExpected, final boolean metadataExpected) throws IOException { - Segment obj = new Segment(ref); + final Segment obj = new Segment(ref); Element elem; while ((elem = untilElement(ref, ID_INFO, ID_TRACKS, ID_CLUSTER)) != null) { if (elem.type == ID_CLUSTER) { @@ -293,18 +293,18 @@ public class WebMReader { if (metadataExpected && (obj.info == null || obj.tracks == null)) { throw new RuntimeException( "Cluster element found without Info and/or Tracks element at position " - + String.valueOf(ref.offset)); + + ref.offset); } return obj; } private WebMTrack[] readTracks(final Element ref, final int lacingExpected) throws IOException { - ArrayList trackEntries = new ArrayList<>(2); + final ArrayList trackEntries = new ArrayList<>(2); Element elemTrackEntry; while ((elemTrackEntry = untilElement(ref, ID_TRACK_ENTRY)) != null) { - WebMTrack entry = new WebMTrack(); + final WebMTrack entry = new WebMTrack(); boolean drop = false; Element elem; while ((elem = untilElement(elemTrackEntry)) != null) { @@ -348,10 +348,10 @@ public class WebMReader { ensure(elemTrackEntry); } - WebMTrack[] entries = new WebMTrack[trackEntries.size()]; + final WebMTrack[] entries = new WebMTrack[trackEntries.size()]; trackEntries.toArray(entries); - for (WebMTrack entry : entries) { + for (final WebMTrack entry : entries) { switch (entry.trackType) { case 1: entry.kind = TrackKind.Video; @@ -369,7 +369,7 @@ public class WebMReader { } private SimpleBlock readSimpleBlock(final Element ref) throws IOException { - SimpleBlock obj = new SimpleBlock(ref); + final SimpleBlock obj = new SimpleBlock(ref); obj.trackNumber = readEncodedNumber(); obj.relativeTimeCode = stream.readShort(); obj.flags = (byte) stream.read(); @@ -385,11 +385,11 @@ public class WebMReader { } private Cluster readCluster(final Element ref) throws IOException { - Cluster obj = new Cluster(ref); + final Cluster obj = new Cluster(ref); - Element elem = untilElement(ref, ID_TIMECODE); + final Element elem = untilElement(ref, ID_TIMECODE); if (elem == null) { - throw new NoSuchElementException("Cluster at " + String.valueOf(ref.offset) + throw new NoSuchElementException("Cluster at " + ref.offset + " without Timecode element"); } obj.timecode = readNumber(elem); @@ -443,7 +443,7 @@ public class WebMReader { } ensure(segment.currentCluster); - Element elem = untilElement(segment.ref, ID_CLUSTER); + final Element elem = untilElement(segment.ref, ID_CLUSTER); if (elem == null) { return null; } @@ -520,7 +520,7 @@ public class WebMReader { currentSimpleBlock = readSimpleBlock(elem); if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) { - currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize); + currentSimpleBlock.data = stream.getView(currentSimpleBlock.dataSize); // calculate the timestamp in nanoseconds currentSimpleBlock.absoluteTimeCodeNs = currentSimpleBlock.relativeTimeCode diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index c3cd2a2e4..55792d099 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -107,7 +107,7 @@ public class WebMWriter implements Closeable { done = true; parsed = true; - for (SharpStream src : sourceTracks) { + for (final SharpStream src : sourceTracks) { src.close(); } @@ -128,12 +128,12 @@ public class WebMWriter implements Closeable { makeEBML(out); - long offsetSegmentSizeSet = written + 5; - long offsetInfoDurationSet = written + 94; - long offsetClusterSet = written + 58; - long offsetCuesSet = written + 75; + final long offsetSegmentSizeSet = written + 5; + final long offsetInfoDurationSet = written + 94; + final long offsetClusterSet = written + 58; + final long offsetCuesSet = written + 75; - ArrayList listBuffer = new ArrayList<>(4); + final ArrayList listBuffer = new ArrayList<>(4); /* segment */ listBuffer.add(new byte[]{ @@ -141,38 +141,34 @@ public class WebMWriter implements Closeable { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size }); - long segmentOffset = written + listBuffer.get(0).length; + final long segmentOffset = written + listBuffer.get(0).length; /* seek head */ listBuffer.add(new byte[]{ 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe, 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53, - (byte) 0xac, (byte) 0x81, /*info offset*/ 0x43, + (byte) 0xac, (byte) 0x81, + /*info offset*/ 0x43, 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81, - /*tracks offset*/ 0x6a, + /*tracks offset*/ 0x56, 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f, - 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00, + 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, + /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00, 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53, - (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00 + (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, + /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00 }); /* info */ listBuffer.add(new byte[]{ - 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1 + 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0x8e, 0x2a, (byte) 0xd7, (byte) 0xb1 }); - listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true)); // this value MUST NOT exceed 4 bytes + // the segment duration MUST NOT exceed 4 bytes + listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true)); listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84, 0x00, 0x00, 0x00, 0x00, // info.duration - - /* MuxingApp */ - 0x4d, (byte) 0x80, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - - /* WritingApp */ - 0x57, 0x41, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string }); /* tracks */ @@ -181,11 +177,11 @@ public class WebMWriter implements Closeable { dump(listBuffer, out); // reserve space for Cues element - long cueOffset = written; + final long cueOffset = written; makeEbmlVoid(out, CUE_RESERVE_SIZE, true); - int[] defaultSampleDuration = new int[infoTracks.length]; - long[] duration = new long[infoTracks.length]; + final int[] defaultSampleDuration = new int[infoTracks.length]; + final long[] duration = new long[infoTracks.length]; for (int i = 0; i < infoTracks.length; i++) { if (infoTracks[i].defaultDuration < 0) { @@ -198,9 +194,9 @@ public class WebMWriter implements Closeable { } // Select a track for the cue - int cuesForTrackId = selectTrackForCue(); + final int cuesForTrackId = selectTrackForCue(); long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0; - ArrayList keyFrames = new ArrayList<>(32); + final ArrayList keyFrames = new ArrayList<>(32); int firstClusterOffset = (int) written; long currentClusterOffset = makeCluster(out, 0, 0, true); @@ -217,7 +213,7 @@ public class WebMWriter implements Closeable { blockWritten = 0; int i = 0; while (i < readers.length) { - Block bloq = getNextBlockFrom(i); + final Block bloq = getNextBlockFrom(i); if (bloq == null) { i++; continue; @@ -276,7 +272,7 @@ public class WebMWriter implements Closeable { makeCluster(out, -1, currentClusterOffset, false); - long segmentSize = written - offsetSegmentSizeSet - 7; + final long segmentSize = written - offsetSegmentSizeSet - 7; /* Segment size */ seekTo(out, offsetSegmentSizeSet); @@ -307,8 +303,8 @@ public class WebMWriter implements Closeable { short cueSize = 0; dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out); // header size is 7 - for (KeyFrame keyFrame : keyFrames) { - int size = makeCuePoint(cuesForTrackId, keyFrame, outBuffer); + for (final KeyFrame keyFrame : keyFrames) { + final int size = makeCuePoint(cuesForTrackId, keyFrame, outBuffer); if ((cueSize + size + 7 + MINIMUM_EBML_VOID_SIZE) > CUE_RESERVE_SIZE) { break; // no space left @@ -327,7 +323,7 @@ public class WebMWriter implements Closeable { /* seek head, seek for cues element */ writeInt(out, offsetCuesSet, (int) (cueOffset - segmentOffset)); - for (ClusterInfo cluster : clustersOffsetsSizes) { + for (final ClusterInfo cluster : clustersOffsetsSizes) { writeInt(out, cluster.offset, cluster.size | 0x10000000); } } @@ -348,13 +344,13 @@ public class WebMWriter implements Closeable { } } - SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock(); + final SimpleBlock res = readersCluster[internalTrackId].getNextSimpleBlock(); if (res == null) { readersCluster[internalTrackId] = null; return new Block(); // fake block to indicate the end of the cluster } - Block bloq = new Block(); + final Block bloq = new Block(); bloq.data = res.data; bloq.dataSize = res.dataSize; bloq.trackNumber = internalTrackId; @@ -388,13 +384,13 @@ public class WebMWriter implements Closeable { private void writeBlock(final SharpStream stream, final Block bloq, final long clusterTimecode) throws IOException { - long relativeTimeCode = bloq.absoluteTimecode - clusterTimecode; + final long relativeTimeCode = bloq.absoluteTimecode - clusterTimecode; if (relativeTimeCode < Short.MIN_VALUE || relativeTimeCode > Short.MAX_VALUE) { throw new IndexOutOfBoundsException("SimpleBlock timecode overflow."); } - ArrayList listBuffer = new ArrayList<>(5); + final ArrayList listBuffer = new ArrayList<>(5); listBuffer.add(new byte[]{(byte) 0xa3}); listBuffer.add(null); // block size listBuffer.add(encode(bloq.trackNumber + 1, false)); @@ -416,9 +412,10 @@ public class WebMWriter implements Closeable { } } - private long makeCluster(final SharpStream stream, final long timecode, long offset, + private long makeCluster(final SharpStream stream, final long timecode, final long offsetStart, final boolean create) throws IOException { ClusterInfo cluster; + long offset = offsetStart; if (offset > 0) { // save the size of the previous cluster (maximum 256 MiB) @@ -449,7 +446,7 @@ public class WebMWriter implements Closeable { } private void makeEBML(final SharpStream stream) throws IOException { - // deafult values + // default values dump(new byte[]{ 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01, @@ -461,7 +458,7 @@ public class WebMWriter implements Closeable { } private ArrayList makeTracks() { - ArrayList buffer = new ArrayList<>(1); + final ArrayList buffer = new ArrayList<>(1); buffer.add(new byte[]{0x16, 0x54, (byte) 0xae, 0x6b}); buffer.add(null); @@ -473,8 +470,8 @@ public class WebMWriter implements Closeable { } private ArrayList makeTrackEntry(final int internalTrackId, final WebMTrack track) { - byte[] id = encode(internalTrackId + 1, true); - ArrayList buffer = new ArrayList<>(12); + final byte[] id = encode(internalTrackId + 1, true); + final ArrayList buffer = new ArrayList<>(12); /* track */ buffer.add(new byte[]{(byte) 0xae}); @@ -539,7 +536,7 @@ public class WebMWriter implements Closeable { private int makeCuePoint(final int internalTrackId, final KeyFrame keyFrame, final byte[] buffer) { - ArrayList cue = new ArrayList<>(5); + final ArrayList cue = new ArrayList<>(5); /* CuePoint */ cue.add(new byte[]{(byte) 0xbb}); @@ -555,7 +552,7 @@ public class WebMWriter implements Closeable { int size = 0; lengthFor(cue); - for (byte[] buff : cue) { + for (final byte[] buff : cue) { System.arraycopy(buff, 0, buffer, size, buff.length); size += buff.length; } @@ -565,7 +562,7 @@ public class WebMWriter implements Closeable { private ArrayList makeCueTrackPosition(final int internalTrackId, final KeyFrame keyFrame) { - ArrayList buffer = new ArrayList<>(8); + final ArrayList buffer = new ArrayList<>(8); /* CueTrackPositions */ buffer.add(new byte[]{(byte) 0xb7}); @@ -588,8 +585,10 @@ public class WebMWriter implements Closeable { return lengthFor(buffer); } - private void makeEbmlVoid(final SharpStream out, int size, final boolean wipe) + private void makeEbmlVoid(final SharpStream out, final int amount, final boolean wipe) throws IOException { + int size = amount; + /* ebml void */ outByteBuffer.putShort(0, (short) 0xec20); outByteBuffer.putShort(2, (short) (size - 4)); @@ -599,7 +598,7 @@ public class WebMWriter implements Closeable { if (wipe) { size -= 4; while (size > 0) { - int write = Math.min(size, outBuffer.length); + final int write = Math.min(size, outBuffer.length); dump(outBuffer, write, out); size -= write; } @@ -618,7 +617,7 @@ public class WebMWriter implements Closeable { private void dump(final ArrayList buffers, final SharpStream stream) throws IOException { - for (byte[] buffer : buffers) { + for (final byte[] buffer : buffers) { stream.write(buffer); written += buffer.length; } @@ -650,9 +649,9 @@ public class WebMWriter implements Closeable { length++; } - int offset = withLength ? 1 : 0; - byte[] buffer = new byte[offset + length]; - long marker = (long) Math.floor((length - 1f) / 8f); + final int offset = withLength ? 1 : 0; + final byte[] buffer = new byte[offset + length]; + final long marker = (long) Math.floor((length - 1f) / 8f); int shift = 0; for (int i = length - 1; i >= 0; i--, shift += 8) { @@ -671,10 +670,9 @@ public class WebMWriter implements Closeable { } private ArrayList encode(final String value) { - byte[] str; - str = value.getBytes(StandardCharsets.UTF_8); // or use "utf-8" + final byte[] str = value.getBytes(StandardCharsets.UTF_8); // or use "utf-8" - ArrayList buffer = new ArrayList<>(2); + final ArrayList buffer = new ArrayList<>(2); buffer.add(encode(str.length, false)); buffer.add(str); @@ -701,7 +699,7 @@ public class WebMWriter implements Closeable { } } - int kind; + final int kind; if (audioTracks == infoTracks.length) { kind = 2; } else if (videoTracks == infoTracks.length) { diff --git a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java index 4fa14ed01..152521018 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java @@ -84,11 +84,11 @@ public final class AnimationUtils { String id; try { id = view.getResources().getResourceEntryName(view.getId()); - } catch (Exception e) { + } catch (final Exception e) { id = view.getId() + ""; } - String msg = String.format("%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", enterOrExit, + final String msg = String.format("%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", enterOrExit, view.getClass().getSimpleName(), id, animationType, duration, delay, execOnEnd); Log.d(TAG, "animateView()" + msg); } @@ -158,17 +158,13 @@ public final class AnimationUtils { } final int[][] empty = new int[][]{new int[0]}; - ValueAnimator viewPropertyAnimator = ValueAnimator + final ValueAnimator viewPropertyAnimator = ValueAnimator .ofObject(new ArgbEvaluator(), colorStart, colorEnd); viewPropertyAnimator.setInterpolator(new FastOutSlowInInterpolator()); viewPropertyAnimator.setDuration(duration); - viewPropertyAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(final ValueAnimator animation) { + viewPropertyAnimator.addUpdateListener(animation -> ViewCompat.setBackgroundTintList(view, - new ColorStateList(empty, new int[]{(int) animation.getAnimatedValue()})); - } - }); + new ColorStateList(empty, new int[]{(int) animation.getAnimatedValue()}))); viewPropertyAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(final Animator animation) { @@ -201,16 +197,12 @@ public final class AnimationUtils { + "colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"); } - ValueAnimator viewPropertyAnimator = ValueAnimator + final ValueAnimator viewPropertyAnimator = ValueAnimator .ofObject(new ArgbEvaluator(), colorStart, colorEnd); viewPropertyAnimator.setInterpolator(new FastOutSlowInInterpolator()); viewPropertyAnimator.setDuration(duration); - viewPropertyAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(final ValueAnimator animation) { - view.setTextColor((int) animation.getAnimatedValue()); - } - }); + viewPropertyAnimator.addUpdateListener(animation -> + view.setTextColor((int) animation.getAnimatedValue())); viewPropertyAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(final Animator animation) { @@ -233,7 +225,7 @@ public final class AnimationUtils { + "from " + height + " to → " + targetHeight + " in: " + view); } - ValueAnimator animator = ValueAnimator.ofFloat(height, targetHeight); + final ValueAnimator animator = ValueAnimator.ofFloat(height, targetHeight); animator.setInterpolator(new FastOutSlowInInterpolator()); animator.setDuration(duration); animator.addUpdateListener(animation -> { @@ -430,7 +422,7 @@ public final class AnimationUtils { final long duration, final long delay, final Runnable execOnEnd) { if (enterOrExit) { - view.setTranslationY(-view.getHeight() / 2); + view.setTranslationY(-view.getHeight() / 2.0f); view.setAlpha(0f); view.animate() .setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).translationY(0) @@ -445,7 +437,7 @@ public final class AnimationUtils { }).start(); } else { view.animate().setInterpolator(new FastOutSlowInInterpolator()) - .alpha(0f).translationY(-view.getHeight() / 2) + .alpha(0f).translationY(-view.getHeight() / 2.0f) .setDuration(duration).setStartDelay(delay) .setListener(new AnimatorListenerAdapter() { @Override @@ -462,7 +454,7 @@ public final class AnimationUtils { public static void slideUp(final View view, final long duration, final long delay, @FloatRange(from = 0.0f, to = 1.0f) final float translationPercent) { - int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels + final int translationY = (int) (view.getResources().getDisplayMetrics().heightPixels * (translationPercent)); view.animate().setListener(null).cancel(); diff --git a/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java b/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java index 5b1c46372..b6f1eaf49 100644 --- a/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/BitmapUtils.java @@ -14,14 +14,14 @@ public final class BitmapUtils { return null; } - float sourceWidth = inputBitmap.getWidth(); - float sourceHeight = inputBitmap.getHeight(); + final float sourceWidth = inputBitmap.getWidth(); + final float sourceHeight = inputBitmap.getHeight(); - float xScale = newWidth / sourceWidth; - float yScale = newHeight / sourceHeight; + final float xScale = newWidth / sourceWidth; + final float yScale = newHeight / sourceHeight; - float newXScale; - float newYScale; + final float newXScale; + final float newYScale; if (yScale > xScale) { newXScale = xScale / yScale; @@ -31,15 +31,14 @@ public final class BitmapUtils { newYScale = yScale / xScale; } - float scaledWidth = newXScale * sourceWidth; - float scaledHeight = newYScale * sourceHeight; + final float scaledWidth = newXScale * sourceWidth; + final float scaledHeight = newYScale * sourceHeight; - int left = (int) ((sourceWidth - scaledWidth) / 2); - int top = (int) ((sourceHeight - scaledHeight) / 2); - int width = (int) scaledWidth; - int height = (int) scaledHeight; + final int left = (int) ((sourceWidth - scaledWidth) / 2); + final int top = (int) ((sourceHeight - scaledHeight) / 2); + final int width = (int) scaledWidth; + final int height = (int) scaledHeight; return Bitmap.createBitmap(inputBitmap, left, top, width, height); } - } diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java index 770592537..cf347e7c4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -37,12 +37,12 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { if (!(v instanceof TextView)) { return false; } - TextView widget = (TextView) v; - Object text = widget.getText(); + final TextView widget = (TextView) v; + final Object text = widget.getText(); if (text instanceof Spanned) { - Spannable buffer = (Spannable) text; + final Spannable buffer = (Spannable) text; - int action = event.getAction(); + final int action = event.getAction(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { @@ -55,11 +55,11 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { x += widget.getScrollX(); y += widget.getScrollY(); - Layout layout = widget.getLayout(); - int line = layout.getLineForVertical(y); - int off = layout.getOffsetForHorizontal(line, x); + final Layout layout = widget.getLayout(); + final int line = layout.getLineForVertical(y); + final int off = layout.getOffsetForHorizontal(line, x); - ClickableSpan[] link = buffer.getSpans(off, off, + final ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class); if (link.length != 0) { @@ -86,17 +86,17 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { private boolean handleUrl(final Context context, final URLSpan urlSpan) { String url = urlSpan.getURL(); int seconds = -1; - Matcher matcher = TIMESTAMP_PATTERN.matcher(url); + final Matcher matcher = TIMESTAMP_PATTERN.matcher(url); if (matcher.matches()) { url = matcher.group(1); seconds = Integer.parseInt(matcher.group(2)); } - StreamingService service; - StreamingService.LinkType linkType; + final StreamingService service; + final StreamingService.LinkType linkType; try { service = NewPipe.getServiceByUrl(url); linkType = service.getLinkTypeByUrl(url); - } catch (ExtractionException e) { + } catch (final ExtractionException e) { return false; } if (linkType == StreamingService.LinkType.NONE) { @@ -112,18 +112,20 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { private boolean playOnPopup(final Context context, final String url, final StreamingService service, final int seconds) { - LinkHandlerFactory factory = service.getStreamLHFactory(); - String cleanUrl = null; + final LinkHandlerFactory factory = service.getStreamLHFactory(); + final String cleanUrl; try { cleanUrl = factory.getUrl(factory.getId(url)); - } catch (ParsingException e) { + } catch (final ParsingException e) { return false; } - Single single = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); + final Single single + = ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); single.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(info -> { - PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds * 1000); + final PlayQueue playQueue + = new SinglePlayQueue((StreamInfo) info, seconds * 1000); NavigationHelper.playOnPopupPlayer(context, playQueue, false); }); return true; diff --git a/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java b/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java index d8b81b4ce..d970ec472 100644 --- a/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/CookieUtils.java @@ -12,8 +12,8 @@ public final class CookieUtils { } public static String concatCookies(final Collection cookieStrings) { - Set cookieSet = new HashSet<>(); - for (String cookies : cookieStrings) { + final Set cookieSet = new HashSet<>(); + for (final String cookies : cookieStrings) { cookieSet.addAll(splitCookies(cookies)); } return TextUtils.join("; ", cookieSet).trim(); diff --git a/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java similarity index 60% rename from app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java rename to app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java index db2ab4aa7..7592d2f35 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -6,27 +6,30 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.BatteryManager; import android.os.Build; +import android.util.DisplayMetrics; +import android.util.TypedValue; import android.view.KeyEvent; +import androidx.annotation.NonNull; import org.schabi.newpipe.App; import static android.content.Context.BATTERY_SERVICE; import static android.content.Context.UI_MODE_SERVICE; -public final class AndroidTvUtils { +public final class DeviceUtils { private static final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv"; private static Boolean isTV = null; - private AndroidTvUtils() { + private DeviceUtils() { } public static boolean isTv(final Context context) { - if (AndroidTvUtils.isTV != null) { - return AndroidTvUtils.isTV; + if (isTV != null) { + return isTV; } - PackageManager pm = App.getApp().getPackageManager(); + final PackageManager pm = App.getApp().getPackageManager(); // from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check boolean isTv = ((UiModeManager) context.getSystemService(UI_MODE_SERVICE)) @@ -36,7 +39,8 @@ public final class AndroidTvUtils { // from https://stackoverflow.com/a/58932366 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - boolean isBatteryAbsent = ((BatteryManager) context.getSystemService(BATTERY_SERVICE)) + final boolean isBatteryAbsent + = ((BatteryManager) context.getSystemService(BATTERY_SERVICE)) .getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) == 0; isTv = isTv || (isBatteryAbsent && !pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN) @@ -48,8 +52,15 @@ public final class AndroidTvUtils { isTv = isTv || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); } - AndroidTvUtils.isTV = isTv; - return AndroidTvUtils.isTV; + DeviceUtils.isTV = isTv; + return DeviceUtils.isTV; + } + + public static boolean isTablet(@NonNull final Context context) { + return (context + .getResources() + .getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) + >= Configuration.SCREENLAYOUT_SIZE_LARGE; } public static boolean isConfirmKey(final int keyCode) { @@ -63,4 +74,17 @@ public final class AndroidTvUtils { return false; } } + + /* + * Compares current status bar height with default status bar height in Android and decides, + * does the device has cutout or not + * */ + public static boolean hasCutout(final float statusBarHeight, final DisplayMetrics metrics) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + final float defaultStatusBarHeight = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 25, metrics); + return statusBarHeight > defaultStatusBarHeight; + } + return false; + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index cd5992fb4..a1a73d7ac 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -33,6 +33,7 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo; @@ -87,21 +88,20 @@ public final class ExtractorHelper { final String searchString, final List contentFilter, final String sortFilter, - final String pageUrl) { + final Page page) { checkServiceId(serviceId); return Single.fromCallable(() -> SearchInfo.getMoreItems(NewPipe.getService(serviceId), NewPipe.getService(serviceId) .getSearchQHFactory() - .fromQuery(searchString, contentFilter, sortFilter), - pageUrl)); + .fromQuery(searchString, contentFilter, sortFilter), page)); } public static Single> suggestionsFor(final int serviceId, final String query) { checkServiceId(serviceId); return Single.fromCallable(() -> { - SuggestionExtractor extractor = NewPipe.getService(serviceId) + final SuggestionExtractor extractor = NewPipe.getService(serviceId) .getSuggestionExtractor(); return extractor != null ? extractor.suggestionList(query) @@ -125,10 +125,10 @@ public final class ExtractorHelper { } public static Single getMoreChannelItems(final int serviceId, final String url, - final String nextStreamsUrl) { + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); + ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } public static Single> getFeedInfoFallbackToChannelInfo( @@ -157,10 +157,10 @@ public final class ExtractorHelper { public static Single getMoreCommentItems(final int serviceId, final CommentsInfo info, - final String nextPageUrl) { + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPageUrl)); + CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage)); } public static Single getPlaylistInfo(final int serviceId, final String url, @@ -172,10 +172,10 @@ public final class ExtractorHelper { } public static Single getMorePlaylistItems(final int serviceId, final String url, - final String nextStreamsUrl) { + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); + PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } public static Single getKioskInfo(final int serviceId, final String url, @@ -184,12 +184,10 @@ public final class ExtractorHelper { Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMoreKioskItems(final int serviceId, - final String url, - final String nextStreamsUrl) { + public static Single getMoreKioskItems(final int serviceId, final String url, + final Page nextPage) { return Single.fromCallable(() -> - KioskInfo.getMoreItems(NewPipe.getService(serviceId), - url, nextStreamsUrl)); + KioskInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } /*////////////////////////////////////////////////////////////////////////// @@ -214,10 +212,10 @@ public final class ExtractorHelper { final InfoItem.InfoType infoType, final Single loadFromNetwork) { checkServiceId(serviceId); - Single actualLoadFromNetwork = loadFromNetwork + final Single actualLoadFromNetwork = loadFromNetwork .doOnSuccess(info -> CACHE.putInfo(serviceId, url, info, infoType)); - Single load; + final Single load; if (forceLoad) { CACHE.removeInfo(serviceId, url, infoType); load = actualLoadFromNetwork; @@ -240,12 +238,12 @@ public final class ExtractorHelper { * @param infoType the {@link InfoItem.InfoType} of the item * @return a {@link Single} that loads the item */ - public static Maybe loadFromCache(final int serviceId, final String url, - final InfoItem.InfoType infoType) { + private static Maybe loadFromCache(final int serviceId, final String url, + final InfoItem.InfoType infoType) { checkServiceId(serviceId); return Maybe.defer(() -> { //noinspection unchecked - I info = (I) CACHE.getFromKey(serviceId, url, infoType); + final I info = (I) CACHE.getFromKey(serviceId, url, infoType); if (MainActivity.DEBUG) { Log.d(TAG, "loadFromCache() called, info > " + info); } @@ -285,7 +283,7 @@ public final class ExtractorHelper { if (exception instanceof ReCaptchaException) { Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); // Starting ReCaptcha Challenge Activity - Intent intent = new Intent(context, ReCaptchaActivity.class); + final Intent intent = new Intent(context, ReCaptchaActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } else if (ExceptionUtils.isNetworkRelated(exception)) { @@ -295,7 +293,7 @@ public final class ExtractorHelper { } else if (exception instanceof ContentNotSupportedException) { Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show(); } else { - int errorId = exception instanceof YoutubeStreamExtractor.DecryptException + final int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error : exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java index 3179662ba..27e1f3048 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java @@ -2,7 +2,7 @@ package org.schabi.newpipe.util; import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; @@ -22,7 +22,7 @@ public final class FilenameUtils { * @return the filename */ public static String createFilename(final Context context, final String title) { - SharedPreferences sharedPreferences = PreferenceManager + final SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(context); final String charsetLd = context.getString(R.string.charset_letters_and_digits_value); @@ -48,7 +48,7 @@ public final class FilenameUtils { charset = selectedCharset; // Is the user using a custom charset? } - Pattern pattern = Pattern.compile(charset); + final Pattern pattern = Pattern.compile(charset); return createFilename(title, pattern, replacementChar); } diff --git a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java index 035416dcd..a07f05828 100644 --- a/app/src/main/java/org/schabi/newpipe/util/InfoCache.java +++ b/app/src/main/java/org/schabi/newpipe/util/InfoCache.java @@ -59,7 +59,7 @@ public final class InfoCache { } private static void removeStaleCache() { - for (Map.Entry entry : InfoCache.LRU_CACHE.snapshot().entrySet()) { + for (final Map.Entry entry : InfoCache.LRU_CACHE.snapshot().entrySet()) { final CacheData data = entry.getValue(); if (data != null && data.isExpired()) { InfoCache.LRU_CACHE.remove(entry.getKey()); diff --git a/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java b/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java index 2ca128409..fd50d2edb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java +++ b/app/src/main/java/org/schabi/newpipe/util/LayoutManagerSmoothScroller.java @@ -20,7 +20,7 @@ public class LayoutManagerSmoothScroller extends LinearLayoutManager { @Override public void smoothScrollToPosition(final RecyclerView recyclerView, final RecyclerView.State state, final int position) { - RecyclerView.SmoothScroller smoothScroller + final RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext()); smoothScroller.setTargetPosition(position); startSmoothScroll(smoothScroller); diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 189b6823e..7a428a05d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.util; import android.content.Context; import android.content.SharedPreferences; import android.net.ConnectivityManager; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -44,7 +44,7 @@ public final class ListHelper { */ public static int getDefaultResolutionIndex(final Context context, final List videoStreams) { - String defaultResolution = computeDefaultResolution(context, + final String defaultResolution = computeDefaultResolution(context, R.string.default_resolution_key, R.string.default_resolution_value); return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } @@ -70,7 +70,7 @@ public final class ListHelper { */ public static int getPopupDefaultResolutionIndex(final Context context, final List videoStreams) { - String defaultResolution = computeDefaultResolution(context, + final String defaultResolution = computeDefaultResolution(context, R.string.default_popup_resolution_key, R.string.default_popup_resolution_value); return getDefaultResolutionWithDefaultFormat(context, defaultResolution, videoStreams); } @@ -90,8 +90,8 @@ public final class ListHelper { public static int getDefaultAudioFormat(final Context context, final List audioStreams) { - MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_audio_format_key, - R.string.default_audio_format_value); + final MediaFormat defaultFormat = getDefaultFormat(context, + R.string.default_audio_format_key, R.string.default_audio_format_value); // If the user has chosen to limit resolution to conserve mobile data // usage then we should also limit our audio usage. @@ -117,12 +117,13 @@ public final class ListHelper { final List videoOnlyStreams, final boolean ascendingOrder) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences preferences + = PreferenceManager.getDefaultSharedPreferences(context); - boolean showHigherResolutions = preferences.getBoolean( + final boolean showHigherResolutions = preferences.getBoolean( context.getString(R.string.show_higher_resolutions_key), false); - MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, - R.string.default_video_format_value); + final MediaFormat defaultFormat = getDefaultFormat(context, + R.string.default_video_format_key, R.string.default_video_format_value); return getSortedStreamVideosList(defaultFormat, showHigherResolutions, videoStreams, videoOnlyStreams, ascendingOrder); @@ -134,14 +135,15 @@ public final class ListHelper { private static String computeDefaultResolution(final Context context, final int key, final int value) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences preferences + = PreferenceManager.getDefaultSharedPreferences(context); // Load the prefered resolution otherwise the best available String resolution = preferences != null ? preferences.getString(context.getString(key), context.getString(value)) : context.getString(R.string.best_resolution_key); - String maxResolution = getResolutionLimit(context); + final String maxResolution = getResolutionLimit(context); if (maxResolution != null && (resolution.equals(context.getString(R.string.best_resolution_key)) || compareVideoStreamResolution(maxResolution, resolution) < 1)) { @@ -173,7 +175,7 @@ public final class ListHelper { return 0; } - int defaultStreamIndex + final int defaultStreamIndex = getVideoStreamIndex(defaultResolution, defaultFormat, videoStreams); // this is actually an error, @@ -200,11 +202,11 @@ public final class ListHelper { final List videoStreams, final List videoOnlyStreams, final boolean ascendingOrder) { - ArrayList retList = new ArrayList<>(); - HashMap hashMap = new HashMap<>(); + final ArrayList retList = new ArrayList<>(); + final HashMap hashMap = new HashMap<>(); if (videoOnlyStreams != null) { - for (VideoStream stream : videoOnlyStreams) { + for (final VideoStream stream : videoOnlyStreams) { if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) { continue; @@ -213,7 +215,7 @@ public final class ListHelper { } } if (videoStreams != null) { - for (VideoStream stream : videoStreams) { + for (final VideoStream stream : videoStreams) { if (!showHigherResolutions && HIGH_RESOLUTION_LIST.contains(stream.getResolution())) { continue; @@ -223,12 +225,12 @@ public final class ListHelper { } // Add all to the hashmap - for (VideoStream videoStream : retList) { + for (final VideoStream videoStream : retList) { hashMap.put(videoStream.getResolution(), videoStream); } // Override the values when the key == resolution, with the defaultFormat - for (VideoStream videoStream : retList) { + for (final VideoStream videoStream : retList) { if (videoStream.getFormat() == defaultFormat) { hashMap.put(videoStream.getResolution(), videoStream); } @@ -262,7 +264,7 @@ public final class ListHelper { private static void sortStreamList(final List videoStreams, final boolean ascendingOrder) { Collections.sort(videoStreams, (o1, o2) -> { - int result = compareVideoStreamResolution(o1, o2); + final int result = compareVideoStreamResolution(o1, o2); return result == 0 ? 0 : (ascendingOrder ? result : -result); }); } @@ -282,7 +284,7 @@ public final class ListHelper { while (result == -1) { AudioStream prevStream = null; for (int idx = 0; idx < audioStreams.size(); idx++) { - AudioStream stream = audioStreams.get(idx); + final AudioStream stream = audioStreams.get(idx); if ((format == null || stream.getFormat() == format) && (prevStream == null || compareAudioStreamBitrate(prevStream, stream, AUDIO_FORMAT_QUALITY_RANKING) < 0)) { @@ -314,7 +316,7 @@ public final class ListHelper { while (result == -1) { AudioStream prevStream = null; for (int idx = 0; idx < audioStreams.size(); idx++) { - AudioStream stream = audioStreams.get(idx); + final AudioStream stream = audioStreams.get(idx); if ((format == null || stream.getFormat() == format) && (prevStream == null || compareAudioStreamBitrate(prevStream, stream, AUDIO_FORMAT_EFFICIENCY_RANKING) > 0)) { @@ -357,12 +359,13 @@ public final class ListHelper { int resMatchOnlyIndex = -1; int resMatchOnlyNoRefreshIndex = -1; int lowerResMatchNoRefreshIndex = -1; - String targetResolutionNoRefresh = targetResolution.replaceAll("p\\d+$", "p"); + final String targetResolutionNoRefresh = targetResolution.replaceAll("p\\d+$", "p"); for (int idx = 0; idx < videoStreams.size(); idx++) { - MediaFormat format = targetFormat == null ? null : videoStreams.get(idx).getFormat(); - String resolution = videoStreams.get(idx).getResolution(); - String resolutionNoRefresh = resolution.replaceAll("p\\d+$", "p"); + final MediaFormat format + = targetFormat == null ? null : videoStreams.get(idx).getFormat(); + final String resolution = videoStreams.get(idx).getResolution(); + final String resolutionNoRefresh = resolution.replaceAll("p\\d+$", "p"); if (format == targetFormat && resolution.equals(targetResolution)) { fullMatchIndex = idx; @@ -414,8 +417,8 @@ public final class ListHelper { private static int getDefaultResolutionWithDefaultFormat(final Context context, final String defaultResolution, final List videoStreams) { - MediaFormat defaultFormat = getDefaultFormat(context, R.string.default_video_format_key, - R.string.default_video_format_value); + final MediaFormat defaultFormat = getDefaultFormat(context, + R.string.default_video_format_key, R.string.default_video_format_value); return getDefaultResolutionIndex(defaultResolution, context.getString(R.string.best_resolution_key), defaultFormat, videoStreams); } @@ -423,10 +426,11 @@ public final class ListHelper { private static MediaFormat getDefaultFormat(final Context context, @StringRes final int defaultFormatKey, @StringRes final int defaultFormatValueKey) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences preferences + = PreferenceManager.getDefaultSharedPreferences(context); - String defaultFormat = context.getString(defaultFormatValueKey); - String defaultFormatString = preferences.getString( + final String defaultFormat = context.getString(defaultFormatValueKey); + final String defaultFormatString = preferences.getString( context.getString(defaultFormatKey), defaultFormat); MediaFormat defaultMediaFormat = getMediaFormatFromKey(context, defaultFormatString); @@ -479,9 +483,9 @@ public final class ListHelper { } private static int compareVideoStreamResolution(final String r1, final String r2) { - int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1") + final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1") .replaceAll("[^\\d.]", "")); - int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1") + final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1") .replaceAll("[^\\d.]", "")); return res1 - res2; } @@ -496,7 +500,7 @@ public final class ListHelper { return 1; } - int resComp = compareVideoStreamResolution(streamA.getResolution(), + final int resComp = compareVideoStreamResolution(streamA.getResolution(), streamB.getResolution()); if (resComp != 0) { return resComp; @@ -521,9 +525,10 @@ public final class ListHelper { private static String getResolutionLimit(final Context context) { String resolutionLimit = null; if (isMeteredNetwork(context)) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - String defValue = context.getString(R.string.limit_data_usage_none_key); - String value = preferences.getString( + final SharedPreferences preferences + = PreferenceManager.getDefaultSharedPreferences(context); + final String defValue = context.getString(R.string.limit_data_usage_none_key); + final String value = preferences.getString( context.getString(R.string.limit_mobile_data_usage_key), defValue); resolutionLimit = defValue.equals(value) ? null : value; } @@ -536,8 +541,8 @@ public final class ListHelper { * @param context App context * @return {@code true} if connected to a metered network */ - private static boolean isMeteredNetwork(final Context context) { - ConnectivityManager manager + public static boolean isMeteredNetwork(final Context context) { + final ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (manager == null || manager.getActiveNetworkInfo() == null) { return false; diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 7e336f02d..767cebcea 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -5,7 +5,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.text.TextUtils; import android.util.DisplayMetrics; @@ -110,19 +110,19 @@ public final class Localization { } public static Locale getPreferredLocale(final Context context) { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - String languageCode = sp.getString(context.getString(R.string.content_language_key), + final String languageCode = sp.getString(context.getString(R.string.content_language_key), context.getString(R.string.default_localization_key)); try { if (languageCode.length() == 2) { return new Locale(languageCode); } else if (languageCode.contains("_")) { - String country = languageCode.substring(languageCode.indexOf("_")); + final String country = languageCode.substring(languageCode.indexOf("_")); return new Locale(languageCode.substring(0, 2), country); } - } catch (Exception ignored) { + } catch (final Exception ignored) { } return Locale.getDefault(); @@ -133,7 +133,7 @@ public final class Localization { } public static String localizeNumber(final Context context, final double number) { - NumberFormat nf = NumberFormat.getInstance(getAppLocale(context)); + final NumberFormat nf = NumberFormat.getInstance(getAppLocale(context)); return nf.format(number); } @@ -184,7 +184,7 @@ public final class Localization { } public static String shortCount(final Context context, final long count) { - double value = (double) count; + final double value = (double) count; if (count >= 1000000000) { return localizeNumber(context, round(value / 1000000000, 1)) + context.getString(R.string.short_billion); @@ -230,8 +230,8 @@ public final class Localization { // is not the responsibility of this method handle long numbers // (it probably will fall in the "other" category, // or some language have some specific rule... then we have to change it) - int safeCount = count > Integer.MAX_VALUE ? Integer.MAX_VALUE : count < Integer.MIN_VALUE - ? Integer.MIN_VALUE : (int) count; + final int safeCount = count > Integer.MAX_VALUE ? Integer.MAX_VALUE + : count < Integer.MIN_VALUE ? Integer.MIN_VALUE : (int) count; return context.getResources().getQuantityString(pluralId, safeCount, formattedCount); } @@ -305,30 +305,30 @@ public final class Localization { } public static String relativeTime(final Calendar calendarTime) { - String time = getPrettyTime().formatUnrounded(calendarTime); + final String time = getPrettyTime().formatUnrounded(calendarTime); return time.startsWith("-") ? time.substring(1) : time; //workaround fix for russian showing -1 day ago, -19hrs ago… } private static void changeAppLanguage(final Locale loc, final Resources res) { - DisplayMetrics dm = res.getDisplayMetrics(); - Configuration conf = res.getConfiguration(); + final DisplayMetrics dm = res.getDisplayMetrics(); + final Configuration conf = res.getConfiguration(); conf.setLocale(loc); res.updateConfiguration(conf, dm); } public static Locale getAppLocale(final Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); String lang = prefs.getString(context.getString(R.string.app_language_key), "en"); - Locale loc; + final Locale loc; if (lang.equals(context.getString(R.string.default_localization_key))) { loc = Locale.getDefault(); } else if (lang.matches(".*-.*")) { //to differentiate different versions of the language //for example, pt (portuguese in Portugal) and pt-br (portuguese in Brazil) - String[] localisation = lang.split("-"); + final String[] localisation = lang.split("-"); lang = localisation[0]; - String country = localisation[1]; + final String country = localisation[1]; loc = new Locale(lang, country); } else { loc = new Locale(lang); diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index ccaa79f98..693265fcd 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -7,12 +7,13 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -46,14 +47,12 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; -import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; import org.schabi.newpipe.player.BasePlayer; -import org.schabi.newpipe.player.MainVideoPlayer; -import org.schabi.newpipe.player.PopupVideoPlayer; -import org.schabi.newpipe.player.PopupVideoPlayerActivity; +import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.settings.SettingsActivity; import java.util.ArrayList; @@ -75,7 +74,7 @@ public final class NavigationHelper { @NonNull final PlayQueue playQueue, @Nullable final String quality, final boolean resumePlayback) { - Intent intent = new Intent(context, targetClazz); + final Intent intent = new Intent(context, targetClazz); final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); if (cacheKey != null) { @@ -85,6 +84,7 @@ public final class NavigationHelper { intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); } intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO); return intent; } @@ -112,11 +112,13 @@ public final class NavigationHelper { public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, - final int repeatMode, final float playbackSpeed, + final int repeatMode, + final float playbackSpeed, final float playbackPitch, final boolean playbackSkipSilence, @Nullable final String playbackQuality, - final boolean resumePlayback, final boolean startPaused, + final boolean resumePlayback, + final boolean startPaused, final boolean isMuted) { return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) @@ -124,12 +126,42 @@ public final class NavigationHelper { .putExtra(BasePlayer.IS_MUTED, isMuted); } - public static void playOnMainPlayer(final Context context, final PlayQueue queue, + public static void playOnMainPlayer( + final AppCompatActivity activity, + final PlayQueue queue, + final boolean autoPlay) { + playOnMainPlayer(activity.getSupportFragmentManager(), queue, autoPlay); + } + + public static void playOnMainPlayer( + final FragmentManager fragmentManager, + final PlayQueue queue, + final boolean autoPlay) { + final PlayQueueItem currentStream = queue.getItem(); + openVideoDetailFragment( + fragmentManager, + currentStream.getServiceId(), + currentStream.getUrl(), + currentStream.getTitle(), + autoPlay, + queue); + } + + public static void playOnMainPlayer(@NonNull final Context context, + @NonNull final PlayQueue queue, + @NonNull final StreamingService.LinkType linkType, + @NonNull final String url, + @NonNull final String title, + final boolean autoPlay, final boolean resumePlayback) { - final Intent playerIntent - = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback); - playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(playerIntent); + + final Intent intent = getPlayerIntent(context, MainActivity.class, queue, resumePlayback); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Constants.KEY_LINK_TYPE, linkType); + intent.putExtra(Constants.KEY_URL, url); + intent.putExtra(Constants.KEY_TITLE, title); + intent.putExtra(VideoDetailFragment.AUTO_PLAY, autoPlay); + context.startActivity(intent); } public static void playOnPopupPlayer(final Context context, final PlayQueue queue, @@ -140,16 +172,19 @@ public final class NavigationHelper { } Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - startService(context, - getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback)); + final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); + startService(context, intent); } - public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, + public static void playOnBackgroundPlayer(final Context context, + final PlayQueue queue, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) .show(); - startService(context, - getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback)); + final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); + startService(context, intent); } public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, @@ -166,8 +201,10 @@ public final class NavigationHelper { } Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, - selectOnAppend, resumePlayback)); + final Intent intent = getPlayerEnqueueIntent( + context, MainPlayer.class, queue, selectOnAppend, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); + startService(context, intent); } public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, @@ -175,12 +212,15 @@ public final class NavigationHelper { enqueueOnBackgroundPlayer(context, queue, false, resumePlayback); } - public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, + public static void enqueueOnBackgroundPlayer(final Context context, + final PlayQueue queue, final boolean selectOnAppend, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, - selectOnAppend, resumePlayback)); + final Intent intent = getPlayerEnqueueIntent( + context, MainPlayer.class, queue, selectOnAppend, resumePlayback); + intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); + startService(context, intent); } public static void startService(@NonNull final Context context, @NonNull final Intent intent) { @@ -203,27 +243,27 @@ public final class NavigationHelper { return; } - AudioStream audioStream = info.getAudioStreams().get(index); + final AudioStream audioStream = info.getAudioStreams().get(index); playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream); } public static void playOnExternalVideoPlayer(final Context context, final StreamInfo info) { - ArrayList videoStreamsList = new ArrayList<>( + final ArrayList videoStreamsList = new ArrayList<>( ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false)); - int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList); + final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList); if (index == -1) { Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show(); return; } - VideoStream videoStream = videoStreamsList.get(index); + final VideoStream videoStream = videoStreamsList.get(index); playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream); } public static void playOnExternalPlayer(final Context context, final String name, final String artist, final Stream stream) { - Intent intent = new Intent(); + final Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType()); intent.putExtra(Intent.EXTRA_TITLE, name); @@ -242,7 +282,7 @@ public final class NavigationHelper { new AlertDialog.Builder(context) .setMessage(R.string.no_player_found) .setPositiveButton(R.string.install, (dialog, which) -> { - Intent i = new Intent(); + final Intent i = new Intent(); i.setAction(Intent.ACTION_VIEW); i.setData(Uri.parse(context.getString(R.string.fdroid_vlc_url))); context.startActivity(i); @@ -273,7 +313,7 @@ public final class NavigationHelper { public static void gotoMainFragment(final FragmentManager fragmentManager) { ImageLoader.getInstance().clearMemoryCache(); - boolean popped = fragmentManager.popBackStackImmediate(MAIN_FRAGMENT_TAG, 0); + final boolean popped = fragmentManager.popBackStackImmediate(MAIN_FRAGMENT_TAG, 0); if (!popped) { openMainFragment(fragmentManager); } @@ -311,31 +351,43 @@ public final class NavigationHelper { public static void openVideoDetailFragment(final FragmentManager fragmentManager, final int serviceId, final String url, final String title) { - openVideoDetailFragment(fragmentManager, serviceId, url, title, false); + openVideoDetailFragment(fragmentManager, serviceId, url, title, true, null); } - public static void openVideoDetailFragment(final FragmentManager fragmentManager, - final int serviceId, final String url, - final String name, final boolean autoPlay) { - Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_holder); + public static void openVideoDetailFragment( + final FragmentManager fragmentManager, + final int serviceId, + final String url, + final String title, + final boolean autoPlay, + final PlayQueue playQueue) { + final Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_player_holder); if (fragment instanceof VideoDetailFragment && fragment.isVisible()) { - VideoDetailFragment detailFragment = (VideoDetailFragment) fragment; + expandMainPlayer(fragment.requireActivity()); + final VideoDetailFragment detailFragment = (VideoDetailFragment) fragment; detailFragment.setAutoplay(autoPlay); - detailFragment.selectAndLoadVideo(serviceId, url, name == null ? "" : name); + detailFragment + .selectAndLoadVideo(serviceId, url, title == null ? "" : title, playQueue); + detailFragment.scrollToTop(); return; } - VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, - name == null ? "" : name); + final VideoDetailFragment instance = VideoDetailFragment + .getInstance(serviceId, url, title == null ? "" : title, playQueue); instance.setAutoplay(autoPlay); defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, instance) - .addToBackStack(null) + .replace(R.id.fragment_player_holder, instance) + .runOnCommit(() -> expandMainPlayer(instance.requireActivity())) .commit(); } + public static void expandMainPlayer(final Context context) { + final Intent intent = new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER); + context.sendBroadcast(intent); + } + public static void openChannelFragment(final FragmentManager fragmentManager, final int serviceId, final String url, final String name) { @@ -431,7 +483,7 @@ public final class NavigationHelper { public static void openSearch(final Context context, final int serviceId, final String searchString) { - Intent mIntent = new Intent(context, MainActivity.class); + final Intent mIntent = new Intent(context, MainActivity.class); mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); mIntent.putExtra(Constants.KEY_SEARCH_STRING, searchString); mIntent.putExtra(Constants.KEY_OPEN_SEARCH, true); @@ -444,7 +496,7 @@ public final class NavigationHelper { public static void openChannel(final Context context, final int serviceId, final String url, final String name) { - Intent openIntent = getOpenIntent(context, url, serviceId, + final Intent openIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL); if (name != null && !name.isEmpty()) { openIntent.putExtra(Constants.KEY_TITLE, name); @@ -459,7 +511,7 @@ public final class NavigationHelper { public static void openVideoDetail(final Context context, final int serviceId, final String url, final String title) { - Intent openIntent = getOpenIntent(context, url, serviceId, + final Intent openIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.STREAM); if (title != null && !title.isEmpty()) { openIntent.putExtra(Constants.KEY_TITLE, title); @@ -468,26 +520,26 @@ public final class NavigationHelper { } public static void openMainActivity(final Context context) { - Intent mIntent = new Intent(context, MainActivity.class); + final Intent mIntent = new Intent(context, MainActivity.class); mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); context.startActivity(mIntent); } public static void openRouterActivity(final Context context, final String url) { - Intent mIntent = new Intent(context, RouterActivity.class); + final Intent mIntent = new Intent(context, RouterActivity.class); mIntent.setData(Uri.parse(url)); mIntent.putExtra(RouterActivity.INTERNAL_ROUTE_KEY, true); context.startActivity(mIntent); } public static void openAbout(final Context context) { - Intent intent = new Intent(context, AboutActivity.class); + final Intent intent = new Intent(context, AboutActivity.class); context.startActivity(intent); } public static void openSettings(final Context context) { - Intent intent = new Intent(context, SettingsActivity.class); + final Intent intent = new Intent(context, SettingsActivity.class); context.startActivity(intent); } @@ -496,7 +548,7 @@ public final class NavigationHelper { activity, PermissionHelper.DOWNLOADS_REQUEST_CODE)) { return false; } - Intent intent = new Intent(activity, DownloadActivity.class); + final Intent intent = new Intent(activity, DownloadActivity.class); activity.startActivity(intent); return true; } @@ -505,13 +557,9 @@ public final class NavigationHelper { return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class); } - public static Intent getPopupPlayerActivityIntent(final Context context) { - return getServicePlayerActivityIntent(context, PopupVideoPlayerActivity.class); - } - private static Intent getServicePlayerActivityIntent(final Context context, final Class activityClass) { - Intent intent = new Intent(context, activityClass); + final Intent intent = new Intent(context, activityClass); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } @@ -523,7 +571,7 @@ public final class NavigationHelper { private static Intent getOpenIntent(final Context context, final String url, final int serviceId, final StreamingService.LinkType type) { - Intent mIntent = new Intent(context, MainActivity.class); + final Intent mIntent = new Intent(context, MainActivity.class); mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); mIntent.putExtra(Constants.KEY_URL, url); mIntent.putExtra(Constants.KEY_LINK_TYPE, type); @@ -537,14 +585,14 @@ public final class NavigationHelper { public static Intent getIntentByLink(final Context context, final StreamingService service, final String url) throws ExtractionException { - StreamingService.LinkType linkType = service.getLinkTypeByUrl(url); + final StreamingService.LinkType linkType = service.getLinkTypeByUrl(url); if (linkType == StreamingService.LinkType.NONE) { throw new ExtractionException("Url not known to service. service=" + service + " url=" + url); } - Intent rIntent = getOpenIntent(context, url, service.getServiceId(), linkType); + final Intent rIntent = getOpenIntent(context, url, service.getServiceId(), linkType); if (linkType == StreamingService.LinkType.STREAM) { rIntent.putExtra(VideoDetailFragment.AUTO_PLAY, @@ -573,7 +621,7 @@ public final class NavigationHelper { try { // Try market:// scheme context.startActivity(new Intent(Intent.ACTION_VIEW, openMarketUrl(packageName))); - } catch (ActivityNotFoundException e) { + } catch (final ActivityNotFoundException e) { // Fall back to google play URL (don't worry F-Droid can handle it :) context.startActivity(new Intent(Intent.ACTION_VIEW, getGooglePlayUrl(packageName))); } @@ -600,7 +648,7 @@ public final class NavigationHelper { * @param videoURL the url to the video */ public static void playWithKore(final Context context, final Uri videoURL) { - Intent intent = new Intent(Intent.ACTION_VIEW); + final Intent intent = new Intent(Intent.ACTION_VIEW); intent.setPackage(context.getString(R.string.kore_package)); intent.setData(videoURL); context.startActivity(intent); diff --git a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java index e89cbf5db..e28095798 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java @@ -2,7 +2,7 @@ package org.schabi.newpipe.util; import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; @@ -23,27 +23,27 @@ public final class PeertubeHelper { private PeertubeHelper() { } public static List getInstanceList(final Context context) { - SharedPreferences sharedPreferences = PreferenceManager + final SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(context); - String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key); + final String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key); final String savedJson = sharedPreferences.getString(savedInstanceListKey, null); if (null == savedJson) { return Collections.singletonList(getCurrentInstance()); } try { - JsonArray array = JsonParser.object().from(savedJson).getArray("instances"); - List result = new ArrayList<>(); - for (Object o : array) { + final JsonArray array = JsonParser.object().from(savedJson).getArray("instances"); + final List result = new ArrayList<>(); + for (final Object o : array) { if (o instanceof JsonObject) { - JsonObject instance = (JsonObject) o; - String name = instance.getString("name"); - String url = instance.getString("url"); + final JsonObject instance = (JsonObject) o; + final String name = instance.getString("name"); + final String url = instance.getString("url"); result.add(new PeertubeInstance(url, name)); } } return result; - } catch (JsonParserException e) { + } catch (final JsonParserException e) { return Collections.singletonList(getCurrentInstance()); } @@ -51,13 +51,14 @@ public final class PeertubeHelper { public static PeertubeInstance selectInstance(final PeertubeInstance instance, final Context context) { - SharedPreferences sharedPreferences = PreferenceManager + final SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(context); - String selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key); - JsonStringWriter jsonWriter = JsonWriter.string().object(); + final String selectedInstanceKey + = context.getString(R.string.peertube_selected_instance_key); + final JsonStringWriter jsonWriter = JsonWriter.string().object(); jsonWriter.value("name", instance.getName()); jsonWriter.value("url", instance.getUrl()); - String jsonToSave = jsonWriter.end().done(); + final String jsonToSave = jsonWriter.end().done(); sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply(); ServiceList.PeerTube.setInstance(instance); return instance; diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index 9ba6ed36c..03400bdbb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -26,10 +26,8 @@ public final class PermissionHelper { private PermissionHelper() { } public static boolean checkStoragePermissions(final Activity activity, final int requestCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - if (!checkReadStoragePermissions(activity, requestCode)) { - return false; - } + if (!checkReadStoragePermissions(activity, requestCode)) { + return false; } return checkWriteStoragePermissions(activity, requestCode); } @@ -101,12 +99,12 @@ public final class PermissionHelper { @RequiresApi(api = Build.VERSION_CODES.M) public static boolean checkSystemAlertWindowPermission(final Context context) { if (!Settings.canDrawOverlays(context)) { - Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + final Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName())); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { context.startActivity(i); - } catch (ActivityNotFoundException ignored) { + } catch (final ActivityNotFoundException ignored) { } return false; } else { @@ -120,8 +118,9 @@ public final class PermissionHelper { } public static void showPopupEnablementToast(final Context context) { - Toast toast = Toast.makeText(context, R.string.msg_popup_permission, Toast.LENGTH_LONG); - TextView messageView = toast.getView().findViewById(android.R.id.message); + final Toast toast + = Toast.makeText(context, R.string.msg_popup_permission, Toast.LENGTH_LONG); + final TextView messageView = toast.getView().findViewById(android.R.id.message); if (messageView != null) { messageView.setGravity(Gravity.CENTER); } diff --git a/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java b/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java index ce642da5e..81e203b1f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java +++ b/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java @@ -4,41 +4,24 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class RelatedStreamInfo extends ListInfo { - - private StreamInfoItem nextStream; - public RelatedStreamInfo(final int serviceId, final ListLinkHandler listUrlIdHandler, final String name) { super(serviceId, listUrlIdHandler, name); } public static RelatedStreamInfo getInfo(final StreamInfo info) { - ListLinkHandler handler = new ListLinkHandler( + final ListLinkHandler handler = new ListLinkHandler( info.getOriginalUrl(), info.getUrl(), info.getId(), Collections.emptyList(), null); - RelatedStreamInfo relatedStreamInfo = new RelatedStreamInfo( + final RelatedStreamInfo relatedStreamInfo = new RelatedStreamInfo( info.getServiceId(), handler, info.getName()); - List streams = new ArrayList<>(); - if (info.getNextVideo() != null) { - streams.add(info.getNextVideo()); - } - streams.addAll(info.getRelatedStreams()); + final List streams = new ArrayList<>(info.getRelatedStreams()); relatedStreamInfo.setRelatedItems(streams); - relatedStreamInfo.setNextStream(info.getNextVideo()); return relatedStreamInfo; } - - public StreamInfoItem getNextStream() { - return nextStream; - } - - public void setNextStream(final StreamInfoItem nextStream) { - this.nextStream = nextStream; - } } diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java index 081d981a1..8c697d327 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java @@ -39,9 +39,9 @@ public class SecondaryStreamHelper { return null; } - boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4; + final boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4; - for (AudioStream audio : audioStreams) { + for (final AudioStream audio : audioStreams) { if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) { return audio; } @@ -53,7 +53,7 @@ public class SecondaryStreamHelper { // retry, but this time in reverse order for (int i = audioStreams.size() - 1; i >= 0; i--) { - AudioStream audio = audioStreams.get(i); + final AudioStream audio = audioStreams.get(i); if (audio.getFormat() == MediaFormat.WEBMA_OPUS) { return audio; } diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index dacf7d844..8f3423b22 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -2,7 +2,7 @@ package org.schabi.newpipe.util; import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; @@ -117,7 +117,7 @@ public final class ServiceHelper { int serviceId; try { serviceId = NewPipe.getService(serviceName).getServiceId(); - } catch (ExtractionException e) { + } catch (final ExtractionException e) { serviceId = DEFAULT_FALLBACK_SERVICE.getServiceId(); } @@ -128,7 +128,7 @@ public final class ServiceHelper { String serviceName; try { serviceName = NewPipe.getService(serviceId).getServiceInfo().getName(); - } catch (ExtractionException e) { + } catch (final ExtractionException e) { serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName(); } @@ -136,7 +136,7 @@ public final class ServiceHelper { } public static void setSelectedServiceId(final Context context, final String serviceName) { - int serviceId = NewPipe.getIdOfService(serviceName); + final int serviceId = NewPipe.getIdOfService(serviceName); if (serviceId == -1) { setSelectedServicePreferences(context, DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName()); @@ -170,29 +170,29 @@ public final class ServiceHelper { public static void initService(final Context context, final int serviceId) { if (serviceId == ServiceList.PeerTube.getServiceId()) { - SharedPreferences sharedPreferences = PreferenceManager + final SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(context); - String json = sharedPreferences.getString(context.getString( + final String json = sharedPreferences.getString(context.getString( R.string.peertube_selected_instance_key), null); if (null == json) { return; } - JsonObject jsonObject = null; + final JsonObject jsonObject; try { jsonObject = JsonParser.object().from(json); - } catch (JsonParserException e) { + } catch (final JsonParserException e) { return; } - String name = jsonObject.getString("name"); - String url = jsonObject.getString("url"); - PeertubeInstance instance = new PeertubeInstance(url, name); + final String name = jsonObject.getString("name"); + final String url = jsonObject.getString("url"); + final PeertubeInstance instance = new PeertubeInstance(url, name); ServiceList.PeerTube.setInstance(instance); } } public static void initServices(final Context context) { - for (StreamingService s : ServiceList.all()) { + for (final StreamingService s : ServiceList.all()) { initService(context, s.getServiceId()); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java index 0ec2d571d..1283f67f5 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java @@ -31,8 +31,9 @@ public final class ShareUtils { // no browser set as default openInDefaultApp(context, url); } else { - final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - intent.setPackage(defaultBrowserPackageName); + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)) + .setPackage(defaultBrowserPackageName) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } @@ -48,7 +49,8 @@ public final class ShareUtils { private static void openInDefaultApp(final Context context, final String url) { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); context.startActivity(Intent.createChooser( - intent, context.getString(R.string.share_dialog_title))); + intent, context.getString(R.string.share_dialog_title)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } /** @@ -60,7 +62,8 @@ public final class ShareUtils { * @return the package name of the default browser, or "android" if there's no default */ private static String getDefaultBrowserPackageName(final Context context) { - final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")); + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity( intent, PackageManager.MATCH_DEFAULT_ONLY); return resolveInfo.activityInfo.packageName; @@ -74,12 +77,13 @@ public final class ShareUtils { * @param url the url to share */ public static void shareUrl(final Context context, final String subject, final String url) { - Intent intent = new Intent(Intent.ACTION_SEND); + final Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, subject); intent.putExtra(Intent.EXTRA_TEXT, url); context.startActivity(Intent.createChooser( - intent, context.getString(R.string.share_dialog_title))); + intent, context.getString(R.string.share_dialog_title)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } /** diff --git a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java index 2a1dff5c9..ba095a4c5 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java +++ b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java @@ -36,7 +36,6 @@ import org.schabi.newpipe.MainActivity; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FilenameFilter; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; @@ -66,7 +65,7 @@ public final class StateSaver { * @param context used to get the available cache dir */ public static void init(final Context context) { - File externalCacheDir = context.getExternalCacheDir(); + final File externalCacheDir = context.getExternalCacheDir(); if (externalCacheDir != null) { cacheDirPath = externalCacheDir.getAbsolutePath(); } @@ -86,7 +85,7 @@ public final class StateSaver { return null; } - SavedState savedState = outState.getParcelable(KEY_SAVED_STATE); + final SavedState savedState = outState.getParcelable(KEY_SAVED_STATE); if (savedState == null) { return null; } @@ -122,7 +121,7 @@ public final class StateSaver { return savedState; } - File file = new File(savedState.getPathFileSaved()); + final File file = new File(savedState.getPathFileSaved()); if (!file.exists()) { if (MainActivity.DEBUG) { Log.d(TAG, "Cache file doesn't exist: " + file.getAbsolutePath()); @@ -131,7 +130,7 @@ public final class StateSaver { } fileInputStream = new FileInputStream(file); - ObjectInputStream inputStream = new ObjectInputStream(fileInputStream); + final ObjectInputStream inputStream = new ObjectInputStream(fileInputStream); //noinspection unchecked savedObjects = (Queue) inputStream.readObject(); if (savedObjects != null) { @@ -139,13 +138,13 @@ public final class StateSaver { } return savedState; - } catch (Exception e) { + } catch (final Exception e) { Log.e(TAG, "Failed to restore state", e); } finally { if (fileInputStream != null) { try { fileInputStream.close(); - } catch (IOException ignored) { + } catch (final IOException ignored) { } } } @@ -165,7 +164,7 @@ public final class StateSaver { @Nullable final SavedState savedState, final Bundle outState, final WriteRead writeRead) { @NonNull - String currentSavedPrefix; + final String currentSavedPrefix; if (savedState == null || TextUtils.isEmpty(savedState.getPrefixFileSaved())) { // Generate unique prefix currentSavedPrefix = System.nanoTime() - writeRead.hashCode() + ""; @@ -215,7 +214,7 @@ public final class StateSaver { + "writeRead = [" + writeRead + "]"); } - LinkedList savedObjects = new LinkedList<>(); + final LinkedList savedObjects = new LinkedList<>(); writeRead.writeTo(savedObjects); if (isChangingConfig) { @@ -247,36 +246,32 @@ public final class StateSaver { } } - File file = new File(cacheDir, prefixFileName + final File file = new File(cacheDir, prefixFileName + (TextUtils.isEmpty(suffixFileName) ? ".cache" : suffixFileName)); if (file.exists() && file.length() > 0) { // If the file already exists, just return it return new SavedState(prefixFileName, file.getAbsolutePath()); } else { // Delete any file that contains the prefix - File[] files = cacheDir.listFiles(new FilenameFilter() { - @Override - public boolean accept(final File dir, final String name) { - return name.contains(prefixFileName); - } - }); - for (File fileToDelete : files) { + final File[] files = cacheDir.listFiles((dir, name) -> + name.contains(prefixFileName)); + for (final File fileToDelete : files) { fileToDelete.delete(); } } fileOutputStream = new FileOutputStream(file); - ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream); + final ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream); outputStream.writeObject(savedObjects); return new SavedState(prefixFileName, file.getAbsolutePath()); - } catch (Exception e) { + } catch (final Exception e) { Log.e(TAG, "Failed to save state", e); } finally { if (fileOutputStream != null) { try { fileOutputStream.close(); - } catch (IOException ignored) { } + } catch (final IOException ignored) { } } } return null; @@ -298,7 +293,7 @@ public final class StateSaver { try { //noinspection ResultOfMethodCallIgnored new File(savedState.getPathFileSaved()).delete(); - } catch (Exception ignored) { + } catch (final Exception ignored) { } } } @@ -319,7 +314,7 @@ public final class StateSaver { cacheDir = new File(cacheDir, CACHE_DIR_NAME); if (cacheDir.exists()) { - for (File file : cacheDir.listFiles()) { + for (final File file : cacheDir.listFiles()) { file.delete(); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java index 92aee8ba7..a1e2e6eb9 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java @@ -76,7 +76,7 @@ public enum StreamDialogEntry { */ public static void setEnabledEntries(final StreamDialogEntry... entries) { // cleanup from last time StreamDialogEntry was used - for (StreamDialogEntry streamDialogEntry : values()) { + for (final StreamDialogEntry streamDialogEntry : values()) { streamDialogEntry.customAction = null; } @@ -84,7 +84,7 @@ public enum StreamDialogEntry { } public static String[] getCommands(final Context context) { - String[] commands = new String[enabledEntries.length]; + final String[] commands = new String[enabledEntries.length]; for (int i = 0; i != enabledEntries.length; ++i) { commands[i] = context.getResources().getString(enabledEntries[i].resource); } diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 6a244a69b..2bfd27c47 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -111,7 +111,7 @@ public class StreamItemAdapter extends BaseA String qualityString; if (stream instanceof VideoStream) { - VideoStream videoStream = ((VideoStream) stream); + final VideoStream videoStream = ((VideoStream) stream); qualityString = videoStream.getResolution(); if (secondaryStreams != null) { @@ -123,7 +123,7 @@ public class StreamItemAdapter extends BaseA } } } else if (stream instanceof AudioStream) { - AudioStream audioStream = ((AudioStream) stream); + final AudioStream audioStream = ((AudioStream) stream); qualityString = audioStream.getAverageBitrate() > 0 ? audioStream.getAverageBitrate() + "kbps" : audioStream.getFormat().getName(); @@ -137,10 +137,11 @@ public class StreamItemAdapter extends BaseA } if (streamsWrapper.getSizeInBytes(position) > 0) { - SecondaryStreamHelper secondary = secondaryStreams == null ? null + final SecondaryStreamHelper secondary = secondaryStreams == null ? null : secondaryStreams.get(position); if (secondary != null) { - long size = secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position); + final long size + = secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position); sizeView.setText(Utility.formatBytes(size)); } else { sizeView.setText(streamsWrapper.getFormattedSize(position)); @@ -204,7 +205,7 @@ public class StreamItemAdapter extends BaseA final StreamSizeWrapper streamsWrapper) { final Callable fetchAndSet = () -> { boolean hasChanged = false; - for (X stream : streamsWrapper.getStreamsList()) { + for (final X stream : streamsWrapper.getStreamsList()) { if (streamsWrapper.getSizeInBytes(stream) > -2) { continue; } diff --git a/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java b/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java index 105af5086..89dc08f38 100644 --- a/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java +++ b/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java @@ -27,7 +27,7 @@ public class TLSSocketFactoryCompat extends SSLSocketFactory { private SSLSocketFactory internalSSLSocketFactory; public TLSSocketFactoryCompat() throws KeyManagementException, NoSuchAlgorithmException { - SSLContext context = SSLContext.getInstance("TLS"); + final SSLContext context = SSLContext.getInstance("TLS"); context.init(null, null, null); internalSSLSocketFactory = context.getSocketFactory(); } @@ -35,7 +35,7 @@ public class TLSSocketFactoryCompat extends SSLSocketFactory { public TLSSocketFactoryCompat(final TrustManager[] tm) throws KeyManagementException, NoSuchAlgorithmException { - SSLContext context = SSLContext.getInstance("TLS"); + final SSLContext context = SSLContext.getInstance("TLS"); context.init(null, tm, new java.security.SecureRandom()); internalSSLSocketFactory = context.getSocketFactory(); } @@ -106,7 +106,7 @@ public class TLSSocketFactoryCompat extends SSLSocketFactory { } private Socket enableTLSOnSocket(final Socket socket) { - if (socket != null && (socket instanceof SSLSocket)) { + if (socket instanceof SSLSocket) { ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"}); } return socket; diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 74ea34fcc..051fbbcf8 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -21,7 +21,7 @@ package org.schabi.newpipe.util; import android.content.Context; import android.content.res.TypedArray; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.TypedValue; import android.view.ContextThemeWrapper; @@ -126,11 +126,11 @@ public final class ThemeHelper { */ @StyleRes public static int getThemeForService(final Context context, final int serviceId) { - String lightTheme = context.getResources().getString(R.string.light_theme_key); - String darkTheme = context.getResources().getString(R.string.dark_theme_key); - String blackTheme = context.getResources().getString(R.string.black_theme_key); + final String lightTheme = context.getResources().getString(R.string.light_theme_key); + final String darkTheme = context.getResources().getString(R.string.dark_theme_key); + final String blackTheme = context.getResources().getString(R.string.black_theme_key); - String selectedTheme = getSelectedThemeString(context); + final String selectedTheme = getSelectedThemeString(context); int defaultTheme = R.style.DarkTheme; if (selectedTheme.equals(lightTheme)) { @@ -148,7 +148,7 @@ public final class ThemeHelper { final StreamingService service; try { service = NewPipe.getService(serviceId); - } catch (ExtractionException ignored) { + } catch (final ExtractionException ignored) { return defaultTheme; } @@ -162,7 +162,7 @@ public final class ThemeHelper { } themeName += "." + service.getServiceInfo().getName(); - int resourceId = context + final int resourceId = context .getResources() .getIdentifier(themeName, "style", context.getPackageName()); @@ -175,11 +175,11 @@ public final class ThemeHelper { @StyleRes public static int getSettingsThemeStyle(final Context context) { - String lightTheme = context.getResources().getString(R.string.light_theme_key); - String darkTheme = context.getResources().getString(R.string.dark_theme_key); - String blackTheme = context.getResources().getString(R.string.black_theme_key); + final String lightTheme = context.getResources().getString(R.string.light_theme_key); + final String darkTheme = context.getResources().getString(R.string.dark_theme_key); + final String blackTheme = context.getResources().getString(R.string.black_theme_key); - String selectedTheme = getSelectedThemeString(context); + final String selectedTheme = getSelectedThemeString(context); if (selectedTheme.equals(lightTheme)) { return R.style.LightSettingsTheme; @@ -201,8 +201,8 @@ public final class ThemeHelper { * @return resource ID */ public static int resolveResourceIdFromAttr(final Context context, @AttrRes final int attr) { - TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); - int attributeResourceId = a.getResourceId(0, 0); + final TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + final int attributeResourceId = a.getResourceId(0, 0); a.recycle(); return attributeResourceId; } @@ -226,8 +226,8 @@ public final class ThemeHelper { } private static String getSelectedThemeString(final Context context) { - String themeKey = context.getString(R.string.theme_key); - String defaultTheme = context.getResources().getString(R.string.default_theme_value); + final String themeKey = context.getString(R.string.theme_key); + final String defaultTheme = context.getResources().getString(R.string.default_theme_value); return PreferenceManager.getDefaultSharedPreferences(context) .getString(themeKey, defaultTheme); } diff --git a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java index 31f5fd222..7ed90d693 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java @@ -44,10 +44,10 @@ public final class ZipHelper { */ public static void addFileToZip(final ZipOutputStream outZip, final String file, final String name) throws Exception { - byte[] data = new byte[BUFFER_SIZE]; - FileInputStream fi = new FileInputStream(file); - BufferedInputStream inputStream = new BufferedInputStream(fi, BUFFER_SIZE); - ZipEntry entry = new ZipEntry(name); + final byte[] data = new byte[BUFFER_SIZE]; + final FileInputStream fi = new FileInputStream(file); + final BufferedInputStream inputStream = new BufferedInputStream(fi, BUFFER_SIZE); + final ZipEntry entry = new ZipEntry(name); outZip.putNextEntry(entry); int count; while ((count = inputStream.read(data, 0, BUFFER_SIZE)) != -1) { @@ -69,11 +69,11 @@ public final class ZipHelper { public static boolean extractFileFromZip(final String filePath, final String file, final String name) throws Exception { - ZipInputStream inZip = new ZipInputStream( + final ZipInputStream inZip = new ZipInputStream( new BufferedInputStream( new FileInputStream(filePath))); - byte[] data = new byte[BUFFER_SIZE]; + final byte[] data = new byte[BUFFER_SIZE]; boolean found = false; @@ -82,14 +82,14 @@ public final class ZipHelper { if (ze.getName().equals(name)) { found = true; // delete old file first - File oldFile = new File(file); + final File oldFile = new File(file); if (oldFile.exists()) { if (!oldFile.delete()) { throw new Exception("Could not delete " + file); } } - FileOutputStream outFile = new FileOutputStream(file); + final FileOutputStream outFile = new FileOutputStream(file); int count = 0; while ((count = inZip.read(data)) != -1) { outFile.write(data, 0, count); diff --git a/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java b/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java index 0fbf6a254..b1fabe715 100644 --- a/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java +++ b/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java @@ -59,7 +59,7 @@ public final class AnimatedProgressBar extends ProgressBar { @Override protected void applyTransformation(final float interpolatedTime, final Transformation t) { super.applyTransformation(interpolatedTime, t); - float value = from + (to - from) * interpolatedTime; + final float value = from + (to - from) * interpolatedTime; progressBar.setProgress((int) value); } } diff --git a/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java b/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java index 028e9b674..b34a6be63 100644 --- a/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java +++ b/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java @@ -178,7 +178,7 @@ public class CollapsibleView extends LinearLayout { } public void broadcastState() { - for (StateListener listener : listeners) { + for (final StateListener listener : listeners) { listener.onStateChanged(currentState); } } diff --git a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java new file mode 100644 index 000000000..798712b6b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java @@ -0,0 +1,114 @@ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.SurfaceView; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; + +import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + +public class ExpandableSurfaceView extends SurfaceView { + private int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + private int baseHeight = 0; + private int maxHeight = 0; + private float videoAspectRatio = 0.0f; + private float scaleX = 1.0f; + private float scaleY = 1.0f; + + public ExpandableSurfaceView(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (videoAspectRatio == 0.0f) { + return; + } + + int width = MeasureSpec.getSize(widthMeasureSpec); + final boolean verticalVideo = videoAspectRatio < 1; + // Use maxHeight only on non-fit resize mode and in vertical videos + int height = maxHeight != 0 + && resizeMode != AspectRatioFrameLayout.RESIZE_MODE_FIT + && verticalVideo ? maxHeight : baseHeight; + + if (height == 0) { + return; + } + + final float viewAspectRatio = width / ((float) height); + final float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; + scaleX = 1.0f; + scaleY = 1.0f; + + switch (resizeMode) { + case AspectRatioFrameLayout.RESIZE_MODE_FIT: + if (aspectDeformation > 0) { + height = (int) (width / videoAspectRatio); + } else { + width = (int) (height * videoAspectRatio); + } + + break; + case RESIZE_MODE_ZOOM: + if (aspectDeformation < 0) { + scaleY = viewAspectRatio / videoAspectRatio; + } else { + scaleX = videoAspectRatio / viewAspectRatio; + } + + break; + default: + break; + } + super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + + /** + * Scale view only in {@link #onLayout} to make transition for ZOOM mode as smooth as possible. + */ + @Override + protected void onLayout(final boolean changed, + final int left, final int top, final int right, final int bottom) { + setScaleX(scaleX); + setScaleY(scaleY); + } + + /** + * @param base The height that will be used in every resize mode as a minimum height + * @param max The max height for vertical videos in non-FIT resize modes + */ + public void setHeights(final int base, final int max) { + if (baseHeight == base && maxHeight == max) { + return; + } + baseHeight = base; + maxHeight = max; + requestLayout(); + } + + public void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int newResizeMode) { + if (resizeMode == newResizeMode) { + return; + } + + resizeMode = newResizeMode; + requestLayout(); + } + + @AspectRatioFrameLayout.ResizeMode + public int getResizeMode() { + return resizeMode; + } + + public void setAspectRatio(final float aspectRatio) { + if (videoAspectRatio == aspectRatio) { + return; + } + + videoAspectRatio = aspectRatio; + requestLayout(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java index 0da42fab6..500562668 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareDrawerLayout.java @@ -55,9 +55,10 @@ public final class FocusAwareDrawerLayout extends DrawerLayout { boolean hasOpenPanels = false; for (int i = 0; i < getChildCount(); ++i) { - View child = getChildAt(i); + final View child = getChildAt(i); - DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) child.getLayoutParams(); + final DrawerLayout.LayoutParams lp + = (DrawerLayout.LayoutParams) child.getLayoutParams(); if (lp.gravity != 0 && isDrawerVisible(child)) { hasOpenPanels = true; @@ -82,9 +83,10 @@ public final class FocusAwareDrawerLayout extends DrawerLayout { View content = null; for (int i = 0; i < getChildCount(); ++i) { - View child = getChildAt(i); + final View child = getChildAt(i); - DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) child.getLayoutParams(); + final DrawerLayout.LayoutParams lp + = (DrawerLayout.LayoutParams) child.getLayoutParams(); if (lp.gravity == 0) { content = child; diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java index 6dbcded48..a50d5a64c 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusAwareSeekBar.java @@ -26,7 +26,7 @@ import android.widget.SeekBar; import androidx.appcompat.widget.AppCompatSeekBar; -import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.DeviceUtils; /** * SeekBar, adapted for directional navigation. It emulates touch-related callbacks @@ -60,7 +60,7 @@ public final class FocusAwareSeekBar extends AppCompatSeekBar { @Override public boolean onKeyDown(final int keyCode, final KeyEvent event) { - if (!isInTouchMode() && AndroidTvUtils.isConfirmKey(keyCode)) { + if (!isInTouchMode() && DeviceUtils.isConfirmKey(keyCode)) { releaseTrack(); } diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java index 1c868f66a..dc5bf7133 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java @@ -38,6 +38,7 @@ import android.view.ViewTreeObserver; import android.view.Window; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.view.WindowCallbackWrapper; @@ -74,46 +75,30 @@ public final class FocusOverlayView extends Drawable implements @Override public void onGlobalFocusChanged(final View oldFocus, final View newFocus) { - int l = focusRect.left; - int r = focusRect.right; - int t = focusRect.top; - int b = focusRect.bottom; - - if (newFocus != null && newFocus.getWidth() > 0 && newFocus.getHeight() > 0) { - newFocus.getGlobalVisibleRect(focusRect); - + if (newFocus != null) { focused = new WeakReference<>(newFocus); } else { - focusRect.setEmpty(); - focused = null; } - if (l != focusRect.left || r != focusRect.right - || t != focusRect.top || b != focusRect.bottom) { - invalidateSelf(); - } - - focused = new WeakReference<>(newFocus); + updateRect(); animator.sendEmptyMessageDelayed(0, 1000); } private void updateRect() { - if (focused == null) { - return; + final View focusedView = focused == null ? null : this.focused.get(); + + final int l = focusRect.left; + final int r = focusRect.right; + final int t = focusRect.top; + final int b = focusRect.bottom; + + if (focusedView != null && isShown(focusedView)) { + focusedView.getGlobalVisibleRect(focusRect); } - View focusedView = this.focused.get(); - - int l = focusRect.left; - int r = focusRect.right; - int t = focusRect.top; - int b = focusRect.bottom; - - if (focusedView != null) { - focusedView.getGlobalVisibleRect(focusRect); - } else { + if (shouldClearFocusRect(focusedView, focusRect)) { focusRect.setEmpty(); } @@ -123,6 +108,10 @@ public final class FocusOverlayView extends Drawable implements } } + private boolean isShown(@NonNull final View view) { + return view.getWidth() != 0 && view.getHeight() != 0 && view.isShown(); + } + @Override public void onDraw() { updateRect(); @@ -184,45 +173,56 @@ public final class FocusOverlayView extends Drawable implements public void setColorFilter(final ColorFilter colorFilter) { } - public static void setupFocusObserver(final Dialog dialog) { - Rect displayRect = new Rect(); + /* + * When any view in the player looses it's focus (after setVisibility(GONE)) the focus gets + * added to the whole fragment which has a width and height equal to the window frame. + * The easiest way to avoid the unneeded frame is to skip highlighting of rect that is + * equal to the overlayView bounds + * */ + private boolean shouldClearFocusRect(@Nullable final View focusedView, final Rect focusedRect) { + return focusedView == null || focusedRect.equals(getBounds()); + } - Window window = dialog.getWindow(); + public static void setupFocusObserver(final Dialog dialog) { + final Rect displayRect = new Rect(); + + final Window window = dialog.getWindow(); assert window != null; - View decor = window.getDecorView(); + final View decor = window.getDecorView(); decor.getWindowVisibleDisplayFrame(displayRect); - FocusOverlayView overlay = new FocusOverlayView(dialog.getContext()); + final FocusOverlayView overlay = new FocusOverlayView(dialog.getContext()); overlay.setBounds(0, 0, displayRect.width(), displayRect.height()); setupOverlay(window, overlay); } public static void setupFocusObserver(final Activity activity) { - Rect displayRect = new Rect(); + final Rect displayRect = new Rect(); - Window window = activity.getWindow(); - View decor = window.getDecorView(); + final Window window = activity.getWindow(); + final View decor = window.getDecorView(); decor.getWindowVisibleDisplayFrame(displayRect); - FocusOverlayView overlay = new FocusOverlayView(activity); + final FocusOverlayView overlay = new FocusOverlayView(activity); overlay.setBounds(0, 0, displayRect.width(), displayRect.height()); setupOverlay(window, overlay); } private static void setupOverlay(final Window window, final FocusOverlayView overlay) { - ViewGroup decor = (ViewGroup) window.getDecorView(); + final ViewGroup decor = (ViewGroup) window.getDecorView(); decor.getOverlay().add(overlay); fixFocusHierarchy(decor); - ViewTreeObserver observer = decor.getViewTreeObserver(); + final ViewTreeObserver observer = decor.getViewTreeObserver(); observer.addOnScrollChangedListener(overlay); observer.addOnGlobalFocusChangeListener(overlay); observer.addOnGlobalLayoutListener(overlay); observer.addOnTouchModeChangeListener(overlay); + observer.addOnDrawListener(overlay); overlay.setCurrentFocus(decor.getFocusedChild()); @@ -235,7 +235,7 @@ public final class FocusOverlayView extends Drawable implements window.setCallback(new WindowCallbackWrapper(window.getCallback()) { @Override public boolean dispatchKeyEvent(final KeyEvent event) { - boolean res = super.dispatchKeyEvent(event); + final boolean res = super.dispatchKeyEvent(event); overlay.onKey(event); return res; } @@ -259,7 +259,7 @@ public final class FocusOverlayView extends Drawable implements // keyboard META key for moving between clusters). We have to fix this unfortunate accident // While we are at it, let's deal with touchscreenBlocksFocus too. - if (Build.VERSION.SDK_INT < 26) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return; } @@ -280,10 +280,10 @@ public final class FocusOverlayView extends Drawable implements return; // clusters aren't supposed to nest } - int childCount = viewGroup.getChildCount(); + final int childCount = viewGroup.getChildCount(); for (int i = 0; i < childCount; ++i) { - View view = viewGroup.getChildAt(i); + final View view = viewGroup.getChildAt(i); if (view instanceof ViewGroup) { clearFocusObstacles((ViewGroup) view); diff --git a/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java b/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java index 3a3384b51..1219304e1 100644 --- a/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java +++ b/app/src/main/java/org/schabi/newpipe/views/LargeTextMovementMethod.java @@ -64,7 +64,7 @@ public class LargeTextMovementMethod extends LinkMovementMethod { final int keyCode, final int movementMetaState, final KeyEvent event) { - int newDir = keyToDir(keyCode); + final int newDir = keyToDir(keyCode); if (direction != 0 && newDir != direction) { return false; @@ -72,7 +72,7 @@ public class LargeTextMovementMethod extends LinkMovementMethod { this.direction = 0; - ViewGroup root = findScrollableParent(widget); + final ViewGroup root = findScrollableParent(widget); widget.getHitRect(visibleRect); @@ -118,50 +118,50 @@ public class LargeTextMovementMethod extends LinkMovementMethod { } private boolean gotoPrev(final TextView view, final Spannable buffer) { - Layout layout = view.getLayout(); + final Layout layout = view.getLayout(); if (layout == null) { return false; } - View root = findScrollableParent(view); + final View root = findScrollableParent(view); - int rootHeight = root.getHeight(); + final int rootHeight = root.getHeight(); if (visibleRect.top >= 0) { // we fit entirely into the viewport, no need for fancy footwork return false; } - int topExtra = -visibleRect.top; + final int topExtra = -visibleRect.top; - int firstVisibleLineNumber = layout.getLineForVertical(topExtra); + final int firstVisibleLineNumber = layout.getLineForVertical(topExtra); // when deciding whether to pass "focus" to span, account for one more line // this ensures, that focus is never passed to spans partially outside scroll window - int visibleStart = firstVisibleLineNumber == 0 + final int visibleStart = firstVisibleLineNumber == 0 ? 0 : layout.getLineStart(firstVisibleLineNumber - 1); - ClickableSpan[] candidates = buffer.getSpans( + final ClickableSpan[] candidates = buffer.getSpans( visibleStart, buffer.length(), ClickableSpan.class); if (candidates.length != 0) { - int a = Selection.getSelectionStart(buffer); - int b = Selection.getSelectionEnd(buffer); + final int a = Selection.getSelectionStart(buffer); + final int b = Selection.getSelectionEnd(buffer); - int selStart = Math.min(a, b); - int selEnd = Math.max(a, b); + final int selStart = Math.min(a, b); + final int selEnd = Math.max(a, b); int bestStart = -1; int bestEnd = -1; - for (int i = 0; i < candidates.length; i++) { - int start = buffer.getSpanStart(candidates[i]); - int end = buffer.getSpanEnd(candidates[i]); + for (final ClickableSpan candidate : candidates) { + final int start = buffer.getSpanStart(candidate); + final int end = buffer.getSpanEnd(candidate); if ((end < selEnd || selStart == selEnd) && start >= visibleStart) { if (end > bestEnd) { - bestStart = buffer.getSpanStart(candidates[i]); + bestStart = buffer.getSpanStart(candidate); bestEnd = end; } } @@ -173,7 +173,7 @@ public class LargeTextMovementMethod extends LinkMovementMethod { } } - float fourLines = view.getTextSize() * 4; + final float fourLines = view.getTextSize() * 4; visibleRect.left = 0; visibleRect.right = view.getWidth(); @@ -184,54 +184,54 @@ public class LargeTextMovementMethod extends LinkMovementMethod { } private boolean gotoNext(final TextView view, final Spannable buffer) { - Layout layout = view.getLayout(); + final Layout layout = view.getLayout(); if (layout == null) { return false; } - View root = findScrollableParent(view); + final View root = findScrollableParent(view); - int rootHeight = root.getHeight(); + final int rootHeight = root.getHeight(); if (visibleRect.bottom <= rootHeight) { // we fit entirely into the viewport, no need for fancy footwork return false; } - int bottomExtra = visibleRect.bottom - rootHeight; + final int bottomExtra = visibleRect.bottom - rootHeight; - int visibleBottomBorder = view.getHeight() - bottomExtra; + final int visibleBottomBorder = view.getHeight() - bottomExtra; - int lineCount = layout.getLineCount(); + final int lineCount = layout.getLineCount(); - int lastVisibleLineNumber = layout.getLineForVertical(visibleBottomBorder); + final int lastVisibleLineNumber = layout.getLineForVertical(visibleBottomBorder); // when deciding whether to pass "focus" to span, account for one more line // this ensures, that focus is never passed to spans partially outside scroll window - int visibleEnd = lastVisibleLineNumber == lineCount - 1 + final int visibleEnd = lastVisibleLineNumber == lineCount - 1 ? buffer.length() : layout.getLineEnd(lastVisibleLineNumber - 1); - ClickableSpan[] candidates = buffer.getSpans(0, visibleEnd, ClickableSpan.class); + final ClickableSpan[] candidates = buffer.getSpans(0, visibleEnd, ClickableSpan.class); if (candidates.length != 0) { - int a = Selection.getSelectionStart(buffer); - int b = Selection.getSelectionEnd(buffer); + final int a = Selection.getSelectionStart(buffer); + final int b = Selection.getSelectionEnd(buffer); - int selStart = Math.min(a, b); - int selEnd = Math.max(a, b); + final int selStart = Math.min(a, b); + final int selEnd = Math.max(a, b); int bestStart = Integer.MAX_VALUE; int bestEnd = Integer.MAX_VALUE; - for (int i = 0; i < candidates.length; i++) { - int start = buffer.getSpanStart(candidates[i]); - int end = buffer.getSpanEnd(candidates[i]); + for (final ClickableSpan candidate : candidates) { + final int start = buffer.getSpanStart(candidate); + final int end = buffer.getSpanEnd(candidate); if ((start > selStart || selStart == selEnd) && end <= visibleEnd) { if (start < bestStart) { bestStart = start; - bestEnd = buffer.getSpanEnd(candidates[i]); + bestEnd = buffer.getSpanEnd(candidate); } } } @@ -245,7 +245,7 @@ public class LargeTextMovementMethod extends LinkMovementMethod { // there are no links within visible area, but still some text past visible area // scroll visible area further in required direction - float fourLines = view.getTextSize() * 4; + final float fourLines = view.getTextSize() * 4; visibleRect.left = 0; visibleRect.right = view.getWidth(); diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java index 655b86818..a4f7a0506 100644 --- a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java +++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java @@ -104,14 +104,14 @@ public class NewPipeRecyclerView extends RecyclerView { // can mess with focused View by moving it off-screen and detaching) if (focused != null) { - View focusedItem = findContainingItemView(focused); + final View focusedItem = findContainingItemView(focused); if (focusedItem != null) { focusedItem.getHitRect(focusRect); } } // call focusSearch() to initiate layout, but disregard returned View for now - View adapterResult = super.focusSearch(focused, direction); + final View adapterResult = super.focusSearch(focused, direction); if (adapterResult != null && !isOutside(adapterResult)) { adapterResult.requestFocus(direction); return true; @@ -142,22 +142,22 @@ public class NewPipeRecyclerView extends RecyclerView { } private boolean tryFocusFinder(final int direction) { - if (Build.VERSION.SDK_INT >= 28) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // Android 9 implemented bunch of handy changes to focus, that render code below less // useful, and also broke findNextFocusFromRect in way, that render this hack useless return false; } - FocusFinder finder = FocusFinder.getInstance(); + final FocusFinder finder = FocusFinder.getInstance(); // try to use FocusFinder instead of adapter - ViewGroup root = (ViewGroup) getRootView(); + final ViewGroup root = (ViewGroup) getRootView(); tempFocus.set(focusRect); root.offsetDescendantRectToMyCoords(this, tempFocus); - View focusFinderResult = finder.findNextFocusFromRect(root, tempFocus, direction); + final View focusFinderResult = finder.findNextFocusFromRect(root, tempFocus, direction); if (focusFinderResult != null && !isOutside(focusFinderResult)) { focusFinderResult.requestFocus(direction); return true; @@ -172,7 +172,7 @@ public class NewPipeRecyclerView extends RecyclerView { parent.offsetDescendantRectToMyCoords(this, tempFocus); - View candidate = finder.findNextFocusFromRect(parent, tempFocus, direction); + final View candidate = finder.findNextFocusFromRect(parent, tempFocus, direction); if (candidate != null && candidate.requestFocus(direction)) { return true; } diff --git a/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java b/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java index 48e8ef81c..fb21a8083 100644 --- a/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java +++ b/app/src/main/java/org/schabi/newpipe/views/ScrollableTabLayout.java @@ -118,7 +118,7 @@ public class ScrollableTabLayout extends TabLayout { final int count = getTabCount(); int contentWidth = 0; for (int i = 0; i < count; i++) { - View child = getTabAt(i).view; + final View child = getTabAt(i).view; if (child.getVisibility() == View.VISIBLE) { // Use tab's minimum requested width should actual content be too small contentWidth += Math.max(child.getMinimumWidth(), child.getMeasuredWidth()); diff --git a/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java index 6c4d20603..62465d2a4 100644 --- a/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java +++ b/app/src/main/java/org/schabi/newpipe/views/SuperScrollLayoutManager.java @@ -64,12 +64,12 @@ public final class SuperScrollLayoutManager extends LinearLayoutManager { @Nullable @Override public View onInterceptFocusSearch(@NonNull final View focused, final int direction) { - View focusedItem = findContainingItemView(focused); + final View focusedItem = findContainingItemView(focused); if (focusedItem == null) { return super.onInterceptFocusSearch(focused, direction); } - int listDirection = getAbsoluteDirection(direction); + final int listDirection = getAbsoluteDirection(direction); if (listDirection == 0) { return super.onInterceptFocusSearch(focused, direction); } @@ -82,9 +82,9 @@ public final class SuperScrollLayoutManager extends LinearLayoutManager { // Fortunately we can intercept focus search and implement our own logic, based purely // on position along the LinearLayoutManager axis - ViewGroup recycler = (ViewGroup) focusedItem.getParent(); + final ViewGroup recycler = (ViewGroup) focusedItem.getParent(); - int sourcePosition = getPosition(focusedItem); + final int sourcePosition = getPosition(focusedItem); if (sourcePosition == 0 && listDirection < 0) { return super.onInterceptFocusSearch(focused, direction); } @@ -100,7 +100,7 @@ public final class SuperScrollLayoutManager extends LinearLayoutManager { : View.FOCUSABLES_ALL); try { - for (View view : focusables) { + for (final View view : focusables) { if (view == focused || view == recycler) { continue; } @@ -111,7 +111,7 @@ public final class SuperScrollLayoutManager extends LinearLayoutManager { continue; } - int candidate = getDistance(sourcePosition, view, listDirection); + final int candidate = getDistance(sourcePosition, view, listDirection); if (candidate < 0) { continue; } @@ -162,12 +162,12 @@ public final class SuperScrollLayoutManager extends LinearLayoutManager { } private int getDistance(final int sourcePosition, final View candidate, final int direction) { - View itemView = findContainingItemView(candidate); + final View itemView = findContainingItemView(candidate); if (itemView == null) { return -1; } - int position = getPosition(itemView); + final int position = getPosition(itemView); return direction * (position - sourcePosition); } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 3f651d2ee..d7c586083 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -633,7 +633,7 @@ public class DownloadMission extends Mission { calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; calculated -= offsets[0];// don't count reserved space - return calculated > nearLength ? calculated : nearLength; + return Math.max(calculated, nearLength); } /** diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java index 14ac392a0..ab158ec51 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java @@ -155,7 +155,7 @@ public class DownloadMissionRecover extends Thread { for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) { String tag = subtitles.getLanguageTag(); if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) { - url = subtitles.getURL(); + url = subtitles.getUrl(); break; } } diff --git a/app/src/main/java/us/shandian/giga/io/FileStream.java b/app/src/main/java/us/shandian/giga/io/FileStream.java index 2cc4d5db8..bbc56b20c 100644 --- a/app/src/main/java/us/shandian/giga/io/FileStream.java +++ b/app/src/main/java/us/shandian/giga/io/FileStream.java @@ -30,12 +30,12 @@ public class FileStream extends SharpStream { } @Override - public int read(byte b[]) throws IOException { + public int read(byte[] b) throws IOException { return source.read(b); } @Override - public int read(byte b[], int off, int len) throws IOException { + public int read(byte[] b, int off, int len) throws IOException { return source.read(b, off, len); } diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java index ad3ceec3d..eba9437e1 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java @@ -315,6 +315,7 @@ public class StoredFileHelper implements Serializable { return false; if (this.isInvalid() || storage.isInvalid()) { + if (this.srcName == null || storage.srcName == null || this.srcType == null || storage.srcType == null) return false; return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType); } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index e93e83a87..50dbd1c3d 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -89,7 +89,7 @@ public abstract class Postprocessing implements Serializable { } public void setTemporalDir(@NonNull File directory) { - long rnd = (int) (Math.random() * 100000f); + long rnd = (int) (Math.random() * 100000.0f); tempFile = new File(directory, rnd + "_" + System.nanoTime() + ".tmp"); } @@ -115,7 +115,7 @@ public abstract class Postprocessing implements Serializable { mission.done = 0; long length = mission.storage.length() - mission.offsets[0]; - mission.length = length > mission.nearLength ? length : mission.nearLength; + mission.length = Math.max(length, mission.nearLength); final ProgressReport readProgress = (long position) -> { position -= mission.offsets[0]; diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 3da0e75b8..828f1adaf 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -24,7 +24,7 @@ import android.os.Handler.Callback; import android.os.IBinder; import android.os.Message; import android.os.Parcelable; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.util.SparseArray; import android.widget.Toast; @@ -160,7 +160,7 @@ public class DownloadManagerService extends Service { mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { @@ -240,7 +240,7 @@ public class DownloadManagerService extends Service { manageLock(false); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL); else unregisterReceiver(mNetworkStateListener); @@ -466,7 +466,7 @@ public class DownloadManagerService extends Service { if (downloadDoneCount < 1) { downloadDoneList.append(name); - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { downloadDoneNotification.setContentTitle(getString(R.string.app_name)); } else { downloadDoneNotification.setContentTitle(null); @@ -505,7 +505,7 @@ public class DownloadManagerService extends Service { .setContentIntent(mOpenDownloadList); } - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { downloadFailedNotification.setContentTitle(getString(R.string.app_name)); downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() .bigText(getString(R.string.download_failed).concat(": ").concat(mission.storage.getName()))); diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index c2d3a9b9e..c9518e477 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -49,6 +49,7 @@ import java.io.File; import java.lang.ref.WeakReference; import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import us.shandian.giga.get.DownloadMission; @@ -210,7 +211,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb } else { h.progress.setMarquee(false); h.status.setText("100%"); - h.progress.setProgress(1f); + h.progress.setProgress(1.0f); h.size.setText(Utility.formatBytes(item.mission.length)); } } @@ -243,7 +244,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb double progress; if (mission.unknownLength) { progress = Double.NaN; - h.progress.setProgress(0f); + h.progress.setProgress(0.0f); } else { progress = done / length; } @@ -302,15 +303,13 @@ public class MissionAdapter extends Adapter implements Handler.Callb float averageSpeed = speed; if (h.lastSpeedIdx < 0) { - for (int i = 0; i < h.lastSpeed.length; i++) { - h.lastSpeed[i] = speed; - } + Arrays.fill(h.lastSpeed, speed); h.lastSpeedIdx = 0; } else { for (int i = 0; i < h.lastSpeed.length; i++) { averageSpeed += h.lastSpeed[i]; } - averageSpeed /= h.lastSpeed.length + 1f; + averageSpeed /= h.lastSpeed.length + 1.0f; } String speedStr = Utility.formatSpeed(averageSpeed); diff --git a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java index 3f638d418..bec947540 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java +++ b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java @@ -26,7 +26,7 @@ public class ProgressDrawable extends Drawable { public ProgressDrawable() { mMarqueeLine = null;// marquee disabled - mMarqueeProgress = 0f; + mMarqueeProgress = 0.0f; mMarqueeSize = 0; mMarqueeNext = 0; } @@ -122,7 +122,7 @@ public class ProgressDrawable extends Drawable { } private void setupMarquee(int width, int height) { - mMarqueeSize = (int) ((width * 10f) / 100f);// the size is 10% of the width + mMarqueeSize = (int) ((width * 10.0f) / 100.0f);// the size is 10% of the width mMarqueeLine.rewind(); mMarqueeLine.moveTo(-mMarqueeSize, -mMarqueeSize); diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 09f4d0c79..35f40aa82 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -11,7 +11,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.IBinder; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -97,7 +97,7 @@ public class MissionsFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.missions, container, false); - mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + mPrefs = PreferenceManager.getDefaultSharedPreferences(requireActivity()); mLinear = mPrefs.getBoolean("linear", false); // Bind the service diff --git a/app/src/main/res/drawable/ic_next_white_24dp.xml b/app/src/main/res/drawable/ic_next_white_24dp.xml new file mode 100644 index 000000000..603880c2b --- /dev/null +++ b/app/src/main/res/drawable/ic_next_white_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_previous_white_24dp.xml b/app/src/main/res/drawable/ic_previous_white_24dp.xml new file mode 100644 index 000000000..14279ecb2 --- /dev/null +++ b/app/src/main/res/drawable/ic_previous_white_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index 84a29e0c8..a7872a83a 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -185,7 +185,7 @@ android:orientation="horizontal" tools:ignore="RtlHardcoded"> - @@ -238,7 +238,7 @@ app:srcCompat="@drawable/ic_shuffle_white_24dp" tools:ignore="ContentDescription"/> - diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index cb2b9ccfe..f69832b81 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -1,7 +1,12 @@ - + + + + + @@ -155,7 +169,6 @@ android:id="@+id/detail_content_root_layout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:windowBackground" app:layout_scrollFlags="scroll"> @@ -555,25 +568,22 @@ - + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + - - - + @@ -586,3 +596,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/player.xml similarity index 67% rename from app/src/main/res/layout-large-land/activity_main_player.xml rename to app/src/main/res/layout-large-land/player.xml index 16dcff639..46edda8b7 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -2,32 +2,24 @@ - + android:layout_centerInParent="true"/> - - - - - - + - - - - - - - - - - - - - - - - + + + + + - + + + + + android:layout_marginTop="6dp" + android:layout_marginRight="8dp" + tools:ignore="RtlHardcoded" + android:layout_weight="1"> @@ -192,94 +139,86 @@ android:singleLine="true" android:textColor="@android:color/white" android:textSize="12sp" - android:clickable="true" tools:text="The Video Artist LONG very LONG very Long"/>