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

Commit 04164186 authored by Riddle Hsu's avatar Riddle Hsu
Browse files

Don't involve decor inset into app bounds of fixed aspect ratio app

Assume the display size is 1000x2000 with a system decor height 100 at
top (e.g. cutout). For an activity declared max aspect ratio as 1.5:1,
its bounds will be (0,0,1000,1600) because decor region is also included.
And the app bounds should be 1000x1500 (0,100,1000,1600) that fits the
declared aspect ratio.

The original problem is that the app bounds is set to override bounds
directly (without intersecting with parent app bounds), so the aspect
ratio becomes 1.6:1 that doesn't match the declaration.

Since non-resizable activity with fixed aspect ratio will follow the
policy of size compatibility mode: fixed screen relative configuration
across display changes, now the app bounds (intersected with parent app
bounds) also becomes a part of override configuration. That makes sure
the app bounds keeps the declared aspect ratio.

Because the app bounds may contain offset that is occupied by the decor
insets, the offset is applied when parent also has the insets at the
corresponding side. This is just to avoid losing the original insets
information, although currently it may not be used.

Bug: 112288258
Fixes: 127661671
Fixes: 127667028
Test: atest AspectRatioTests
Test: atest AspectRatioSdk25Tests
Test: atest ActivityRecordTests# \
      testSizeCompatMode_FixedAspectRatioBoundsWithDecor
Test: atest ActivityDisplayTests#testHandleActivitySizeCompatMode

