From 4b8439116e7cb273ebfa4bcc651e05c743e2ed02 Mon Sep 17 00:00:00 2001 From: Oleg Blinnikov Date: Thu, 5 Jan 2023 19:30:17 +0000 Subject: [PATCH] Show letterbox rounded corners behind navbar Use height of the expanded taskbar to determine if the rounded corners must appear behind or above the taskbar. Use mGlobalScale instead of CompatScale for scaling the letterbox, which fixes rounded corners in case per-app scaling override is enabled. Change-Id: I133a6bfc79cdb90f9a04f9c52286cf205dae1fb9 Merged-In: I133a6bfc79cdb90f9a04f9c52286cf205dae1fb9 Test: atest WmTests:LetterboxUiControllerTest WmTests:SizeCompatTests Bug: 262360063 --- .../server/wm/LetterboxUiController.java | 172 ++++++++--------- .../server/wm/LetterboxUiControllerTest.java | 180 ++++++++++++++++++ .../android/server/wm/SizeCompatTests.java | 2 +- 3 files changed, 264 insertions(+), 90 deletions(-) diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 9c43c1d62ab8f..a79272126efbf 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -105,6 +105,12 @@ final class LetterboxUiController { private final ActivityRecord mActivityRecord; + /** + * Taskbar expanded height. Used to determine when to crop an app window to display the + * rounded corners above the expanded taskbar. + */ + private final float mExpandedTaskBarHeight; + /* * WindowContainerListener responsible to make translucent activities inherit * constraints from the first opaque activity beneath them. It's null for not @@ -184,6 +190,9 @@ final class LetterboxUiController { () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( /* checkDeviceConfig */ true), PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE); + + mExpandedTaskBarHeight = + getResources().getDimensionPixelSize(R.dimen.taskbar_frame_height); } /** @@ -422,7 +431,7 @@ final class LetterboxUiController { if (w == null) { return; } - adjustBoundsForTaskbar(w, outBounds); + adjustBoundsIfNeeded(w, outBounds); } else { outBounds.setEmpty(); } @@ -465,13 +474,13 @@ final class LetterboxUiController { if (w == null || winHint != null && w != winHint) { return; } - updateRoundedCorners(w); + updateRoundedCornersIfNeeded(w); // If there is another main window that is not an application-starting window, we should // update rounded corners for it as well, to avoid flickering rounded corners. final WindowState nonStartingAppW = mActivityRecord.findMainWindow( /* includeStartingApp= */ false); if (nonStartingAppW != null && nonStartingAppW != w) { - updateRoundedCorners(nonStartingAppW); + updateRoundedCornersIfNeeded(nonStartingAppW); } updateWallpaperForLetterbox(w); @@ -755,8 +764,8 @@ final class LetterboxUiController { return isSurfaceReadyAndVisible(mainWindow) && mainWindow.areAppWindowBoundsLetterboxed() // Check for FLAG_SHOW_WALLPAPER explicitly instead of using // WindowContainer#showWallpaper because the later will return true when this - // activity is using blurred wallpaper for letterbox backgroud. - && (mainWindow.mAttrs.flags & FLAG_SHOW_WALLPAPER) == 0; + // activity is using blurred wallpaper for letterbox background. + && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0; } @VisibleForTesting @@ -808,106 +817,107 @@ final class LetterboxUiController { return mLetterboxConfiguration.getLetterboxBackgroundColor(); } - private void updateRoundedCorners(WindowState mainWindow) { + private void updateRoundedCornersIfNeeded(final WindowState mainWindow) { final SurfaceControl windowSurface = mainWindow.getSurfaceControl(); - if (windowSurface != null && windowSurface.isValid()) { - final Transaction transaction = mActivityRecord.getSyncTransaction(); - - if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) { - // We don't want corner radius on the window. - // In the case the ActivityRecord requires a letterboxed animation we never want - // rounded corners on the window because rounded corners are applied at the - // animation-bounds surface level and rounded corners on the window would interfere - // with that leading to unexpected rounded corner positioning during the animation. - transaction - .setWindowCrop(windowSurface, null) - .setCornerRadius(windowSurface, 0); - return; - } - - Rect cropBounds = null; - - if (hasVisibleTaskbar(mainWindow)) { - cropBounds = new Rect(mActivityRecord.getBounds()); - - // Rounded corners should be displayed above the taskbar. - // It is important to call adjustBoundsForTaskbarUnchecked before offsetTo - // because taskbar bounds are in screen coordinates - adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds); - - // Activity bounds are in screen coordinates while (0,0) for activity's surface - // control is at the top left corner of an app window so offsetting bounds - // accordingly. - cropBounds.offsetTo(0, 0); - } - - transaction - .setWindowCrop(windowSurface, cropBounds) - .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow)); + if (windowSurface == null || !windowSurface.isValid()) { + return; } + + // cropBounds must be non-null for the cornerRadius to be ever applied. + mActivityRecord.getSyncTransaction() + .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow)) + .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow)); } - private boolean requiresRoundedCorners(WindowState mainWindow) { - final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow); + @VisibleForTesting + @Nullable + Rect getCropBoundsIfNeeded(final WindowState mainWindow) { + if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) { + // We don't want corner radius on the window. + // In the case the ActivityRecord requires a letterboxed animation we never want + // rounded corners on the window because rounded corners are applied at the + // animation-bounds surface level and rounded corners on the window would interfere + // with that leading to unexpected rounded corner positioning during the animation. + return null; + } + final Rect cropBounds = new Rect(mActivityRecord.getBounds()); + + // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo} + // because taskbar bounds used in {@link #adjustBoundsIfNeeded} + // are in screen coordinates + adjustBoundsIfNeeded(mainWindow, cropBounds); + + // ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface + // control is in the top left corner of an app window so offsetting bounds + // accordingly. + cropBounds.offsetTo(0, 0); + return cropBounds; + } + + private boolean requiresRoundedCorners(final WindowState mainWindow) { return isLetterboxedNotForDisplayCutout(mainWindow) - && mLetterboxConfiguration.isLetterboxActivityCornersRounded() - && taskbarInsetsSource != null; + && mLetterboxConfiguration.isLetterboxActivityCornersRounded(); } // Returns rounded corners radius the letterboxed activity should have based on override in // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii. - // Device corners can be different on the right and left sides but we use the same radius + // Device corners can be different on the right and left sides, but we use the same radius // for all corners for consistency and pick a minimal bottom one for consistency with a // taskbar rounded corners. - int getRoundedCornersRadius(WindowState mainWindow) { - if (!requiresRoundedCorners(mainWindow)) { + int getRoundedCornersRadius(final WindowState mainWindow) { + if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) { return 0; } + final int radius; if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) { - return mLetterboxConfiguration.getLetterboxActivityCornersRadius(); + radius = mLetterboxConfiguration.getLetterboxActivityCornersRadius(); + } else { + final InsetsState insetsState = mainWindow.getInsetsState(); + radius = Math.min( + getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT), + getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT)); } - final InsetsState insetsState = mainWindow.getInsetsState(); - return Math.min( - getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT), - getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT)); + final float scale = mainWindow.mInvGlobalScale; + return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius; } /** - * Returns whether the taskbar is visible. Returns false if the window is in immersive mode, - * since the user can swipe to show/hide the taskbar as an overlay. + * Returns the taskbar in case it is visible and expanded in height, otherwise returns null. */ - private boolean hasVisibleTaskbar(WindowState mainWindow) { - final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow); - - return taskbarInsetsSource != null - && taskbarInsetsSource.isVisible(); + @VisibleForTesting + @Nullable + InsetsSource getExpandedTaskbarOrNull(final WindowState mainWindow) { + final InsetsSource taskbar = mainWindow.getInsetsState().peekSource( + InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + if (taskbar != null && taskbar.isVisible() + && taskbar.getFrame().height() >= mExpandedTaskBarHeight) { + return taskbar; + } + return null; } - private InsetsSource getTaskbarInsetsSource(WindowState mainWindow) { - final InsetsState insetsState = mainWindow.getInsetsState(); - return insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); - } - - private void adjustBoundsForTaskbar(WindowState mainWindow, Rect bounds) { + private void adjustBoundsIfNeeded(final WindowState mainWindow, final Rect bounds) { // Rounded corners should be displayed above the taskbar. When taskbar is hidden, // an insets frame is equal to a navigation bar which shouldn't affect position of // rounded corners since apps are expected to handle navigation bar inset. // This condition checks whether the taskbar is visible. // Do not crop the taskbar inset if the window is in immersive mode - the user can // swipe to show/hide the taskbar as an overlay. - if (hasVisibleTaskbar(mainWindow)) { - adjustBoundsForTaskbarUnchecked(mainWindow, bounds); + // Adjust the bounds only in case there is an expanded taskbar, + // otherwise the rounded corners will be shown behind the navbar. + final InsetsSource expandedTaskbarOrNull = getExpandedTaskbarOrNull(mainWindow); + if (expandedTaskbarOrNull != null) { + // Rounded corners should be displayed above the expanded taskbar. + bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top); } - } - private void adjustBoundsForTaskbarUnchecked(WindowState mainWindow, Rect bounds) { - // Rounded corners should be displayed above the taskbar. - bounds.bottom = - Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top); - scaleIfNeeded(bounds); + final float scale = mainWindow.mInvGlobalScale; + if (scale != 1f && scale > 0f) { + bounds.scale(scale); + } } private int getInsetsStateCornerRadius( @@ -1246,20 +1256,4 @@ final class LetterboxUiController { mInheritedSizeCompatScale = 1f; mInheritedCompatDisplayInsets = null; } - - private void scaleIfNeeded(Rect bounds) { - if (boundsNeedToScale()) { - bounds.scale(1.0f / mActivityRecord.getCompatScale()); - } - } - - private boolean boundsNeedToScale() { - if (hasInheritedLetterboxBehavior()) { - return mIsInheritedInSizeCompatMode - && mInheritedSizeCompatScale < 1.0f; - } else { - return mActivityRecord.inSizeCompatMode() - && mActivityRecord.getCompatScale() < 1.0f; - } - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 5e087f06b36be..478bd855902ad 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -28,21 +28,36 @@ import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENT import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; +import android.annotation.Nullable; import android.compat.testing.PlatformCompatChangeRule; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.PackageManager.Property; +import android.content.res.Resources; +import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.RoundedCorner; +import android.view.RoundedCorners; +import android.view.WindowManager; import androidx.test.filters.SmallTest; +import com.android.internal.R; + import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; import org.junit.Before; @@ -61,6 +76,14 @@ import org.junit.runner.RunWith; @Presubmit @RunWith(WindowTestRunner.class) public class LetterboxUiControllerTest extends WindowTestsBase { + private static final int TASKBAR_COLLAPSED_HEIGHT = 10; + private static final int TASKBAR_EXPANDED_HEIGHT = 20; + private static final int SCREEN_WIDTH = 200; + private static final int SCREEN_HEIGHT = 100; + private static final Rect TASKBAR_COLLAPSED_BOUNDS = new Rect(0, + SCREEN_HEIGHT - TASKBAR_COLLAPSED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT); + private static final Rect TASKBAR_EXPANDED_BOUNDS = new Rect(0, + SCREEN_HEIGHT - TASKBAR_EXPANDED_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT); @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); @@ -69,6 +92,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { private DisplayContent mDisplayContent; private LetterboxUiController mController; private LetterboxConfiguration mLetterboxConfiguration; + private final Rect mLetterboxedPortraitTaskBounds = new Rect(); @Before public void setUp() throws Exception { @@ -308,6 +332,162 @@ public class LetterboxUiControllerTest extends WindowTestsBase { assertTrue(mController.shouldForceRotateForCameraCompat()); } + @Test + public void testGetCropBoundsIfNeeded_noCrop() { + final InsetsSource taskbar = new InsetsSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar); + + // Do not apply crop if taskbar is collapsed + taskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS); + assertNull(mController.getExpandedTaskbarOrNull(mainWindow)); + + mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4, + SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT - SCREEN_HEIGHT / 4); + + final Rect noCrop = mController.getCropBoundsIfNeeded(mainWindow); + assertNotEquals(null, noCrop); + assertEquals(0, noCrop.left); + assertEquals(0, noCrop.top); + assertEquals(mLetterboxedPortraitTaskBounds.width(), noCrop.right); + assertEquals(mLetterboxedPortraitTaskBounds.height(), noCrop.bottom); + } + + @Test + public void testGetCropBoundsIfNeeded_appliesCrop() { + final InsetsSource taskbar = new InsetsSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar); + + // Apply crop if taskbar is expanded + taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS); + assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow)); + + mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4, + SCREEN_HEIGHT); + + final Rect crop = mController.getCropBoundsIfNeeded(mainWindow); + assertNotEquals(null, crop); + assertEquals(0, crop.left); + assertEquals(0, crop.top); + assertEquals(mLetterboxedPortraitTaskBounds.width(), crop.right); + assertEquals(mLetterboxedPortraitTaskBounds.height() - TASKBAR_EXPANDED_HEIGHT, + crop.bottom); + } + + @Test + public void testGetCropBoundsIfNeeded_appliesCropWithSizeCompatScaling() { + final InsetsSource taskbar = new InsetsSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar); + final float scaling = 2.0f; + + // Apply crop if taskbar is expanded + taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS); + assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow)); + // With SizeCompat scaling + doReturn(true).when(mActivity).inSizeCompatMode(); + mainWindow.mInvGlobalScale = scaling; + + mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4, + SCREEN_HEIGHT); + + final int appWidth = mLetterboxedPortraitTaskBounds.width(); + final int appHeight = mLetterboxedPortraitTaskBounds.height(); + + final Rect crop = mController.getCropBoundsIfNeeded(mainWindow); + assertNotEquals(null, crop); + assertEquals(0, crop.left); + assertEquals(0, crop.top); + assertEquals((int) (appWidth * scaling), crop.right); + assertEquals((int) ((appHeight - TASKBAR_EXPANDED_HEIGHT) * scaling), crop.bottom); + } + + @Test + public void testGetRoundedCornersRadius_withRoundedCornersFromInsets() { + final float invGlobalScale = 0.5f; + final int expectedRadius = 7; + final int configurationRadius = 15; + + final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null); + mainWindow.mInvGlobalScale = invGlobalScale; + final InsetsState insets = mainWindow.getInsetsState(); + + RoundedCorners roundedCorners = new RoundedCorners( + /*topLeft=*/ null, + /*topRight=*/ null, + /*bottomRight=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT, + configurationRadius, /*centerX=*/ 1, /*centerY=*/ 1), + /*bottomLeft=*/ new RoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT, + configurationRadius * 2 /*2 is to test selection of the min radius*/, + /*centerX=*/ 1, /*centerY=*/ 1) + ); + doReturn(roundedCorners).when(insets).getRoundedCorners(); + mLetterboxConfiguration.setLetterboxActivityCornersRadius(-1); + + assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow)); + } + + @Test + public void testGetRoundedCornersRadius_withLetterboxActivityCornersRadius() { + final float invGlobalScale = 0.5f; + final int expectedRadius = 7; + final int configurationRadius = 15; + + final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null); + mainWindow.mInvGlobalScale = invGlobalScale; + mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius); + + assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow)); + + } + + @Test + public void testGetRoundedCornersRadius_noScalingApplied() { + final int configurationRadius = 15; + + final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null); + mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius); + + mainWindow.mInvGlobalScale = -1f; + assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow)); + + mainWindow.mInvGlobalScale = 0f; + assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow)); + + mainWindow.mInvGlobalScale = 1f; + assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow)); + } + + private WindowState mockForGetCropBoundsAndRoundedCorners(@Nullable InsetsSource taskbar) { + final WindowState mainWindow = mock(WindowState.class); + final InsetsState insets = mock(InsetsState.class); + final Resources resources = mWm.mContext.getResources(); + final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(); + + mainWindow.mInvGlobalScale = 1f; + spyOn(resources); + spyOn(mActivity); + + if (taskbar != null) { + taskbar.setVisible(true); + doReturn(taskbar).when(insets).peekSource(taskbar.getType()); + } + doReturn(mLetterboxedPortraitTaskBounds).when(mActivity).getBounds(); + doReturn(true).when(mActivity).isVisible(); + doReturn(true).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio(); + doReturn(insets).when(mainWindow).getInsetsState(); + doReturn(attrs).when(mainWindow).getAttrs(); + doReturn(true).when(mainWindow).isDrawn(); + doReturn(false).when(mainWindow).isLetterboxedForDisplayCutout(); + doReturn(true).when(mainWindow).areAppWindowBoundsLetterboxed(); + doReturn(true).when(mLetterboxConfiguration).isLetterboxActivityCornersRounded(); + doReturn(TASKBAR_EXPANDED_HEIGHT).when(resources).getDimensionPixelSize( + R.dimen.taskbar_frame_height); + + // Need to reinitialise due to the change in resources getDimensionPixelSize output. + mController = new LetterboxUiController(mWm, mActivity); + + return mainWindow; + } + private void mockThatProperty(String propertyName, boolean value) throws Exception { Property property = new Property(propertyName, /* value */ value, /* packageName */ "", /* className */ ""); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 995932c46201c..701857ffe2036 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -2568,7 +2568,7 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mRootWindowContainer.performSurfacePlacement(); final ArgumentCaptor cropCapturer = ArgumentCaptor.forClass(Rect.class); - verify(mTransaction, times(2)).setWindowCrop( + verify(mTransaction, times(2)).setCrop( eq(w1.getSurfaceControl()), cropCapturer.capture() );