diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index c664ccba4ca78..8de8be8c20400 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -25,8 +25,8 @@ import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.KeyguardManager; +import android.app.WindowConfiguration; import android.compat.annotation.UnsupportedAppUsage; -import android.content.Context; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; @@ -59,12 +59,8 @@ import java.util.List; * an application window, excluding the system decorations. The application display area may * be smaller than the real display area because the system subtracts the space needed * for decor elements such as the status bar. Use {@link WindowMetrics#getBounds()} to query the - * application window bounds. - *
  • The real display area specifies the part of the display that contains content - * including the system decorations. Even so, the real display area may be smaller than the - * physical size of the display if the window manager is emulating a smaller display - * using (adb shell wm size). Use the following methods to query the - * real display area: {@link #getRealSize}, {@link #getRealMetrics}.
  • + * application window bounds. Generally, use {@link WindowManager#getCurrentWindowMetrics()} to + * query the metrics and perform UI-related actions. * *

    * A logical display does not necessarily represent a particular physical display device @@ -677,9 +673,9 @@ public final class Display { @UnsupportedAppUsage public DisplayAdjustments getDisplayAdjustments() { if (mResources != null) { - final DisplayAdjustments currentAdjustements = mResources.getDisplayAdjustments(); - if (!mDisplayAdjustments.equals(currentAdjustements)) { - mDisplayAdjustments = new DisplayAdjustments(currentAdjustements); + final DisplayAdjustments currentAdjustments = mResources.getDisplayAdjustments(); + if (!mDisplayAdjustments.equals(currentAdjustments)) { + mDisplayAdjustments = new DisplayAdjustments(currentAdjustments); } } @@ -1191,30 +1187,34 @@ public final class Display { } /** - * Gets the real size of the display without subtracting any window decor or - * applying any compatibility scale factors. + * Provides the largest {@link Point outSize} an app may expect in the current system state, + * without subtracting any window decor. *

    - * The size is adjusted based on the current rotation of the display. + * The size describes the largest potential area the window might occupy. The size is adjusted + * based on the current rotation of the display. *

    * The real size may be smaller than the physical size of the screen when the * window manager is emulating a smaller display (using adb shell wm size). - *

    - * In general, {@link #getRealSize(Point)} and {@link WindowManager#getMaximumWindowMetrics()} - * report the same bounds except that certain areas of the display may not be available to - * windows created in the {@link WindowManager}'s {@link Context}. - * - * For example, imagine a device which has a multi-task mode that limits windows to half of the - * screen. In this case, {@link WindowManager#getMaximumWindowMetrics()} reports the - * bounds of the screen half where the window is located, while {@link #getRealSize(Point)} - * still reports the bounds of the whole display. + *

    * * @param outSize Set to the real size of the display. - * - * @see WindowManager#getMaximumWindowMetrics() */ public void getRealSize(Point outSize) { synchronized (this) { updateDisplayInfoLocked(); + if (shouldReportMaxBounds()) { + final Rect bounds = mResources.getConfiguration() + .windowConfiguration.getMaxBounds(); + outSize.x = bounds.width(); + outSize.y = bounds.height(); + if (DEBUG) { + Log.d(TAG, "getRealSize determined from max bounds: " + outSize + + " for uid " + Process.myUid()); + } + // Skip adjusting by fixed rotation, since if it is necessary, the configuration + // should already reflect the expected rotation. + return; + } outSize.x = mDisplayInfo.logicalWidth; outSize.y = mDisplayInfo.logicalHeight; if (mMayAdjustByFixedRotation) { @@ -1224,9 +1224,11 @@ public final class Display { } /** - * Gets display metrics based on the real size of this display. + * Provides the largest {@link DisplayMetrics outMetrics} an app may expect in the current + * system state, without subtracting any window decor. *

    - * The size is adjusted based on the current rotation of the display. + * The size describes the largest potential area the window might occupy. The size is adjusted + * based on the current rotation of the display. *

    * The real size may be smaller than the physical size of the screen when the * window manager is emulating a smaller display (using adb shell wm size). @@ -1237,6 +1239,18 @@ public final class Display { public void getRealMetrics(DisplayMetrics outMetrics) { synchronized (this) { updateDisplayInfoLocked(); + if (shouldReportMaxBounds()) { + mDisplayInfo.getMaxBoundsMetrics(outMetrics, + CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, + mResources.getConfiguration()); + if (DEBUG) { + Log.d(TAG, "getRealMetrics determined from max bounds: " + outMetrics + + " for uid " + Process.myUid()); + } + // Skip adjusting by fixed rotation, since if it is necessary, the configuration + // should already reflect the expected rotation. + return; + } mDisplayInfo.getLogicalMetrics(outMetrics, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); if (mMayAdjustByFixedRotation) { @@ -1245,6 +1259,20 @@ public final class Display { } } + /** + * Determines if {@link WindowConfiguration#getMaxBounds()} should be reported as the + * display dimensions. The max bounds field may be smaller than the logical dimensions + * when apps need to be sandboxed. + * @return {@code true} when max bounds should be applied. + */ + private boolean shouldReportMaxBounds() { + if (mResources == null) { + return false; + } + final Configuration config = mResources.getConfiguration(); + return config != null && !config.windowConfiguration.getMaxBounds().isEmpty(); + } + /** * Gets the state of the display, such as whether it is on or off. * diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index d200a328773b9..7cabb04767bb1 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -24,6 +24,7 @@ import static android.view.DisplayInfoProto.LOGICAL_WIDTH; import static android.view.DisplayInfoProto.NAME; import android.annotation.Nullable; +import android.app.WindowConfiguration; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; @@ -605,11 +606,29 @@ public final class DisplayInfo implements Parcelable { getMetricsWithSize(outMetrics, ci, configuration, appWidth, appHeight); } + /** + * Populates {@code outMetrics} with details of the logical display. Bounds are limited + * by the logical size of the display. + * + * @param outMetrics the {@link DisplayMetrics} to be populated + * @param compatInfo the {@link CompatibilityInfo} to be applied + * @param configuration the {@link Configuration} + */ public void getLogicalMetrics(DisplayMetrics outMetrics, CompatibilityInfo compatInfo, Configuration configuration) { getMetricsWithSize(outMetrics, compatInfo, configuration, logicalWidth, logicalHeight); } + /** + * Similar to {@link #getLogicalMetrics}, but the limiting bounds are determined from + * {@link WindowConfiguration#getMaxBounds()} + */ + public void getMaxBoundsMetrics(DisplayMetrics outMetrics, CompatibilityInfo compatInfo, + Configuration configuration) { + Rect bounds = configuration.windowConfiguration.getMaxBounds(); + getMetricsWithSize(outMetrics, compatInfo, configuration, bounds.width(), bounds.height()); + } + public int getNaturalWidth() { return rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 ? logicalWidth : logicalHeight; diff --git a/core/tests/mockingcoretests/src/android/view/DisplayTests.java b/core/tests/mockingcoretests/src/android/view/DisplayTests.java new file mode 100644 index 0000000000000..5a3ea35b1194f --- /dev/null +++ b/core/tests/mockingcoretests/src/android/view/DisplayTests.java @@ -0,0 +1,527 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_90; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.display.DisplayManagerGlobal; +import android.util.DisplayMetrics; +import android.view.DisplayAdjustments.FixedRotationAdjustments; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.quality.Strictness; + +import java.util.function.Consumer; + +/** + * Tests for {@link Display}. + * + *

    Build/Install/Run: + * + * atest FrameworksMockingCoreTests:android.view.DisplayTests + */ +@RunWith(AndroidJUnit4.class) +public class DisplayTests { + + private static final int APP_WIDTH = 272; + private static final int APP_HEIGHT = 700; + // Tablet size device, ROTATION_0 corresponds to portrait. + private static final int LOGICAL_WIDTH = 700; + private static final int LOGICAL_HEIGHT = 1800; + + // Bounds of the app when the device is in portrait mode. + private static Rect sAppBoundsPortrait = buildAppBounds(LOGICAL_WIDTH, LOGICAL_HEIGHT); + private static Rect sAppBoundsLandscape = buildAppBounds(LOGICAL_HEIGHT, LOGICAL_WIDTH); + + private StaticMockitoSession mMockitoSession; + + private DisplayManagerGlobal mDisplayManagerGlobal; + private Context mApplicationContext; + private DisplayInfo mDisplayInfo = new DisplayInfo(); + + @Before + public void setupTests() { + mMockitoSession = mockitoSession() + .mockStatic(DisplayManagerGlobal.class) + .strictness(Strictness.LENIENT) + .startMocking(); + + // Ensure no adjustments are set before each test. + mApplicationContext = ApplicationProvider.getApplicationContext(); + DisplayAdjustments displayAdjustments = + mApplicationContext.getResources().getDisplayAdjustments(); + displayAdjustments.setFixedRotationAdjustments(null); + mApplicationContext.getResources().overrideDisplayAdjustments(null); + mApplicationContext.getResources().getConfiguration().windowConfiguration.setAppBounds( + null); + mApplicationContext.getResources().getConfiguration().windowConfiguration.setMaxBounds( + null); + mDisplayInfo.rotation = ROTATION_0; + + mDisplayManagerGlobal = mock(DisplayManagerGlobal.class); + doReturn(mDisplayInfo).when(mDisplayManagerGlobal).getDisplayInfo(anyInt()); + } + + @After + public void teardownTests() { + if (mMockitoSession != null) { + mMockitoSession.finishMocking(); + } + Mockito.framework().clearInlineMocks(); + } + + @Test + public void testConstructor_defaultDisplayAdjustments_matchesDisplayInfo() { + setDisplayInfoPortrait(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); + assertThat(display.getDisplayAdjustments()).isEqualTo( + DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); + DisplayInfo actualDisplayInfo = new DisplayInfo(); + display.getDisplayInfo(actualDisplayInfo); + verifyDisplayInfo(actualDisplayInfo, mDisplayInfo); + } + + @Test + public void testConstructor_defaultResources_matchesDisplayInfo() { + setDisplayInfoPortrait(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + assertThat(display.getDisplayAdjustments()).isEqualTo( + mApplicationContext.getResources().getDisplayAdjustments()); + DisplayInfo actualDisplayInfo = new DisplayInfo(); + display.getDisplayInfo(actualDisplayInfo); + verifyDisplayInfo(actualDisplayInfo, mDisplayInfo); + } + + @Test + public void testGetRotation_defaultDisplayAdjustments_rotationNotAdjusted() { + setDisplayInfoPortrait(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); + assertThat(display.getRotation()).isEqualTo(ROTATION_0); + } + + @Test + public void testGetRotation_displayAdjustmentsWithoutOverride_rotationNotAdjusted() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated, but no override is set. + DisplayAdjustments displayAdjustments = DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; + final FixedRotationAdjustments fixedRotationAdjustments = + new FixedRotationAdjustments(ROTATION_90, APP_WIDTH, APP_HEIGHT, + DisplayCutout.NO_CUTOUT); + displayAdjustments.setFixedRotationAdjustments(fixedRotationAdjustments); + // GIVEN display is constructed with display adjustments. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + displayAdjustments); + // THEN rotation is not adjusted since no override was set. + assertThat(display.getRotation()).isEqualTo(ROTATION_0); + } + + @Test + public void testGetRotation_resourcesWithoutOverride_rotationNotAdjusted() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated, but no override is set. + setFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_90); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN rotation is not adjusted since no override is set. + assertThat(display.getRotation()).isEqualTo(ROTATION_0); + } + + @Test + public void testGetRotation_resourcesWithOverrideDisplayAdjustments_rotationAdjusted() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated, and an override is set. + setOverrideFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_90); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN rotation is adjusted since an override is set. + assertThat(display.getRotation()).isEqualTo(ROTATION_90); + } + + @Test + public void testGetRealSize_defaultResourcesPortrait_matchesLogicalSize() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real size matches display orientation. + verifyRealSizeIsPortrait(display); + } + + @Test + public void testGetRealSize_defaultResourcesLandscape_matchesRotatedLogicalSize() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real size matches display orientation. + verifyRealSizeIsLandscape(display); + } + + @Test + public void testGetRealSize_defaultDisplayAdjustmentsPortrait_matchesLogicalSize() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); + // THEN real size matches display orientation. + verifyRealSizeIsPortrait(display); + } + + @Test + public void testGetRealSize_defaultDisplayAdjustmentsLandscape_matchesLogicalSize() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); + // THEN real size matches display orientation. + verifyRealSizeIsLandscape(display); + } + + @Test + public void testGetRealSize_resourcesPortraitWithFixedRotation_notRotatedLogicalSize() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated. + setFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_0); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real size matches display orientation. + verifyRealSizeIsLandscape(display); + } + + @Test + public void testGetRealSize_resourcesWithLandscapeFixedRotation_notRotatedLogicalSize() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated. + setFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_90); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real size matches display orientation. + verifyRealSizeIsPortrait(display); + } + + @Test + public void testGetRealSize_resourcesWithPortraitOverrideRotation_rotatedLogicalSize() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated, and an override is set. + setOverrideFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_0); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real size matches app orientation. + verifyRealSizeIsPortrait(display); + } + + @Test + public void testGetRealSize_resourcesWithLandscapeOverrideRotation_rotatedLogicalSize() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated, and an override is set. + setOverrideFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_90); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real size matches app orientation. + verifyRealSizeIsLandscape(display); + } + + @Test + public void testGetRealSize_resourcesPortraitSandboxed_matchesSandboxBounds() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + // GIVEN app is letterboxed. + setMaxBoundsSandboxedToMatchAppBounds(mApplicationContext.getResources(), + sAppBoundsPortrait); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real size matches app bounds. + verifyRealSizeMatchesApp(display, sAppBoundsPortrait); + } + + @Test + public void testGetRealSize_resourcesLandscapeSandboxed_matchesSandboxBounds() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + // GIVEN app is letterboxed. + setMaxBoundsSandboxedToMatchAppBounds(mApplicationContext.getResources(), + sAppBoundsLandscape); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real size matches app bounds. + verifyRealSizeMatchesApp(display, sAppBoundsLandscape); + } + + @Test + public void testGetRealMetrics_defaultResourcesPortrait_matchesLogicalSize() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real metrics matches display orientation. + verifyRealMetricsIsPortrait(display); + } + + @Test + public void testGetRealMetrics_defaultResourcesLandscape_matchesRotatedLogicalSize() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real metrics matches display orientation. + verifyRealMetricsIsLandscape(display); + } + + @Test + public void testGetRealMetrics_defaultDisplayAdjustmentsPortrait_matchesLogicalSize() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); + // THEN real metrics matches display orientation. + verifyRealMetricsIsPortrait(display); + } + + @Test + public void testGetRealMetrics_defaultDisplayAdjustmentsLandscape_matchesLogicalSize() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); + // THEN real metrics matches display orientation. + verifyRealMetricsIsLandscape(display); + } + + @Test + public void testGetRealMetrics_resourcesPortraitWithFixedRotation_notRotatedLogicalSize() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated. + setFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_0); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real metrics matches display orientation. + verifyRealMetricsIsLandscape(display); + } + + @Test + public void testGetRealMetrics_resourcesWithLandscapeFixedRotation_notRotatedLogicalSize() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated. + setFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_90); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real metrics matches display orientation. + verifyRealMetricsIsPortrait(display); + } + + @Test + public void testGetRealMetrics_resourcesWithPortraitOverrideRotation_rotatedLogicalSize() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated with an override. + setOverrideFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_0); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real metrics matches app orientation. + verifyRealMetricsIsPortrait(display); + } + + @Test + public void testGetRealMetrics_resourcesWithLandscapeOverrideRotation_rotatedLogicalSize() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + // GIVEN fixed rotation adjustments are rotated. + setOverrideFixedRotationAdjustments(mApplicationContext.getResources(), ROTATION_90); + // GIVEN display is constructed with default resources. + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real metrics matches app orientation. + verifyRealMetricsIsLandscape(display); + } + + @Test + public void testGetRealMetrics_resourcesPortraitSandboxed_matchesSandboxBounds() { + // GIVEN display is not rotated. + setDisplayInfoPortrait(mDisplayInfo); + // GIVEN app is letterboxed. + setMaxBoundsSandboxedToMatchAppBounds(mApplicationContext.getResources(), + sAppBoundsPortrait); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real metrics matches app bounds. + verifyRealMetricsMatchesApp(display, sAppBoundsPortrait); + } + + @Test + public void testGetRealMetrics_resourcesLandscapeSandboxed_matchesSandboxBounds() { + // GIVEN display is rotated. + setDisplayInfoLandscape(mDisplayInfo); + // GIVEN app is letterboxed. + setMaxBoundsSandboxedToMatchAppBounds(mApplicationContext.getResources(), + sAppBoundsLandscape); + final Display display = new Display(mDisplayManagerGlobal, DEFAULT_DISPLAY, mDisplayInfo, + mApplicationContext.getResources()); + // THEN real metrics matches app bounds. + verifyRealMetricsMatchesApp(display, sAppBoundsLandscape); + } + + // Given rotated display dimensions, calculate the letterboxed app bounds. + private static Rect buildAppBounds(int displayWidth, int displayHeight) { + final int midWidth = displayWidth / 2; + final int left = midWidth - (APP_WIDTH / 2); + final int right = midWidth + (APP_WIDTH / 2); + final int midHeight = displayHeight / 2; + // Coordinate system starts at top left. + final int top = midHeight - (APP_HEIGHT / 2); + final int bottom = midHeight + (APP_HEIGHT / 2); + return new Rect(left, top, right, bottom); + } + + private static void setDisplayInfoLandscape(DisplayInfo displayInfo) { + displayInfo.rotation = ROTATION_90; + // Flip width & height assignment since the device is rotated. + displayInfo.logicalWidth = LOGICAL_HEIGHT; + displayInfo.logicalHeight = LOGICAL_WIDTH; + } + + private static void setDisplayInfoPortrait(DisplayInfo displayInfo) { + displayInfo.rotation = ROTATION_0; + displayInfo.logicalWidth = LOGICAL_WIDTH; + displayInfo.logicalHeight = LOGICAL_HEIGHT; + } + + /** + * Set max bounds to be sandboxed to the app bounds, indicating the app is in + * size compat mode or letterbox. + */ + private static void setMaxBoundsSandboxedToMatchAppBounds(Resources resources, Rect appBounds) { + resources.getConfiguration().windowConfiguration.setMaxBounds(appBounds); + } + + /** + * Do not compare entire display info, since it is updated to match display the test is run on. + */ + private static void verifyDisplayInfo(DisplayInfo actual, DisplayInfo expected) { + assertThat(actual.displayId).isEqualTo(expected.displayId); + assertThat(actual.rotation).isEqualTo(expected.rotation); + assertThat(actual.logicalWidth).isEqualTo(LOGICAL_WIDTH); + assertThat(actual.logicalHeight).isEqualTo(LOGICAL_HEIGHT); + } + + private static void verifyRealSizeIsLandscape(Display display) { + Point size = new Point(); + display.getRealSize(size); + // Flip the width and height check since the device is rotated. + assertThat(size).isEqualTo(new Point(LOGICAL_HEIGHT, LOGICAL_WIDTH)); + } + + private static void verifyRealMetricsIsLandscape(Display display) { + DisplayMetrics metrics = new DisplayMetrics(); + display.getRealMetrics(metrics); + // Flip the width and height check since the device is rotated. + assertThat(metrics.widthPixels).isEqualTo(LOGICAL_HEIGHT); + assertThat(metrics.heightPixels).isEqualTo(LOGICAL_WIDTH); + } + + private static void verifyRealSizeIsPortrait(Display display) { + Point size = new Point(); + display.getRealSize(size); + assertThat(size).isEqualTo(new Point(LOGICAL_WIDTH, LOGICAL_HEIGHT)); + } + + private static void verifyRealMetricsIsPortrait(Display display) { + DisplayMetrics metrics = new DisplayMetrics(); + display.getRealMetrics(metrics); + assertThat(metrics.widthPixels).isEqualTo(LOGICAL_WIDTH); + assertThat(metrics.heightPixels).isEqualTo(LOGICAL_HEIGHT); + } + + private static void verifyRealSizeMatchesApp(Display display, Rect appBounds) { + Point size = new Point(); + display.getRealSize(size); + assertThat(size).isEqualTo(new Point(appBounds.width(), appBounds.height())); + } + + private static void verifyRealMetricsMatchesApp(Display display, Rect appBounds) { + DisplayMetrics metrics = new DisplayMetrics(); + display.getRealMetrics(metrics); + assertThat(metrics.widthPixels).isEqualTo(appBounds.width()); + assertThat(metrics.heightPixels).isEqualTo(appBounds.height()); + } + + private static FixedRotationAdjustments setOverrideFixedRotationAdjustments( + Resources resources, @Surface.Rotation int rotation) { + FixedRotationAdjustments fixedRotationAdjustments = + setFixedRotationAdjustments(resources, rotation); + resources.overrideDisplayAdjustments( + buildOverrideRotationAdjustments(fixedRotationAdjustments)); + return fixedRotationAdjustments; + } + + private static FixedRotationAdjustments setFixedRotationAdjustments(Resources resources, + @Surface.Rotation int rotation) { + final FixedRotationAdjustments fixedRotationAdjustments = + new FixedRotationAdjustments(rotation, APP_WIDTH, APP_HEIGHT, + DisplayCutout.NO_CUTOUT); + resources.getDisplayAdjustments().setFixedRotationAdjustments(fixedRotationAdjustments); + return fixedRotationAdjustments; + } + + private static Consumer buildOverrideRotationAdjustments( + FixedRotationAdjustments fixedRotationAdjustments) { + return consumedDisplayAdjustments + -> consumedDisplayAdjustments.setFixedRotationAdjustments(fixedRotationAdjustments); + } +} diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 222c9bdf2cb42..ac8a296123c6a 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1903,12 +1903,6 @@ "group": "WM_DEBUG_FOCUS_LIGHT", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "123161180": { - "message": "SEVER CHILDREN", - "level": "INFO", - "group": "WM_SHOW_TRANSACTIONS", - "at": "com\/android\/server\/wm\/WindowSurfaceController.java" - }, "140319294": { "message": "IME target changed within ActivityRecord", "level": "DEBUG", @@ -2143,6 +2137,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "332390227": { + "message": "Sandbox max bounds for uid %s to bounds %s due to letterboxing? %s mismatch with parent bounds? %s size compat mode %s", + "level": "DEBUG", + "group": "WM_DEBUG_CONFIGURATION", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "342460966": { "message": "DRAG %s: pos=(%d,%d)", "level": "INFO", @@ -2623,6 +2623,12 @@ "group": "WM_DEBUG_WINDOW_ORGANIZER", "at": "com\/android\/server\/wm\/WindowOrganizerController.java" }, + "910200295": { + "message": "Sandbox max bounds due to mismatched orientation with parent, to %s vs DisplayArea %s", + "level": "DEBUG", + "group": "WM_DEBUG_CONFIGURATION", + "at": "com\/android\/server\/wm\/Task.java" + }, "913494177": { "message": "removeAllWindowsIfPossible: removing win=%s", "level": "WARN", diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 4e359f2577c27..ecbc01f33fa22 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; import static android.app.ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND; import static android.app.ActivityOptions.ANIM_CLIP_REVEAL; @@ -81,6 +82,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.isFixedOrientationLandscape; import static android.content.pm.ActivityInfo.isFixedOrientationPortrait; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.res.Configuration.EMPTY; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; @@ -201,6 +203,7 @@ import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; import static com.android.server.wm.WindowContainerChildProto.ACTIVITY; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM; +import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_CONFIGURATION; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WINDOW_VERBOSE; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; @@ -266,6 +269,7 @@ import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; import android.os.storage.StorageManager; +import android.permission.PermissionManager; import android.service.dreams.DreamActivity; import android.service.dreams.DreamManagerInternal; import android.service.voice.IVoiceInteractionSession; @@ -6737,6 +6741,20 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // layout traversals. mConfigurationSeq = Math.max(++mConfigurationSeq, 1); getResolvedOverrideConfiguration().seq = mConfigurationSeq; + + // Sandbox max bounds by setting it to the app bounds, if activity is letterboxed or in + // size compat mode. + if (providesMaxBounds()) { + if (DEBUG_CONFIGURATION) { + ProtoLog.d(WM_DEBUG_CONFIGURATION, "Sandbox max bounds for uid %s to bounds %s " + + "due to letterboxing? %s mismatch with parent bounds? %s size compat " + + "mode %s", getUid(), + resolvedConfig.windowConfiguration.getBounds(), mLetterbox != null, + !matchParentBounds(), inSizeCompatMode()); + } + resolvedConfig.windowConfiguration + .setMaxBounds(resolvedConfig.windowConfiguration.getBounds()); + } } /** @@ -6920,6 +6938,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return super.getBounds(); } + @Override + public boolean providesMaxBounds() { + // System and SystemUI should always be able to access the physical display bounds, + // so do not provide it with the overridden maximum bounds. + // TODO(b/179179513) check WindowState#mOwnerCanAddInternalSystemWindow instead + if (getUid() == SYSTEM_UID || PermissionManager.checkPermission(INTERNAL_SYSTEM_WINDOW, + getPid(), info.applicationInfo.uid) == PERMISSION_GRANTED) { + return false; + } + // Max bounds should be sandboxed when this is letterboxed or in size compat mode. + return mLetterbox != null || !matchParentBounds() || inSizeCompatMode(); + } + @VisibleForTesting @Override Rect getAnimationBounds(int appRootTaskClipMode) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index f3f608bb90705..0406bc9db8731 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -77,6 +77,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.internal.policy.DecorView.DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP; import static com.android.internal.policy.DecorView.DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE; +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; @@ -144,6 +145,7 @@ import static com.android.server.wm.TaskProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN; import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; import static com.android.server.wm.WindowContainerChildProto.TASK; +import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_CONFIGURATION; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ROOT_TASK; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_MOVEMENT; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; @@ -2878,6 +2880,16 @@ class Task extends WindowContainer { // In FULLSCREEN mode, always start with empty bounds to indicate "fill parent". outBounds.setEmpty(); computeLetterboxBounds(outBounds, newParentConfig); + // Since the task is letterboxed due to mismatched orientation against its parent, + // sandbox max bounds to the app bounds. + if (!outBounds.isEmpty()) { + if (DEBUG_CONFIGURATION) { + ProtoLog.d(WM_DEBUG_CONFIGURATION, "Sandbox max bounds due to mismatched " + + "orientation with parent, to %s vs DisplayArea %s", outBounds, + getDisplayArea() != null ? getDisplayArea().getBounds() : "null"); + } + getResolvedOverrideConfiguration().windowConfiguration.setMaxBounds(outBounds); + } } /** Computes bounds for {@link WindowConfiguration#WINDOWING_MODE_FREEFORM}. */ 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 cc4d4eaa9e8b6..6f775cf301b51 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -39,6 +39,8 @@ import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; import static com.android.server.wm.Task.ActivityState.STOPPED; import static com.android.server.wm.WindowContainer.POSITION_TOP; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -117,13 +119,13 @@ public class SizeCompatTests extends WindowTestsBase { @Test public void testKeepBoundsWhenChangingFromFreeformToFullscreen() { removeGlobalMinSizeRestriction(); - // create freeform display and a freeform app + // Create landscape freeform display and a freeform app. DisplayContent display = new TestDisplayContent.Builder(mAtm, 2000, 1000) .setCanRotate(false) .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM).build(); setUpApp(display); - // Put app window into freeform and then make it a compat app. + // Put app window into portrait freeform and then make it a compat app. final Rect bounds = new Rect(100, 100, 400, 600); mTask.setBounds(bounds); prepareUnresizable(mActivity, -1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); @@ -136,7 +138,7 @@ public class SizeCompatTests extends WindowTestsBase { final int density = mActivity.getConfiguration().densityDpi; - // change display configuration to fullscreen + // Change display configuration to fullscreen. Configuration c = new Configuration(display.getRequestedOverrideConfiguration()); c.windowConfiguration.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); display.onRequestedOverrideConfigurationChanged(c); @@ -146,6 +148,8 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(bounds.width(), mActivity.getBounds().width()); assertEquals(bounds.height(), mActivity.getBounds().height()); assertEquals(density, mActivity.getConfiguration().densityDpi); + // Size compat mode is sandboxed at the activity level. + assertActivityMaxBoundsSandboxedForSizeCompat(); } @Test @@ -171,6 +175,12 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(appBounds.height(), appBounds.width() * aspectRatio, 0.5f /* delta */); // The decor height should be a part of the effective bounds. assertEquals(mActivity.getBounds().height(), appBounds.height() + notchHeight); + // Activity max bounds should be sandboxed; activity is letterboxed due to aspect ratio. + assertActivityMaxBoundsSandboxedForLetterbox(); + // Activity max bounds ignore notch, since an app can be shown past the notch (although app + // is currently limited by the notch). + assertThat(mActivity.getWindowConfiguration().getMaxBounds().height()) + .isEqualTo(displayBounds.height()); mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); assertFitted(); @@ -180,9 +190,17 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(appBounds.width(), appBounds.height() * aspectRatio, 0.5f /* delta */); // The notch is no longer on top. assertEquals(appBounds, mActivity.getBounds()); + // Activity max bounds are sandboxed. + assertActivityMaxBoundsSandboxedForLetterbox(); mActivity.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); assertFitted(); + // Activity max bounds should be sandboxed; activity is letterboxed due to aspect ratio. + assertActivityMaxBoundsSandboxedForLetterbox(); + // Activity max bounds ignore notch, since an app can be shown past the notch (although app + // is currently limited by the notch). + assertThat(mActivity.getWindowConfiguration().getMaxBounds().height()) + .isEqualTo(displayBounds.height()); } @Test @@ -210,6 +228,9 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(originalBounds.width(), mActivity.getBounds().width()); assertEquals(originalBounds.height(), mActivity.getBounds().height()); assertEquals(originalDpi, mActivity.getConfiguration().densityDpi); + // Activity is sandboxed; it is in size compat mode since it is not resizable and has a + // max aspect ratio. + assertActivityMaxBoundsSandboxedForSizeCompat(); assertScaled(); } @@ -217,11 +238,13 @@ public class SizeCompatTests extends WindowTestsBase { public void testFixedScreenBoundsWhenDisplaySizeChanged() { setUpDisplaySizeWithApp(1000, 2500); prepareUnresizable(mActivity, -1f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + final DisplayContent display = mActivity.mDisplayContent; assertFitted(); + // Activity and task inherit bounds from TaskDisplayArea, since not sandboxed. + assertMaxBoundsInheritDisplayAreaBounds(); final Rect origBounds = new Rect(mActivity.getBounds()); final Rect currentBounds = mActivity.getWindowConfiguration().getBounds(); - final DisplayContent display = mActivity.mDisplayContent; // Change the size of current display. resizeDisplay(display, 1000, 2000); @@ -238,6 +261,8 @@ public class SizeCompatTests extends WindowTestsBase { // The position of configuration bounds should be the same as compat bounds. assertEquals(mActivity.getBounds().left, currentBounds.left); assertEquals(mActivity.getBounds().top, currentBounds.top); + // Activity is sandboxed to the offset size compat bounds. + assertActivityMaxBoundsSandboxedForSizeCompat(); // Change display size to a different orientation resizeDisplay(display, 2000, 1000); @@ -246,6 +271,8 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(origBounds.height(), currentBounds.height()); assertEquals(ORIENTATION_LANDSCAPE, display.getConfiguration().orientation); assertEquals(Configuration.ORIENTATION_PORTRAIT, mActivity.getConfiguration().orientation); + // Activity is sandboxed to the offset size compat bounds. + assertActivityMaxBoundsSandboxedForSizeCompat(); // The previous resize operation doesn't consider the rotation change after size changed. // These setups apply the requested orientation to rotation as real case that the top fixed @@ -265,6 +292,8 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(origBounds.height(), currentBounds.height()); assertEquals(offsetX, currentBounds.left); assertScaled(); + // Activity is sandboxed due to size compat mode. + assertActivityMaxBoundsSandboxedForSizeCompat(); } @Test @@ -280,6 +309,8 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(bounds.width(), bounds.height() * maxAspect, 0.0001f /* delta */); // The position should be horizontal centered. assertEquals((displayWidth - bounds.width()) / 2, bounds.left); + // Activity max bounds should be sandboxed since it is letterboxed. + assertActivityMaxBoundsSandboxedForLetterbox(); mActivity.mDisplayContent.setImeLayeringTarget(addWindowToActivity(mActivity)); // Make sure IME cannot attach to the app, otherwise IME window will also be shifted. @@ -291,6 +322,8 @@ public class SizeCompatTests extends WindowTestsBase { // It should keep non-attachable because the resolved bounds will be computed according to // the aspect ratio that won't match its parent bounds. assertFalse(mActivity.mDisplayContent.isImeAttachedToApp()); + // Activity max bounds should be sandboxed since it is letterboxed. + assertActivityMaxBoundsSandboxedForLetterbox(); } @Test @@ -316,14 +349,13 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - public void testMoveToDifferentOrientDisplay() { + public void testMoveToDifferentOrientationDisplay() { setUpDisplaySizeWithApp(1000, 2500); prepareUnresizable(mActivity, -1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); assertFitted(); - final Rect configBounds = mActivity.getWindowConfiguration().getBounds(); - final int origWidth = configBounds.width(); - final int origHeight = configBounds.height(); + final Rect currentBounds = mActivity.getWindowConfiguration().getBounds(); + final Rect originalBounds = new Rect(mActivity.getWindowConfiguration().getBounds()); final int notchHeight = 100; final DisplayContent newDisplay = new TestDisplayContent.Builder(mAtm, 2000, 1000) @@ -332,37 +364,44 @@ public class SizeCompatTests extends WindowTestsBase { // Move the non-resizable activity to the new display. mTask.reparent(newDisplay.getDefaultTaskDisplayArea(), true /* onTop */); // The configuration bounds [820, 0 - 1820, 2500] should keep the same. - assertEquals(origWidth, configBounds.width()); - assertEquals(origHeight, configBounds.height()); + assertEquals(originalBounds.width(), currentBounds.width()); + assertEquals(originalBounds.height(), currentBounds.height()); assertScaled(); + // Activity max bounds are sandboxed due to size compat mode on the new display. + assertActivityMaxBoundsSandboxedForSizeCompat(); final Rect newDisplayBounds = newDisplay.getWindowConfiguration().getBounds(); // The scaled bounds should exclude notch area (1000 - 100 == 360 * 2500 / 1000 = 900). assertEquals(newDisplayBounds.height() - notchHeight, - (int) ((float) mActivity.getBounds().width() * origHeight / origWidth)); + (int) ((float) mActivity.getBounds().width() * originalBounds.height() + / originalBounds.width())); // Recompute the natural configuration in the new display. mActivity.clearSizeCompatMode(); mActivity.ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */); // 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(), configBounds.height()); - assertEquals(configBounds.height() * newDisplayBounds.height() / newDisplayBounds.width(), - configBounds.width()); + assertEquals(newDisplayBounds.height(), currentBounds.height()); + assertEquals(currentBounds.height() * newDisplayBounds.height() / newDisplayBounds.width(), + currentBounds.width()); assertFitted(); // The appBounds should be [200, 100 - 700, 1000]. final Rect appBounds = mActivity.getWindowConfiguration().getAppBounds(); - assertEquals(configBounds.width(), appBounds.width()); - assertEquals(configBounds.height() - notchHeight, appBounds.height()); + assertEquals(currentBounds.width(), appBounds.width()); + assertEquals(currentBounds.height() - notchHeight, appBounds.height()); + // Task max bounds are sandboxed due to letterboxing from orientation mismatch with display. + assertTaskMaxBoundsSandboxed(); } @Test - public void testFixedOrientRotateCutoutDisplay() { + public void testFixedOrientationRotateCutoutDisplay() { // Create a display with a notch/cutout final int notchHeight = 60; - setUpApp(new TestDisplayContent.Builder(mAtm, 1000, 2500) + final int width = 1000; + setUpApp(new TestDisplayContent.Builder(mAtm, width, 2500) .setNotch(notchHeight).build()); - // Bounds=[0, 0 - 1000, 1460], AppBounds=[0, 60 - 1000, 1460]. + // Bounds=[0, 0 - 1000, 1400], AppBounds=[0, 60 - 1000, 1460]. + final float maxAspect = 1.4f; prepareUnresizable(mActivity, 1.4f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); final Rect currentBounds = mActivity.getWindowConfiguration().getBounds(); @@ -370,6 +409,11 @@ public class SizeCompatTests extends WindowTestsBase { final Rect origBounds = new Rect(currentBounds); final Rect origAppBounds = new Rect(appBounds); + // Activity is sandboxed, and bounds include the area consumed by the notch. + assertActivityMaxBoundsSandboxedForLetterbox(); + assertThat(mActivity.getConfiguration().windowConfiguration.getMaxBounds().height()) + .isEqualTo(Math.round(width * maxAspect) + notchHeight); + // Although the activity is fixed orientation, force rotate the display. rotateDisplay(mActivity.mDisplayContent, ROTATION_270); assertEquals(ROTATION_270, mTask.getWindowConfiguration().getRotation()); @@ -385,10 +429,13 @@ public class SizeCompatTests extends WindowTestsBase { // The position in configuration should be global coordinates. assertEquals(mActivity.getBounds().left, currentBounds.left); assertEquals(mActivity.getBounds().top, currentBounds.top); + + // Activity max bounds are sandboxed due to size compat mode. + assertActivityMaxBoundsSandboxedForSizeCompat(); } @Test - public void testFixedAspOrientChangeOrient() { + public void testFixedAspectRatioOrientationChangeOrientation() { setUpDisplaySizeWithApp(1000, 2500); final float maxAspect = 1.4f; @@ -400,6 +447,8 @@ public class SizeCompatTests extends WindowTestsBase { final Rect originalAppBounds = new Rect(mActivity.getWindowConfiguration().getAppBounds()); assertEquals((int) (originalBounds.width() * maxAspect), originalBounds.height()); + // Activity is sandboxed due to fixed aspect ratio. + assertActivityMaxBoundsSandboxedForLetterbox(); // Change the fixed orientation. mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); @@ -411,6 +460,8 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.getWindowConfiguration().getAppBounds().height()); assertEquals(originalAppBounds.height(), mActivity.getWindowConfiguration().getAppBounds().width()); + // Activity is sandboxed due to fixed aspect ratio. + assertActivityMaxBoundsSandboxedForLetterbox(); } @Test @@ -459,6 +510,8 @@ public class SizeCompatTests extends WindowTestsBase { // restarted and the override configuration won't be cleared. verify(mActivity, never()).restartProcessIfVisible(); assertScaled(); + // Activity max bounds are sandboxed due to size compat mode, even if is not visible. + assertActivityMaxBoundsSandboxedForSizeCompat(); // Change display density display.mBaseDisplayDensity = (int) (0.7f * display.mBaseDisplayDensity); @@ -533,12 +586,16 @@ public class SizeCompatTests extends WindowTestsBase { // in multi-window mode. mTask.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM); assertFalse(activity.shouldUseSizeCompatMode()); + // Activity and task should not be sandboxed. + assertMaxBoundsInheritDisplayAreaBounds(); // The non-resizable activity should not be size compat because the display support // changing windowing mode from fullscreen to freeform. mTask.mDisplayContent.setDisplayWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM); mTask.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); assertFalse(activity.shouldUseSizeCompatMode()); + // Activity and task should not be sandboxed. + assertMaxBoundsInheritDisplayAreaBounds(); } @Test @@ -602,6 +659,9 @@ public class SizeCompatTests extends WindowTestsBase { // be transparent. assertFalse(displayPolicy.isFullyTransparentAllowed(w, TYPE_STATUS_BAR)); + // Activity is sandboxed. + assertActivityMaxBoundsSandboxedForLetterbox(); + // Make the activity fill the display. prepareUnresizable(mActivity, 10 /* maxAspect */, SCREEN_ORIENTATION_LANDSCAPE); w.mWinAnimator.mDrawState = WindowStateAnimator.HAS_DRAWN; @@ -611,6 +671,7 @@ public class SizeCompatTests extends WindowTestsBase { // The letterbox should only cover the notch area, so status bar can be transparent. assertEquals(new Rect(notchHeight, 0, 0, 0), mActivity.getLetterboxInsets()); assertTrue(displayPolicy.isFullyTransparentAllowed(w, TYPE_STATUS_BAR)); + assertActivityMaxBoundsSandboxedForLetterbox(); } @Test @@ -635,6 +696,8 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(mTask.isTaskLetterboxed()); assertFalse(mActivity.inSizeCompatMode()); assertEquals(taskBounds, activityBounds); + // Activity inherits max bounds from task, since sandboxing applied to task. + assertTaskMaxBoundsSandboxed(); // Task bounds should be 700x1400 with the ratio as the display. assertEquals(displayBounds.height(), taskBounds.height()); @@ -665,6 +728,8 @@ public class SizeCompatTests extends WindowTestsBase { assertScaled(); assertEquals(activityBounds.width(), newActivityBounds.width()); assertEquals(activityBounds.height(), newActivityBounds.height()); + // Activity max bounds are sandboxed due to size compat mode. + assertActivityMaxBoundsSandboxedForSizeCompat(); } @Test @@ -676,29 +741,30 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait fixed app without max aspect. prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); - Rect displayBounds = new Rect(mActivity.mDisplayContent.getBounds()); - Rect activityBounds = new Rect(mActivity.getBounds()); - // App should launch in fullscreen. assertFalse(mTask.isTaskLetterboxed()); assertFalse(mActivity.inSizeCompatMode()); - assertEquals(displayBounds, activityBounds); + // Activity and task inherit max bounds from TaskDisplayArea. + assertMaxBoundsInheritDisplayAreaBounds(); // Rotate display to landscape. rotateDisplay(mActivity.mDisplayContent, ROTATION_90); - displayBounds = new Rect(mActivity.mDisplayContent.getBounds()); - activityBounds = new Rect(mActivity.getBounds()); - assertTrue(displayBounds.width() > displayBounds.height()); + final Rect rotatedDisplayBounds = new Rect(mActivity.mDisplayContent.getBounds()); + final Rect rotatedActivityBounds = new Rect(mActivity.getBounds()); + assertTrue(rotatedDisplayBounds.width() > rotatedDisplayBounds.height()); // App should be in size compat. assertFalse(mTask.isTaskLetterboxed()); assertScaled(); + assertThat(mActivity.inSizeCompatMode()).isTrue(); + // Activity max bounds are sandboxed due to size compat mode. + assertActivityMaxBoundsSandboxedForSizeCompat(); // App bounds should be 700x1400 with the ratio as the display. - assertEquals(displayBounds.height(), activityBounds.height()); - assertEquals(displayBounds.height() * displayBounds.height() / displayBounds.width(), - activityBounds.width()); + assertEquals(rotatedDisplayBounds.height(), rotatedActivityBounds.height()); + assertEquals(rotatedDisplayBounds.height() * rotatedDisplayBounds.height() + / rotatedDisplayBounds.width(), rotatedActivityBounds.width()); } @Test @@ -731,14 +797,17 @@ public class SizeCompatTests extends WindowTestsBase { final Rect displayBounds = new Rect(display.getBounds()); final Rect taskBounds = new Rect(mTask.getBounds()); final Rect newActivityBounds = new Rect(newActivity.getBounds()); + final float displayAspectRatio = (float) displayBounds.height() / displayBounds.width(); // Task and app bounds should be 700x1400 with the ratio as the display. assertTrue(mTask.isTaskLetterboxed()); assertFalse(newActivity.inSizeCompatMode()); assertEquals(taskBounds, newActivityBounds); assertEquals(displayBounds.height(), taskBounds.height()); - assertEquals(displayBounds.height() * displayBounds.height() / displayBounds.width(), - taskBounds.width()); + assertThat(taskBounds.width()) + .isEqualTo(Math.round(displayBounds.height() * displayAspectRatio)); + // Task max bounds are sandboxed due to letterbox, with the ratio of the display. + assertTaskMaxBoundsSandboxed(); } @Test @@ -778,6 +847,14 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(displayBounds.height(), taskBounds.height()); assertEquals((long) Math.rint(taskBounds.height() / newActivity.info.maxAspectRatio), taskBounds.width()); + // New activity max bounds are sandboxed due to letterbox. + assertThat(newActivity.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(taskBounds); + // Task max bounds are sandboxed due to letterbox, with the ratio of the display. + assertThat(mTask.getConfiguration().windowConfiguration.getMaxBounds().height()) + .isEqualTo(displayBounds.height()); + assertThat(mTask.getConfiguration().windowConfiguration.getMaxBounds().width()) + .isEqualTo(Math.round(displayBounds.height() / newActivity.info.maxAspectRatio)); // App bounds should be fullscreen in Task bounds. assertFalse(newActivity.inSizeCompatMode()); @@ -806,6 +883,9 @@ public class SizeCompatTests extends WindowTestsBase { assertFalse(mTask.isTaskLetterboxed()); assertScaled(); assertEquals(mTask.getLastTaskBoundsComputeActivity(), mActivity); + assertThat(mActivity.inSizeCompatMode()).isTrue(); + // Activity max bounds are sandboxed due to size compat mode. + assertActivityMaxBoundsSandboxedForSizeCompat(); final Rect activityBounds = new Rect(mActivity.getBounds()); mTask.resumeTopActivityUncheckedLocked(null /* prev */, null /* options */); @@ -816,6 +896,8 @@ public class SizeCompatTests extends WindowTestsBase { assertScaled(); assertEquals(mTask.getLastTaskBoundsComputeActivity(), mActivity); assertEquals(activityBounds, mActivity.getBounds()); + // Activity max bounds are sandboxed due to size compat. + assertActivityMaxBoundsSandboxedForSizeCompat(); } @Test @@ -831,6 +913,7 @@ public class SizeCompatTests extends WindowTestsBase { // In Task letterbox assertTrue(mTask.isTaskLetterboxed()); assertFalse(mActivity.inSizeCompatMode()); + assertTaskMaxBoundsSandboxed(); // Rotate display to portrait. rotateDisplay(display, ROTATION_90); @@ -838,6 +921,7 @@ public class SizeCompatTests extends WindowTestsBase { // App should be in size compat. assertFalse(mTask.isTaskLetterboxed()); assertScaled(); + assertActivityMaxBoundsSandboxedForSizeCompat(); // Rotate display to landscape. rotateDisplay(display, ROTATION_180); @@ -845,6 +929,7 @@ public class SizeCompatTests extends WindowTestsBase { // In Task letterbox assertTrue(mTask.isTaskLetterboxed()); assertFalse(mActivity.inSizeCompatMode()); + assertTaskMaxBoundsSandboxed(); } @Test @@ -862,20 +947,26 @@ public class SizeCompatTests extends WindowTestsBase { // In Task letterbox assertTrue(mTask.isTaskLetterboxed()); assertFalse(mActivity.inSizeCompatMode()); + // Task is letterboxed due to mismatched orientation request. + assertTaskMaxBoundsSandboxed(); - // Rotate display to portrait. + // Rotate display to landscape. rotateDisplay(display, ROTATION_90); // App should be in size compat. assertFalse(mTask.isTaskLetterboxed()); assertScaled(); + // Activity max bounds are sandboxed due to unresizable app. + assertActivityMaxBoundsSandboxedForSizeCompat(); - // Rotate display to landscape. + // Rotate display to portrait. rotateDisplay(display, ROTATION_180); // In Task letterbox assertTrue(mTask.isTaskLetterboxed()); assertFalse(mActivity.inSizeCompatMode()); + // Task is letterboxed, as in first case. + assertTaskMaxBoundsSandboxed(); } @Test @@ -892,12 +983,18 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(ORIENTATION_LANDSCAPE, display.getConfiguration().orientation); assertEquals(2800, displayBounds.width()); assertEquals(1400, displayBounds.height()); - taskDisplayArea.setBounds(0, 0, 2400, 1000); + Rect displayAreaBounds = new Rect(0, 0, 2400, 1000); + taskDisplayArea.setBounds(displayAreaBounds); final Rect activityBounds = new Rect(mActivity.getBounds()); assertFalse(mActivity.inSizeCompatMode()); assertEquals(2400, activityBounds.width()); assertEquals(1000, activityBounds.height()); + // Task and activity maximum bounds inherit from TaskDisplayArea bounds. + assertThat(mActivity.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(displayAreaBounds); + assertThat(mTask.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(displayAreaBounds); } @Test @@ -1045,6 +1142,48 @@ public class SizeCompatTests extends WindowTestsBase { assertFalse(mActivity.hasSizeCompatBounds()); } + /** Asserts both the activity and task max bounds inherit from the TaskDisplayArea. */ + private void assertMaxBoundsInheritDisplayAreaBounds() { + final Rect taskDisplayAreaBounds = mTask.getDisplayArea().getBounds(); + assertThat(mActivity.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(taskDisplayAreaBounds); + assertThat(mTask.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(taskDisplayAreaBounds); + } + + /** + * Asserts task-level letterboxing, so both activity and task max bounds + * are sandboxed to the letterbox bounds. + */ + private void assertTaskMaxBoundsSandboxed() { + // Activity inherits max bounds from task, since sandboxing applied to task. + assertThat(mActivity.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(mTask.getBounds()); + // Task max bounds are sandboxed due to letterbox. + assertThat(mTask.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(mTask.getBounds()); + } + + /** Asserts activity-level size compat mode, so only activity max bounds are sandboxed. */ + private void assertActivityMaxBoundsSandboxedForSizeCompat() { + // Activity max bounds are sandboxed due to size compat mode. + assertThat(mActivity.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(mActivity.getWindowConfiguration().getBounds()); + // Task inherits max bounds from display. + assertThat(mTask.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(mTask.getDisplayContent().getBounds()); + } + + /** Asserts activity-level letterboxing, so only activity max bounds are sandboxed. */ + private void assertActivityMaxBoundsSandboxedForLetterbox() { + // Activity is sandboxed due to fixed aspect ratio. + assertThat(mActivity.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(mActivity.getBounds()); + // Task inherits bounds from display. + assertThat(mTask.getConfiguration().windowConfiguration.getMaxBounds()) + .isEqualTo(mTask.getDisplayContent().getBounds()); + } + static Configuration rotateDisplay(DisplayContent display, int rotation) { final Configuration c = new Configuration(); display.getDisplayRotation().setRotation(rotation); 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 ae85ceb729587..d71993df86020 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -136,6 +136,11 @@ class TestDisplayContent extends DisplayContent { final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId, mInfo, DEFAULT_DISPLAY_ADJUSTMENTS); final TestDisplayContent newDisplay = createInternal(display); + // Ensure letterbox aspect ratio is not overridden on any device target. + // {@link com.android.internal.R.dimen.config_taskLetterboxAspectRatio}, provided by + // the below method, is set on some device form factors. + mService.mWindowManager.setTaskLetterboxAspectRatio(0); + // disable the normal system decorations final DisplayPolicy displayPolicy = newDisplay.getDisplayPolicy(); spyOn(displayPolicy);