From 0177507be7eb04af6e4c1bcb987650dc203158c6 Mon Sep 17 00:00:00 2001 From: Evan Rosky Date: Wed, 11 Sep 2019 17:28:07 -0700 Subject: [PATCH] Separate and add more SizeCompat tests; add Display Builder The number of SizeCompat tests is growing so this separates them out into their own class. Additionally, it streamlines display and activity creation for the variety of scenarios that need testing and facilitates consistent display creation for tests (previous code would create displays based on the device display which meant the tests would potentially test different things on different devices). Also added a Builder to TestActivityDisplay. This makes it possible to create displays with/without notch and custom windowingmode/rotatibility/density/etc. Also fixed a small bug where aspect-ratio was calculated incorrectly. Some SizeCompat tests were added too: testFixedAspOrientChangeOrient to verify that a sizecompat app can still change fixed orientation properly testMoveToDifferentOrientDisplay to verify that a sizecompat app properly moves between different non-rotateble displays testLetterboxFullscreenBounds to sanity check that sizecompat apps start properly when letterboxed by policy testFixedOrientRotateCutoutDisplay to verify that a sizecompat app stays fixed when on a rotating display with cutout. Bug: 138594779 Test: this Change-Id: Ic78374ae9e58297c609349c382efc6738521872c --- .../com/android/server/wm/ActivityRecord.java | 3 +- .../server/wm/ActivityDisplayTests.java | 55 --- .../server/wm/ActivityRecordTests.java | 267 ----------- .../server/wm/DisplayContentTests.java | 6 +- .../android/server/wm/SizeCompatTests.java | 426 ++++++++++++++++++ .../server/wm/TestActivityDisplay.java | 95 ++++ 6 files changed, 523 insertions(+), 329 deletions(-) create mode 100644 services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 3d41608763cb6..ad93b7ee45f32 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -6171,8 +6171,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // The rest of the condition is that only one side is smaller than the parent, but it still // needs to exclude the cases where the size is limited by the fixed aspect ratio. if (info.maxAspectRatio > 0) { - final float aspectRatio = (0.5f + Math.max(appWidth, appHeight)) - / Math.min(appWidth, appHeight); + final float aspectRatio = Math.max(appWidth, appHeight) / Math.min(appWidth, appHeight); if (aspectRatio >= info.maxAspectRatio) { // The current size has reached the max aspect ratio. return false; diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityDisplayTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityDisplayTests.java index 9df7b4576427d..864f94678797e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityDisplayTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityDisplayTests.java @@ -38,12 +38,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.when; -import android.app.TaskStackListener; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.os.IBinder; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; @@ -51,10 +46,6 @@ import androidx.test.filters.SmallTest; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.ArrayList; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - /** * Tests for the {@link ActivityDisplay} class. * @@ -331,50 +322,4 @@ public class ActivityDisplayTests extends ActivityTestsBase { verify(mSupervisor).removeTaskByIdLocked(eq(task1.mTaskId), anyBoolean(), anyBoolean(), any()); } - - /** - * Ensures that {@link TaskStackListener} can receive callback about the activity in size - * compatibility mode. - */ - @Test - public void testHandleActivitySizeCompatMode() throws Exception { - final ActivityDisplay display = mRootActivityContainer.getDefaultDisplay(); - final ActivityRecord activity = createFullscreenStackWithSimpleActivityAt( - display).topRunningActivityLocked(); - activity.setState(ActivityStack.ActivityState.RESUMED, "testHandleActivitySizeCompatMode"); - when(activity.getRequestedOrientation()).thenReturn( - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - activity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE; - activity.info.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; - activity.visible = true; - activity.ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */); - - final ArrayList> resultWrapper = new ArrayList<>(); - mService.getTaskChangeNotificationController().registerTaskStackListener( - new TaskStackListener() { - @Override - public void onSizeCompatModeActivityChanged(int displayId, - IBinder activityToken) { - resultWrapper.get(0).complete(activityToken); - } - }); - - resultWrapper.add(new CompletableFuture<>()); - - // resize the display to exercise size-compat mode - final DisplayContent displayContent = display.mDisplayContent; - displayContent.mBaseDisplayHeight = (int) (0.8f * displayContent.mBaseDisplayHeight); - Configuration c = new Configuration(); - displayContent.computeScreenConfiguration(c); - display.onRequestedOverrideConfigurationChanged(c); - - assertEquals(activity.appToken, resultWrapper.get(0).get(2, TimeUnit.SECONDS)); - - // 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); - - assertNull(resultWrapper.get(0).get(2, TimeUnit.SECONDS)); - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index c51a46a76f4cf..ac1da7c5a2aa9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -18,24 +18,18 @@ package com.android.server.wm; import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT; -import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.os.Process.NOBODY_UID; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.Surface.ROTATION_0; -import static android.view.Surface.ROTATION_90; import static android.view.WindowManager.TRANSIT_TASK_CLOSE; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; @@ -61,18 +55,13 @@ 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; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.when; -import android.app.ActivityManager; -import android.app.ActivityManagerInternal; import android.app.ActivityOptions; -import android.app.WindowConfiguration; import android.app.servertransaction.ActivityConfigurationChangeItem; import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.PauseActivityItem; @@ -80,13 +69,11 @@ import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Rect; import android.os.Bundle; import android.os.PersistableBundle; import android.platform.test.annotations.Presubmit; import android.util.MergedConfiguration; import android.util.MutableBoolean; -import android.view.DisplayInfo; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner.Stub; import android.view.RemoteAnimationAdapter; @@ -218,23 +205,6 @@ public class ActivityRecordTests extends ActivityTestsBase { assertFalse(activity.canBeLaunchedOnDisplay(DEFAULT_DISPLAY + 1)); } - @Test - public void testRestartProcessIfVisible() { - doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity); - mActivity.visible = true; - mActivity.setSavedState(null /* savedState */); - mActivity.setState(ActivityStack.ActivityState.RESUMED, "testRestart"); - prepareFixedAspectRatioUnresizableActivity(); - - final Rect originalOverrideBounds = new Rect(mActivity.getBounds()); - setupDisplayAndParentSize(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()); - assertNotEquals(originalOverrideBounds, mActivity.getBounds()); - } - @Test public void testsApplyOptionsLocked() { ActivityOptions activityOptions = ActivityOptions.makeBasic(); @@ -483,214 +453,6 @@ public class ActivityRecordTests extends ActivityTestsBase { assertEquals(STARTED, mActivity.getState()); } - @Test - public void testSizeCompatMode_KeepBoundsWhenChangingFromFreeformToFullscreen() { - setupDisplayContentForCompatDisplayInsets(); - - // put display in freeform mode - ActivityDisplay display = mActivity.getDisplay(); - final Configuration c = new Configuration(display.getRequestedOverrideConfiguration()); - c.windowConfiguration.setBounds(new Rect(0, 0, 2000, 1000)); - c.densityDpi = 300; - c.windowConfiguration.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM); - display.onRequestedOverrideConfigurationChanged(c); - - // launch compat activity in freeform and store bounds - when(mActivity.getRequestedOrientation()).thenReturn( - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - mTask.getRequestedOverrideConfiguration().orientation = Configuration.ORIENTATION_PORTRAIT; - mTask.setBounds(100, 100, 400, 600); - mActivity.info.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; - mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE; - mActivity.visible = true; - ensureActivityConfiguration(); - - final Rect bounds = new Rect(mActivity.getBounds()); - final int density = mActivity.getConfiguration().densityDpi; - - // change display configuration to fullscreen - c.windowConfiguration.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); - display.onRequestedOverrideConfigurationChanged(c); - - // check if dimensions stay the same - assertTrue(mActivity.inSizeCompatMode()); - assertEquals(bounds.width(), mActivity.getBounds().width()); - assertEquals(bounds.height(), mActivity.getBounds().height()); - assertEquals(density, mActivity.getConfiguration().densityDpi); - assertEquals(WindowConfiguration.WINDOWING_MODE_FULLSCREEN, mActivity.getWindowingMode()); - } - - @Test - public void testSizeCompatMode_FixedAspectRatioBoundsWithDecor() { - setupDisplayContentForCompatDisplayInsets(); - final int decorHeight = 200; // e.g. The device has cutout. - final DisplayPolicy policy = setupDisplayAndParentSize(600, 800).getDisplayPolicy(); - spyOn(policy); - doAnswer(invocationOnMock -> { - final int rotation = invocationOnMock.getArgument(0); - final Rect insets = invocationOnMock.getArgument(4); - if (rotation == ROTATION_0) { - insets.top = decorHeight; - } else if (rotation == ROTATION_90) { - insets.left = decorHeight; - } - return null; - }).when(policy).getNonDecorInsetsLw(anyInt() /* rotation */, anyInt() /* width */, - anyInt() /* height */, any() /* displayCutout */, any() /* outInsets */); - // set appBounds to incorporate decor - final Configuration c = - new Configuration(mStack.getDisplay().getRequestedOverrideConfiguration()); - c.windowConfiguration.getAppBounds().top = decorHeight; - mStack.getDisplay().onRequestedOverrideConfigurationChanged(c); - - doReturn(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) - .when(mActivity).getRequestedOrientation(); - mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE; - mActivity.info.minAspectRatio = mActivity.info.maxAspectRatio = 1; - mActivity.visible = true; - ensureActivityConfiguration(); - // The parent configuration doesn't change since the first resolved configuration, so the - // activity shouldn't be in the size compatibility mode. - assertFalse(mActivity.inSizeCompatMode()); - - 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().windowConfiguration.setRotation(ROTATION_90); - 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 Rect newDisplayBounds = new Rect(0, 0, 1000, 2000); - DisplayInfo info = new DisplayInfo(); - mService.mContext.getDisplay().getDisplayInfo(info); - info.logicalWidth = newDisplayBounds.width(); - info.logicalHeight = newDisplayBounds.height(); - info.logicalDensityDpi = 300; - - final ActivityDisplay newDisplay = - addNewActivityDisplayAt(info, ActivityDisplay.POSITION_TOP); - - final Configuration c = - new Configuration(mStack.getDisplay().getRequestedOverrideConfiguration()); - c.densityDpi = 200; - mStack.getDisplay().onRequestedOverrideConfigurationChanged(c); - mActivity = new ActivityBuilder(mService) - .setTask(mTask) - .setResizeMode(RESIZE_MODE_UNRESIZEABLE) - .setMaxAspectRatio(1.5f) - .build(); - mActivity.visible = true; - - final Rect originalBounds = new Rect(mActivity.getBounds()); - final int originalDpi = mActivity.getConfiguration().densityDpi; - - // Move the non-resizable activity to the new display. - mStack.reparent(newDisplay.mDisplayContent, true /* onTop */); - - assertEquals(originalBounds.width(), - mActivity.getWindowConfiguration().getBounds().width()); - assertEquals(originalBounds.height(), - mActivity.getWindowConfiguration().getBounds().height()); - assertEquals(originalDpi, mActivity.getConfiguration().densityDpi); - assertTrue(mActivity.inSizeCompatMode()); - } - - @Test - public void testSizeCompatMode_FixedScreenBoundsWhenDisplaySizeChanged() { - setupDisplayContentForCompatDisplayInsets(); - when(mActivity.getRequestedOrientation()).thenReturn( - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - mTask.getWindowConfiguration().setAppBounds(mStack.getDisplay().getBounds()); - mTask.getRequestedOverrideConfiguration().orientation = Configuration.ORIENTATION_PORTRAIT; - mActivity.info.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; - mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE; - mActivity.visible = true; - - ensureActivityConfiguration(); - final Rect originalBounds = new Rect(mActivity.getBounds()); - - // Change the size of current display. - setupDisplayAndParentSize(1000, 2000); - ensureActivityConfiguration(); - - assertEquals(originalBounds.width(), - mActivity.getWindowConfiguration().getBounds().width()); - assertEquals(originalBounds.height(), - mActivity.getWindowConfiguration().getBounds().height()); - assertTrue(mActivity.inSizeCompatMode()); - } - - @Test - public void testSizeCompatMode_FixedScreenLayoutSizeBits() { - final int fixedScreenLayout = Configuration.SCREENLAYOUT_LONG_NO - | Configuration.SCREENLAYOUT_SIZE_NORMAL; - final int layoutMask = Configuration.SCREENLAYOUT_LONG_MASK - | Configuration.SCREENLAYOUT_SIZE_MASK - | Configuration.SCREENLAYOUT_LAYOUTDIR_MASK; - mTask.getRequestedOverrideConfiguration().screenLayout = fixedScreenLayout - | Configuration.SCREENLAYOUT_LAYOUTDIR_LTR; - prepareFixedAspectRatioUnresizableActivity(); - - // The initial configuration should inherit from parent. - assertEquals(mTask.getConfiguration().screenLayout & layoutMask, - mActivity.getConfiguration().screenLayout & layoutMask); - - mTask.getConfiguration().screenLayout = Configuration.SCREENLAYOUT_LAYOUTDIR_RTL - | Configuration.SCREENLAYOUT_LONG_YES | Configuration.SCREENLAYOUT_SIZE_LARGE; - mActivity.onConfigurationChanged(mTask.getConfiguration()); - - // The size and aspect ratio bits don't change, but the layout direction should be updated. - assertEquals(fixedScreenLayout | Configuration.SCREENLAYOUT_LAYOUTDIR_RTL, - mActivity.getConfiguration().screenLayout & layoutMask); - } - - @Test - public void testSizeCompatMode_ResetNonVisibleActivity() { - final ActivityDisplay display = mStack.getDisplay(); - spyOn(display); - - prepareFixedAspectRatioUnresizableActivity(); - mActivity.setState(STOPPED, "testSizeCompatMode"); - mActivity.visible = false; - mActivity.app.setReportedProcState(ActivityManager.PROCESS_STATE_CACHED_ACTIVITY); - // Make the parent bounds to be different so the activity is in size compatibility mode. - setupDisplayAndParentSize(600, 1200); - - // Simulate the display changes orientation. - doReturn(ActivityInfo.CONFIG_SCREEN_SIZE | CONFIG_ORIENTATION - | ActivityInfo.CONFIG_WINDOW_CONFIGURATION) - .when(display).getLastOverrideConfigurationChanges(); - mActivity.onConfigurationChanged(mTask.getConfiguration()); - when(display.getLastOverrideConfigurationChanges()).thenCallRealMethod(); - // The override configuration should not change so it is still in size compatibility mode. - assertTrue(mActivity.inSizeCompatMode()); - - // Change display density - final DisplayContent displayContent = mStack.getDisplay().mDisplayContent; - displayContent.mBaseDisplayDensity = (int) (0.7f * displayContent.mBaseDisplayDensity); - final Configuration c = new Configuration(); - displayContent.computeScreenConfiguration(c); - mService.mAmInternal = mock(ActivityManagerInternal.class); - mStack.getDisplay().onRequestedOverrideConfigurationChanged(c); - - // The override configuration should be reset and the activity's process will be killed. - assertFalse(mActivity.inSizeCompatMode()); - verify(mActivity).restartProcessIfVisible(); - waitHandlerIdle(mService.mH); - verify(mService.mAmInternal).killProcess( - eq(mActivity.app.mName), eq(mActivity.app.mUid), anyString()); - } - @Test public void testTakeOptions() { ActivityOptions opts = ActivityOptions.makeRemoteAnimation( @@ -1340,33 +1102,4 @@ public class ActivityRecordTests extends ActivityTestsBase { verify(mActivity).removeFromHistory(anyString()); } - - /** Setup {@link #mActivity} as a size-compat-mode-able activity without fixed orientation. */ - private void prepareFixedAspectRatioUnresizableActivity() { - setupDisplayContentForCompatDisplayInsets(); - mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE; - mActivity.info.maxAspectRatio = 1.5f; - mActivity.visible = true; - ensureActivityConfiguration(); - } - - private void setupDisplayContentForCompatDisplayInsets() { - final Rect displayBounds = mStack.getDisplay().getBounds(); - setupDisplayAndParentSize(displayBounds.width(), displayBounds.height()); - } - - private DisplayContent setupDisplayAndParentSize(int width, int height) { - final DisplayContent displayContent = mStack.getDisplay().mDisplayContent; - displayContent.mBaseDisplayWidth = width; - displayContent.mBaseDisplayHeight = height; - final Configuration c = - new Configuration(mStack.getDisplay().getRequestedOverrideConfiguration()); - c.windowConfiguration.setBounds(new Rect(0, 0, width, height)); - c.windowConfiguration.setAppBounds(0, 0, width, height); - c.windowConfiguration.setRotation(ROTATION_0); - c.orientation = width > height - ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT; - mStack.getDisplay().onRequestedOverrideConfigurationChanged(c); - return displayContent; - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 9f4143ff95fa6..21f6c3cddfa3b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -715,14 +715,10 @@ public class DisplayContentTests extends WindowTestsBase { activity.setRequestedOrientation(newOrientation); - final ArgumentCaptor captor = ArgumentCaptor.forClass(Configuration.class); - verify(dc.mActivityDisplay).updateDisplayOverrideConfigurationLocked(captor.capture(), - same(activity), anyBoolean(), same(null)); - final Configuration newDisplayConfig = captor.getValue(); final int expectedOrientation = newOrientation == SCREEN_ORIENTATION_PORTRAIT ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; - assertEquals(expectedOrientation, newDisplayConfig.orientation); + assertEquals(expectedOrientation, dc.getConfiguration().orientation); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java new file mode 100644 index 0000000000000..c9331afc8f615 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2019 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 com.android.server.wm; + +import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; +import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.wm.ActivityStack.ActivityState.STOPPED; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.TaskStackListener; +import android.app.WindowConfiguration; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.MediumTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +/** + * Tests for Size Compatibility mode. + * + * Build/Install/Run: + * atest WmTests:SizeCompatTests + */ +@MediumTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class SizeCompatTests extends ActivityTestsBase { + private ActivityStack mStack; + private Task mTask; + private ActivityRecord mActivity; + + private void setUpApp(ActivityDisplay display) { + mStack = new StackBuilder(mRootActivityContainer).setDisplay(display).build(); + mTask = mStack.getChildAt(0); + mActivity = mTask.getTopActivity(); + } + + private void ensureActivityConfiguration() { + mActivity.ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */); + } + + @Test + public void testRestartProcessIfVisible() { + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build()); + doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity); + mActivity.visible = true; + mActivity.setSavedState(null /* savedState */); + mActivity.setState(ActivityStack.ActivityState.RESUMED, "testRestart"); + prepareUnresizable(1.5f /* maxAspect */, SCREEN_ORIENTATION_UNSPECIFIED); + + final Rect originalOverrideBounds = new Rect(mActivity.getBounds()); + resizeDisplay(mStack.getDisplay(), 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()); + assertNotEquals(originalOverrideBounds, mActivity.getBounds()); + } + + @Test + public void testKeepBoundsWhenChangingFromFreeformToFullscreen() { + removeGlobalMinSizeRestriction(); + // create freeform display and a freeform app + ActivityDisplay display = new TestActivityDisplay.Builder(mService, 2000, 1000) + .setCanRotate(false) + .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM).build(); + setUpApp(display); + + // Put app window into freeform and then make it a compat app. + mTask.setBounds(100, 100, 400, 600); + prepareUnresizable(-1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + + final Rect bounds = new Rect(mActivity.getBounds()); + final int density = mActivity.getConfiguration().densityDpi; + + // change display configuration to fullscreen + Configuration c = new Configuration(display.getRequestedOverrideConfiguration()); + c.windowConfiguration.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); + display.onRequestedOverrideConfigurationChanged(c); + + // check if dimensions stay the same + assertTrue(mActivity.inSizeCompatMode()); + assertEquals(bounds.width(), mActivity.getBounds().width()); + assertEquals(bounds.height(), mActivity.getBounds().height()); + assertEquals(density, mActivity.getConfiguration().densityDpi); + } + + @Test + public void testFixedAspectRatioBoundsWithDecor() { + final int decorHeight = 200; // e.g. The device has cutout. + setUpApp(new TestActivityDisplay.Builder(mService, 600, 800) + .setNotch(decorHeight).build()); + + mActivity.info.minAspectRatio = mActivity.info.maxAspectRatio = 1; + prepareUnresizable(-1f, SCREEN_ORIENTATION_UNSPECIFIED); + + // The parent configuration doesn't change since the first resolved configuration, so the + // activity shouldn't be in the size compatibility mode. + assertFalse(mActivity.inSizeCompatMode()); + + 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().windowConfiguration.setRotation(ROTATION_90); + 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 testFixedScreenConfigurationWhenMovingToDisplay() { + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build()); + + // Make a new less-tall display with lower density + final ActivityDisplay newDisplay = + new TestActivityDisplay.Builder(mService, 1000, 2000) + .setDensityDpi(200).build(); + + mActivity = new ActivityBuilder(mService) + .setTask(mTask) + .setResizeMode(RESIZE_MODE_UNRESIZEABLE) + .setMaxAspectRatio(1.5f) + .build(); + mActivity.visible = true; + + final Rect originalBounds = new Rect(mActivity.getBounds()); + final int originalDpi = mActivity.getConfiguration().densityDpi; + + // Move the non-resizable activity to the new display. + mStack.reparent(newDisplay.mDisplayContent, true /* onTop */); + + assertEquals(originalBounds.width(), mActivity.getBounds().width()); + assertEquals(originalBounds.height(), mActivity.getBounds().height()); + assertEquals(originalDpi, mActivity.getConfiguration().densityDpi); + assertTrue(mActivity.inSizeCompatMode()); + } + + @Test + public void testFixedScreenBoundsWhenDisplaySizeChanged() { + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build()); + prepareUnresizable(-1f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + assertFalse(mActivity.inSizeCompatMode()); + + final Rect origBounds = new Rect(mActivity.getBounds()); + + // Change the size of current display. + resizeDisplay(mStack.getDisplay(), 1000, 2000); + ensureActivityConfiguration(); + + assertEquals(origBounds.width(), mActivity.getWindowConfiguration().getBounds().width()); + assertEquals(origBounds.height(), mActivity.getWindowConfiguration().getBounds().height()); + assertTrue(mActivity.inSizeCompatMode()); + + // Change display size to a different orientation + resizeDisplay(mStack.getDisplay(), 2000, 1000); + ensureActivityConfiguration(); + assertEquals(origBounds.width(), mActivity.getWindowConfiguration().getBounds().width()); + assertEquals(origBounds.height(), mActivity.getWindowConfiguration().getBounds().height()); + } + + @Test + public void testLetterboxFullscreenBounds() { + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build()); + + // Fill out required fields on default display since WM-side is mocked out + prepareUnresizable(-1.f /* maxAspect */, SCREEN_ORIENTATION_LANDSCAPE); + assertFalse(mActivity.inSizeCompatMode()); + assertTrue(mActivity.getBounds().width() > mActivity.getBounds().height()); + } + + @Test + public void testMoveToDifferentOrientDisplay() { + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build()); + + final ActivityDisplay newDisplay = + new TestActivityDisplay.Builder(mService, 2000, 1000) + .setCanRotate(false).build(); + + prepareUnresizable(-1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + assertFalse(mActivity.inSizeCompatMode()); + + final Rect origBounds = new Rect(mActivity.getBounds()); + + // Move the non-resizable activity to the new display. + mStack.reparent(newDisplay.mDisplayContent, true /* onTop */); + ensureActivityConfiguration(); + assertEquals(origBounds.width(), mActivity.getWindowConfiguration().getBounds().width()); + assertEquals(origBounds.height(), mActivity.getWindowConfiguration().getBounds().height()); + assertTrue(mActivity.inSizeCompatMode()); + } + + @Test + public void testFixedOrientRotateCutoutDisplay() { + // Create a display with a notch/cutout + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).setNotch(60).build()); + prepareUnresizable(1.4f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + + final Rect origBounds = new Rect(mActivity.getBounds()); + final Rect origAppBounds = new Rect(mActivity.getWindowConfiguration().getAppBounds()); + + // Rotate the display + Configuration c = new Configuration(); + mStack.getDisplay().mDisplayContent.getDisplayRotation().setRotation(ROTATION_270); + mStack.getDisplay().mDisplayContent.computeScreenConfiguration(c); + mStack.getDisplay().onRequestedOverrideConfigurationChanged(c); + + // Make sure the app size is the same + assertEquals(ROTATION_270, mStack.getWindowConfiguration().getRotation()); + assertEquals(origBounds.width(), mActivity.getWindowConfiguration().getBounds().width()); + assertEquals(origBounds.height(), mActivity.getWindowConfiguration().getBounds().height()); + assertEquals(origAppBounds.width(), + mActivity.getWindowConfiguration().getAppBounds().width()); + assertEquals(origAppBounds.height(), + mActivity.getWindowConfiguration().getAppBounds().height()); + } + + @Test + public void testFixedAspOrientChangeOrient() { + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build()); + + prepareUnresizable(1.4f /* maxAspect */, SCREEN_ORIENTATION_LANDSCAPE); + assertTrue(mActivity.inSizeCompatMode()); + + final Rect originalBounds = new Rect(mActivity.getBounds()); + final Rect originalAppBounds = new Rect(mActivity.getWindowConfiguration().getAppBounds()); + + // Change the fixed orientation + mActivity.mOrientation = SCREEN_ORIENTATION_PORTRAIT; + mActivity.info.screenOrientation = SCREEN_ORIENTATION_PORTRAIT; + // TaskRecord's configuration actually depends on the activity config right now for + // pillarboxing. + mActivity.getTask().onRequestedOverrideConfigurationChanged( + mActivity.getTask().getRequestedOverrideConfiguration()); + + assertEquals(originalBounds.width(), mActivity.getBounds().height()); + assertEquals(originalBounds.height(), mActivity.getBounds().width()); + assertEquals(originalAppBounds.width(), + mActivity.getWindowConfiguration().getAppBounds().height()); + assertEquals(originalAppBounds.height(), + mActivity.getWindowConfiguration().getAppBounds().width()); + } + + @Test + public void testFixedScreenLayoutSizeBits() { + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build()); + final int fixedScreenLayout = Configuration.SCREENLAYOUT_LONG_NO + | Configuration.SCREENLAYOUT_SIZE_NORMAL; + final int layoutMask = Configuration.SCREENLAYOUT_LONG_MASK + | Configuration.SCREENLAYOUT_SIZE_MASK + | Configuration.SCREENLAYOUT_LAYOUTDIR_MASK; + Configuration c = new Configuration(mTask.getRequestedOverrideConfiguration()); + c.screenLayout = fixedScreenLayout | Configuration.SCREENLAYOUT_LAYOUTDIR_LTR; + mTask.onRequestedOverrideConfigurationChanged(c); + prepareUnresizable(1.5f, SCREEN_ORIENTATION_UNSPECIFIED); + + // The initial configuration should inherit from parent. + assertEquals(fixedScreenLayout | Configuration.SCREENLAYOUT_LAYOUTDIR_LTR, + mActivity.getConfiguration().screenLayout & layoutMask); + + mTask.getConfiguration().screenLayout = Configuration.SCREENLAYOUT_LAYOUTDIR_RTL + | Configuration.SCREENLAYOUT_LONG_YES | Configuration.SCREENLAYOUT_SIZE_LARGE; + mActivity.onConfigurationChanged(mTask.getConfiguration()); + + // The size and aspect ratio bits don't change, but the layout direction should be updated. + assertEquals(fixedScreenLayout | Configuration.SCREENLAYOUT_LAYOUTDIR_RTL, + mActivity.getConfiguration().screenLayout & layoutMask); + } + + @Test + public void testResetNonVisibleActivity() { + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build()); + final ActivityDisplay display = mStack.getDisplay(); + spyOn(display); + + prepareUnresizable(1.5f, SCREEN_ORIENTATION_UNSPECIFIED); + mActivity.setState(STOPPED, "testSizeCompatMode"); + mActivity.visible = false; + mActivity.app.setReportedProcState(ActivityManager.PROCESS_STATE_CACHED_ACTIVITY); + // Make the parent bounds to be different so the activity is in size compatibility mode. + mTask.getWindowConfiguration().setAppBounds(new Rect(0, 0, 600, 1200)); + + // Simulate the display changes orientation. + when(display.getLastOverrideConfigurationChanges()).thenReturn( + ActivityInfo.CONFIG_SCREEN_SIZE | CONFIG_ORIENTATION + | ActivityInfo.CONFIG_WINDOW_CONFIGURATION); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + when(display.getLastOverrideConfigurationChanges()).thenCallRealMethod(); + // The override configuration should not change so it is still in size compatibility mode. + assertTrue(mActivity.inSizeCompatMode()); + + // Change display density + final DisplayContent displayContent = mStack.getDisplay().mDisplayContent; + displayContent.mBaseDisplayDensity = (int) (0.7f * displayContent.mBaseDisplayDensity); + final Configuration c = new Configuration(); + displayContent.computeScreenConfiguration(c); + mService.mAmInternal = mock(ActivityManagerInternal.class); + mStack.getDisplay().onRequestedOverrideConfigurationChanged(c); + + // The override configuration should be reset and the activity's process will be killed. + assertFalse(mActivity.inSizeCompatMode()); + verify(mActivity).restartProcessIfVisible(); + mLockRule.runWithScissors(mService.mH, () -> { }, TimeUnit.SECONDS.toMillis(3)); + verify(mService.mAmInternal).killProcess( + eq(mActivity.app.mName), eq(mActivity.app.mUid), anyString()); + } + + /** + * Ensures that {@link TaskStackListener} can receive callback about the activity in size + * compatibility mode. + */ + @Test + public void testHandleActivitySizeCompatMode() { + setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2000).build()); + ActivityRecord activity = mActivity; + activity.setState(ActivityStack.ActivityState.RESUMED, "testHandleActivitySizeCompatMode"); + prepareUnresizable(-1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + ensureActivityConfiguration(); + assertFalse(mActivity.inSizeCompatMode()); + + final ArrayList compatTokens = new ArrayList<>(); + mService.getTaskChangeNotificationController().registerTaskStackListener( + new TaskStackListener() { + @Override + public void onSizeCompatModeActivityChanged(int displayId, + IBinder activityToken) { + compatTokens.add(activityToken); + } + }); + + // Resize the display so that the activity exercises size-compat mode. + resizeDisplay(mStack.getDisplay(), 1000, 2500); + + // Expect the exact token when the activity is in size compatibility mode. + assertEquals(1, compatTokens.size()); + assertEquals(activity.appToken, compatTokens.get(0)); + + compatTokens.clear(); + // Make the activity resizable again by restarting it + activity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE; + activity.visible = true; + activity.restartProcessIfVisible(); + // The full lifecycle isn't hooked up so manually set state to resumed + activity.setState(ActivityStack.ActivityState.RESUMED, "testHandleActivitySizeCompatMode"); + mStack.getDisplay().handleActivitySizeCompatModeIfNeeded(activity); + + // Expect null token when switching to non-size-compat mode activity. + assertEquals(1, compatTokens.size()); + assertEquals(null, compatTokens.get(0)); + } + + /** + * Setup {@link #mActivity} as a size-compat-mode-able activity with fixed aspect and/or + * orientation. + */ + private void prepareUnresizable(float maxAspect, int screenOrientation) { + mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE; + mActivity.visible = true; + if (maxAspect >= 0) { + mActivity.info.maxAspectRatio = maxAspect; + } + if (screenOrientation != SCREEN_ORIENTATION_UNSPECIFIED) { + mActivity.mOrientation = screenOrientation; + mActivity.info.screenOrientation = screenOrientation; + // TaskRecord's configuration actually depends on the activity config right now for + // pillarboxing. + mActivity.getTask().onRequestedOverrideConfigurationChanged( + mActivity.getTask().getRequestedOverrideConfiguration()); + } + ensureActivityConfiguration(); + } + + private void resizeDisplay(ActivityDisplay display, int width, int height) { + final DisplayContent displayContent = display.mDisplayContent; + displayContent.mBaseDisplayWidth = width; + displayContent.mBaseDisplayHeight = height; + Configuration c = new Configuration(); + displayContent.computeScreenConfiguration(c); + display.onRequestedOverrideConfigurationChanged(c); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/TestActivityDisplay.java b/services/tests/wmtests/src/com/android/server/wm/TestActivityDisplay.java index 9c3ff654ddf01..48ec261f88f67 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestActivityDisplay.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestActivityDisplay.java @@ -23,12 +23,17 @@ import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.mockito.ArgumentMatchers.any; +import android.content.res.Configuration; +import android.graphics.Insets; +import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; import android.view.Display; +import android.view.DisplayCutout; import android.view.DisplayInfo; class TestActivityDisplay extends ActivityDisplay { @@ -107,4 +112,94 @@ class TestActivityDisplay extends ActivityDisplay { .setCreateActivity(false) .build(); } + + public static class Builder { + private final DisplayInfo mInfo; + private boolean mCanRotate = true; + private int mWindowingMode = WINDOWING_MODE_FULLSCREEN; + private int mPosition = POSITION_TOP; + private final ActivityTaskManagerService mService; + private boolean mSystemDecorations = false; + + Builder(ActivityTaskManagerService service, int width, int height) { + mService = service; + mInfo = new DisplayInfo(); + mService.mContext.getDisplay().getDisplayInfo(mInfo); + mInfo.logicalWidth = width; + mInfo.logicalHeight = height; + mInfo.logicalDensityDpi = 300; + mInfo.displayCutout = null; + } + Builder(ActivityTaskManagerService service, DisplayInfo info) { + mService = service; + mInfo = info; + } + Builder setSystemDecorations(boolean yes) { + mSystemDecorations = yes; + return this; + } + Builder setPosition(int position) { + mPosition = position; + return this; + } + Builder setUniqueId(String uniqueId) { + mInfo.uniqueId = uniqueId; + return this; + } + Builder setType(int type) { + mInfo.type = type; + return this; + } + Builder setOwnerUid(int ownerUid) { + mInfo.ownerUid = ownerUid; + return this; + } + Builder setNotch(int height) { + mInfo.displayCutout = new DisplayCutout( + Insets.of(0, height, 0, 0), null, new Rect(20, 0, 80, height), null, null); + return this; + } + Builder setCanRotate(boolean canRotate) { + mCanRotate = canRotate; + return this; + } + Builder setWindowingMode(int windowingMode) { + mWindowingMode = windowingMode; + return this; + } + Builder setDensityDpi(int dpi) { + mInfo.logicalDensityDpi = dpi; + return this; + } + TestActivityDisplay build() { + final int displayId = SystemServicesTestRule.sNextDisplayId++; + final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId, + mInfo, DEFAULT_DISPLAY_ADJUSTMENTS); + final TestActivityDisplay newDisplay; + synchronized (mService.mGlobalLock) { + newDisplay = new TestActivityDisplay(mService.mStackSupervisor, display); + mService.mRootActivityContainer.addChild(newDisplay, mPosition); + } + // disable the normal system decorations + final DisplayPolicy displayPolicy = newDisplay.mDisplayContent.getDisplayPolicy(); + spyOn(displayPolicy); + if (mSystemDecorations) { + doReturn(true).when(newDisplay).supportsSystemDecorations(); + } else { + doReturn(false).when(displayPolicy).hasNavigationBar(); + doReturn(false).when(displayPolicy).hasStatusBar(); + doReturn(false).when(newDisplay).supportsSystemDecorations(); + } + Configuration c = new Configuration(); + newDisplay.mDisplayContent.computeScreenConfiguration(c); + c.windowConfiguration.setWindowingMode(mWindowingMode); + newDisplay.onRequestedOverrideConfigurationChanged(c); + // This is a rotating display + if (mCanRotate) { + doReturn(false).when(newDisplay.mDisplayContent) + .handlesOrientationChangeFromDescendant(); + } + return newDisplay; + } + } }