diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f61e320c9d0..93af9d87261 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/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 655a1b96c77..dbc1c05a5b5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,8 +6,6 @@ assignees: '' --- -# We have fixed the decrpytion exception and a new release is on its wait. Please do not report the bug, that no videos can be played. -If you are about to report something else, please remove this and the above line. Thank you :) + + + + diff --git a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java index 11f457b6c8b..5840b0f7ecf 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 Object instantiateItem(@NonNull final ViewGroup container, final int posi // 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 Object instantiateItem(@NonNull final ViewGroup container, final int posi 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 Object instantiateItem(@NonNull final ViewGroup container, final int posi @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 void destroyItem(@NonNull final ViewGroup container, final int position, @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 Parcelable saveState() { 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,9 +294,9 @@ public Parcelable saveState() { @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) { @@ -304,11 +304,11 @@ public void restoreState(@Nullable final Parcelable state, @Nullable final Class mSavedState.add((Fragment.SavedState) fss[i]); } } - 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 09f9aea586f..174631fde3f 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.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 FlingBehavior(final Context context, final AttributeSet attrs) { 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,22 @@ public boolean onRequestChildRectangleOnScreen( 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; + } + } + allowScroll = true; switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: // remove reference to old nested scrolling child @@ -68,17 +82,37 @@ public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBa 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 +121,35 @@ private OverScroller getScrollerField() { @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 0071d2efebc..00842ec4615 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -104,7 +104,7 @@ public void onCreate() { } protected Downloader getDownloader() { - DownloaderImpl downloader = DownloaderImpl.init(null); + final DownloaderImpl downloader = DownloaderImpl.init(null); setCookiesToDownloader(downloader); return downloader; } @@ -208,7 +208,7 @@ protected void initACRA() { .setBuildConfigClass(BuildConfig.class) .build(); ACRA.init(this, acraConfig); - } catch (ACRAConfigurationException ace) { + } catch (final ACRAConfigurationException ace) { ace.printStackTrace(); ErrorActivity.reportError(this, ace, @@ -231,10 +231,10 @@ public void initNotificationChannel() { // 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); @@ -255,11 +255,11 @@ private void setUpUpdateNotificationChannel(final int importance) { 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 625f514e983..0d211fdfd20 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -62,7 +62,7 @@ private static String getCertificateSHA1Fingerprint() { 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 @@ private static String getCertificateSHA1Fingerprint() { 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 @@ private static String getCertificateSHA1Fingerprint() { 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 @@ protected void onPostExecute(final String response) { 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 @@ protected void onPostExecute(final String response) { 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 95d3c2b7c9f..9cba0667bbf 100644 --- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java +++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java @@ -94,18 +94,18 @@ public static DownloaderImpl getInstance() { 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 @@ private static void enableModernTLS(final OkHttpClient.Builder builder) { // 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<>(); + final List cipherSuites = new ArrayList<>(); cipherSuites.addAll(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 @@ private static void enableModernTLS(final OkHttpClient.Builder builder) { } 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 void removeCookie(final String key) { } 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 long getContentLength(final String url) throws IOException { 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 InputStream stream(final String siteUrl) throws IOException { .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 InputStream stream(final String siteUrl) throws IOException { } return body.byteStream(); - } catch (ReCaptchaException e) { + } catch (final ReCaptchaException e) { throw new IOException(e.getMessage(), e.getCause()); } } @@ -240,18 +240,18 @@ public Response execute(@NonNull final Request request) .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 94eff956064..d4a4e3125e6 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 @@ 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 diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 37d6d62f598..c3086d02c18 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -29,6 +29,8 @@ import android.os.Looper; import android.preference.PreferenceManager; import android.util.Log; + +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -37,10 +39,10 @@ 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; @@ -51,6 +53,7 @@ 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; @@ -61,14 +64,18 @@ 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; @@ -133,11 +140,11 @@ && getSupportFragmentManager().getBackStackEntryCount() == 0) { 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); } } @@ -148,8 +155,8 @@ private void setupDrawer() throws Exception { 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; @@ -221,7 +228,7 @@ private boolean drawerItemSelected(final MenuItem item) { case R.id.menu_tabs_group: try { tabSelected(item); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError(this, e); } break; @@ -262,8 +269,8 @@ private void tabSelected(final MenuItem item) throws ExtractionException { 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; @@ -292,8 +299,8 @@ private void optionsAboutSelected(final MenuItem item) { } 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); @@ -328,7 +335,7 @@ private void toggleServices() { } else { try { showTabs(); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError(this, e); } } @@ -337,11 +344,11 @@ private void toggleServices() { 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())); @@ -355,20 +362,20 @@ private void showServices() { } 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); @@ -377,7 +384,7 @@ private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuIt @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; } @@ -403,8 +410,8 @@ private void showTabs() throws ExtractionException { 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; @@ -469,11 +476,12 @@ protected void onResume() { 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..."); @@ -506,7 +514,7 @@ protected void onNewIntent(final Intent intent) { 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; @@ -518,25 +526,60 @@ protected void onNewIntent(final Intent intent) { 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; } } @@ -552,7 +595,7 @@ public void onBackPressed() { 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; } @@ -562,8 +605,8 @@ public void onRequestPermissionsResult(final int requestCode, 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(); } @@ -614,17 +657,14 @@ public boolean onCreateOptionsMenu(final Menu menu) { } 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); } @@ -639,7 +679,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { 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: @@ -660,6 +700,13 @@ private void initFragments() { } 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()); @@ -700,16 +747,22 @@ private void handleIntent(final Intent intent) { } 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(), @@ -729,7 +782,7 @@ private void handleIntent(final Intent intent) { 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, @@ -738,8 +791,21 @@ private void handleIntent(final Intent intent) { } 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 c59c48367fb..988a5ed9806 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 static void checkpoint() { 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 2e1abd59877..75304a2b904 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 diff --git a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java index 40ea4fd5889..c962ed99dc4 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 @@ protected void onCreate(final Bundle savedInstanceState) { 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 @@ protected void onCreate(final Bundle savedInstanceState) { 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 @@ protected void onCreate(final Bundle savedInstanceState) { @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 void onPageFinished(final WebView view, final String url) { // 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 void onPageFinished(final WebView view, final String url) { 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 void onBackPressed() { @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 @@ private void saveCookiesAndFinish() { 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 @@ private void handleCookiesFromUrl(@Nullable final String url) { 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 39f6b217dfd..70fceaf0753 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -44,7 +44,7 @@ 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; @@ -310,7 +310,7 @@ private void showDialog(final List choices) { }; 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); @@ -330,7 +330,7 @@ private void showDialog(final List choices) { 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; @@ -347,7 +347,7 @@ private void showDialog(final List choices) { alertDialog.show(); - if (AndroidTvUtils.isTv(this)) { + if (DeviceUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(alertDialog); } } @@ -362,9 +362,9 @@ private List getChoicesForService(final StreamingService serv 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), @@ -410,9 +410,9 @@ private void setDialogButtonsState(final AlertDialog dialog, final boolean state } 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); @@ -479,14 +479,14 @@ private void openDownloadDialog() { .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); @@ -504,7 +504,7 @@ private void openDownloadDialog() { 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; @@ -634,7 +634,7 @@ protected void onHandleIntent(@Nullable final Intent intent) { if (!(serializable instanceof Choice)) { return; } - Choice playerChoice = (Choice) serializable; + final Choice playerChoice = (Choice) serializable; handleChoice(playerChoice); } @@ -682,13 +682,13 @@ public Consumer getResultHandler(final Choice choice) { 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) { @@ -701,7 +701,7 @@ public Consumer getResultHandler(final Choice choice) { 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)) { @@ -716,7 +716,7 @@ public Consumer getResultHandler(final Choice choice) { : 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)) { @@ -726,6 +726,11 @@ public Consumer getResultHandler(final Choice choice) { }; } + 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 b5be2dde6b1..3041c3d6cb1 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 @@ protected void onCreate(final Bundle savedInstanceState) { 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 @@ protected void onCreate(final Bundle savedInstanceState) { 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 static AboutFragment newInstance() { @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))); 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 bc6310601f0..404f1260255 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.java @@ -31,8 +31,8 @@ public static LicenseFragment newInstance(final SoftwareComponent[] softwareComp 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; 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 1c425567f56..01a01bc8834 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 @@ private static String getFormattedLicense(@NonNull final Context context, // 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/Converters.java b/app/src/main/java/org/schabi/newpipe/database/Converters.java index e1a2fe2f373..ca2d8d875c7 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 static Integer integerOf(final FeedGroupIcon feedGroupIcon) { @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/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index a47f17d1346..1cf38dbca6a 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,7 +124,7 @@ public void setData(final String n, final String au, final String d, final Long @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()); 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 e46ded40d42..bc160d87396 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -13,7 +13,7 @@ import androidx.appcompat.widget.Toolbar; 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 @@ protected void onCreate(final Bundle savedInstanceState) { 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 void onGlobalLayout() { } }); - 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 @@ private void updateFragments() { @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 834a6cf2b74..c8a5c924f17 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -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 void onCreate(@Nullable final Bundle savedInstanceState) { 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 void onCreate(@Nullable final Bundle savedInstanceState) { 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(); @@ -296,7 +297,7 @@ public void onViewCreated(@NonNull final View view, @Nullable final Bundle saved prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - 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 void onActivityResult(final int requestCode, final int resultCode, final } 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; @@ -580,7 +581,7 @@ private int getSubtitleIndexBy(final List streams) { } 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); } @@ -607,10 +608,10 @@ private void showErrorActivity(final Exception e) { } private void prepareSelectedDownload() { - StoredDirectoryHelper mainStorage; - MediaFormat format; - String mime; - String selectedMediaType; + 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 @@ -708,15 +709,17 @@ private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, 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: @@ -769,8 +772,7 @@ private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, 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); @@ -812,7 +814,7 @@ private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, // 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; @@ -850,18 +852,18 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) { 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; @@ -882,7 +884,7 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) { kind = 'v'; selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex); - SecondaryStreamHelper secondary = videoStreamsAdapter + final SecondaryStreamHelper secondary = videoStreamsAdapter .getAllSecondary() .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); @@ -896,7 +898,7 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) { } 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 255841857ac..c687c4a6a04 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 void onReCaptchaException(final ReCaptchaException exception) { } 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 709dac36849..472dc340901 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -104,7 +104,7 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { public void onResume() { super.onResume(); - boolean youtubeRestrictedModeEnabled = + final boolean youtubeRestrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(getContext()) .getBoolean(youtubeRestrictedModeEnabledKey, false); if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) { @@ -137,7 +137,7 @@ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { } 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 boolean onOptionsItemSelected(final MenuItem item) { 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 Fragment getItem(final int position) { 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 28ce91f555e..e93c333cb9b 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 void onScrolled(final RecyclerView recyclerView, final int dx, final int 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 void onScrolled(final RecyclerView recyclerView, final int dx, final int 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 f966880b194..2fe615764fc 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 @@ public String getUrl() { 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/TabAdaptor.java index 38f013200b6..74609777c6b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java @@ -50,7 +50,7 @@ public void updateItem(final int position, final Fragment fragment) { } 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); } 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 336e3997e27..50ad1ae57d7 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,14 +1,23 @@ 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.os.Handler; +import android.os.IBinder; import android.preference.PreferenceManager; +import android.provider.Settings; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; @@ -16,32 +25,35 @@ 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.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 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,147 @@ 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 TabLayout tabLayout; private FrameLayout relatedStreamsLayout; - /*////////////////////////////////////////////////////////////////////////*/ + private ContentObserver settingsContentObserver; + private ServiceConnection serviceConnection; + private boolean bound; + private MainPlayer playerService; + private VideoPlayerImpl player; - 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"; + /*////////////////////////////////////////////////////////////////////////// + // Service management + //////////////////////////////////////////////////////////////////////////*/ - /** - * Stack that contains the "navigation history".
- * The peek is the current video. - */ - private final LinkedList stack = new LinkedList<>(); + 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; + } + } + + 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)); + } + + + /*////////////////////////////////////////////////////////////////////////*/ 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 +385,6 @@ public static VideoDetailFragment getInstance(final int serviceId, final String @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 +397,20 @@ public void onCreate(final Bundle savedInstanceState) { 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,6 +425,7 @@ public void onPause() { if (currentWorker != null) { currentWorker.dispose(); } + setupBrightness(true); PreferenceManager.getDefaultSharedPreferences(getContext()) .edit() .putString(getString(R.string.stream_info_selected_tab_key), @@ -261,40 +437,54 @@ public void onPause() { public void onResume() { super.onResume(); + activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED)); + + setupBrightness(false); + 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 +492,10 @@ public void onDestroy() { 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 +505,7 @@ public void onActivityResult(final int requestCode, final int resultCode, final 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 +522,6 @@ public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, 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; @@ -369,6 +540,9 @@ public void onSaveInstanceState(final Bundle outState) { outState.putSerializable(INFO_KEY, currentInfo); } + if (playQueue != null) { + outState.putSerializable(VideoPlayer.PLAY_QUEUE_KEY, playQueue); + } outState.putSerializable(STACK_KEY, stack); } @@ -388,6 +562,7 @@ protected void onRestoreInstanceState(@NonNull final Bundle savedState) { //noinspection unchecked stack.addAll((Collection) serializable); } + playQueue = (PlayQueue) savedState.getSerializable(VideoPlayer.PLAY_QUEUE_KEY); } /*////////////////////////////////////////////////////////////////////////// @@ -396,10 +571,6 @@ protected void onRestoreInstanceState(@NonNull final Bundle savedState) { @Override public void onClick(final View v) { - if (isLoading.get() || currentInfo == null) { - return; - } - switch (v.getId()) { case R.id.detail_controls_background: openBackgroundPlayer(false); @@ -408,9 +579,9 @@ public void onClick(final View v) { 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: @@ -434,27 +605,38 @@ public void onClick(final View v) { } break; case R.id.detail_thumbnail_root_layout: - if (currentInfo.getVideoStreams().isEmpty() - && currentInfo.getVideoOnlyStreams().isEmpty()) { - openBackgroundPlayer(false); + 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.detail_title_root_layout: - toggleTitleAndDescription(); + 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); } } @@ -473,7 +655,13 @@ public boolean onLongClick(final View v) { 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())) { @@ -515,11 +703,10 @@ private void toggleTitleAndDescription() { @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); @@ -553,6 +740,15 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { 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()); @@ -566,7 +762,7 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { 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); @@ -596,8 +792,20 @@ protected void initListeners() { 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() { @@ -617,6 +825,7 @@ private View.OnTouchListener getOnControlsTouchListener() { 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() { @@ -644,171 +853,94 @@ public void onLoadingFailed(final String imageUri, final View view, } /*////////////////////////////////////////////////////////////////////////// - // Menu + // OwnStack //////////////////////////////////////////////////////////////////////////*/ - @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)); - } + /** + * Stack that contains the "navigation history".
+ * The peek is the current video. + */ + protected final LinkedList stack = new LinkedList<>(); @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"); + public boolean onKeyDown(final int keyCode) { + return player != null && player.onKeyDown(keyCode); } - 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) { + @Override + public boolean onBackPressed() { if (DEBUG) { - Log.d(TAG, "pushToStack() called with: serviceId = [" - + sid + "], videoUrl = [" + videoUrl + "], title = [" + title + "]"); - } - - 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"); + Log.d(TAG, "onBackPressed() called"); } - 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); - } + // 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; } - } - @Override - public boolean onBackPressed() { - if (DEBUG) { - Log.d(TAG, "onBackPressed() called"); + // 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 { @@ -816,9 +948,16 @@ protected void doInitialLoadLogic() { } } - 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) { @@ -827,22 +966,19 @@ private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToT + "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); } @@ -856,20 +992,46 @@ public void startLoading(final boolean forceLoad) { 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); @@ -884,8 +1046,8 @@ private void initTabs() { 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) { @@ -902,7 +1064,7 @@ private void initTabs() { 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); } @@ -916,22 +1078,33 @@ private boolean shouldShowComments() { .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 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 && android.os.Build.VERSION.SDK_INT >= 16) { openNormalBackgroundPlayer(append); } else { @@ -945,58 +1118,108 @@ private void openPopupPlayer(final boolean append) { 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); } - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ + private void hideMainPlayer() { + if (playerService == null + || playerService.getView() == null + || !player.videoPlayerSelected()) { + return; + } - public void setAutoplay(final boolean autoplay) { - this.autoPlayEnabled = autoplay; + removeVideoPlayerView(); + playerService.stop(isAutoplayEnabled()); + playerService.getView().setVisibility(View.GONE); } - private void startOnExternalPlayer(@NonNull final Context context, - @NonNull final StreamInfo info, - @NonNull final Stream selectedStream) { + 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; + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + public void setAutoplay(final boolean autoplay) { + this.autoPlayEnabled = autoplay; + } + + private void startOnExternalPlayer(@NonNull final Context context, + @NonNull final StreamInfo info, + @NonNull final Stream selectedStream) { NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(), currentInfo.getSubChannelName(), selectedStream); @@ -1008,9 +1231,67 @@ private void startOnExternalPlayer(@NonNull final Context context, )); } - @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) { @@ -1022,7 +1303,7 @@ private void prepareDescription(final Description description) { if (description.getType() == Description.HTML) { disposables.add(Single.just(description.getContent()) .map((@NonNull String descriptionText) -> { - Spanned parsedDescription; + final Spanned parsedDescription; if (Build.VERSION.SDK_INT >= 24) { parsedDescription = Html.fromHtml(descriptionText, 0); } else { @@ -1051,25 +1332,47 @@ private void prepareDescription(final Description description) { } } + /** + * 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) { @@ -1094,6 +1397,45 @@ protected void showError(final String message, final boolean showRetryButton, 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 //////////////////////////////////////////////////////////////////////////*/ @@ -1108,7 +1450,6 @@ public void showLoading() { contentRootLayoutHiding.setVisibility(View.INVISIBLE); } - animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); animateView(detailDurationView, false, 100); animateView(detailPositionView, false, 100); @@ -1124,7 +1465,8 @@ public void showLoading() { if (relatedStreamsLayout != null) { if (showRelatedStreams) { - relatedStreamsLayout.setVisibility(View.INVISIBLE); + relatedStreamsLayout.setVisibility( + player != null && player.isFullscreen() ? View.GONE : View.INVISIBLE); } else { relatedStreamsLayout.setVisibility(View.GONE); } @@ -1140,24 +1482,23 @@ public void showLoading() { 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); @@ -1170,7 +1511,7 @@ public void handleResult(@NonNull final StreamInfo info) { 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); @@ -1248,15 +1589,20 @@ public void handleResult(@NonNull final StreamInfo info) { 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(), @@ -1270,7 +1616,6 @@ public void handleResult(@NonNull final StreamInfo info) { case LIVE_STREAM: case AUDIO_LIVE_STREAM: detailControlsDownload.setVisibility(View.GONE); - spinnerToolbar.setVisibility(View.GONE); break; default: if (info.getAudioStreams().isEmpty()) { @@ -1279,18 +1624,10 @@ public void handleResult(@NonNull final StreamInfo info) { 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() { @@ -1331,15 +1668,15 @@ private void displayBothUploaderAndSubChannel(final StreamInfo info) { 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()) @@ -1347,10 +1684,10 @@ public void openDownloadDialog() { .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); } } @@ -1364,11 +1701,11 @@ protected boolean onError(final Throwable exception) { 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); @@ -1384,17 +1721,28 @@ private void updateProgressInfo(@NonNull final StreamInfo info) { 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()); @@ -1405,11 +1753,7 @@ private void updateProgressInfo(@NonNull final StreamInfo info) { .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 -> { @@ -1417,8 +1761,583 @@ private void updateProgressInfo(@NonNull final StreamInfo info) { 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) { + 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; + } + + 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; + } + + 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 setupBrightness(final boolean save) { + if (activity == null) { + return; + } + + final WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); + if (save) { + // Save current brightness level + PlayerHelper.setScreenBrightness(activity, lp.screenBrightness); + + // Restore the old brightness when fragment.onPause() called. + // It means when user leaves this fragment brightness will be set to system brightness + lp.screenBrightness = -1; + } else { + // Restore already saved brightness level + final float brightnessLevel = PlayerHelper.getScreenBrightness(activity); + if (brightnessLevel <= 0.0f && brightnessLevel > 1.0f) { + 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) { + 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 9ce62a0df1a..152221c4c55 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 @@ -136,7 +136,7 @@ private int getFocusedPosition() { final RecyclerView.ViewHolder itemHolder = itemsList.findContainingViewHolder(focusedItem); return itemHolder.getAdapterPosition(); - } catch (NullPointerException e) { + } catch (final NullPointerException e) { return -1; } } @@ -169,7 +169,7 @@ private void restoreFocus(final Integer position) { } itemsList.post(() -> { - RecyclerView.ViewHolder focusedHolder = + final RecyclerView.ViewHolder focusedHolder = itemsList.findViewHolderForAdapterPosition(position); if (focusedHolder != null) { @@ -279,7 +279,7 @@ public void selected(final ChannelInfoItem selectedItem) { selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } } @@ -294,7 +294,7 @@ public void selected(final PlaylistInfoItem selectedItem) { selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } } @@ -367,7 +367,7 @@ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + "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/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 330aa7b423d..8902834e4e3 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 @@ protected void initListeners() { @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 void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { 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 @@ private Consumer> getSubscribeUpdateMonitor(final Chann 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 @@ private void updateSubscribeButton(final boolean isSubscribed) { + "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); @@ -426,10 +426,10 @@ public void onClick(final View v) { 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 void handleResult(@NonNull final ChannelInfo result) { 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 void handleResult(@NonNull final ChannelInfo result) { 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,7 +549,7 @@ private PlayQueue getPlayQueue() { 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); } @@ -581,7 +581,7 @@ protected boolean onError(final Throwable exception) { 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 c8e18f61074..ff02dd790b8 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 @@ -30,7 +30,7 @@ public class CommentsFragment extends BaseListInfoFragment { 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; } 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 4b758a9c044..1797191b64b 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 @@ -45,7 +45,7 @@ private void updateSelectedDefaultKiosk() { currentInfo = null; currentNextPage = null; - } catch (ExtractionException e) { + } 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 a9dc599518c..68097e21eea 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 static KioskFragment getInstance(final int serviceId) throws ExtractionEx 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 void setUserVisibleHint(final boolean isVisibleToUser) { 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 void onResume() { @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); } 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 e2ec9c1f3b5..38594553b13 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; } @@ -286,11 +286,9 @@ public void handleResult(@NonNull final PlaylistInfo result) { 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 void handleResult(@NonNull final PlaylistInfo result) { .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 @@ private PlayQueue getPlayQueue() { 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); } @@ -375,7 +373,7 @@ protected boolean onError(final Throwable exception) { 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 4f21565f420..44cdb816484 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 @@ -48,7 +48,7 @@ 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; @@ -161,7 +161,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); @@ -458,7 +460,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { private void restoreFilterChecked(final Menu menu, final int itemId) { if (itemId != -1) { - MenuItem item = menu.findItem(itemId); + final MenuItem item = menu.findItem(itemId); if (item == null) { return; } @@ -482,16 +484,16 @@ private void showSearchOnStart() { if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) { searchToolbarContainer.setTranslationX(100); - searchToolbarContainer.setAlpha(0f); + searchToolbarContainer.setAlpha(0.0f); searchToolbarContainer.setVisibility(View.VISIBLE); searchToolbarContainer.animate() .translationX(0) - .alpha(1f) + .alpha(1.0f) .setDuration(200) .setInterpolator(new DecelerateInterpolator()).start(); } else { searchToolbarContainer.setTranslationX(0); - searchToolbarContainer.setAlpha(1f); + searchToolbarContainer.setAlpha(1.0f); searchToolbarContainer.setVisibility(View.VISIBLE); } } @@ -505,7 +507,7 @@ private void initSearchListeners() { Log.d(TAG, "onClick() called with: v = [" + v + "]"); } if (TextUtils.isEmpty(searchEditText.getText())) { - NavigationHelper.gotoMainFragment(getFragmentManager()); + NavigationHelper.gotoMainFragment(getFM()); return; } @@ -525,7 +527,7 @@ private void initSearchListeners() { if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) { showSuggestionsPanel(); } - if (AndroidTvUtils.isTv(getContext())) { + if (DeviceUtils.isTv(getContext())) { showKeyboardSearch(); } }); @@ -578,7 +580,7 @@ public void onTextChanged(final CharSequence s, final int start, @Override public void afterTextChanged(final Editable s) { - String newText = searchEditText.getText().toString(); + final String newText = searchEditText.getText().toString(); suggestionPublisher.onNext(newText); } }; @@ -644,7 +646,7 @@ private void showKeyboardSearch() { } if (searchEditText.requestFocus()) { - InputMethodManager imm = (InputMethodManager) activity.getSystemService( + final InputMethodManager imm = (InputMethodManager) activity.getSystemService( Context.INPUT_METHOD_SERVICE); imm.showSoftInput(searchEditText, InputMethodManager.SHOW_FORCED); } @@ -658,7 +660,7 @@ private void hideKeyboardSearch() { return; } - InputMethodManager imm = (InputMethodManager) activity + final InputMethodManager imm = (InputMethodManager) activity .getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN); @@ -725,8 +727,8 @@ private void initSuggestionObserver() { .getRelatedSearches(query, 3, 25); final Observable> 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; @@ -742,15 +744,15 @@ private void initSuggestionObserver() { .suggestionsFor(serviceId, query) .toObservable() .map(strings -> { - 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); } @@ -759,7 +761,7 @@ private void initSuggestionObserver() { final Iterator 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; @@ -807,13 +809,13 @@ private void search(final String ss, final String[] cf, final String sf) { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(intent -> { - getFragmentManager().popBackStackImmediate(); + getFM().popBackStackImmediate(); activity.startActivity(intent); }, throwable -> showError(getString(R.string.url_not_supported_toast), false))); return; } - } catch (Exception ignored) { + } catch (final Exception ignored) { // Exception occurred, it's not a url } @@ -935,7 +937,7 @@ public void onSuggestionError(final Throwable exception) { return; } - int errorId = exception instanceof ParsingException + final int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, @@ -1051,7 +1053,7 @@ protected boolean onError(final Throwable exception) { infoListAdapter.clearStreamItemList(); showEmptyState(); } else { - int errorId = exception instanceof ParsingException + final int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.SEARCHED, diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java index 608ea77d281..d4bb4eebde8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java @@ -33,7 +33,7 @@ public void setItems(final List items) { this.items.addAll(items); } else { // remove history items if history is disabled - for (SuggestionItem item : items) { + for (final SuggestionItem item : items) { if (!item.fromHistory) { this.items.add(item); } @@ -123,8 +123,8 @@ private SuggestionItemHolder(final View rootView) { private 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; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java index cf2101111aa..0aef68d901c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java @@ -42,7 +42,7 @@ public class RelatedVideosFragment extends BaseListInfoFragment data) { + infoItemList.size() + ", data.size() = " + data.size()); } - int offsetStart = sizeConsideringHeaderOffset(); + final int offsetStart = sizeConsideringHeaderOffset(); infoItemList.addAll(data); if (DEBUG) { @@ -135,7 +135,7 @@ public void addInfoItemList(@Nullable final List data) { notifyItemRangeInserted(offsetStart, data.size()); if (footer != null && showFooter) { - int footerNow = sizeConsideringHeaderOffset(); + final int footerNow = sizeConsideringHeaderOffset(); notifyItemMoved(offsetStart, footerNow); if (DEBUG) { @@ -160,7 +160,7 @@ public void addInfoItem(@Nullable final InfoItem data) { + infoItemList.size() + ", thread = " + Thread.currentThread()); } - int positionInserted = sizeConsideringHeaderOffset(); + final int positionInserted = sizeConsideringHeaderOffset(); infoItemList.add(data); if (DEBUG) { @@ -172,7 +172,7 @@ public void addInfoItem(@Nullable final InfoItem data) { notifyItemInserted(positionInserted); if (footer != null && showFooter) { - int footerNow = sizeConsideringHeaderOffset(); + final int footerNow = sizeConsideringHeaderOffset(); notifyItemMoved(positionInserted, footerNow); if (DEBUG) { @@ -191,7 +191,7 @@ public void clearStreamItemList() { } public void setHeader(final View header) { - boolean changed = header != this.header; + final boolean changed = header != this.header; this.header = header; if (changed) { notifyDataSetChanged(); @@ -219,7 +219,7 @@ public void showFooter(final boolean show) { } private int sizeConsideringHeaderOffset() { - int i = infoItemList.size() + (header != null ? 1 : 0); + final int i = infoItemList.size() + (header != null ? 1 : 0); if (DEBUG) { Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i); } @@ -347,7 +347,7 @@ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position, @NonNull final List 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 28d44733737..cf1ed255b75 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 @@ protected String getDetailLine(final ChannelInfoItem item) { 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 863273a88e8..944b578f52d 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.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 void updateFromItem(final InfoItem infoItem, 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 @@ private void openCommentAuthor(final CommentsInfoItem item) { item.getServiceId(), item.getUploaderUrl(), item.getUploaderName()); - } catch (Exception e) { + } catch (final Exception e) { ErrorActivity.reportUiError((AppCompatActivity) itemBuilder.getContext(), e); } } @@ -164,7 +164,7 @@ private boolean shouldFocusLinks() { return false; } - URLSpan[] urls = itemContentView.getUrls(); + final URLSpan[] urls = itemContentView.getUrls(); return urls != null && urls.length != 0; } @@ -181,12 +181,13 @@ private void ellipsize() { 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/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index da6c9e82f17..c0096ed101a 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 void updateFromItem(final InfoItem infoItem, 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 void updateState(final InfoItem infoItem, 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/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index ad0524f92f3..5b67f51da9d 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 void addItems(@Nullable final List data) { + localItems.size() + ", data.size() = " + data.size()); } - int offsetStart = sizeConsideringHeader(); + final int offsetStart = sizeConsideringHeader(); localItems.addAll(data); if (DEBUG) { @@ -113,7 +113,7 @@ public void addItems(@Nullable final List data) { notifyItemRangeInserted(offsetStart, data.size()); if (footer != null && showFooter) { - int footerNow = sizeConsideringHeader(); + final int footerNow = sizeConsideringHeader(); notifyItemMoved(offsetStart, footerNow); if (DEBUG) { @@ -158,7 +158,7 @@ public void setUseGridVariant(final boolean useGridVariant) { } public void setHeader(final View header) { - boolean changed = header != this.header; + final boolean changed = header != this.header; this.header = header; if (changed) { notifyDataSetChanged(); @@ -316,7 +316,7 @@ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position, @NonNull final List payloads) { if (!payloads.isEmpty() && holder instanceof LocalItemHolder) { - for (Object payload : payloads) { + for (final Object payload : payloads) { if (payload instanceof StreamStateEntity) { ((LocalItemHolder) holder).updateState(localItems .get(header == null ? position : position - 1), recordManager); diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index 7e11d7a2e33..b27b1c1d1e6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -265,11 +265,11 @@ private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) { } private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { - View dialogView = View.inflate(getContext(), R.layout.dialog_bookmark, null); - EditText editText = dialogView.findViewById(R.id.playlist_name_edit_text); + final View dialogView = View.inflate(getContext(), R.layout.dialog_bookmark, null); + final EditText editText = dialogView.findViewById(R.id.playlist_name_edit_text); editText.setText(selectedItem.name); - Builder builder = new AlertDialog.Builder(activity); + final Builder builder = new AlertDialog.Builder(activity); builder.setView(dialogView) .setPositiveButton(R.string.rename_playlist, (dialog, which) -> { changeLocalPlaylistName(selectedItem.uid, editText.getText().toString()); diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java index 4eb97bbbf6f..5bb03f531dd 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java @@ -39,14 +39,14 @@ public final class PlaylistAppendDialog extends PlaylistDialog { private CompositeDisposable playlistDisposables = new CompositeDisposable(); public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) { - PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + final PlaylistAppendDialog dialog = new PlaylistAppendDialog(); dialog.setInfo(Collections.singletonList(new StreamEntity(info))); return dialog; } public static PlaylistAppendDialog fromStreamInfoItems(final List items) { - PlaylistAppendDialog dialog = new PlaylistAppendDialog(); - List entities = new ArrayList<>(items.size()); + final PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + final List entities = new ArrayList<>(items.size()); for (final StreamInfoItem item : items) { entities.add(new StreamEntity(item)); } @@ -55,8 +55,8 @@ public static PlaylistAppendDialog fromStreamInfoItems(final List items) { - PlaylistAppendDialog dialog = new PlaylistAppendDialog(); - List entities = new ArrayList<>(items.size()); + final PlaylistAppendDialog dialog = new PlaylistAppendDialog(); + final List entities = new ArrayList<>(items.size()); for (final PlayQueueItem item : items) { entities.add(new StreamEntity(item)); } diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java index b25ec7288a5..ff696664401 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java @@ -21,7 +21,7 @@ public final class PlaylistCreationDialog extends PlaylistDialog { public static PlaylistCreationDialog newInstance(final List streams) { - PlaylistCreationDialog dialog = new PlaylistCreationDialog(); + final PlaylistCreationDialog dialog = new PlaylistCreationDialog(); dialog.setInfo(streams); return dialog; } @@ -37,8 +37,8 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { return super.onCreateDialog(savedInstanceState); } - View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); - EditText nameInput = dialogView.findViewById(R.id.playlist_name); + final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); + final EditText nameInput = dialogView.findViewById(R.id.playlist_name); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()) .setTitle(R.string.create_playlist) diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 96a385ca806..1a70b6b73e1 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -88,7 +88,7 @@ public Maybe onViewed(final StreamInfo info) { final Date currentTime = new Date(); return Maybe.fromCallable(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); - StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId); + final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId); if (latestEntry != null) { streamHistoryTable.delete(latestEntry); @@ -129,7 +129,7 @@ public Flowable> getStreamStatistics() { } public Single> insertStreamHistory(final Collection entries) { - List entities = new ArrayList<>(entries.size()); + final List entities = new ArrayList<>(entries.size()); for (final StreamHistoryEntry entry : entries) { entities.add(entry.toStreamHistoryEntity()); } @@ -138,7 +138,7 @@ public Single> insertStreamHistory(final Collection deleteStreamHistory(final Collection entries) { - List entities = new ArrayList<>(entries.size()); + final List entities = new ArrayList<>(entries.size()); for (final StreamHistoryEntry entry : entries) { entities.add(entry.toStreamHistoryEntity()); } @@ -163,7 +163,7 @@ public Maybe onSearched(final int serviceId, final String search) { final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search); return Maybe.fromCallable(() -> database.runInTransaction(() -> { - SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry(); + final SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry(); if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) { latestEntry.setCreationDate(currentTime); return (long) searchHistoryTable.update(latestEntry); @@ -256,7 +256,7 @@ public Single loadStreamState(final InfoItem info) { public Single> loadStreamStateBatch(final List infos) { return Single.fromCallable(() -> { final List result = new ArrayList<>(infos.size()); - for (InfoItem info : infos) { + for (final InfoItem info : infos) { final List entities = streamTable .getStream(info.getServiceId(), info.getUrl()).blockingFirst(); if (entities.isEmpty()) { @@ -279,8 +279,8 @@ public Single> loadLocalStreamStateBatch( final List items) { return Single.fromCallable(() -> { final List result = new ArrayList<>(items.size()); - for (LocalItem item : items) { - long streamId; + for (final LocalItem item : items) { + final long streamId; if (item instanceof StreamStatisticsEntry) { streamId = ((StreamStatisticsEntry) item).getStreamId(); } else if (item instanceof PlaylistStreamEntity) { diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 582be00d9bc..887e5d12494 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -321,7 +321,7 @@ public void handleResult(@NonNull final List result) { } headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true)); headerPopupButton.setOnClickListener(view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> @@ -455,7 +455,7 @@ private PlayQueue getPlayQueue(final int index) { } final List infoItems = itemListAdapter.getItemsList(); - List streamInfoItems = new ArrayList<>(infoItems.size()); + final List streamInfoItems = new ArrayList<>(infoItems.size()); for (final LocalItem item : infoItems) { if (item instanceof StreamStatisticsEntry) { streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem()); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index ece5f099448..331149e17e6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -70,7 +70,7 @@ public void updateFromItem(final LocalItem localItem, R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - StreamStateEntity state = historyRecordManager + final StreamStateEntity state = historyRecordManager .loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); @@ -116,7 +116,7 @@ public void updateState(final LocalItem localItem, } final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; - StreamStateEntity state = historyRecordManager + final StreamStateEntity state = historyRecordManager .loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index a83c6ba6746..8eaef807a8a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -98,7 +98,7 @@ public void updateFromItem(final LocalItem localItem, R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - StreamStateEntity state = historyRecordManager + final StreamStateEntity state = historyRecordManager .loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); @@ -146,7 +146,7 @@ public void updateState(final LocalItem localItem, } final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - StreamStateEntity state = historyRecordManager + final StreamStateEntity state = historyRecordManager .loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0); diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 485d3f39143..4c0521e4445 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -98,7 +98,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment result) { setVideoCount(itemListAdapter.getItemsList().size()); headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), true)); headerPopupButton.setOnClickListener(view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> @@ -544,7 +544,7 @@ private void createRenameDialog() { } final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null); - EditText nameEdit = dialogView.findViewById(R.id.playlist_name); + final EditText nameEdit = dialogView.findViewById(R.id.playlist_name); nameEdit.setText(name); nameEdit.setSelection(nameEdit.getText().length()); @@ -601,7 +601,7 @@ private void changeThumbnailUrl(final String thumbnailUrl) { } private void updateThumbnailUrl() { - String newThumbnailUrl; + final String newThumbnailUrl; if (!itemListAdapter.getItemsList().isEmpty()) { newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0)) @@ -662,7 +662,7 @@ private void saveImmediate() { } final List items = itemListAdapter.getItemsList(); - List streamIds = new ArrayList<>(items.size()); + final List streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { if (item instanceof PlaylistStreamEntry) { streamIds.add(((PlaylistStreamEntry) item).getStreamId()); @@ -815,7 +815,7 @@ private PlayQueue getPlayQueue(final int index) { } final List infoItems = itemListAdapter.getItemsList(); - List streamInfoItems = new ArrayList<>(infoItems.size()); + final List streamInfoItems = new ArrayList<>(infoItems.size()); for (final LocalItem item : infoItems) { if (item instanceof PlaylistStreamEntry) { streamInfoItems.add(((PlaylistStreamEntry) item).toStreamInfoItem()); diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java index 21164497a8a..2dec53fae44 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java @@ -61,7 +61,7 @@ private List upsertStreams(final long playlistId, final List streams, final int indexOffset) { - List joinEntities = new ArrayList<>(streams.size()); + final List joinEntities = new ArrayList<>(streams.size()); final List streamIds = streamTable.upsertAll(streams); for (int index = 0; index < streamIds.size(); index++) { joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(index), @@ -71,7 +71,7 @@ private List upsertStreams(final long playlistId, } public Completable updateJoin(final long playlistId, final List streamIds) { - List joinEntities = new ArrayList<>(streamIds.size()); + final List joinEntities = new ArrayList<>(streamIds.size()); for (int i = 0; i < streamIds.size(); i++) { joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(i), i)); } @@ -115,7 +115,7 @@ private Maybe modifyPlaylist(final long playlistId, .firstElement() .filter(playlistEntities -> !playlistEntities.isEmpty()) .map(playlistEntities -> { - PlaylistEntity playlist = playlistEntities.get(0); + final PlaylistEntity playlist = playlistEntities.get(0); if (name != null) { playlist.setName(name); } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java index 1ae8a22a8dd..331eb5b08df 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java @@ -42,7 +42,7 @@ public Single onBookmark(final PlaylistInfo playlistInfo) { public Single onUpdate(final long playlistId, final PlaylistInfo playlistInfo) { return Single.fromCallable(() -> { - PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); + final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); playlist.setUid(playlistId); return playlistRemoteTable.update(playlist); }).subscribeOn(Schedulers.io()); 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 d812a2a576d..a2a848b4bda 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 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { 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 @@ private void setupServiceVariables() { 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 66387d29852..80036cd4a5c 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 @@ -40,7 +40,7 @@ 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.AndroidTvUtils +import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.ThemeHelper class FeedGroupDialog : DialogFragment(), BackPressable { @@ -237,7 +237,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable { } toolbar_search_edit_text.setOnClickListener { - if (AndroidTvUtils.isTv(context)) { + if (DeviceUtils.isTv(context)) { showKeyboardSearch() } } 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 e6e0816899b..8e3aad89364 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 static List readFrom( 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 static List readFrom( } } } - } catch (Throwable e) { + } catch (final Throwable e) { throw new InvalidSourceException("Couldn't parse json", e); } @@ -117,7 +117,7 @@ public static List readFrom( */ 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 static void writeTo(final List items, final JsonSink wr 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 12b64d89dd2..f7d09971210 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 int onStartCommand(final Intent intent, final int flags, final int startI 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 @@ private void startExport() { .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 06ba5510619..51afc9fdad7 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 int onStartCommand(final Intent intent, final int flags, final int startI try { inputStream = new FileInputStream(new File(filePath)); - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException e) { handleError(e); return START_NOT_STICKY; } @@ -187,7 +187,7 @@ private void startImport() { .getChannelInfo(subscriptionItem.getServiceId(), subscriptionItem.getUrl(), true) .blockingGet()); - } catch (Throwable e) { + } catch (final Throwable e) { return Notification.createOnError(e); } }) @@ -239,7 +239,7 @@ public void onComplete() { 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 @@ private Consumer> getNotificationsConsumer() { 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 943d685b104..00000000000 --- 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 9da3c3c86a1..0e5222f5e5f 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 String getTag() { @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 boolean onPlayerOptionSelected(final MenuItem item) { } 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 61c5d9e689b..e04dc868100 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -78,6 +78,7 @@ 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; @@ -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 void onReceive(final Context ctx, final Intent intent) { public void setup() { if (simpleExoPlayer == null) { - initPlayer(/*playOnInit=*/true); + initPlayer(true); } initListeners(); } @@ -250,7 +257,8 @@ public void initPlayer(final boolean playOnReady) { registerBroadcastReceiver(); } - public void initListeners() { } + public void initListeners() { + } public void handleIntent(final Intent intent) { if (DEBUG) { @@ -272,7 +280,7 @@ public void handleIntent(final Intent intent) { // 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 void handleIntent(final Intent intent) { 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,8 +369,11 @@ public void handleIntent(final Intent intent) { } } // 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() { @@ -410,6 +459,7 @@ public void destroy() { databaseUpdateReactor.clear(); progressUpdateReactor.set(null); + ImageLoader.getInstance().stop(); } /*////////////////////////////////////////////////////////////////////////// @@ -561,7 +611,8 @@ public void onPaused() { } } - public void onPausedSeek() { } + public void onPausedSeek() { + } public void onCompleted() { if (DEBUG) { @@ -1017,14 +1068,6 @@ protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { registerView(); } - @Override - public void onPlaybackShutdown() { - if (DEBUG) { - Log.d(TAG, "Shutting down..."); - } - destroy(); - } - /*////////////////////////////////////////////////////////////////////////// // General Player //////////////////////////////////////////////////////////////////////////*/ @@ -1089,6 +1132,7 @@ public void onPlay() { } simpleExoPlayer.setPlayWhenReady(true); + savePlaybackState(); } public void onPause() { @@ -1101,6 +1145,7 @@ public void onPause() { audioReactor.abandonAudioFocus(); simpleExoPlayer.setPlayWhenReady(false); + savePlaybackState(); } public void onPlayPause() { @@ -1407,7 +1452,7 @@ public boolean isLiveEdge() { return false; } - Timeline.Window timelineWindow = new Timeline.Window(); + final Timeline.Window timelineWindow = new Timeline.Window(); currentTimeline.getWindow(currentWindowIndex, timelineWindow); return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); } @@ -1418,7 +1463,7 @@ public boolean isLive() { } 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) { @@ -1433,6 +1478,10 @@ public boolean isPlaying() { return simpleExoPlayer != null && simpleExoPlayer.isPlaying(); } + public boolean isLoading() { + return simpleExoPlayer != null && simpleExoPlayer.isLoading(); + } + @Player.RepeatMode public int getRepeatMode() { return simpleExoPlayer == null @@ -1473,8 +1522,9 @@ public PlaybackParameters getPlaybackParameters() { /** * 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 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, @@ -1490,11 +1540,11 @@ public void setPlaybackParameters(final float speed, final float pitch, 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 00000000000..f62cf27b48d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -0,0 +1,483 @@ +/* + * 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 android.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); + 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); + } + + 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) { + 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 56744d858bb..00000000000 --- 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 0ccec30674a..00000000000 --- 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 efb4176a65c..00000000000 --- 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 72becef8ff1..0ffd7f5949d 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 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 boolean onCreateOptionsMenu(final Menu m) { 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 boolean onOptionsItemSelected(final MenuItem item) { 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 @@ protected void onDestroy() { 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 void onServiceConnected(final ComponentName name, final IBinder service) 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 @@ private void openPlaybackParameterDialog() { return; } PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), - player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag()); + player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag()); } @Override @@ -560,7 +580,7 @@ private void openPlaylistAppendDialog(final List playlist) { //////////////////////////////////////////////////////////////////////////// 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 @@ private void shareUrl(final String subject, final String url) { // 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 void onProgressUpdate(final int currentProgress, final int duration, } @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 @@ private void onMaybePlaybackAdapterChanged() { 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 576d42a00ee..2416403897b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -34,16 +34,16 @@ import android.os.Handler; import android.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 +69,7 @@ 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 +118,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 +135,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 +167,7 @@ public VideoPlayer(final String debugTag, final Context context) { // 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 +182,6 @@ public void setup(final View view) { 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,12 +206,10 @@ public void initViews(final View view) { 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); } @@ -282,7 +279,7 @@ public void buildQualityMenu() { 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 +311,7 @@ private void buildCaptionMenu(final List availableLanguages) { } 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 +323,7 @@ private void buildCaptionMenu(final List availableLanguages) { && !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 +339,7 @@ private void buildCaptionMenu(final List availableLanguages) { // 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); @@ -520,7 +517,6 @@ public void onCompleted() { super.onCompleted(); showControls(500); - animateView(endScreen, true, 800); animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); loadingPanel.setVisibility(View.GONE); @@ -555,7 +551,7 @@ public void onVideoSizeChanged(final int width, final int height, + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], " + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); } - aspectRatioFrameLayout.setAspectRatio(((float) width) / height); + getSurfaceView().setAspectRatio(((float) width) / height); } @Override @@ -583,7 +579,7 @@ private void onTextTrackUpdate() { .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 +616,6 @@ public void onPrepared(final boolean playWhenReady) { 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 +665,7 @@ public void onLoadingComplete(final String imageUri, final View view, } } - protected void onFullScreenButtonClicked() { + protected void toggleFullscreen() { changeState(STATE_BLOCKED); } @@ -739,8 +729,8 @@ public boolean onMenuItemClick(final MenuItem menuItem) { 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 +789,16 @@ private void onCaptionClicked() { 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 +906,9 @@ public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) 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 +921,10 @@ public void onAnimationEnd(final Animator animation) { 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 +1010,9 @@ private Runnable hideControlsAndButtonHandler(final long duration, final View vi animateView(controlsRoot, false, duration); }; } + + public abstract void hideSystemUIIfNeeded(); + /*////////////////////////////////////////////////////////////////////////// // Getters and Setters //////////////////////////////////////////////////////////////////////////*/ @@ -1033,11 +1026,7 @@ public void setPlaybackQuality(final String quality) { this.resolver.setPlaybackQuality(quality); } - public AspectRatioFrameLayout getAspectRatioFrameLayout() { - return aspectRatioFrameLayout; - } - - public SurfaceView getSurfaceView() { + public ExpandableSurfaceView getSurfaceView() { return surfaceView; } @@ -1096,7 +1085,7 @@ public TextView getPlaybackEndTime() { return playbackEndTime; } - public View getTopControlsRoot() { + public LinearLayout getTopControlsRoot() { return topControlsRoot; } @@ -1108,6 +1097,10 @@ public PopupMenu getQualityPopupMenu() { 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 00000000000..0e06c1aaf4e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -0,0 +1,2177 @@ +/* + * 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 android.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 int width = isFullscreen + ? (service.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) || !service.isLandscape(); + controlsRoot.getLayoutParams().height = isFullscreen && !isInMultiWindow() + && navBarAtTheBottom ? size.y : ViewGroup.LayoutParams.MATCH_PARENT; + controlsRoot.requestLayout(); + + final int topPadding = isFullscreen && !isInMultiWindow() ? getStatusBarHeight() : 0; + getRootView().findViewById(R.id.playbackWindowRoot).setPadding(0, topPadding, 0, 0); + getRootView().findViewById(R.id.playbackWindowRoot).requestLayout(); + } + + /** + * @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.getResources().getIdentifier( + "status_bar_height_landscape", "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 < android.os.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 00000000000..1d0b3ae264a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java @@ -0,0 +1,64 @@ +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.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); + + @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())) { + 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 00000000000..fc1f9d80ddf --- /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 0809fa0f5b1..b5520e8bee7 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 @@ 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 00000000000..5ee8485cf40 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -0,0 +1,622 @@ +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) { + 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 00000000000..f8d03087e0d --- /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 369e3236e73..f434b062120 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 void onAudioFocusChange(final int focusChange) { 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 @@ private void onAudioFocusLossCanDuck() { } 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 2ef22f2eb36..9703a35884b 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 @@ public void tryDeleteCacheFiles() { } 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 92ae009f699..e164e056343 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 @@ private LoadController(final int initialPlaybackBufferMs, 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 e101e2185a5..849593e8911 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 void setLockScreenArt(final NotificationCompat.Builder builder, .build() ); - MediaStyle mediaStyle = new MediaStyle() + final MediaStyle mediaStyle = new MediaStyle() .setMediaSession(mediaSession.getSessionToken()); builder.setStyle(mediaStyle); @@ -76,7 +76,7 @@ public void clearLockScreenArt(final NotificationCompat.Builder builder) { .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 0d511d565de..2b6560534c9 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 @@ -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 static PlaybackParameterDialog newInstance(final double playbackTempo, @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 @@ private void setupControlViews(@NonNull final View rootView) { 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 @@ private void setupTempoControl(@NonNull final View rootView) { 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); @@ -267,12 +269,12 @@ private void setupSkipSilenceControl(@NonNull final View rootView) { } 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 e63e56bf99f..09072340b1e 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 @@ -4,6 +4,7 @@ import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; +import android.provider.Settings; import android.view.accessibility.CaptioningManager; import androidx.annotation.IntDef; @@ -45,6 +46,9 @@ 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 +60,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 +76,10 @@ private 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 @@ -203,6 +216,11 @@ public static boolean isAutoQueueEnabled(@NonNull final Context context) { 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); @@ -219,6 +237,18 @@ public static int getMinimizeOnExitAction(@NonNull final Context context) { } } + @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; @@ -308,7 +338,7 @@ public static float getCaptionScale(@NonNull final Context context) { final CaptioningManager captioningManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); if (captioningManager == null || !captioningManager.isEnabled()) { - return 1f; + return 1.0f; } return captioningManager.getFontScale(); @@ -324,6 +354,13 @@ public static void setScreenBrightness(@NonNull final Context context, 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 //////////////////////////////////////////////////////////////////////////// @@ -368,7 +405,7 @@ private static boolean isAutoQueueEnabled(@NonNull final Context context, final 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(); @@ -376,8 +413,8 @@ private static void setScreenBrightness(@NonNull final Context context, 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 @@ -396,9 +433,15 @@ private static String getMinimizeOnExitAction(@NonNull final Context context, .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 1f1152b627f..764c375afae 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 @@ private void publishFloatingQueueWindow() { } // 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 0154716e078..5b20077c396 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 MediaDescriptionCompat getQueueMetadata(final int index) { } 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 e554059d9a5..d70707fdbf1 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 @@ protected Pair selectTextTrack( 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 3c15cd3422d..ec364c4df7f 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 @@ -129,7 +129,7 @@ public void dispose() { } 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/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index 7391294ba7a..dc580346202 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -51,16 +51,24 @@ public abstract class PlayQueue implements Serializable { @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 void dispose() { eventBroadcast = null; broadcastReceiver = null; reportingReactor = null; + disposed = true; } /** @@ -149,6 +158,9 @@ public synchronized void setIndex(final int index) { 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 synchronized void append(@NonNull final PlayQueueItem... items) { * @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 synchronized void remove(final int index) { 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 @@ private synchronized void removeInternal(final int removeIndex) { 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 synchronized void move(final int source, final int target) { 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 synchronized void shuffle() { 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 synchronized void unshuffle() { } 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/SinglePlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java index 79cf0601c39..527e804703c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java @@ -26,7 +26,7 @@ public SinglePlayQueue(final List items, final int index) { } private static List 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 2eb766769bc..814fda4ba98 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 MediaSource resolve(@NonNull final StreamInfo info) { 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 MediaSource resolve(@NonNull final StreamInfo info) { 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); } } 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 52761e46769..7ff3af91a32 100644 --- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java @@ -112,9 +112,9 @@ public static void reportError(final Context context, final List el, 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); @@ -154,9 +154,9 @@ public static void reportError(final Handler handler, final Context context, 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); @@ -172,7 +172,7 @@ private static String getStackTrace(final Throwable throwable) { // 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)); } @@ -186,12 +186,12 @@ protected void onCreate(final Bundle savedInstanceState) { 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); @@ -203,11 +203,11 @@ protected void onCreate(final Bundle savedInstanceState) { final Button reportGithubButton = findViewById(R.id.errorReportGitHubButton); userCommentBox = findViewById(R.id.errorCommentBox); - TextView errorView = findViewById(R.id.errorView); - TextView infoView = findViewById(R.id.errorInfosView); - TextView errorMessageView = findViewById(R.id.errorMessageView); + final TextView errorView = findViewById(R.id.errorView); + final TextView infoView = findViewById(R.id.errorInfosView); + final TextView errorMessageView = findViewById(R.id.errorMessageView); - ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); + final ActivityCommunicator ac = ActivityCommunicator.getCommunicator(); returnActivity = ac.getReturnActivity(); errorInfo = intent.getParcelableExtra(ERROR_INFO); errorList = intent.getStringArrayExtra(ERROR_LIST); @@ -242,27 +242,27 @@ protected void onCreate(final Bundle savedInstanceState) { errorView.setText(formErrorText(errorList)); // print stack trace once again for debugging: - for (String e : errorList) { + 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"); @@ -304,9 +304,9 @@ private void openPrivacyPolicyDialog(final Context context, final String action) } 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); } } @@ -334,19 +334,19 @@ static Class getReturnActivity(final Class returnActivity } 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")); @@ -383,7 +383,7 @@ private String buildJson() { .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(); } @@ -441,7 +441,7 @@ private String buildMarkdown() { } htmlErrorReport.append("
\n"); return htmlErrorReport.toString(); - } catch (Throwable e) { + } catch (final Throwable e) { Log.e(TAG, "Error while erroring: Could not build markdown"); e.printStackTrace(); return ""; @@ -478,7 +478,7 @@ private String getOsString() { 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); @@ -491,7 +491,7 @@ public void onBackPressed() { } 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 a9531693cef..ab875ed5d32 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 boolean onPreferenceChange(final Preference preference, final Object newV @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 boolean onPreferenceTreeClick(final Preference preference) { 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 125931ee113..d8b68629791 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -39,7 +39,7 @@ public void onResume() { 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 b0bb30aa70c..6ea2cc8a6c7 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -32,7 +32,6 @@ 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; @@ -91,7 +90,7 @@ public boolean onPreferenceTreeClick(final Preference preference) { } 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 boolean onPreferenceTreeClick(final Preference preference) { @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 void onCreatePreferences(final Bundle savedInstanceState, final String ro 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 void onCreatePreferences(final Bundle savedInstanceState, final String ro 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, @@ -175,12 +174,12 @@ public void onActivityResult(final int requestCode, final int resultCode, 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 @@ private void exportDatabase(final String path) { //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 @@ private void exportDatabase(final String path) { 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 @@ private void saveSharedPreferencesToFile(final File dst) { ObjectOutputStream output = null; try { output = new ObjectOutputStream(new FileOutputStream(dst)); - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext()); + final SharedPreferences pref + = PreferenceManager.getDefaultSharedPreferences(getContext()); output.writeObject(pref.getAll()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { + } catch (final IOException e) { e.printStackTrace(); } finally { try { @@ -230,7 +228,7 @@ private void saveSharedPreferencesToFile(final File dst) { output.flush(); output.close(); } - } catch (IOException ex) { + } catch (final IOException ex) { ex.printStackTrace(); } } @@ -241,14 +239,14 @@ private void importDatabase(final String filePath) { 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 @@ private void importDatabase(final String filePath) { //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 @@ private void importDatabase(final String filePath) { // restart app to properly load db System.exit(0); } - } catch (Exception e) { + } catch (final Exception e) { onError(e); } } @@ -300,13 +298,13 @@ private void loadSharedPreferences(final File src) { ObjectInputStream input = null; try { input = new ObjectInputStream(new FileInputStream(src)); - SharedPreferences.Editor prefEdit = PreferenceManager + final SharedPreferences.Editor prefEdit = PreferenceManager .getDefaultSharedPreferences(getContext()).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 @@ private void loadSharedPreferences(final File src) { } } 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 aaa572eab9b..a4b29fc4941 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 @@ private void showPathInSummary(final String prefKey, @StringRes final int defaul try { rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { + } catch (final UnsupportedEncodingException e) { // nothing to do } @@ -132,7 +132,7 @@ private boolean isFileUri(final String path) { } 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 @@ private void forgetSAFTree(final Context context, final String oldPath) { } 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 boolean onPreferenceTreeClick(final Preference preference) { + "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 boolean onPreferenceTreeClick(final Preference preference) { 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 void onActivityResult(final int requestCode, final int resultCode, final 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 void onActivityResult(final int requestCode, final int resultCode, final 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/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 47a16f6f3fe..8ce5fe4c2b9 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 @@ private static void getAudioDownloadFolder(final Context context) { 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 03e2465333e..dfa975eefaa 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -96,16 +96,16 @@ public void onViewCreated(@NonNull final View rootView, } 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 @@ private void selectInstance(final PeertubeInstance instance) { 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 @@ private void updateTitle() { } 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(); } @@ -222,12 +222,12 @@ 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 +236,13 @@ private void showAddItemDialog(final Context c) { } 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 +273,7 @@ private String cleanUrl(final String url) { 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 +331,7 @@ public boolean isItemViewSwipeEnabled() { @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 +372,7 @@ public void swapItems(final int fromPosition, final int toPosition) { @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 df529fee0b0..1bc65da7038 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -94,10 +94,10 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { @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 View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup emptyView.setVisibility(View.GONE); - SubscriptionManager subscriptionManager = new SubscriptionManager(getContext()); + final SubscriptionManager subscriptionManager = new SubscriptionManager(getContext()); subscriptionManager.subscriptions().toObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -130,7 +130,7 @@ public void onCancel(final DialogInterface dialogInterface) { private void clickedItem(final int position) { if (onSelectedListener != null) { - SubscriptionEntity entry = subscriptions.get(position); + final SubscriptionEntity entry = subscriptions.get(position); onSelectedListener .onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName()); } @@ -199,14 +199,14 @@ private class SelectChannelAdapter @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 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 13d34dec871..9d0fece4fe6 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -76,12 +76,12 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { @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); @@ -135,9 +135,9 @@ private class SelectKioskAdapter 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 int getItemCount() { } 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 index 1d5c94421a7..c858c7f7719 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -74,7 +74,7 @@ public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup inflater.inflate(R.layout.select_playlist_fragment, container, false); recyclerView = v.findViewById(R.id.items_list); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter(); + final SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter(); recyclerView.setAdapter(playlistAdapter); progressBar = v.findViewById(R.id.progressBar); 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 18cbece6fb4..d2d4c240439 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.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 @@ protected void onCreate(final Bundle savedInstanceBundle) { 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 @@ protected void onCreate(final Bundle savedInstanceBundle) { .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 boolean onCreateOptionsMenu(final Menu menu) { @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 boolean onOptionsItemSelected(final MenuItem item) { @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 2b103e79427..476cf97ab11 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 bef9a7b56ab..ce6d6dad5b8 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 void onCreate(@Nullable final Bundle savedInstanceState) { // 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 @@ private void updateSeekOptions() { 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,7 +81,7 @@ private void updateSeekOptions() { 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 } @@ -96,7 +96,7 @@ private void updateSeekOptions() { 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 1b26cd529e9..44fe987ee69 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 @@ -173,7 +173,7 @@ private void initButton(final View rootView) { return; } - Dialog.OnClickListener actionListener = (dialog, which) -> { + final Dialog.OnClickListener actionListener = (dialog, which) -> { final ChooseTabListItem selected = availableTabs[which]; addTab(selected.tabId); }; @@ -201,19 +201,19 @@ private void addTab(final int tabId) { switch (type) { case KIOSK: - SelectKioskFragment selectKioskFragment = new SelectKioskFragment(); + 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(); + 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: - SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment(); + final SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment(); selectPlaylistFragment.setOnSelectedListener( new SelectPlaylistFragment.OnSelectedListener() { @Override @@ -238,7 +238,7 @@ public void onRemotePlaylistSelected( 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: @@ -329,7 +329,7 @@ public boolean isItemViewSwipeEnabled() { @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); 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 b0511cd1156..8e440c93df0 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 @@ -65,7 +65,7 @@ public static Tab from(final int tabId) { @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; } @@ -481,7 +481,7 @@ private String getDefaultKioskId(final Context context) { 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)); 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 d18aad9d37b..057ca50f0e2 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 static List getTabsFromJson(@Nullable final String tabsJson) 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 static List getTabsFromJson(@Nullable final String tabsJson) returnTabs.add(tab); } } - } catch (JsonParserException e) { + } catch (final JsonParserException e) { throw new InvalidJsonException(e); } @@ -93,7 +93,7 @@ public static String getJsonToSave(@Nullable final List tabList) { 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 c76df704796..316e3a8357b 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 @@ -30,7 +30,7 @@ public List getTabs() { 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 dcd751e8182..b9d1026f08b 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -70,7 +70,7 @@ public int readInt() throws IOException { } public long readUnsignedInt() throws IOException { - long value = readInt(); + final long value = readInt(); return value & 0xffffffffL; } @@ -82,8 +82,9 @@ public short readShort() throws IOException { 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; } @@ -114,7 +115,7 @@ public int read(final byte[] buffer, final int off, final int c) throws IOExcept 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; @@ -169,7 +170,7 @@ public int read() throws IOException { if (viewSize < 1) { return -1; } - int res = DataReader.this.read(); + final int res = DataReader.this.read(); if (res > 0) { viewSize--; } @@ -188,7 +189,7 @@ public int read(final byte[] buffer, final int offset, final int count) 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; @@ -199,7 +200,7 @@ public long skip(final long amount) throws IOException { 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; @@ -230,8 +231,8 @@ public boolean markSupported() { 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 " 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 ff3aabd78c0..60390946b75 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java @@ -116,7 +116,7 @@ public void parse() throws IOException, NoSuchElementException { 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 Mp4Track[] getAvailableTracks() { } 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 Mp4DashChunk getNextChunk(final boolean infoOnly) throws IOException { 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); @@ -261,13 +261,13 @@ private String boxName(final Box ref) { private String boxName(final int type) { try { return new String(ByteBuffer.allocate(4).putInt(type).array(), "UTF-8"); - } catch (UnsupportedEncodingException e) { + } catch (final UnsupportedEncodingException e) { return "0x" + Integer.toHexString(type); } } 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 +280,7 @@ private Box readBox() throws IOException { } 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 +290,13 @@ private Box readBox(final int expected) throws IOException { 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 +307,7 @@ private byte[] readFullBox(final Box ref) throws IOException { } 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 +325,7 @@ private Box untilBox(final Box ref, final int... expected) throws IOException { 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 +345,7 @@ private Box untilAnyBox(final Box ref) throws IOException { } 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 +372,7 @@ private int parseMfhd() throws IOException { } 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 +397,7 @@ private Traf parseTraf(final Box ref, final int trackId) throws IOException { } 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 +426,13 @@ private Tfhd parseTfhd(final int trackId) throws IOException { } 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 +461,7 @@ private Trun parseTrun() throws IOException { 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 +480,7 @@ private Trun parseTrun() throws IOException { 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 +494,14 @@ private int[] parseFtyp(final Box ref) throws IOException { } 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 +520,9 @@ private Mvhd parseMvhd() throws IOException { } 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 +553,7 @@ private Tkhd parseTkhd() throws IOException { } 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 +576,7 @@ private Trak parseTrak(final Box ref) throws IOException { } 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 +585,8 @@ private Mdia parseMdia(final Box ref) throws IOException { 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 +608,7 @@ private Hdlr parseHdlr(final Box ref) throws IOException { // flags stream.skipBytes(4); - Hdlr obj = new Hdlr(); + final Hdlr obj = new Hdlr(); obj.bReserved = new byte[12]; obj.type = stream.readInt(); @@ -623,11 +623,11 @@ private Hdlr parseHdlr(final Box ref) throws IOException { 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 +648,7 @@ private Moov parseMoov(final Box ref) throws IOException { } 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 +664,7 @@ private Trex parseTrex() throws IOException { // 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 +675,17 @@ private Trex parseTrex() throws IOException { } 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 +707,7 @@ private Elst parseEdts(final Box ref) throws IOException { } 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 +738,7 @@ private Minf parseMinf(final Box ref) throws IOException { * @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 +796,8 @@ public class Trun { 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 +819,7 @@ public TrunEntry getEntry(final int i) { } 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 +928,7 @@ public Mp4DashSample getNextSample() throws IOException { 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 2baf8fe55ff..8f71c69342d 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 void close() throws IOException { done = true; parsed = true; - for (SharpStream src : sourceTracks) { + for (final SharpStream src : sourceTracks) { src.close(); } @@ -157,17 +157,17 @@ public void build(final SharpStream output) throws IOException { 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 void build(final SharpStream output) throws IOException { } - 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 void build(final SharpStream output) throws IOException { // 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 void build(final SharpStream output) throws IOException { 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 void build(final SharpStream output) throws IOException { 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 void build(final SharpStream output) throws IOException { 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 @@ private Mp4DashSample getNextSample(final int track) throws IOException { } } - Mp4DashSample sample = readersChunks[track].getNextSample(); + final Mp4DashSample sample = readersChunks[track].getNextSample(); if (sample == null) { readersChunks[track] = null; return getNextSample(track); @@ -434,8 +435,8 @@ private int writeEntryArray(final int offset, final int count, final int... valu 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]); @@ -466,10 +467,10 @@ private void outRestore() throws IOException { private void initChunkTables(final TablesInfo tables, final int firstCount, final int successiveCount) { // tables.stsz holds amount of samples of the track (total) - int totalSamples = (tables.stsz - firstCount); - float chunkAmount = totalSamples / (float) successiveCount; - 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; @@ -529,7 +530,7 @@ private void outSkip(final long amount) throws IOException { } 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 @@ private int lengthFor(final int offset) throws IOException { 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 @@ private int makeFtyp() throws IOException { size += 4; } - ByteBuffer buffer = ByteBuffer.allocate(size); + final ByteBuffer buffer = ByteBuffer.allocate(size); buffer.putInt(size); buffer.putInt(0x66747970); // "ftyp" @@ -631,7 +632,7 @@ private int makeFtyp() throws IOException { buffer.putInt(0x6D703432); // "mp42" compatible brand } - for (Integer brand : compatibleBrands) { + for (final Integer brand : compatibleBrands) { buffer.putInt(brand); // compatible brand } @@ -648,7 +649,7 @@ private byte[] makeMdat(final long refSize, final boolean is64) { size += 8; } - ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8) + final ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8) .putInt(is64 ? 0x01 : (int) size) .putInt(0x6D646174); // mdat @@ -689,14 +690,14 @@ private void makeMvhd(final long longestTrack) throws IOException { 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( @@ -723,7 +724,7 @@ private int makeMoov(final int[] defaultMediaTime, final TablesInfo[] tablesInfo 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 @@ -732,7 +733,7 @@ private void makeTrak(final int index, final long duration, final int defaultMed 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); @@ -757,8 +758,8 @@ private void makeTrak(final int index, final long duration, final int defaultMed 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? @@ -784,17 +785,17 @@ private void makeTrak(final int index, final long duration, final int defaultMed 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); @@ -838,7 +839,7 @@ private void makeMdia(final Mdia mdia, final TablesInfo tablesInfo, final boolea } private byte[] makeHdlr(final Hdlr hdlr) { - ByteBuffer buffer = ByteBuffer.wrap(new byte[]{ + 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, @@ -854,7 +855,7 @@ private byte[] makeHdlr(final Hdlr hdlr) { } private int makeSbgp() throws IOException { - int offset = auxOffset(); + final int offset = auxOffset(); auxWrite(new byte[] { 0x00, 0x00, 0x00, 0x1C, // box size @@ -883,7 +884,7 @@ private byte[] makeSgpd() { * 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 00a29c7ab3b..44104f133de 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -1,416 +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 - 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 - 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(final 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 (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++) { - 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++) { - int reg = (crc >>> 24) & 0xff; - crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; - } - - return crc; - } -} +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 eddb951e59e..8cb31141bef 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 void build(final SharpStream ttml) throws IOException { */ // 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 void build(final SharpStream ttml) throws IOException { 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 56cea9f2d8c..6e2ba8360bd 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 Segment getNextSegment() throws IOException { 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 @@ private long readNumber(final Element parent) throws IOException { 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 @@ private String readString(final Element parent) throws IOException { } 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 @@ private long readEncodedNumber() throws IOException { } 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 @@ private Element readElement() throws IOException { } 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 @@ private Element untilElement(final Element ref, final int... expected) throws IO 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 @@ private String elementID(final long type) { } 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 @@ private boolean readEbml(final Element ref, final int minReadVersion, 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 @@ private Info readInfo(final Element ref) throws IOException { 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) { @@ -300,11 +300,11 @@ private Segment readSegment(final Element ref, final int trackLacingExpected, } 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 @@ private WebMTrack[] readTracks(final Element ref, final int lacingExpected) thro 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 @@ private WebMTrack[] readTracks(final Element ref, final int lacingExpected) thro } 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,9 +385,9 @@ private SimpleBlock readSimpleBlock(final Element ref) throws IOException { } 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) + " without Timecode element"); @@ -443,7 +443,7 @@ public Cluster getNextCluster() throws IOException { } ensure(segment.currentCluster); - Element elem = untilElement(segment.ref, ID_CLUSTER); + final Element elem = untilElement(segment.ref, ID_CLUSTER); if (elem == null) { return null; } 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 02b22965d5b..55792d09991 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 void close() { done = true; parsed = true; - for (SharpStream src : sourceTracks) { + for (final SharpStream src : sourceTracks) { src.close(); } @@ -128,12 +128,12 @@ public void build(final SharpStream out) throws IOException, RuntimeException { 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,7 +141,7 @@ public void build(final SharpStream out) throws IOException, RuntimeException { 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[]{ @@ -177,11 +177,11 @@ public void build(final SharpStream out) throws IOException, RuntimeException { 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) { @@ -194,9 +194,9 @@ public void build(final SharpStream out) throws IOException, RuntimeException { } // 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); @@ -213,7 +213,7 @@ public void build(final SharpStream out) throws IOException, RuntimeException { blockWritten = 0; int i = 0; while (i < readers.length) { - Block bloq = getNextBlockFrom(i); + final Block bloq = getNextBlockFrom(i); if (bloq == null) { i++; continue; @@ -272,7 +272,7 @@ public void build(final SharpStream out) throws IOException, RuntimeException { makeCluster(out, -1, currentClusterOffset, false); - long segmentSize = written - offsetSegmentSizeSet - 7; + final long segmentSize = written - offsetSegmentSizeSet - 7; /* Segment size */ seekTo(out, offsetSegmentSizeSet); @@ -303,8 +303,8 @@ public void build(final SharpStream out) throws IOException, RuntimeException { 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 @@ -323,7 +323,7 @@ public void build(final SharpStream out) throws IOException, RuntimeException { /* 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); } } @@ -344,13 +344,13 @@ private Block getNextBlockFrom(final int internalTrackId) throws IOException { } } - 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; @@ -384,13 +384,13 @@ private void writeInt(final SharpStream stream, final long offset, final int num 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)); @@ -458,7 +458,7 @@ private void makeEBML(final SharpStream stream) throws IOException { } 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); @@ -470,8 +470,8 @@ private ArrayList makeTracks() { } 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}); @@ -536,7 +536,7 @@ private ArrayList makeTrackEntry(final int internalTrackId, final WebMTr 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}); @@ -552,7 +552,7 @@ private int makeCuePoint(final int internalTrackId, final KeyFrame keyFrame, 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; } @@ -562,7 +562,7 @@ private int makeCuePoint(final int internalTrackId, final KeyFrame keyFrame, 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}); @@ -598,7 +598,7 @@ private void makeEbmlVoid(final SharpStream out, final int amount, final boolean 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; } @@ -617,7 +617,7 @@ private void dump(final byte[] buffer, final int count, final SharpStream stream 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; } @@ -649,9 +649,9 @@ private byte[] encode(final long number, final boolean withLength) { 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) { @@ -670,10 +670,9 @@ private byte[] encode(final long number, final boolean withLength) { } 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); @@ -700,7 +699,7 @@ private int selectTrackForCue() { } } - 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 4fa14ed014a..9220891a98a 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 static void animateView(final View view, final Type animationType, 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,7 +158,7 @@ public static void animateBackgroundColor(final View view, final long duration, } 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); @@ -201,7 +201,7 @@ public static void animateTextColor(final TextView view, final long duration, + "colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"); } - ValueAnimator viewPropertyAnimator = ValueAnimator + final ValueAnimator viewPropertyAnimator = ValueAnimator .ofObject(new ArgbEvaluator(), colorStart, colorEnd); viewPropertyAnimator.setInterpolator(new FastOutSlowInInterpolator()); viewPropertyAnimator.setDuration(duration); @@ -233,7 +233,7 @@ public static ValueAnimator animateHeight(final View view, final long duration, + "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 -> { @@ -462,7 +462,7 @@ public void onAnimationEnd(final Animator animation) { 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 5b1c4637214..b6f1eaf491b 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 static Bitmap centerCrop(final Bitmap inputBitmap, final int newWidth, 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 static Bitmap centerCrop(final Bitmap inputBitmap, final int newWidth, 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 770592537c1..cf347e7c48d 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 boolean onTouch(final View v, final MotionEvent event) { 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 boolean onTouch(final View v, final MotionEvent event) { 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 boolean onTouch(final View v, final MotionEvent event) { 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 @@ private boolean handleUrl(final Context context, final URLSpan urlSpan) { 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 d8b81b4cef8..d970ec472e9 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 @@ private 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 74% 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 db2ab4aa709..d852c2296fe 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AndroidTvUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -8,25 +8,26 @@ import android.os.Build; 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 +37,8 @@ public static boolean isTv(final Context context) { // 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 +50,15 @@ public static boolean isTv(final Context context) { 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) { 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 9b8b2494e67..a1a73d7acbf 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -101,7 +101,7 @@ public static Single getMoreSearchItems(final int serviceId, 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) @@ -212,10 +212,10 @@ private static Single checkCache(final boolean forceLoad, 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; @@ -243,7 +243,7 @@ private static Maybe loadFromCache(final int serviceId, fina 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); } @@ -283,7 +283,7 @@ public static void handleGeneralException(final Context context, final int servi 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)) { @@ -293,7 +293,7 @@ public static void handleGeneralException(final Context context, final int servi } 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 3179662bab1..dda01b60cee 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java @@ -22,7 +22,7 @@ private 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 static String createFilename(final Context context, final String title) { 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 035416dcdbc..a07f05828fe 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 @@ private static String keyOf(final int serviceId, @NonNull final String url, } 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 2ca1284099e..fd50d2edbd8 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 LayoutManagerSmoothScroller(final Context context, final int orientation, @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 189b6823e93..be0630cace8 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -44,7 +44,7 @@ private 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 static int getResolutionIndex(final Context context, */ 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 static int getPopupResolutionIndex(final Context context, 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 static List getSortedStreamVideosList(final Context context, 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 static List getSortedStreamVideosList(final Context context, 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 @@ static int getDefaultResolutionIndex(final String defaultResolution, return 0; } - int defaultStreamIndex + final int defaultStreamIndex = getVideoStreamIndex(defaultResolution, defaultFormat, videoStreams); // this is actually an error, @@ -200,11 +202,11 @@ static List getSortedStreamVideosList(final MediaFormat defaultForm 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 @@ static List getSortedStreamVideosList(final MediaFormat defaultForm } } 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 @@ static List getSortedStreamVideosList(final MediaFormat defaultForm } // 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 @@ static List getSortedStreamVideosList(final MediaFormat defaultForm 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 @@ static int getHighestQualityAudioIndex(@Nullable MediaFormat format, 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 @@ static int getMostCompactAudioIndex(@Nullable MediaFormat format, 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 @@ static int getVideoStreamIndex(final String targetResolution, final MediaFormat 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 @@ static int getVideoStreamIndex(final String targetResolution, final MediaFormat 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 @@ private static int getDefaultResolutionWithDefaultFormat(final Context context, 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 @@ private static int compareAudioStreamBitrate(final AudioStream streamA, } 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 @@ private static int compareVideoStreamResolution(final VideoStream streamA, return 1; } - int resComp = compareVideoStreamResolution(streamA.getResolution(), + final int resComp = compareVideoStreamResolution(streamA.getResolution(), streamB.getResolution()); if (resComp != 0) { return resComp; @@ -521,9 +525,10 @@ private static boolean isLimitingDataUsage(final Context context) { 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 @@ private static String getResolutionLimit(final Context context) { * @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 7e336f02d5d..3642216ff95 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -110,19 +110,19 @@ public static ContentCountry getPreferredContentCountry(final Context context) { } 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 static String localizeNumber(final Context context, final long number) { } 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 static String localizeWatchingCount(final Context context, final long wat } 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 @@ private static String getQuantity(final Context context, @PluralsRes final int p // 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 @@ private static PrettyTime getPrettyTime() { } 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 ccaa79f9888..96dfc1925df 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -13,6 +13,7 @@ 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.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 static Intent getPlayerIntent(@NonNull final Context context, @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 static Intent getPlayerIntent(@NonNull final Context context, 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 static Intent getPlayerEnqueueIntent(@NonNull final Context context, 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 static Intent getPlayerIntent(@NonNull final Context context, .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 static void playOnPopupPlayer(final Context context, final PlayQueue queu } 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 static void enqueueOnPopupPlayer(final Context context, final PlayQueue q } 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 static void enqueueOnBackgroundPlayer(final Context context, final PlayQu 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 static void playOnExternalAudioPlayer(final Context context, final Stream 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 static void resolveActivityOrAskToInstall(final Context context, final In 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 @@ private static FragmentTransaction defaultTransaction(final FragmentManager frag 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 static void openSearchFragment(final FragmentManager fragmentManager, 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 static void openSubscriptionsImportFragment(final FragmentManager fragmen 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 static void openChannel(final Context context, final int serviceId, final 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 static void openVideoDetail(final Context context, final int serviceId, 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 static void openVideoDetail(final Context context, final int serviceId, } 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 static boolean openDownloads(final Activity activity) { 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 static Intent getBackgroundPlayerActivityIntent(final Context context) { 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 @@ private static Intent getServicePlayerActivityIntent(final Context context, 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 static Intent getIntentByLink(final Context context, final String url) 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 @@ private static void installApp(final Context context, final String packageName) 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 static void installKore(final Context context) { * @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 e89cbf5db7a..5481b519ce7 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java @@ -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 static List getInstanceList(final Context context) { 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 9ba6ed36c61..ba157e6bea8 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -101,12 +101,12 @@ public static boolean checkWriteStoragePermissions(final Activity activity, @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 +120,9 @@ public static boolean isPopupEnabled(final Context context) { } 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 fcd392d67d6..2526956c55a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java +++ b/app/src/main/java/org/schabi/newpipe/util/RelatedStreamInfo.java @@ -16,11 +16,11 @@ public RelatedStreamInfo(final int serviceId, final ListLinkHandler listUrlIdHan } 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<>(); + final List streams = new ArrayList<>(); streams.addAll(info.getRelatedStreams()); relatedStreamInfo.setRelatedItems(streams); return relatedStreamInfo; 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 081d981a1d0..8c697d32730 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 static AudioStream getAudioStreamFor(@NonNull final List aud 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 static AudioStream getAudioStreamFor(@NonNull final List aud // 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 dacf7d8442c..1ab54343a74 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -117,7 +117,7 @@ public static int getSelectedServiceId(final Context context) { 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 static void setSelectedServiceId(final Context context, final int service 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 static void setSelectedServiceId(final Context context, final int service } 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 static boolean isBeta(final StreamingService s) { 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 0ec2d571db9..1283f67f519 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 static void openUrlInBrowser(final Context context, final String url) { // 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 static void openUrlInBrowser(final Context context, final String url) { 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 @@ private static void openInDefaultApp(final Context context, final String url) { * @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 @@ private static String getDefaultBrowserPackageName(final Context context) { * @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 2a1dff5c962..cfe2b32721c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StateSaver.java +++ b/app/src/main/java/org/schabi/newpipe/util/StateSaver.java @@ -66,7 +66,7 @@ private 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 +86,7 @@ public static SavedState tryToRestore(final Bundle outState, final WriteRead wri return null; } - SavedState savedState = outState.getParcelable(KEY_SAVED_STATE); + final SavedState savedState = outState.getParcelable(KEY_SAVED_STATE); if (savedState == null) { return null; } @@ -122,7 +122,7 @@ private static SavedState tryToRestore(@NonNull final SavedState savedState, 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 +131,7 @@ private static SavedState tryToRestore(@NonNull final SavedState savedState, } 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 +139,13 @@ private static SavedState tryToRestore(@NonNull final SavedState savedState, } 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 +165,7 @@ public static SavedState tryToSave(final boolean isChangingConfig, @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 +215,7 @@ private static SavedState tryToSave(final boolean isChangingConfig, final String + "writeRead = [" + writeRead + "]"); } - LinkedList savedObjects = new LinkedList<>(); + final LinkedList savedObjects = new LinkedList<>(); writeRead.writeTo(savedObjects); if (isChangingConfig) { @@ -247,36 +247,36 @@ private static SavedState tryToSave(final boolean isChangingConfig, final String } } - 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() { + final File[] files = cacheDir.listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return name.contains(prefixFileName); } }); - for (File fileToDelete : files) { + 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 +298,7 @@ public static void onDestroy(final SavedState savedState) { try { //noinspection ResultOfMethodCallIgnored new File(savedState.getPathFileSaved()).delete(); - } catch (Exception ignored) { + } catch (final Exception ignored) { } } } @@ -319,7 +319,7 @@ public static void clearStateFiles() { 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 92aee8ba704..a1e2e6eb9d2 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 static void setEnabledEntries(final StreamDialogEntry... entries) { } 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 6a244a69b67..2bfd27c4736 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 @@ private View getCustomView(final int position, final View view, final ViewGroup 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 @@ private View getCustomView(final int position, final View view, final ViewGroup } } } 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 @@ private View getCustomView(final int position, final View view, final ViewGroup } 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 static Single fetchSizeForWrapper( 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 105af5086d6..a83e844eade 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 TLSSocketFactoryCompat() throws KeyManagementException, NoSuchAlgorithmEx 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(); } 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 74ea34fcc8e..88e1a31cda1 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -126,11 +126,11 @@ public static int getMinWidthDialogTheme(final Context context) { */ @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 static int getThemeForService(final Context context, final int serviceId) final StreamingService service; try { service = NewPipe.getService(serviceId); - } catch (ExtractionException ignored) { + } catch (final ExtractionException ignored) { return defaultTheme; } @@ -162,7 +162,7 @@ public static int getThemeForService(final Context context, final int serviceId) } themeName += "." + service.getServiceInfo().getName(); - int resourceId = context + final int resourceId = context .getResources() .getIdentifier(themeName, "style", context.getPackageName()); @@ -175,11 +175,11 @@ public static int getThemeForService(final Context context, final int serviceId) @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 static int getSettingsThemeStyle(final Context context) { * @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 static int resolveColorFromAttr(final Context context, @AttrRes final int } 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 31f5fd22250..7ed90d6934d 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 @@ private 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 static void addFileToZip(final ZipOutputStream outZip, final String file, 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 static boolean extractFileFromZip(final String filePath, final String fil 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 0fbf6a254dc..b1fabe71529 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 @@ private static class ProgressBarAnimation extends Animation { @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 028e9b674ed..b34a6be637c 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 void setCurrentState(@ViewMode final int currentState) { } 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 00000000000..798712b6b18 --- /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 0da42fab62e..5005626682c 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 @@ protected boolean onRequestFocusInDescendants(final int direction, 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 void addFocusables(final ArrayList views, final int direction, 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 6dbcded485e..a50d5a64ca8 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 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 void setOnSeekBarChangeListener(final OnSeekBarChangeListener l) { @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 bd5ae10e8e0..4a342784e21 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.Window; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.view.WindowCallbackWrapper; @@ -86,16 +87,18 @@ public void onGlobalFocusChanged(final View oldFocus, final View newFocus) { } private void updateRect() { - View focusedView = focused == null ? null : this.focused.get(); + final View focusedView = focused == null ? null : this.focused.get(); - int l = focusRect.left; - int r = focusRect.right; - int t = focusRect.top; - int b = focusRect.bottom; + 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); - } else { + } + + if (shouldClearFocusRect(focusedView, focusRect)) { focusRect.setEmpty(); } @@ -170,41 +173,51 @@ public void setAlpha(final int alpha) { public void setColorFilter(final ColorFilter colorFilter) { } + /* + * 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()); + } + public static void setupFocusObserver(final Dialog dialog) { - Rect displayRect = new Rect(); + final Rect displayRect = new Rect(); - Window window = dialog.getWindow(); + 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); @@ -222,7 +235,7 @@ private static void setupOverlay(final Window window, final FocusOverlayView ove 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; } @@ -267,10 +280,10 @@ private static void clearFocusObstacles(final ViewGroup viewGroup) { 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 3a3384b5163..36e0eabb05b 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 @@ private boolean doHandleMovement(final TextView widget, 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 @@ private boolean doHandleMovement(final TextView widget, this.direction = 0; - ViewGroup root = findScrollableParent(widget); + final ViewGroup root = findScrollableParent(widget); widget.getHitRect(visibleRect); @@ -118,46 +118,46 @@ protected boolean down(final TextView widget, final Spannable buffer) { } 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]); + final int start = buffer.getSpanStart(candidates[i]); + final int end = buffer.getSpanEnd(candidates[i]); if ((end < selEnd || selStart == selEnd) && start >= visibleStart) { if (end > bestEnd) { @@ -173,7 +173,7 @@ private boolean gotoPrev(final TextView view, final Spannable buffer) { } } - float fourLines = view.getTextSize() * 4; + final float fourLines = view.getTextSize() * 4; visibleRect.left = 0; visibleRect.right = view.getWidth(); @@ -184,49 +184,49 @@ private boolean gotoPrev(final TextView view, final Spannable buffer) { } 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]); + final int start = buffer.getSpanStart(candidates[i]); + final int end = buffer.getSpanEnd(candidates[i]); if ((start > selStart || selStart == selEnd) && end <= visibleEnd) { if (start < bestStart) { @@ -245,7 +245,7 @@ private boolean gotoNext(final TextView view, final Spannable buffer) { // 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 655b8681899..6350f80a5ae 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 boolean dispatchUnhandledMove(final View focused, final int direction) { // 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; @@ -148,16 +148,16 @@ private boolean tryFocusFinder(final int direction) { 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 @@ private boolean tryFocusFinder(final int direction) { 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 48e8ef81cfd..fb21a80832e 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 @@ private void remeasureTabs() { 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 6c4d2060328..62465d2a4f7 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 boolean requestChildRectangleOnScreen(@NonNull final RecyclerView parent, @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 View onInterceptFocusSearch(@NonNull final View focused, final int direct // 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 View onInterceptFocusSearch(@NonNull final View focused, final int direct : View.FOCUSABLES_ALL); try { - for (View view : focusables) { + for (final View view : focusables) { if (view == focused || view == recycler) { continue; } @@ -111,7 +111,7 @@ public View onInterceptFocusSearch(@NonNull final View focused, final int direct continue; } - int candidate = getDistance(sourcePosition, view, listDirection); + final int candidate = getDistance(sourcePosition, view, listDirection); if (candidate < 0) { continue; } @@ -162,12 +162,12 @@ private int getAbsoluteDirection(final int direction) { } 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/DownloadMissionRecover.java b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java index 14ac392a0e9..ab158ec5127 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 @@ private void resolveStream() throws IOException, ExtractionException, HttpError 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/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index e93e83a87a4..680f484e629 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 static Postprocessing getAlgorithm(@NonNull String algorithmName, String[ } 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"); } 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 c2d3a9b9e83..d490bcb0f46 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 @@ -210,7 +210,7 @@ public void onBindViewHolder(@NonNull ViewHolder view, @SuppressLint("RecyclerVi } 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 +243,7 @@ private void updateProgress(ViewHolderItem h) { double progress; if (mission.unknownLength) { progress = Double.NaN; - h.progress.setProgress(0f); + h.progress.setProgress(0.0f); } else { progress = done / length; } @@ -310,7 +310,7 @@ private void updateProgress(ViewHolderItem h) { 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 3f638d4182a..bec947540d2 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 void onBoundsChange(Rect rect) { } 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/res/drawable/ic_next_white_24dp.xml b/app/src/main/res/drawable/ic_next_white_24dp.xml new file mode 100644 index 00000000000..603880c2b40 --- /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 00000000000..14279ecb20d --- /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 84a29e0c831..a7872a83a82 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 cb2b9ccfe67..f69832b810e 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 16dcff63966..46edda8b78e 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"/>