From fab18e35fe1374bec4069fa66883eb1a5dd339c7 Mon Sep 17 00:00:00 2001 From: Shivam Agrawal Date: Tue, 2 Mar 2021 22:33:57 -0500 Subject: [PATCH] Respect App Requested Orientation on Close-to-Square Displays On close-to-square displays, activity requested orientation may not be respected. For example, on a square display, a portrait activity may have a landscape window because of insets such as the status bar and nav bar. Bug: b/168754677 Test: atest ActivityRecordTests Change-Id: I40a071399e3bc159a20ade917da4c94cace0524e --- .../com/android/server/wm/ActivityRecord.java | 136 ++++++++++++++---- .../core/java/com/android/server/wm/Task.java | 2 +- .../server/wm/ActivityRecordTests.java | 34 +++++ .../android/server/wm/SizeCompatTests.java | 5 +- 4 files changed, 145 insertions(+), 32 deletions(-) diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 0f80fcc8a3030..a280513531bad 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -299,6 +299,7 @@ import android.view.InputApplicationHandle; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; import android.view.RemoteAnimationTarget; +import android.view.Surface.Rotation; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.WindowInsets.Type; @@ -780,6 +781,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ private final Configuration mTmpConfig = new Configuration(); private final Rect mTmpBounds = new Rect(); + private final Rect mTmpOutNonDecorBounds = new Rect(); // Token for targeting this activity for assist purposes. final Binder assistToken = new Binder(); @@ -7173,17 +7175,67 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } /** - * Computes bounds (letterbox or pillarbox) when the parent doesn't handle the orientation - * change and the requested orientation is different from the parent. + * In some cases, applying insets to bounds changes the orientation. For example, if a + * close-to-square display rotates to portrait to respect a portrait orientation activity, after + * insets such as the status and nav bars are applied, the activity may actually have a + * landscape orientation. This method checks whether the orientations of the activity window + * with and without insets match or if the orientation with insets already matches the + * 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. + */ + private boolean orientationRespectedWithInsets(Rect parentBounds, Rect outBounds) { + if (mDisplayContent == null) { + return true; + } + // Only need to make changes if activity sets an orientation + final int requestedOrientation = getRequestedConfigurationOrientation(); + if (requestedOrientation == ORIENTATION_UNDEFINED) { + return true; + } + // Compute parent orientation from bounds + final int orientation = parentBounds.height() >= parentBounds.width() + ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE; + // Compute orientation from stable parent bounds (= parent bounds with insets applied) + final Task task = getTask(); + task.calculateInsetFrames(mTmpOutNonDecorBounds /* outNonDecorBounds */, + outBounds /* outStableBounds */, parentBounds /* bounds */, + mDisplayContent.getDisplayInfo()); + final int orientationWithInsets = outBounds.height() >= outBounds.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 + || orientationWithInsets == requestedOrientation; + } + + /** + * Computes bounds (letterbox or pillarbox) when either: + * 1. The parent doesn't handle the orientation change and the requested orientation is + * different from the parent (see {@link DisplayContent#setIgnoreOrientationRequest()}. + * 2. The parent handling the orientation is not enough. This occurs when the display rotation + * may not be enough to respect orientation requests (see {@link + * ActivityRecord#orientationRespectedWithInsets}). * *

