/* * Copyright (C) 2015 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.server.wm; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.server.wm.TestTaskOrganizer.INVALID_TASK_ID; import static android.server.wm.WindowManagerState.STATE_RESUMED; import static android.server.wm.WindowManagerState.STATE_STOPPED; import static android.server.wm.app.Components.LAUNCHING_ACTIVITY; import static android.server.wm.app.Components.NON_RESIZEABLE_ACTIVITY; import static android.server.wm.app.Components.NO_RELAUNCH_ACTIVITY; import static android.server.wm.app.Components.SINGLE_INSTANCE_ACTIVITY; import static android.server.wm.app.Components.SINGLE_TASK_ACTIVITY; import static android.server.wm.app.Components.TEST_ACTIVITY; import static android.server.wm.app.Components.TEST_ACTIVITY_WITH_SAME_AFFINITY; import static android.server.wm.app.Components.TRANSLUCENT_TEST_ACTIVITY; import static android.server.wm.app.Components.TestActivity.TEST_ACTIVITY_ACTION_FINISH_SELF; import static android.server.wm.app27.Components.SDK_27_LAUNCHING_ACTIVITY; import static android.server.wm.app27.Components.SDK_27_SEPARATE_PROCESS_ACTIVITY; import static android.server.wm.app27.Components.SDK_27_TEST_ACTIVITY; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import android.content.ComponentName; import android.content.res.Resources; import android.platform.test.annotations.Presubmit; import android.server.wm.CommandSession.ActivityCallback; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import org.junit.Before; import org.junit.Test; /** * Build/Install/Run: * atest CtsWindowManagerDeviceTestCases:MultiWindowTests */ @Presubmit @android.server.wm.annotation.Group2 public class MultiWindowTests extends ActivityManagerTestBase { private boolean mIsHomeRecentsComponent; @Before @Override public void setUp() throws Exception { super.setUp(); mIsHomeRecentsComponent = mWmState.isHomeRecentsComponent(); assumeTrue("Skipping test: no split multi-window support", supportsSplitScreenMultiWindow()); } @Test public void testMinimumDeviceSize() { mWmState.assertDeviceDefaultDisplaySizeForMultiWindow( "Devices supporting multi-window must be larger than the default minimum" + " task size"); mWmState.assertDeviceDefaultDisplaySizeForSplitScreen( "Devices supporting split-screen multi-window must be larger than the" + " default minimum display size."); } /** Resizeable activity should be able to enter multi-window mode.*/ @Test public void testResizeableActivity() { assertActivitySupportedInSplitScreen(TEST_ACTIVITY); } /** * Depending on the value of * {@link com.android.internal.R.integer.config_supportsNonResizableMultiWindow}, * non-resizeable activity may or may not be able to enter multi-window mode. * * Based on the flag value: * -1: not support non-resizable in multi window. * 0: check the screen smallest width, if it is a large screen, support non-resizable in multi * window. Otherwise, not support. * 1: always support non-resizable in multi window. */ @Test public void testNonResizeableActivity() { createManagedDevEnableNonResizableMultiWindowSession().set(0); final Resources resources = mContext.getResources(); final int configSupportsNonResizableMultiWindow; try { configSupportsNonResizableMultiWindow = resources.getInteger(resources.getIdentifier( "config_supportsNonResizableMultiWindow", "integer", "android")); } catch (Resources.NotFoundException e) { fail("Device must define config_supportsNonResizableMultiWindow"); return; } switch (configSupportsNonResizableMultiWindow) { case -1: assertActivityNotSupportedInSplitScreen(NON_RESIZEABLE_ACTIVITY); break; case 1: assertActivitySupportedInSplitScreen(NON_RESIZEABLE_ACTIVITY); break; case 0: final int configLargeScreenSmallestScreenWidthDp; try { configLargeScreenSmallestScreenWidthDp = resources.getInteger(resources.getIdentifier( "config_largeScreenSmallestScreenWidthDp", "integer", "android")); } catch (Resources.NotFoundException e) { fail("Device must define config_largeScreenSmallestScreenWidthDp"); return; } final int smallestScreenWidthDp = mWmState.getHomeTask() .mFullConfiguration.smallestScreenWidthDp; if (smallestScreenWidthDp >= configLargeScreenSmallestScreenWidthDp) { assertActivitySupportedInSplitScreen(NON_RESIZEABLE_ACTIVITY); } else { assertActivityNotSupportedInSplitScreen(NON_RESIZEABLE_ACTIVITY); } break; default: fail("config_supportsNonResizableMultiWindow must be -1, 0, or 1."); } } /** * Non-resizeable activity can enter split-screen if * {@link android.provider.Settings.Global#DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW} is * set. */ @Test public void testDevEnableNonResizeableMultiWindow_splitScreenPrimary() { createManagedDevEnableNonResizableMultiWindowSession().set(1); assertActivitySupportedInSplitScreen(NON_RESIZEABLE_ACTIVITY); } /** * Non-resizeable activity can enter split-screen if * {@link android.provider.Settings.Global#DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW} is * set. */ @Test public void testDevEnableNonResizeableMultiWindow_splitScreenSecondary() { createManagedDevEnableNonResizableMultiWindowSession().set(1); launchActivitiesInSplitScreen( getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY), getLaunchActivityBuilder().setTargetActivity(NON_RESIZEABLE_ACTIVITY)); mWmState.waitForActivityState(NON_RESIZEABLE_ACTIVITY, STATE_RESUMED); mWmState.assertVisibility(NON_RESIZEABLE_ACTIVITY, true); assertTrue(mWmState.containsActivityInWindowingMode( NON_RESIZEABLE_ACTIVITY, WINDOWING_MODE_MULTI_WINDOW)); } /** Asserts that the give activity can be shown in split screen. */ private void assertActivitySupportedInSplitScreen(ComponentName activity) { launchActivityInPrimarySplit(activity); mWmState.waitForActivityState(activity, STATE_RESUMED); mWmState.assertVisibility(activity, true); assertTrue(mWmState.containsActivityInWindowingMode(activity, WINDOWING_MODE_MULTI_WINDOW)); } /** Asserts that the give activity can NOT be shown in split screen. */ private void assertActivityNotSupportedInSplitScreen(ComponentName activity) { boolean gotAssertionError = false; try { launchActivityInPrimarySplit(activity); } catch (AssertionError e) { gotAssertionError = true; } assertTrue("Trying to put non-resizeable activity in split should throw error.", gotAssertionError); mWmState.waitForActivityState(activity, STATE_RESUMED); mWmState.assertVisibility(activity, true); assertTrue(mWmState.containsActivityInWindowingMode(activity, WINDOWING_MODE_FULLSCREEN)); } @Test public void testLaunchToSideMultiWindowCallbacks() { // Launch two activities in split-screen mode. launchActivitiesInSplitScreen( getLaunchActivityBuilder().setTargetActivity(NO_RELAUNCH_ACTIVITY), getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)); int displayWindowingMode = mWmState.getDisplay( mWmState.getDisplayByActivity(TEST_ACTIVITY)).getWindowingMode(); separateTestJournal(); mTaskOrganizer.dismissSplitScreen(); if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) { // Exit split-screen mode and ensure we only get 1 multi-window mode changed callback. final ActivityLifecycleCounts lifecycleCounts = waitForOnMultiWindowModeChanged( NO_RELAUNCH_ACTIVITY); assertEquals(1, lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED)); } else { // Display is not a fullscreen display, so there won't be a multi-window callback. // Instead just verify that windows are not in split-screen anymore. waitForIdle(); mWmState.computeState(); mWmState.assertDoesNotContainStack("Must have exited split-screen", WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); } } @Test public void testNoUserLeaveHintOnMultiWindowModeChanged() { launchActivity(NO_RELAUNCH_ACTIVITY, WINDOWING_MODE_FULLSCREEN); // Move to primary split. separateTestJournal(); putActivityInPrimarySplit(NO_RELAUNCH_ACTIVITY); ActivityLifecycleCounts lifecycleCounts = waitForOnMultiWindowModeChanged(NO_RELAUNCH_ACTIVITY); assertEquals("mMultiWindowModeChangedCount", 1, lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED)); assertEquals("mUserLeaveHintCount", 0, lifecycleCounts.getCount(ActivityCallback.ON_USER_LEAVE_HINT)); // Make sure primary split is focused. This way when we dismiss it later fullscreen stack // will come up. launchActivity(LAUNCHING_ACTIVITY, WINDOWING_MODE_FULLSCREEN); putActivityInSecondarySplit(LAUNCHING_ACTIVITY); launchActivity(NO_RELAUNCH_ACTIVITY); separateTestJournal(); // Move activities back to fullscreen screen. // TestTaskOrganizer sets windowing modes of tasks to unspecific when putting them to split // screens so we need to explicitly set their windowing modes back to fullscreen to avoid // inheriting freeform windowing mode from the display on freeform first devices. int noRelaunchTaskId = mWmState.getTaskByActivity(NO_RELAUNCH_ACTIVITY).mTaskId; WindowContainerToken noRelaunchTaskToken = mTaskOrganizer.getTaskInfo(noRelaunchTaskId).getToken(); WindowContainerTransaction t = new WindowContainerTransaction() .setWindowingMode(noRelaunchTaskToken, WINDOWING_MODE_FULLSCREEN); mTaskOrganizer.dismissSplitScreen(t, false /* primaryOnTop */); lifecycleCounts = waitForOnMultiWindowModeChanged(NO_RELAUNCH_ACTIVITY); assertEquals("mMultiWindowModeChangedCount", 1, lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED)); assertEquals("mUserLeaveHintCount", 0, lifecycleCounts.getCount(ActivityCallback.ON_USER_LEAVE_HINT)); } @Test public void testLaunchToSideAndBringToFront() { launchActivitiesInSplitScreen( getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY), getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)); mWmState.assertFocusedActivity("Launched to side activity must be in front.", TEST_ACTIVITY); // Launch another activity to side to cover first one. launchActivityInSecondarySplit(NO_RELAUNCH_ACTIVITY); mWmState.assertFocusedActivity("Launched to side covering activity must be in front.", NO_RELAUNCH_ACTIVITY); // Launch activity that was first launched to side. It should be brought to front. launchActivity(TEST_ACTIVITY); mWmState.assertFocusedActivity("Launched to side covering activity must be in front.", TEST_ACTIVITY); } @Test public void testLaunchToSideMultiple() { launchActivitiesInSplitScreen( getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY), getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)); final int taskNumberInitial = mTaskOrganizer.getSecondarySplitTaskCount(); // Try to launch to side same activity again. launchActivity(TEST_ACTIVITY); mWmState.computeState(TEST_ACTIVITY, LAUNCHING_ACTIVITY); final int taskNumberFinal = mTaskOrganizer.getSecondarySplitTaskCount(); assertEquals("Task number mustn't change.", taskNumberInitial, taskNumberFinal); mWmState.assertFocusedActivity("Launched to side activity must remain in front.", TEST_ACTIVITY); } @Test public void testLaunchToSideSingleInstance() { launchTargetToSide(SINGLE_INSTANCE_ACTIVITY, false); } @Test public void testLaunchToSideSingleTask() { launchTargetToSide(SINGLE_TASK_ACTIVITY, false); } @Test public void testLaunchToSideMultipleWithDifferentIntent() { launchTargetToSide(TEST_ACTIVITY, true); } private void launchTargetToSide(ComponentName targetActivityName, boolean taskCountMustIncrement) { launchActivityInPrimarySplit(LAUNCHING_ACTIVITY); // Launch target to side final LaunchActivityBuilder targetActivityLauncher = getLaunchActivityBuilder() .setTargetActivity(targetActivityName) .setToSide(true) .setRandomData(true) .setMultipleTask(false); targetActivityLauncher.execute(); final int secondaryTaskId = mWmState.getTaskByActivity(targetActivityName).mTaskId; mTaskOrganizer.putTaskInSplitSecondary(secondaryTaskId); mWmState.computeState(targetActivityName, LAUNCHING_ACTIVITY); final int taskNumberInitial = mTaskOrganizer.getSecondarySplitTaskCount(); // Try to launch to side same activity again with different data. targetActivityLauncher.execute(); mWmState.computeState(targetActivityName, LAUNCHING_ACTIVITY); final int[] excludeTaskIds = new int[] { secondaryTaskId, INVALID_TASK_ID }; if (taskCountMustIncrement) { mWmState.waitFor("Waiting for new activity to come up.", state -> state.getTaskByActivity(targetActivityName, excludeTaskIds) != null); } WindowManagerState.ActivityTask task = mWmState.getTaskByActivity(targetActivityName, excludeTaskIds); final int secondaryTaskId2; if (task != null) { secondaryTaskId2 = task.mTaskId; mTaskOrganizer.putTaskInSplitSecondary(secondaryTaskId2); } else { secondaryTaskId2 = INVALID_TASK_ID; } final int taskNumberSecondLaunch = mTaskOrganizer.getSecondarySplitTaskCount(); if (taskCountMustIncrement) { assertEquals("Task number must be incremented.", taskNumberInitial + 1, taskNumberSecondLaunch); } else { assertEquals("Task number must not change.", taskNumberInitial, taskNumberSecondLaunch); } mWmState.waitForFocusedActivity("Wait for launched to side activity to be in front.", targetActivityName); mWmState.assertFocusedActivity("Launched to side activity must be in front.", targetActivityName); // Try to launch to side same activity again with different random data. Note that null // cannot be used here, since the first instance of TestActivity is launched with no data // in order to launch into split screen. targetActivityLauncher.execute(); mWmState.computeState(targetActivityName, LAUNCHING_ACTIVITY); excludeTaskIds[1] = secondaryTaskId2; if (taskCountMustIncrement) { mWmState.waitFor("Waiting for the second new activity to come up.", state -> state.getTaskByActivity(targetActivityName, excludeTaskIds) != null); } WindowManagerState.ActivityTask taskFinal = mWmState.getTaskByActivity(targetActivityName, excludeTaskIds); if (taskFinal != null) { int secondaryTaskId3 = taskFinal.mTaskId; mTaskOrganizer.putTaskInSplitSecondary(secondaryTaskId3); } final int taskNumberFinal = mTaskOrganizer.getSecondarySplitTaskCount(); if (taskCountMustIncrement) { assertEquals("Task number must be incremented.", taskNumberSecondLaunch + 1, taskNumberFinal); } else { assertEquals("Task number must not change.", taskNumberSecondLaunch, taskNumberFinal); } mWmState.waitForFocusedActivity("Wait for launched to side activity to be in front.", targetActivityName); mWmState.assertFocusedActivity("Launched to side activity must be in front.", targetActivityName); } @Test public void testLaunchToSideMultipleWithFlag() { launchActivitiesInSplitScreen( getLaunchActivityBuilder() .setTargetActivity(TEST_ACTIVITY), getLaunchActivityBuilder() // Try to launch to side same activity again, // but with Intent#FLAG_ACTIVITY_MULTIPLE_TASK. .setMultipleTask(true) .setTargetActivity(TEST_ACTIVITY)); assertTrue("Primary split must contain TEST_ACTIVITY", mWmState.getRootTask(mTaskOrganizer.getPrimarySplitTaskId()) .containsActivity(TEST_ACTIVITY) ); assertTrue("Secondary split must contain TEST_ACTIVITY", mWmState.getRootTask(mTaskOrganizer.getSecondarySplitTaskId()) .containsActivity(TEST_ACTIVITY) ); mWmState.assertFocusedActivity("Launched to side activity must be in front.", TEST_ACTIVITY); } @Test public void testSameProcessActivityResumedPreQ() { launchActivitiesInSplitScreen( getLaunchActivityBuilder().setTargetActivity(SDK_27_TEST_ACTIVITY), getLaunchActivityBuilder().setTargetActivity(SDK_27_LAUNCHING_ACTIVITY)); assertEquals("There must be only one resumed activity in the package.", 1, mWmState.getResumedActivitiesCountInPackage( SDK_27_TEST_ACTIVITY.getPackageName())); } @Test public void testDifferentProcessActivityResumedPreQ() { launchActivitiesInSplitScreen( getLaunchActivityBuilder().setTargetActivity(SDK_27_TEST_ACTIVITY), getLaunchActivityBuilder().setTargetActivity(SDK_27_SEPARATE_PROCESS_ACTIVITY)); assertEquals("There must be only two resumed activities in the package.", 2, mWmState.getResumedActivitiesCountInPackage( SDK_27_TEST_ACTIVITY.getPackageName())); } @Test public void testDisallowUpdateWindowingModeWhenInLockedTask() { launchActivity(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN); final WindowManagerState.ActivityTask task = mWmState.getStandardRootTaskByWindowingMode( WINDOWING_MODE_FULLSCREEN).getTopTask(); try { // Lock the task runWithShellPermission(() -> mAtm.startSystemLockTaskMode(task.mTaskId)); waitForOrFail("Fail to enter locked task mode", () -> mAm.getLockTaskModeState() != LOCK_TASK_MODE_NONE); // Verify specifying non-fullscreen windowing mode will fail. boolean exceptionThrown = false; try { runWithShellPermission(() -> { final WindowContainerTransaction wct = new WindowContainerTransaction() .setWindowingMode( mTaskOrganizer.getTaskInfo(task.mTaskId).getToken(), WINDOWING_MODE_MULTI_WINDOW); mTaskOrganizer.applyTransaction(wct); }); } catch (UnsupportedOperationException e) { exceptionThrown = true; } assertTrue("Not allowed to specify windowing mode while in locked task mode.", exceptionThrown); } finally { runWithShellPermission(() -> { mAtm.stopSystemLockTaskMode(); }); } } @Test public void testDisallowHierarchyOperationWhenInLockedTask() { launchActivity(TEST_ACTIVITY, WINDOWING_MODE_FULLSCREEN); launchActivity(LAUNCHING_ACTIVITY, WINDOWING_MODE_MULTI_WINDOW); final WindowManagerState.ActivityTask task = mWmState .getStandardRootTaskByWindowingMode(WINDOWING_MODE_FULLSCREEN).getTopTask(); final WindowManagerState.ActivityTask root = mWmState .getStandardRootTaskByWindowingMode(WINDOWING_MODE_MULTI_WINDOW).getTopTask(); try { // Lock the task runWithShellPermission(() -> { mAtm.startSystemLockTaskMode(task.mTaskId); }); waitForOrFail("Fail to enter locked task mode", () -> mAm.getLockTaskModeState() != LOCK_TASK_MODE_NONE); boolean gotAssertionError = false; try { runWithShellPermission(() -> { // Fetch tokens of testing task and multi-window root. final WindowContainerToken multiWindowRoot = mTaskOrganizer.getTaskInfo(root.mTaskId).getToken(); final WindowContainerToken testChild = mTaskOrganizer.getTaskInfo(task.mTaskId).getToken(); // Verify performing reparent operation is no operation. final WindowContainerTransaction wct = new WindowContainerTransaction() .reparent(testChild, multiWindowRoot, true /* onTop */); mTaskOrganizer.applyTransaction(wct); waitForOrFail("Fail to reparent", () -> mTaskOrganizer.getTaskInfo(task.mTaskId).getParentTaskId() == root.mTaskId); }); } catch (AssertionError e) { gotAssertionError = true; } assertTrue("Not allowed to perform hierarchy operation while in locked task mode.", gotAssertionError); } finally { runWithShellPermission(() -> { mAtm.stopSystemLockTaskMode(); }); } } /** * Asserts that the activity is visible when the top opaque activity finishes and with another * translucent activity on top while in split-screen-secondary task. */ @Test public void testVisibilityWithTranslucentAndTopFinishingActivity() { // Launch two activities in split-screen mode. launchActivitiesInSplitScreen( getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY), getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY)); mTaskOrganizer.setLaunchRoot(mTaskOrganizer.getSecondarySplitTaskId()); // Launch two more activities on a different task on top of split-screen-secondary and // only the top opaque activity should be visible. // Explicitly launch them into fullscreen mode because the control windowing mode of the // launch root doesn't include freeform mode. Freeform first devices launch apps in freeform // mode by default, which won't trigger the launch root. getLaunchActivityBuilder().setTargetActivity(TRANSLUCENT_TEST_ACTIVITY) .setUseInstrumentation() .setWaitForLaunched(true) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) .execute(); getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY) .setUseInstrumentation() .setWaitForLaunched(true) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) .execute(); mWmState.assertVisibility(TEST_ACTIVITY, true); mWmState.waitForActivityState(TRANSLUCENT_TEST_ACTIVITY, STATE_STOPPED); mWmState.assertVisibility(TRANSLUCENT_TEST_ACTIVITY, false); mWmState.assertVisibility(TEST_ACTIVITY_WITH_SAME_AFFINITY, false); // Finish the top opaque activity and both the two activities should be visible. mBroadcastActionTrigger.doAction(TEST_ACTIVITY_ACTION_FINISH_SELF); mWmState.computeState(new WaitForValidActivityState(TRANSLUCENT_TEST_ACTIVITY)); mWmState.assertVisibility(TRANSLUCENT_TEST_ACTIVITY, true); mWmState.assertVisibility(TEST_ACTIVITY_WITH_SAME_AFFINITY, true); } }