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 b731d0270e3..328f77f2b43 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 @@ -16,8 +16,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; -import androidx.core.text.HtmlCompat; -import androidx.preference.PreferenceManager; import android.provider.Settings; import android.text.Spanned; import android.text.TextUtils; @@ -46,7 +44,9 @@ import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.ContextCompat; +import androidx.core.text.HtmlCompat; import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; import androidx.viewpager.widget.ViewPager; import com.google.android.exoplayer2.ExoPlaybackException; @@ -1998,7 +1998,7 @@ private void hideSystemUi() { // Prevent jumping of the player on devices with cutout if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { activity.getWindow().getAttributes().layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; } final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java index a5758301c6a..6004aabc6c0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -32,12 +32,12 @@ import android.net.Uri; import android.os.Build; import android.os.Handler; -import androidx.preference.PreferenceManager; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.Display; +import android.view.DisplayCutout; import android.view.GestureDetector; import android.view.Gravity; import android.view.KeyEvent; @@ -56,12 +56,15 @@ 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.preference.PreferenceManager; 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; @@ -71,6 +74,7 @@ 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; @@ -91,9 +95,9 @@ 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.DeviceUtils; import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -203,6 +207,11 @@ public class VideoPlayerImpl extends VideoPlayer private int cachedDuration; private String cachedDurationString; + private boolean gotCutout; + private int insetLeft; + private int insetRight; + private int insetBottom; + // Popup private WindowManager.LayoutParams popupLayoutParams; public WindowManager windowManager; @@ -1490,13 +1499,13 @@ public void hideSystemUIIfNeeded() { * 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. + final Point size = new Point(); display.getSize(size); final boolean isLandscape = service.isLandscape(); @@ -1508,7 +1517,84 @@ public void setControlsSize() { ? Gravity.START : Gravity.END) : Gravity.TOP; - getTopControlsRoot().getLayoutParams().width = width; + final int navBarHeight = DeviceUtils.hasNavBar(windowManager) ? getNavBarHeight() : 0; + final int statusBarHeight = getStatusBarHeight(); + final int controlsPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_controls_padding); + final int playerTopPadding = service.getResources() + .getDimensionPixelSize(R.dimen.player_main_top_padding); + + // Handle rotation + final int ctrlPadLeft; + final int ctrlPadRight; + final int ctrlPadTop = playerTopPadding + statusBarHeight; + final int ctrlPadBottom = navBarHeight + insetBottom; + final int bottom = ctrlPadBottom + (navBarHeight == 0 ? controlsPadding : playerTopPadding); + final int rotation = windowManager.getDefaultDisplay().getRotation(); + + if (DeviceUtils.isTv(service)) { + gotCutout = false; + ctrlPadLeft = controlsPadding; + ctrlPadRight = controlsPadding; + } else if (DeviceUtils.isTablet(service)) { + if (gotCutout) { + ctrlPadLeft = controlsPadding + insetLeft; + ctrlPadRight = controlsPadding + insetRight; + } else { + ctrlPadLeft = controlsPadding; + ctrlPadRight = controlsPadding; + } + } else { + if (rotation == Surface.ROTATION_90) { + if (gotCutout) { + ctrlPadLeft = controlsPadding + insetLeft; + ctrlPadRight = controlsPadding + + (getNavBarMode() == 2 ? 0 : navBarHeight) + insetRight; + } else { + ctrlPadLeft = controlsPadding; + ctrlPadRight = controlsPadding; + } + } else { + if (gotCutout) { + ctrlPadLeft = controlsPadding + + (getNavBarMode() == 2 ? 0 : navBarHeight) + insetLeft; + ctrlPadRight = controlsPadding + insetRight; + } else { + ctrlPadLeft = controlsPadding; + ctrlPadRight = controlsPadding; + } + } + } + + if (gotCutout) { + // We need the real screen size to position the controls correctly + getRealScreenSize(display, size); + getTopControlsRoot().getLayoutParams().width = size.x; + getBottomControlsRoot().getLayoutParams().width = size.x; + } else { + getBottomControlsRoot().getLayoutParams().width = width; + getTopControlsRoot().getLayoutParams().width = width; + } + + if (isLandscape && isFullscreen && !isInMultiWindow()) { + getTopControlsRoot().setPaddingRelative(ctrlPadLeft, ctrlPadTop, ctrlPadRight, + 0); + getBottomControlsRoot().setPaddingRelative(ctrlPadLeft, 0, ctrlPadRight, + getNavBarMode() == 2 ? bottom : (DeviceUtils.isTablet(service) ? ctrlPadBottom + : 0)); + } else if (!isLandscape && isFullscreen && !isInMultiWindow()) { + getTopControlsRoot().setPaddingRelative(controlsPadding, ctrlPadTop, controlsPadding, + 0); + getBottomControlsRoot().setPaddingRelative(controlsPadding, 0, controlsPadding, + getNavBarMode() == 2 ? bottom : ctrlPadBottom); + } else { + final int topPad = isFullscreen && !isInMultiWindow() ? statusBarHeight : 0; + getTopControlsRoot().setPaddingRelative(controlsPadding, playerTopPadding + topPad, + controlsPadding, 0); + getBottomControlsRoot().setPaddingRelative(controlsPadding, 0, + controlsPadding, 0); + } + final RelativeLayout.LayoutParams topParams = ((RelativeLayout.LayoutParams) getTopControlsRoot().getLayoutParams()); topParams.removeRule(RelativeLayout.ALIGN_PARENT_START); @@ -1518,7 +1604,6 @@ public void setControlsSize() { : RelativeLayout.ALIGN_PARENT_START); getTopControlsRoot().requestLayout(); - getBottomControlsRoot().getLayoutParams().width = width; final RelativeLayout.LayoutParams bottomParams = ((RelativeLayout.LayoutParams) getBottomControlsRoot().getLayoutParams()); bottomParams.removeRule(RelativeLayout.ALIGN_PARENT_START); @@ -1527,22 +1612,42 @@ public void setControlsSize() { ? RelativeLayout.ALIGN_PARENT_END : RelativeLayout.ALIGN_PARENT_START); getBottomControlsRoot().requestLayout(); + } - final ViewGroup controlsRoot = getRootView().findViewById(R.id.playbackWindowRoot); - // In tablet navigationBar located at the bottom of the screen. - // And the situations when we need to set custom height is - // in fullscreen mode in tablet in non-multiWindow mode or with vertical video. - // Other than that MATCH_PARENT is good - final boolean navBarAtTheBottom = DeviceUtils.isTablet(service) || !isLandscape; - controlsRoot.getLayoutParams().height = isFullscreen && !isInMultiWindow() - && navBarAtTheBottom ? size.y : ViewGroup.LayoutParams.MATCH_PARENT; - controlsRoot.requestLayout(); + @SuppressWarnings({"ConstantConditions", "JavaReflectionMemberAccess"}) + @SuppressLint("ObsoleteSdkInt") + private void getRealScreenSize(final Display display, final Point size) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + display.getRealSize(size); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + try { + size.x = (Integer) Display.class.getMethod("getRawWidth").invoke(display); + size.y = (Integer) Display.class.getMethod("getRawHeight").invoke(display); + } catch (final Exception e) { + // This should never happen + Log.e(TAG, "getRealScreenSize() failed", e); + } + } + } + + private int getNavBarMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + final int resourceId = service.getResources().getIdentifier( + "config_navBarInteractionMode", "integer", "android"); + if (resourceId > 0) { + return service.getResources().getInteger(resourceId); + } + } + return 0; + } - final DisplayMetrics metrics = getRootView().getResources().getDisplayMetrics(); - int topPadding = isFullscreen && !isInMultiWindow() ? getStatusBarHeight() : 0; - topPadding = !isLandscape && DeviceUtils.hasCutout(topPadding, metrics) ? 0 : topPadding; - getRootView().findViewById(R.id.playbackWindowRoot).setTranslationY(topPadding); - getBottomControlsRoot().setTranslationY(-topPadding); + private int getNavBarHeight() { + final int resourceId = service.getResources().getIdentifier( + "navigation_bar_height", "dimen", "android"); + if (resourceId > 0) { + return service.getResources().getDimensionPixelSize(resourceId); + } + return 0; } /** @@ -2010,6 +2115,21 @@ private boolean popupHasParent() { public void setFragmentListener(final PlayerServiceEventListener listener) { fragmentListener = listener; fragmentIsVisible = true; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + getRootView().setOnApplyWindowInsetsListener((view, windowInsets) -> { + final DisplayCutout cutout = windowInsets.getDisplayCutout(); + if (cutout != null) { + gotCutout = true; + insetLeft = cutout.getSafeInsetLeft(); + insetRight = cutout.getSafeInsetRight(); + insetBottom = cutout.getSafeInsetBottom(); + setControlsSize(); + } + return windowInsets; + }); + } + updateMetadata(); updatePlayback(); triggerProgressUpdate(); diff --git a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java index 7592d2f356a..3b5fb15ea3c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -1,18 +1,25 @@ package org.schabi.newpipe.util; +import android.annotation.SuppressLint; import android.app.UiModeManager; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.BatteryManager; import android.os.Build; -import android.util.DisplayMetrics; -import android.util.TypedValue; +import android.os.IBinder; +import android.util.Log; +import android.view.Display; +import android.view.KeyCharacterMap; import android.view.KeyEvent; +import android.view.WindowManager; import androidx.annotation.NonNull; + import org.schabi.newpipe.App; +import java.lang.reflect.Method; + import static android.content.Context.BATTERY_SERVICE; import static android.content.Context.UI_MODE_SERVICE; @@ -75,16 +82,35 @@ public static boolean isConfirmKey(final int keyCode) { } } - /* - * Compares current status bar height with default status bar height in Android and decides, - * does the device has cutout or not - * */ - public static boolean hasCutout(final float statusBarHeight, final DisplayMetrics metrics) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - final float defaultStatusBarHeight = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 25, metrics); - return statusBarHeight > defaultStatusBarHeight; + + // This method works on real devices and emulators + @SuppressWarnings("ConstantConditions") + @SuppressLint({"PrivateApi", "ObsoleteSdkInt"}) + public static boolean hasNavBar(final WindowManager wm) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + try { + final Class serviceManager = Class.forName("android.os.ServiceManager"); + final IBinder serviceBinder = (IBinder) serviceManager + .getMethod("getService", String.class) + .invoke(serviceManager, "window"); + final Class stub = Class.forName("android.view.IWindowManager$Stub"); + final Object windowManager = stub.getMethod("asInterface", IBinder.class) + .invoke(stub, serviceBinder); + Method hasNavigationBar; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + hasNavigationBar = windowManager.getClass() + .getMethod("hasNavigationBar"); + return (boolean) hasNavigationBar.invoke(windowManager); + } + hasNavigationBar = windowManager.getClass() + .getMethod("hasNavigationBar", int.class); + final Display dsp = wm.getDefaultDisplay(); + return (boolean) hasNavigationBar.invoke(windowManager, dsp.getDisplayId()); + } catch (final Exception e) { + Log.e(".DeviceUtils", "hasNavBar() failed", e); + } } - return false; + return !(KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK) + && KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_HOME)); } }