Change-Id: I7850a556862e1060876ca094fd919d78ddc4185e
parent 153edfc3
Loading
Loading
Loading
Loading
+94 −53
Original line number Diff line number Diff line
@@ -2710,22 +2710,35 @@ final class ActivityRecord extends ConfigurationContainer {
        if (!shouldUseSizeCompatMode()) {
            return false;
        }
        final Configuration parentConfig = getParent().getConfiguration();
        final Configuration resolvedConfig = getResolvedOverrideConfiguration();
        final Rect resolvedAppBounds = resolvedConfig.windowConfiguration.getAppBounds();
        if (resolvedAppBounds == null) {
            // The override configuration has not been resolved yet.
            return false;
        }

        final Configuration parentConfig = getParent().getConfiguration();
        // Although colorMode, screenLayout, smallestScreenWidthDp are also fixed, generally these
        // fields should be changed with density and bounds, so here only compares the most
        // significant field.
        if (parentConfig.densityDpi != resolvedConfig.densityDpi) {
            return true;
        }

        final Rect parentAppBounds = parentConfig.windowConfiguration.getAppBounds();
        final Rect parentBounds = parentAppBounds != null
                ? parentAppBounds : parentConfig.windowConfiguration.getBounds();
        final Rect overrideBounds = resolvedConfig.windowConfiguration.getBounds();
        if (parentAppBounds.width() < resolvedAppBounds.width()
                || parentAppBounds.height() < resolvedAppBounds.height()) {
            // One side is larger than the parent.
            return true;
        }

        final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
        // If the width or height is the same as parent, it is already the best fit of the override
        // bounds, therefore this condition is considered as not size compatibility mode.
        return parentBounds.width() != overrideBounds.width()
                && parentBounds.height() != overrideBounds.height();
        // bounds, therefore this condition is considered as not size compatibility mode. Here uses
        // right and bottom as width and height of parent because the bounds may contain decor
        // insets which has been accounted in override bounds. See {@link #computeBounds}.
        return parentAppBounds.right != resolvedBounds.width()
                && parentAppBounds.bottom != resolvedBounds.height();
    }

    /**
@@ -2778,15 +2791,18 @@ final class ActivityRecord extends ConfigurationContainer {
                // are relative to bounds and density, they will be calculated in
                // {@link TaskRecord#computeConfigResourceOverrides} and the result will also be
                // relatively fixed.
                final Configuration srcConfig = task.getConfiguration();
                overrideConfig.colorMode = srcConfig.colorMode;
                overrideConfig.densityDpi = srcConfig.densityDpi;
                overrideConfig.screenLayout = srcConfig.screenLayout
                final Configuration parentConfig = task.getConfiguration();
                // Don't account decor insets into app bounds.
                mTmpBounds.intersect(parentConfig.windowConfiguration.getAppBounds());
                overrideConfig.windowConfiguration.setAppBounds(mTmpBounds);
                overrideConfig.colorMode = parentConfig.colorMode;
                overrideConfig.densityDpi = parentConfig.densityDpi;
                overrideConfig.screenLayout = parentConfig.screenLayout
                        & (Configuration.SCREENLAYOUT_LONG_MASK
                                | Configuration.SCREENLAYOUT_SIZE_MASK);
                // The smallest screen width is the short side of screen bounds. Because the bounds
                // and density won't be changed, smallestScreenWidthDp is also fixed.
                overrideConfig.smallestScreenWidthDp = srcConfig.smallestScreenWidthDp;
                overrideConfig.smallestScreenWidthDp = parentConfig.smallestScreenWidthDp;
            }
        }
        onRequestedOverrideConfigurationChanged(overrideConfig);
@@ -2794,33 +2810,38 @@ final class ActivityRecord extends ConfigurationContainer {

    @Override
    void resolveOverrideConfiguration(Configuration newParentConfiguration) {
        // If the activity has override bounds, the relative configuration (e.g. screen size,
        // layout) needs to be resolved according to the bounds.
        final boolean hasOverrideBounds = !matchParentBounds();
        if (hasOverrideBounds && shouldUseSizeCompatMode()) {
            resolveSizeCompatModeConfiguration(newParentConfiguration);
        } else {
            super.resolveOverrideConfiguration(newParentConfiguration);
            if (hasOverrideBounds) {
                task.computeConfigResourceOverrides(getResolvedOverrideConfiguration(),
                        newParentConfiguration, true /* insideParentBounds */);
            }
        }

        // Assign configuration sequence number into hierarchy because there is a different way than
        // ensureActivityConfiguration() in this class that uses configuration in WindowState during
        // layout traversals.
        mConfigurationSeq = Math.max(++mConfigurationSeq, 1);
        getResolvedOverrideConfiguration().seq = mConfigurationSeq;

        if (matchParentBounds()) {
            return;
    }

    private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration) {
        final Configuration resolvedConfig = getResolvedOverrideConfiguration();
        if (!shouldUseSizeCompatMode()) {
            computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
                    ORIENTATION_UNDEFINED, true /* insideParentBounds */);
            return;
        }
        final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();

        final Configuration displayConfig = getDisplay().getConfiguration();
        int orientation = getConfiguration().orientation;
        if (orientation != displayConfig.orientation && isConfigurationCompatible(displayConfig)) {
        if (orientation != newParentConfiguration.orientation
                && isConfigurationCompatible(newParentConfiguration)) {
            // The activity is compatible to apply the orientation change or it requests different
            // fixed orientation.
            orientation = displayConfig.orientation;
            orientation = newParentConfiguration.orientation;
        } else {
            if (resolvedConfig.windowConfiguration.getAppBounds() != null) {
            if (!resolvedBounds.isEmpty()) {
                // Keep the computed resolved override configuration.
                return;
            }
@@ -2830,35 +2851,55 @@ final class ActivityRecord extends ConfigurationContainer {
            }
        }

        // Adjust the bounds to match the current orientation.
        if (orientation != ORIENTATION_UNDEFINED) {
            final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
            final int longSide = Math.max(resolvedBounds.height(), resolvedBounds.width());
            final int shortSide = Math.min(resolvedBounds.height(), resolvedBounds.width());
            final boolean toBeLandscape = orientation == ORIENTATION_LANDSCAPE;
            final int width = toBeLandscape ? longSide : shortSide;
            final int height = toBeLandscape ? shortSide : longSide;
            // Assume the bounds is always started from zero because the size may be bigger than its
        // The requested override bounds will set to the resolved bounds.
        super.resolveOverrideConfiguration(newParentConfiguration);

        boolean shouldSwapAppBounds = false;
        int width = resolvedBounds.width();
        int height = resolvedBounds.height();
        if ((orientation == ORIENTATION_LANDSCAPE && height > width)
                || (orientation == ORIENTATION_PORTRAIT && width > height)) {
            // Swap width and height because they are opposite to the orientation.
            width = resolvedBounds.height();
            height = resolvedBounds.width();
            // Assume the bounds always starts from zero because the size may be larger than its
            // parent (task ~ display). The actual letterboxing will be done by surface offset.
            resolvedBounds.set(0, 0, width, height);
        }

        // In size compatible mode, activity is allowed to have larger bounds than its parent.
        computeConfigResourceOverrides(resolvedConfig, newParentConfiguration, orientation,
            shouldSwapAppBounds = true;
        } else if (width == height) {
            // The bounds may contain decor insets, then its app bounds may not be 1:1 and need to
            // be adjusted according to the orientation.
            final int appWidth = resolvedConfig.windowConfiguration.getAppBounds().width();
            final int appHeight = resolvedConfig.windowConfiguration.getAppBounds().height();
            shouldSwapAppBounds = (orientation == ORIENTATION_LANDSCAPE && appHeight > appWidth)
                    || (orientation == ORIENTATION_PORTRAIT && appWidth > appHeight);
        }

        final Rect resolvedAppBounds = resolvedConfig.windowConfiguration.getAppBounds();
        final Rect parentAppBounds = newParentConfiguration.windowConfiguration.getAppBounds();
        if (shouldSwapAppBounds) {
            // Preserve the original decor insets (the left and top of the resolved app bounds) if
            // the parent also has the insets at the corresponding side.
            final int left = parentAppBounds.left > 0 ? resolvedAppBounds.top : 0;
            final int top = parentAppBounds.top > 0 ? resolvedAppBounds.left : 0;
            final int appWidth = resolvedAppBounds.height();
            final int appHeight = resolvedAppBounds.width();
            resolvedAppBounds.set(left, top, appWidth + left, appHeight + top);
        }
        // The horizontal inset included in width is not needed if the activity cannot fill the
        // parent, because the offset will be applied by {@link AppWindowToken#mSizeCompatBounds}.
        if (resolvedBounds.width() < parentAppBounds.width()) {
            resolvedBounds.right -= resolvedAppBounds.left;
        }

        // In size compatibility mode, activity is allowed to have larger bounds than its parent.
        task.computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
                false /* insideParentBounds */);
        // Use parent orientation if it cannot be decided by bounds, so the activity can fit inside
        // the parent bounds appropriately.
        if (resolvedConfig.screenWidthDp == resolvedConfig.screenHeightDp) {
            resolvedConfig.orientation = newParentConfiguration.orientation;
        }

    private void computeConfigResourceOverrides(Configuration inOutConfig,
            Configuration parentConfig, int orientation, boolean insideParentBounds) {
        // Set the real orientation or undefined value to ensure the output orientation won't be the
        // old value. Also reset app bounds so it will be updated according to bounds.
        inOutConfig.orientation = orientation;
        final Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
        if (outAppBounds != null) {
            outAppBounds.setEmpty();
        }

        task.computeConfigResourceOverrides(inOutConfig, parentConfig, insideParentBounds);
    }

    @Override
@@ -2990,14 +3031,14 @@ final class ActivityRecord extends ConfigurationContainer {
            // {@link #getRequestedOverrideBounds()} will be empty (representing no override). If
            // the method has run before, then effect of {@link #getRequestedOverrideBounds()} will
            // already have been applied to the value returned from {@link getConfiguration}. Refer
            // to {@link TaskRecord#computeOverrideConfiguration}.
            // to {@link TaskRecord#computeConfigResourceOverrides()}.
            outBounds.set(getRequestedOverrideBounds());
            return;
        }

        // Compute configuration based on max supported width and height.
        // Also account for the left / top insets (e.g. from display cutouts), which will be clipped
        // away later in StackWindowController.adjustConfigurationForBounds(). Otherwise, the app
        // away later in {@link TaskRecord#computeConfigResourceOverrides()}. Otherwise, the app
        // bounds would end up too small.
        outBounds.set(0, 0, activityWidth + appBounds.left, activityHeight + appBounds.top);
    }
+6 −2
Original line number Diff line number Diff line
@@ -1627,7 +1627,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
                // If the changes come from change-listener, the incoming parent configuration is
                // still the old one. Make sure their orientations are the same to reduce computing
                // the compatibility bounds for the intermediate state.
                && getResolvedOverrideConfiguration().orientation == newParentConfig.orientation) {
                && (task.mTaskRecord == null || task.mTaskRecord
                        .getConfiguration().orientation == newParentConfig.orientation)) {
            final Rect taskBounds = task.getBounds();
            // Since we only center the activity horizontally, if only the fixed height is smaller
            // than its container, the override bounds don't need to take effect.
@@ -1763,7 +1764,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
        final Rect parentAppBounds = newParentConfig.windowConfiguration.getAppBounds();
        final Rect viewportBounds = parentAppBounds != null
                ? parentAppBounds : newParentConfig.windowConfiguration.getBounds();
        final Rect contentBounds = getResolvedOverrideBounds();
        final Rect appBounds = getWindowConfiguration().getAppBounds();
        final Rect contentBounds = appBounds != null ? appBounds : getResolvedOverrideBounds();
        final float contentW = contentBounds.width();
        final float contentH = contentBounds.height();
        final float viewportW = viewportBounds.width();
@@ -1780,6 +1782,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
        mSizeCompatBounds.set(contentBounds);
        mSizeCompatBounds.offsetTo(0, 0);
        mSizeCompatBounds.scale(mSizeCompatScale);
        // The decor inset is included in height.
        mSizeCompatBounds.bottom += viewportBounds.top;
        mSizeCompatBounds.left += offsetX;
        mSizeCompatBounds.right += offsetX;
    }
