Merge "Show letterbox rounded corners behind navbar" into tm-qpr-dev

This commit is contained in:
Mariia Sandrikova
2023-01-24 23:42:20 +00:00
committed by Android (Google) Code Review
3 changed files with 264 additions and 90 deletions

View File

@@ -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);
@@ -775,8 +784,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
@@ -828,106 +837,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(
@@ -1269,20 +1279,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;
}
}
}

View File

@@ -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 */ "");

View File

@@ -2821,7 +2821,7 @@ public class SizeCompatTests extends WindowTestsBase {
mActivity.mRootWindowContainer.performSurfacePlacement();
final ArgumentCaptor<Rect> cropCapturer = ArgumentCaptor.forClass(Rect.class);
verify(mTransaction, times(2)).setWindowCrop(
verify(mTransaction, times(2)).setCrop(
eq(w1.getSurfaceControl()),
cropCapturer.capture()
);