Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fab18e35 authored by Shivam Agrawal's avatar Shivam Agrawal
Browse files

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
parent 4244ff3a
Loading
Loading
Loading
Loading
+107 −29
Original line number Diff line number Diff line
@@ -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();
+1 −1
Original line number Diff line number Diff line
@@ -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);
+34 −0
Original line number Diff line number Diff line
@@ -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);
+3 −2
Original line number Diff line number Diff line
@@ -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();