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; + } + } }