+3 −2
Original line number Diff line number Diff line
@@ -2115,8 +2115,9 @@ class TaskRecord extends ConfigurationContainer {
                // {@link WindowManagerPolicy#getNonDecorInsetsLw}.
                calculateInsetFrames(mTmpNonDecorBounds, mTmpStableBounds, bounds, di);
            } else {
                mTmpNonDecorBounds.set(bounds);
                mTmpStableBounds.set(bounds);
                // Set to app bounds because it excludes decor insets.
                mTmpNonDecorBounds.set(outAppBounds);
                mTmpStableBounds.set(outAppBounds);
            }

            if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
+4 −4
Original line number Diff line number Diff line
@@ -350,7 +350,7 @@ public class ActivityDisplayTests extends ActivityTestsBase {
        activity.setState(ActivityStack.ActivityState.RESUMED, "testHandleActivitySizeCompatMode");
        activity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
        activity.info.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
        activity.getTaskRecord().getConfiguration().windowConfiguration.getBounds().set(
        activity.getTaskRecord().getConfiguration().windowConfiguration.setAppBounds(
                0, 0, 1000, 2000);

        final ArrayList<CompletableFuture<IBinder>> resultWrapper = new ArrayList<>();
@@ -363,15 +363,15 @@ public class ActivityDisplayTests extends ActivityTestsBase {
                    }
                });

        // Expect the exact component name when the activity is in size compatible mode.
        activity.getResolvedOverrideConfiguration().windowConfiguration.getBounds().set(
        // Expect the exact token when the activity is in size compatibility mode.
        activity.getResolvedOverrideConfiguration().windowConfiguration.setAppBounds(
                0, 0, 800, 1600);
        resultWrapper.add(new CompletableFuture<>());
        display.handleActivitySizeCompatModeIfNeeded(activity);

        assertEquals(activity.appToken, resultWrapper.get(0).get(2, TimeUnit.SECONDS));

        // Expect null component name when switching to non-size-compat mode activity.
        // Expect null token when switching to non-size-compat mode activity.
        activity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE;
        resultWrapper.set(0, new CompletableFuture<>());
        display.handleActivitySizeCompatModeIfNeeded(activity);
+32 −5
Original line number Diff line number Diff line
@@ -175,15 +175,16 @@ public class ActivityRecordTests extends ActivityTestsBase {
    @Test
    public void testRestartProcessIfVisible() {
        doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity);
        mActivity.getParent().getWindowConfiguration().setAppBounds(0, 0, 500, 1000);
        mTask.getWindowConfiguration().setAppBounds(0, 0, 500, 1000);
        mActivity.visible = true;
        mActivity.haveState = false;
        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
        mActivity.info.maxAspectRatio = 1.5f;
        mActivity.setState(ActivityStack.ActivityState.RESUMED, "testRestart");
        final Rect originalOverrideBounds = new Rect(0, 0, 400, 600);
        mActivity.setBounds(originalOverrideBounds);

        ensureActivityConfiguration();
        final Rect originalOverrideBounds = new Rect(mActivity.getBounds());
        mTask.getWindowConfiguration().setAppBounds(0, 0, 600, 1200);
        // The visible activity should recompute configuration according to the last parent bounds.
        mService.restartActivityProcessIfVisible(mActivity.appToken);

        assertEquals(ActivityStack.ActivityState.RESTARTING_PROCESS, mActivity.getState());
@@ -425,11 +426,37 @@ public class ActivityRecordTests extends ActivityTestsBase {
        }
    }

    @Test
    public void testSizeCompatMode_FixedAspectRatioBoundsWithDecor() {
        final int decorHeight = 200; // e.g. The device has cutout.
        final Rect parentAppBounds = new Rect(0, decorHeight, 600, 1000);
        mTask.getWindowConfiguration().setAppBounds(parentAppBounds);
        mTask.getConfiguration().orientation = Configuration.ORIENTATION_PORTRAIT;
        doReturn(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
                .when(mActivity.mAppWindowToken).getOrientationIgnoreVisibility();
        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
        mActivity.info.maxAspectRatio = 1;
        ensureActivityConfiguration();

        final Rect appBounds = mActivity.getWindowConfiguration().getAppBounds();
        // Ensure the app bounds keep the declared aspect ratio.
        assertEquals(appBounds.width(), appBounds.height());
        // The decor height should be a part of the effective bounds.
        assertEquals(mActivity.getBounds().height(), appBounds.height() + decorHeight);

        mTask.getConfiguration().orientation = Configuration.ORIENTATION_LANDSCAPE;
        mActivity.onConfigurationChanged(mTask.getConfiguration());
        // After changing orientation, the aspect ratio should be the same.
        assertEquals(appBounds.width(), appBounds.height());
        // The decor height will be included in width.
        assertEquals(mActivity.getBounds().width(), appBounds.width() + decorHeight);
    }

    @Test
    public void testSizeCompatMode_FixedScreenConfigurationWhenMovingToDisplay() {
        // Initialize different bounds on a new display.
        final ActivityDisplay newDisplay = addNewActivityDisplayAt(ActivityDisplay.POSITION_TOP);
        newDisplay.setBounds(0, 0, 1000, 2000);
        newDisplay.getWindowConfiguration().setAppBounds(new Rect(0, 0, 1000, 2000));
        newDisplay.getConfiguration().densityDpi = 300;

        mTask.getWindowConfiguration().setAppBounds(mStack.getDisplay().getBounds());
Loading