blob: 6843df30b4490e325e51524cb677c84b41d53943 [file] [log] [blame]
/*
* 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);
}
}