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() );