diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 5ec824d1612ce..1004b7f17045b 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -698,8 +698,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A private boolean mInSizeCompatModeForBounds = false; // Whether the aspect ratio restrictions applied to the activity bounds in applyAspectRatio(). - // TODO(b/182268157): Aspect ratio can also be applie in resolveFixedOrientationConfiguration - // but that isn't reflected in this boolean. private boolean mIsAspectRatioApplied = false; // Bounds populated in resolveFixedOrientationConfiguration when this activity is letterboxed @@ -7352,11 +7350,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * requested orientation. If not, it may be necessary to letterbox the window. * @param parentBounds are the new parent bounds passed down to the activity and should be used * to compute the stable bounds. - * @param outBounds will store the stable bounds, which are the bounds with insets applied. - * These should be used to compute letterboxed bounds if orientation is not - * respected when insets are applied. + * @param outStableBounds will store the stable bounds, which are the bounds with insets + * applied, if orientation is not respected when insets are applied. + * Otherwise outStableBounds will be empty. Stable bounds should be used + * to compute letterboxed bounds if orientation is not respected when + * insets are applied. */ - private boolean orientationRespectedWithInsets(Rect parentBounds, Rect outBounds) { + private boolean orientationRespectedWithInsets(Rect parentBounds, Rect outStableBounds) { + outStableBounds.setEmpty(); if (mDisplayContent == null) { return true; } @@ -7371,17 +7372,21 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Compute orientation from stable parent bounds (= parent bounds with insets applied) final Task task = getTask(); task.calculateInsetFrames(mTmpOutNonDecorBounds /* outNonDecorBounds */, - outBounds /* outStableBounds */, parentBounds /* bounds */, + outStableBounds /* outStableBounds */, parentBounds /* bounds */, mDisplayContent.getDisplayInfo()); - final int orientationWithInsets = outBounds.height() >= outBounds.width() + final int orientationWithInsets = outStableBounds.height() >= outStableBounds.width() ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE; // If orientation does not match the orientation with insets applied, then a // display rotation will not be enough to respect orientation. However, even if they do // not match but the orientation with insets applied matches the requested orientation, then // there is no need to modify the bounds because when insets are applied, the activity will // have the desired orientation. - return orientation == orientationWithInsets + final boolean orientationRespectedWithInsets = orientation == orientationWithInsets || orientationWithInsets == requestedOrientation; + if (orientationRespectedWithInsets) { + outStableBounds.setEmpty(); + } + return orientationRespectedWithInsets; } /** @@ -7399,9 +7404,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A int windowingMode) { mLetterboxBoundsForFixedOrientationAndAspectRatio = null; final Rect parentBounds = newParentConfig.windowConfiguration.getBounds(); - final Rect containerBounds = new Rect(parentBounds); + final Rect stableBounds = new Rect(); + // If orientation is respected when insets are applied, then stableBounds will be empty. boolean orientationRespectedWithInsets = - orientationRespectedWithInsets(parentBounds, containerBounds); + orientationRespectedWithInsets(parentBounds, stableBounds); if (handlesOrientationChangeFromDescendant() && orientationRespectedWithInsets) { // No need to letterbox because of fixed orientation. Display will handle // fixed-orientation requests and a display rotation is enough to respect requested @@ -7438,71 +7444,68 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } - // TODO(b/182268157) merge aspect ratio logic here and in - // {@link ActivityRecord#applyAspectRatio} - // if no aspect ratio constraints are provided, parent aspect ratio is used - float aspectRatio = computeAspectRatio(parentBounds); + // TODO(b/182268157): Explore using only one type of parentBoundsWithInsets, either app + // bounds or stable bounds to unify aspect ratio logic. + final Rect parentBoundsWithInsets = orientationRespectedWithInsets + ? newParentConfig.windowConfiguration.getAppBounds() : stableBounds; + final Rect containingBounds = new Rect(); + final Rect containingBoundsWithInsets = new Rect(); + // Need to shrink the containing bounds into a square because the parent orientation + // does not match the activity requested orientation. + if (forcedOrientation == ORIENTATION_LANDSCAPE) { + // Landscape is defined as width > height. Make the container respect landscape + // orientation by shrinking height to one less than width. Landscape activity will be + // vertically centered within parent bounds with insets, so position vertical bounds + // within parent bounds with insets to prevent insets from unnecessarily trimming + // vertical bounds. + final int bottom = Math.min(parentBoundsWithInsets.top + parentBounds.width() - 1, + parentBoundsWithInsets.bottom); + containingBounds.set(parentBounds.left, parentBoundsWithInsets.top, parentBounds.right, + bottom); + containingBoundsWithInsets.set(parentBoundsWithInsets.left, parentBoundsWithInsets.top, + parentBoundsWithInsets.right, bottom); + } else { + // Portrait is defined as width <= height. Make the container respect portrait + // orientation by shrinking width to match height. Portrait activity will be + // horizontally centered within parent bounds with insets, so position horizontal bounds + // within parent bounds with insets to prevent insets from unnecessarily trimming + // horizontal bounds. + final int right = Math.min(parentBoundsWithInsets.left + parentBounds.height(), + parentBoundsWithInsets.right); + containingBounds.set(parentBoundsWithInsets.left, parentBounds.top, right, + parentBounds.bottom); + containingBoundsWithInsets.set(parentBoundsWithInsets.left, parentBoundsWithInsets.top, + right, parentBoundsWithInsets.bottom); + } + + // Store the current bounds to be able to revert to size compat mode values below if needed. + final Rect prevResolvedBounds = new Rect(resolvedBounds); + resolvedBounds.set(containingBounds); // Override from config_fixedOrientationLetterboxAspectRatio or via ADB with // set-fixed-orientation-letterbox-aspect-ratio. final float letterboxAspectRatioOverride = mWmService.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(); - aspectRatio = letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO - ? letterboxAspectRatioOverride : aspectRatio; + final float desiredAspectRatio = + letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO + ? letterboxAspectRatioOverride : computeAspectRatio(parentBounds); + // Apply aspect ratio to resolved bounds + mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, containingBoundsWithInsets, + containingBounds, desiredAspectRatio, true); - // Adjust the fixed orientation letterbox bounds to fit the app request aspect ratio in - // order to use the extra available space. - final float maxAspectRatio = info.getMaxAspectRatio(); - final float minAspectRatio = info.getMinAspectRatio(); - if (aspectRatio > maxAspectRatio && maxAspectRatio != 0) { - aspectRatio = maxAspectRatio; - } else if (aspectRatio < minAspectRatio) { - aspectRatio = minAspectRatio; - } - - // Store the current bounds to be able to revert to size compat mode values below if needed. - final Rect prevResolvedBounds = new Rect(resolvedBounds); - - // Compute other dimension based on aspect ratio. Use bounds intersected with insets, stored - // in containerBounds after calling {@link ActivityRecord#orientationRespectedWithInsets()}, - // to ensure that aspect ratio is respected after insets are applied. - int activityWidth; - int activityHeight; + // Vertically center if orientation is landscape. Center within parent bounds with insets + // to ensure that insets do not trim height. Bounds will later be horizontally centered in + // {@link updateResolvedBoundsHorizontalPosition()} regardless of orientation. if (forcedOrientation == ORIENTATION_LANDSCAPE) { - activityWidth = parentBounds.width(); - // Compute height from stable bounds width to ensure orientation respected after insets. - activityHeight = (int) Math.rint(containerBounds.width() / aspectRatio); - // Landscape is defined as width > height. To ensure activity is landscape when aspect - // ratio is close to 1, reduce the height by one pixel. - if (activityWidth == activityHeight) { - activityHeight -= 1; - } - // Center vertically within stable bounds in landscape to ensure insets do not trim - // height. - final int top = containerBounds.centerY() - activityHeight / 2; - resolvedBounds.set(parentBounds.left, top, parentBounds.right, top + activityHeight); - } else { - activityHeight = parentBounds.height(); - // Compute width from stable bounds height to ensure orientation respected after insets. - activityWidth = (int) Math.rint(containerBounds.height() / aspectRatio); - // Center horizontally in portrait. For now, align to left and allow - // {@link ActivityRecord#updateResolvedBoundsHorizontalPosition()} to center - // horizontally. Exclude left insets from parent to ensure cutout does not trim width. - final Rect parentAppBounds = newParentConfig.windowConfiguration.getAppBounds(); - resolvedBounds.set(parentAppBounds.left, parentBounds.top, - parentAppBounds.left + activityWidth, parentBounds.bottom); + final int offsetY = parentBoundsWithInsets.centerY() - resolvedBounds.centerY(); + resolvedBounds.offset(0, offsetY); } if (mCompatDisplayInsets != null) { mCompatDisplayInsets.getBoundsByRotation( mTmpBounds, newParentConfig.windowConfiguration.getRotation()); - // Insets may differ between different rotations, for example in the case of a display - // cutout. To ensure consistent bounds across rotations, compare the activity dimensions - // minus insets from the rotation the compat bounds were computed in. - Task.intersectWithInsetsIfFits(mTmpBounds, parentBounds, - mCompatDisplayInsets.mStableInsets[mCompatDisplayInsets.mOriginalRotation]); - if (activityWidth != mTmpBounds.width() - || activityHeight != mTmpBounds.height()) { + if (resolvedBounds.width() != mTmpBounds.width() + || resolvedBounds.height() != mTmpBounds.height()) { // The app shouldn't be resized, we only do fixed orientation letterboxing if the // compat bounds are also from the same fixed orientation letterbox. Otherwise, // clear the fixed orientation bounds to show app in size compat mode. @@ -7538,8 +7541,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // then they should be aligned later in #updateResolvedBoundsHorizontalPosition(). if (!mTmpBounds.isEmpty()) { resolvedBounds.set(mTmpBounds); - // Exclude the horizontal decor area. - resolvedBounds.left = parentAppBounds.left; } if (!resolvedBounds.isEmpty() && !resolvedBounds.equals(parentBounds)) { // Compute the configuration based on the resolved bounds. If aspect ratio doesn't @@ -7600,13 +7601,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, containingAppBounds, containingBounds); } - // If the bounds are restricted by fixed aspect ratio, the resolved bounds should be put in - // the container app bounds. Otherwise the entire container bounds are available. - final boolean fillContainer = resolvedBounds.equals(containingBounds); - if (!fillContainer) { - // The horizontal position should not cover insets. - resolvedBounds.left = containingAppBounds.left; - } // Use resolvedBounds to compute other override configurations such as appBounds. The bounds // are calculated in compat container space. The actual position on screen will be applied @@ -7673,6 +7667,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Align to top of parent (bounds) - this is a UX choice and exclude the horizontal decor // if needed. Horizontal position is adjusted in updateResolvedBoundsHorizontalPosition. // Above coordinates are in "@" space, now place "*" and "#" to screen space. + final boolean fillContainer = resolvedBounds.equals(containingBounds); final int screenPosX = fillContainer ? containerBounds.left : containerAppBounds.left; final int screenPosY = containerBounds.top; if (screenPosX != 0 || screenPosY != 0) { @@ -7913,6 +7908,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return true; } + private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, + Rect containingBounds) { + return applyAspectRatio(outBounds, containingAppBounds, containingBounds, + 0 /* desiredAspectRatio */, false /* fixedOrientationLetterboxed */); + } + /** * Applies aspect ratio restrictions to outBounds. If no restrictions, then no change is * made to outBounds. @@ -7921,17 +7922,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer. private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, - Rect containingBounds) { + Rect containingBounds, float desiredAspectRatio, boolean fixedOrientationLetterboxed) { final float maxAspectRatio = info.getMaxAspectRatio(); final Task rootTask = getRootTask(); final float minAspectRatio = info.getMinAspectRatio(); if (task == null || rootTask == null - || (inMultiWindowMode() && !shouldCreateCompatDisplayInsets()) - || (maxAspectRatio == 0 && minAspectRatio == 0) + || (inMultiWindowMode() && !shouldCreateCompatDisplayInsets() + && !fixedOrientationLetterboxed) + || (maxAspectRatio < 1 && minAspectRatio < 1 && desiredAspectRatio < 1) || isInVrUiMode(getConfiguration())) { - // We don't enforce aspect ratio if the activity task is in multiwindow unless it - // is in size-compat mode. We also don't set it if we are in VR mode. + // We don't enforce aspect ratio if the activity task is in multiwindow unless it is in + // size-compat mode or is letterboxed from fixed orientation. We also don't set it if we + // are in VR mode. return false; } @@ -7939,20 +7942,30 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final int containingAppHeight = containingAppBounds.height(); final float containingRatio = computeAspectRatio(containingAppBounds); + if (desiredAspectRatio < 1) { + desiredAspectRatio = containingRatio; + } + + if (maxAspectRatio >= 1 && desiredAspectRatio > maxAspectRatio) { + desiredAspectRatio = maxAspectRatio; + } else if (minAspectRatio >= 1 && desiredAspectRatio < minAspectRatio) { + desiredAspectRatio = minAspectRatio; + } + int activityWidth = containingAppWidth; int activityHeight = containingAppHeight; - if (containingRatio > maxAspectRatio && maxAspectRatio != 0) { + if (containingRatio > desiredAspectRatio) { if (containingAppWidth < containingAppHeight) { // Width is the shorter side, so we use that to figure-out what the max. height // should be given the aspect ratio. - activityHeight = (int) ((activityWidth * maxAspectRatio) + 0.5f); + activityHeight = (int) ((activityWidth * desiredAspectRatio) + 0.5f); } else { // Height is the shorter side, so we use that to figure-out what the max. width // should be given the aspect ratio. - activityWidth = (int) ((activityHeight * maxAspectRatio) + 0.5f); + activityWidth = (int) ((activityHeight * desiredAspectRatio) + 0.5f); } - } else if (containingRatio < minAspectRatio) { + } else if (containingRatio < desiredAspectRatio) { boolean adjustWidth; switch (getRequestedConfigurationOrientation()) { case ORIENTATION_LANDSCAPE: @@ -7980,9 +7993,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A break; } if (adjustWidth) { - activityWidth = (int) ((activityHeight / minAspectRatio) + 0.5f); + activityWidth = (int) ((activityHeight / desiredAspectRatio) + 0.5f); } else { - activityHeight = (int) ((activityWidth / minAspectRatio) + 0.5f); + activityHeight = (int) ((activityWidth / desiredAspectRatio) + 0.5f); } } @@ -8006,6 +8019,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } outBounds.set(containingBounds.left, containingBounds.top, right, bottom); + // If the bounds are restricted by fixed aspect ratio, then out bounds should be put in the + // container app bounds. Otherwise the entire container bounds are available. + if (!outBounds.equals(containingBounds)) { + // The horizontal position should not cover insets (e.g. display cutout). + outBounds.left = containingAppBounds.left; + } + return true; } 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 b47e6c1e544f1..cfde289d9fcec 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -1963,6 +1963,61 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(mActivity.areBoundsLetterboxed()); } + /** + * Tests that all three paths in which aspect ratio logic can be applied yield the same + * result, which is that aspect ratio is respected on app bounds. The three paths are + * fixed orientation, no fixed orientation but fixed aspect ratio, and size compat mode. + */ + @Test + public void testAllAspectRatioLogicConsistent() { + // Create display that has all stable insets and does not rotate. Make sure that status bar + // height is greater than notch height so that stable bounds do not equal app bounds. + final int notchHeight = 75; + final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 600) + .setSystemDecorations(true).setNotch(notchHeight) + .setStatusBarHeight(notchHeight + 20).setCanRotate(false).build(); + + // Create task on test display. + final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build(); + + // Target min aspect ratio must be larger than parent aspect ratio to be applied. + final float targetMinAspectRatio = 3.0f; + + // Create fixed portait activity with min aspect ratio greater than parent aspect ratio. + final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm) + .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) + .setMinAspectRatio(targetMinAspectRatio).build(); + final Rect fixedOrientationAppBounds = new Rect(fixedOrientationActivity.getConfiguration() + .windowConfiguration.getAppBounds()); + + // Create activity with no fixed orientation and min aspect ratio greater than parent aspect + // ratio. + final ActivityRecord minAspectRatioActivity = new ActivityBuilder(mAtm).setTask(task) + .setMinAspectRatio(targetMinAspectRatio).build(); + final Rect minAspectRatioAppBounds = new Rect(minAspectRatioActivity.getConfiguration() + .windowConfiguration.getAppBounds()); + + // Create unresizeable fixed portait activity with min aspect ratio greater than parent + // aspect ratio. + final ActivityRecord sizeCompatActivity = new ActivityBuilder(mAtm) + .setTask(task).setResizeMode(RESIZE_MODE_UNRESIZEABLE) + .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) + .setMinAspectRatio(targetMinAspectRatio).build(); + // Resize display running unresizeable activity to make it enter size compat mode. + resizeDisplay(display, 1800, 1000); + final Rect sizeCompatAppBounds = new Rect(sizeCompatActivity.getConfiguration() + .windowConfiguration.getAppBounds()); + + // Check that aspect ratio of app bounds is equal to the min aspect ratio. + final float delta = 0.01f; + assertEquals(targetMinAspectRatio, ActivityRecord + .computeAspectRatio(fixedOrientationAppBounds), delta); + assertEquals(targetMinAspectRatio, ActivityRecord + .computeAspectRatio(minAspectRatioAppBounds), delta); + assertEquals(targetMinAspectRatio, ActivityRecord + .computeAspectRatio(sizeCompatAppBounds), delta); + } + private void assertHorizontalPositionForDifferentDisplayConfigsForLandscapeActivity( float letterboxHorizontalPositionMultiplier) { // Set up a display in landscape and ignoring orientation request. diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java index ce2d748599316..0e504d326280f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -19,7 +19,9 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -77,6 +79,7 @@ class TestDisplayContent extends DisplayContent { private int mPosition = POSITION_BOTTOM; protected final ActivityTaskManagerService mService; private boolean mSystemDecorations = false; + private int mStatusBarHeight = 0; Builder(ActivityTaskManagerService service, int width, int height) { mService = service; @@ -125,6 +128,10 @@ class TestDisplayContent extends DisplayContent { Insets.of(0, height, 0, 0), null, new Rect(20, 0, 80, height), null, null); return this; } + Builder setStatusBarHeight(int height) { + mStatusBarHeight = height; + return this; + } Builder setCanRotate(boolean canRotate) { mCanRotate = canRotate; return this; @@ -158,6 +165,14 @@ class TestDisplayContent extends DisplayContent { doReturn(false).when(displayPolicy).hasStatusBar(); doReturn(false).when(newDisplay).supportsSystemDecorations(); } + if (mStatusBarHeight > 0) { + doReturn(true).when(displayPolicy).hasStatusBar(); + doAnswer(invocation -> { + Rect inOutInsets = (Rect) invocation.getArgument(0); + inOutInsets.top = mStatusBarHeight; + return null; + }).when(displayPolicy).convertNonDecorInsetsToStableInsets(any(), anyInt()); + } Configuration c = new Configuration(); newDisplay.computeScreenConfiguration(c); c.windowConfiguration.setWindowingMode(mWindowingMode);