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
This commit is contained in:
Shivam Agrawal
2021-03-02 22:33:57 -05:00
parent 4244ff3aa8
commit fab18e35fe
4 changed files with 145 additions and 32 deletions

View File

@@ -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}).
*
* <p>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();

View File

@@ -2614,7 +2614,7 @@ class Task extends WindowContainer<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);

View File

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

View File

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