If letterboxed due to fixed orientation then aspect ratio restrictions are also applied - * in this methiod. + * in this method. */ private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig) { mLetterboxBoundsForFixedOrientationAndAspectRatio = null; - if (handlesOrientationChangeFromDescendant()) { + final Rect parentBounds = newParentConfig.windowConfiguration.getBounds(); + final Rect containerBounds = new Rect(parentBounds); + boolean orientationRespectedWithInsets = + orientationRespectedWithInsets(parentBounds, containerBounds); + if (handlesOrientationChangeFromDescendant() && orientationRespectedWithInsets) { // No need to letterbox because of fixed orientation. Display will handle - // fixed-orientation requests. + // fixed-orientation requests and a display rotation is enough to respect requested + // orientation with insets applied. return; } if (newParentConfig.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED) { @@ -7199,7 +7251,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // If the activity requires a different orientation (either by override or activityInfo), // make it fit the available bounds by scaling down its bounds. final int forcedOrientation = getRequestedConfigurationOrientation(); - if (forcedOrientation == ORIENTATION_UNDEFINED || forcedOrientation == parentOrientation) { + if (forcedOrientation == ORIENTATION_UNDEFINED + || (forcedOrientation == parentOrientation && orientationRespectedWithInsets)) { return; } @@ -7211,53 +7264,75 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } - final Rect parentBounds = newParentConfig.windowConfiguration.getBounds(); - final int parentWidth = parentBounds.width(); - final int parentHeight = parentBounds.height(); - float aspect = Math.max(parentWidth, parentHeight) - / (float) Math.min(parentWidth, parentHeight); + // 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); // Override from config_fixedOrientationLetterboxAspectRatio or via ADB with // set-fixed-orientation-letterbox-aspect-ratio. final float letterboxAspectRatioOverride = mWmService.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(); - aspect = letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO - ? letterboxAspectRatioOverride : aspect; + aspectRatio = letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO + ? letterboxAspectRatioOverride : aspectRatio; // 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 (aspect > maxAspectRatio && maxAspectRatio != 0) { - aspect = maxAspectRatio; - } else if (aspect < minAspectRatio) { - aspect = minAspectRatio; + 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. - Rect mTmpFullBounds = new Rect(resolvedBounds); + 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; if (forcedOrientation == ORIENTATION_LANDSCAPE) { - final int height = (int) Math.rint(parentWidth / aspect); - final int top = parentBounds.centerY() - height / 2; - resolvedBounds.set(parentBounds.left, top, parentBounds.right, top + height); + 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 { - final int width = (int) Math.rint(parentHeight / aspect); + 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(); - final int left = width <= parentAppBounds.width() - // Avoid overlapping with the horizontal decor area when possible. - ? parentAppBounds.left : parentBounds.centerX() - width / 2; - resolvedBounds.set(left, parentBounds.top, left + width, parentBounds.bottom); + resolvedBounds.set(parentAppBounds.left, parentBounds.top, + parentAppBounds.left + activityWidth, parentBounds.bottom); } if (mCompatDisplayInsets != null) { mCompatDisplayInsets.getBoundsByRotation( mTmpBounds, newParentConfig.windowConfiguration.getRotation()); - if (resolvedBounds.width() != mTmpBounds.width() - || resolvedBounds.height() != mTmpBounds.height()) { + // 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()) { // 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. - resolvedBounds.set(mTmpFullBounds); + resolvedBounds.set(prevResolvedBounds); return; } } @@ -8502,6 +8577,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * compatibility mode activity compute the configuration without relying on its current display. */ static class CompatDisplayInsets { + /** The original rotation the compat insets were computed in */ + final @Rotation int mOriginalRotation; /** The container width on rotation 0. */ private final int mWidth; /** The container height on rotation 0. */ @@ -8528,6 +8605,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** Constructs the environment to simulate the bounds behavior of the given container. */ CompatDisplayInsets(DisplayContent display, ActivityRecord container, @Nullable Rect fixedOrientationBounds) { + mOriginalRotation = display.getRotation(); mIsFloating = container.getWindowConfiguration().tasksAreFloating(); if (mIsFloating) { final Rect containerBounds = container.getWindowConfiguration().getBounds(); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 620dea6401682..c2b798bc77f94 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2614,7 +2614,7 @@ class Task extends WindowContainer { * @param outStableBounds where to place bounds with stable insets applied. * @param bounds the bounds to inset. */ - private void calculateInsetFrames(Rect outNonDecorBounds, Rect outStableBounds, Rect bounds, + void calculateInsetFrames(Rect outNonDecorBounds, Rect outStableBounds, Rect bounds, DisplayInfo displayInfo) { outNonDecorBounds.set(bounds); outStableBounds.set(bounds); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index aed3f52bd563e..2e37f7d3977c8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2666,6 +2666,40 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse("Starting window should not be present", activity.hasStartingWindow()); } + @Test + public void testCloseToSquareFixedOrientationPortrait() { + // create a square display + final DisplayContent squareDisplay = new TestDisplayContent.Builder(mAtm, 2000, 2000) + .setSystemDecorations(true).build(); + final Task task = new TaskBuilder(mSupervisor).setDisplay(squareDisplay).build(); + + // create a fixed portrait activity + final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task) + .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT).build(); + + // check that both the configuration and app bounds are portrait + assertEquals(ORIENTATION_PORTRAIT, activity.getConfiguration().orientation); + assertTrue(activity.getConfiguration().windowConfiguration.getAppBounds().width() + <= activity.getConfiguration().windowConfiguration.getAppBounds().height()); + } + + @Test + public void testCloseToSquareFixedOrientationLandscape() { + // create a square display + final DisplayContent squareDisplay = new TestDisplayContent.Builder(mAtm, 2000, 2000) + .setSystemDecorations(true).build(); + final Task task = new TaskBuilder(mSupervisor).setDisplay(squareDisplay).build(); + + // create a fixed landscape activity + final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task) + .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE).build(); + + // check that both the configuration and app bounds are landscape + assertEquals(ORIENTATION_LANDSCAPE, activity.getConfiguration().orientation); + assertTrue(activity.getConfiguration().windowConfiguration.getAppBounds().width() + > activity.getConfiguration().windowConfiguration.getAppBounds().height()); + } + private void assertHasStartingWindow(ActivityRecord atoken) { assertNotNull(atoken.mStartingSurface); assertNotNull(atoken.mStartingData); 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 1827730e99aa9..88c895173c876 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -395,6 +395,7 @@ public class SizeCompatTests extends WindowTestsBase { assertFitted(); final Rect currentBounds = mActivity.getWindowConfiguration().getBounds(); + final Rect currentAppBounds = mActivity.getWindowConfiguration().getAppBounds(); final Rect originalBounds = new Rect(mActivity.getWindowConfiguration().getBounds()); final int notchHeight = 100; @@ -422,8 +423,8 @@ public class SizeCompatTests extends WindowTestsBase { // Because the display cannot rotate, the portrait activity will fit the short side of // display with keeping portrait bounds [200, 0 - 700, 1000] in center. assertEquals(newDisplayBounds.height(), currentBounds.height()); - assertEquals(currentBounds.height() * newDisplayBounds.height() / newDisplayBounds.width(), - currentBounds.width()); + assertEquals(currentAppBounds.height() * newDisplayBounds.height() + / newDisplayBounds.width(), currentAppBounds.width()); assertFitted(); // The appBounds should be [200, 100 - 700, 1000]. final Rect appBounds = mActivity.getWindowConfiguration().getAppBounds();