/* * Copyright (C) 2018 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.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; import static android.server.wm.WindowManagerState.STATE_INITIALIZING; import static android.server.wm.WindowManagerState.STATE_STOPPED; import static android.server.wm.app.Components.BROADCAST_RECEIVER_ACTIVITY; import static android.server.wm.app.Components.LAUNCHING_ACTIVITY; import static android.server.wm.app.Components.NO_RELAUNCH_ACTIVITY; import static android.server.wm.app.Components.TEST_ACTIVITY; import static android.server.wm.app.Components.TRANSLUCENT_ACTIVITY; import static android.server.wm.app.Components.TestActivity.COMMAND_NAVIGATE_UP_TO; import static android.server.wm.app.Components.TestActivity.COMMAND_START_ACTIVITIES; import static android.server.wm.app.Components.TestActivity.EXTRA_INTENT; import static android.server.wm.app.Components.TestActivity.EXTRA_INTENTS; import static android.server.wm.app27.Components.SDK_27_LAUNCHING_ACTIVITY; import static android.server.wm.second.Components.SECOND_ACTIVITY; import static android.view.Display.DEFAULT_DISPLAY; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import android.app.Activity; import android.app.ActivityOptions; import android.content.ComponentName; import android.content.Intent; import android.os.Bundle; import android.platform.test.annotations.Presubmit; import android.server.wm.CommandSession.ActivitySession; import android.server.wm.intent.Activities; import org.junit.Test; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Build/Install/Run: * atest CtsWindowManagerDeviceTestCases:StartActivityTests */ @Presubmit public class StartActivityTests extends ActivityManagerTestBase { @Test public void testStartHomeIfNoActivities() { if (!hasHomeScreen()) { return; } final ComponentName defaultHome = getDefaultHomeComponent(); final int[] allActivityTypes = Arrays.copyOf(ALL_ACTIVITY_TYPE_BUT_HOME, ALL_ACTIVITY_TYPE_BUT_HOME.length + 1); allActivityTypes[allActivityTypes.length - 1] = ACTIVITY_TYPE_HOME; removeRootTasksWithActivityTypes(allActivityTypes); waitAndAssertResumedActivity(defaultHome, "Home activity should be restarted after force-finish"); stopTestPackage(defaultHome.getPackageName()); waitAndAssertResumedActivity(defaultHome, "Home activity should be restarted after force-stop"); } /** * Ensures {@link Activity} without {@link Intent#FLAG_ACTIVITY_NEW_TASK} can only be launched * from an {@link Activity} {@link android.content.Context}. */ @Test public void testStartActivityContexts() { // Note by default LaunchActivityBuilder will use LAUNCHING_ACTIVITY to launch the target. // Launch Activity from application context without FLAG_ACTIVITY_NEW_TASK. getLaunchActivityBuilder() .setTargetActivity(TEST_ACTIVITY) .setUseApplicationContext(true) .setSuppressExceptions(true) .setWaitForLaunched(false) .execute(); // Launch another activity from activity to ensure previous one has done. getLaunchActivityBuilder() .setTargetActivity(NO_RELAUNCH_ACTIVITY) .execute(); mWmState.computeState(NO_RELAUNCH_ACTIVITY); // Verify Activity was not started. assertFalse(mWmState.containsActivity(TEST_ACTIVITY)); mWmState.assertResumedActivity( "Activity launched from activity context should be present", NO_RELAUNCH_ACTIVITY); } /** * Ensures you can start an {@link Activity} from a non {@link Activity} * {@link android.content.Context} with the {@code FLAG_ACTIVITY_NEW_TASK}. */ @Test public void testStartActivityNewTask() throws Exception { // Launch Activity from application context. getLaunchActivityBuilder() .setTargetActivity(TEST_ACTIVITY) .setUseApplicationContext(true) .setSuppressExceptions(true) .setNewTask(true) .execute(); mWmState.computeState(TEST_ACTIVITY); mWmState.assertResumedActivity("Test Activity should be started with new task flag", TEST_ACTIVITY); } @Test public void testStartActivityTaskLaunchBehind() { // launch an activity getLaunchActivityBuilder() .setTargetActivity(TEST_ACTIVITY) .setUseInstrumentation() .setNewTask(true) .execute(); // launch an activity behind getLaunchActivityBuilder() .setTargetActivity(TRANSLUCENT_ACTIVITY) .setUseInstrumentation() .setIntentFlags(FLAG_ACTIVITY_NEW_DOCUMENT) .setNewTask(true) .setLaunchTaskBehind(true) .execute(); waitAndAssertActivityState(TRANSLUCENT_ACTIVITY, STATE_STOPPED, "Activity should be stopped"); mWmState.assertResumedActivity("Test Activity should be remained on top and resumed", TEST_ACTIVITY); } @Test public void testStartActivityFromFinishingActivity() { // launch TEST_ACTIVITY from LAUNCHING_ACTIVITY getLaunchActivityBuilder() .setTargetActivity(TEST_ACTIVITY) .setFinishBeforeLaunch(true) .execute(); // launch LAUNCHING_ACTIVITY again getLaunchActivityBuilder() .setTargetActivity(LAUNCHING_ACTIVITY) .setUseInstrumentation() .execute(); // make sure TEST_ACTIVITY is still on top and resumed mWmState.computeState(TEST_ACTIVITY); mWmState.assertResumedActivity("Test Activity should be remained on top and resumed", TEST_ACTIVITY); } /** * Ensures you can start an {@link Activity} from a non {@link Activity} * {@link android.content.Context} when the target sdk is between N and O Mr1. * @throws Exception */ @Test public void testLegacyStartActivityFromNonActivityContext() { getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY) .setLaunchingActivity(SDK_27_LAUNCHING_ACTIVITY) .setUseApplicationContext(true) .execute(); mWmState.computeState(TEST_ACTIVITY); mWmState.assertResumedActivity("Test Activity should be resumed without older sdk", TEST_ACTIVITY); } /** * Starts 3 activities A, B, C in the same task. A and B belong to current package and are not * exported. C belongs to a different package with different uid. After C called * {@link Activity#navigateUpTo(Intent)} with the intent of A, the activities B, C should be * finished and instead of creating a new instance of A, the original A should become the top * activity because the caller C in different uid cannot launch a non-exported activity. */ @Test public void testStartActivityByNavigateUpToFromDiffUid() { final Intent intent1 = new Intent(mContext, Activities.RegularActivity.class); final String regularActivityName = Activities.RegularActivity.class.getName(); final TestActivitySession activitySession1 = createManagedTestActivitySession(); activitySession1.launchTestActivityOnDisplaySync(regularActivityName, intent1, DEFAULT_DISPLAY); final TestActivitySession activitySession2 = createManagedTestActivitySession(); activitySession2.launchTestActivityOnDisplaySync(Activities.SingleTopActivity.class, DEFAULT_DISPLAY); final CommandSession.ActivitySession activitySession3 = createManagedActivityClientSession().startActivity( new CommandSession.DefaultLaunchProxy() { @Override public void execute() { final Intent intent = new Intent().setComponent(TEST_ACTIVITY); mLaunchInjector.setupIntent(intent); activitySession2.getActivity().startActivity(intent); } }); final Bundle data = new Bundle(); data.putParcelable(EXTRA_INTENT, intent1); activitySession3.sendCommand(COMMAND_NAVIGATE_UP_TO, data); waitAndAssertTopResumedActivity(intent1.getComponent(), DEFAULT_DISPLAY, "navigateUpTo should return to the first activity"); // Make sure the resumed first activity is the original instance. assertFalse("The target of navigateUpTo should not be destroyed", activitySession1.getActivity().isDestroyed()); // The activities above the first one should be destroyed. mWmState.waitAndAssertActivityRemoved( activitySession3.getOriginalLaunchIntent().getComponent()); mWmState.waitAndAssertActivityRemoved(activitySession2.getActivity().getComponentName()); } /** * Assume there are 3 activities (A1, A2, A3) with different task affinities and the same uid. * After A1 called {@link Activity#startActivities} to start A2 (with NEW_TASK) and A3, the * result should be 2 tasks: [A1] and [A2, A3]. */ @Test public void testStartActivitiesInNewAndSameTask() { final int[] taskIds = startActivitiesAndGetTaskIds(new Intent[] { new Intent().setComponent(NO_RELAUNCH_ACTIVITY) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), new Intent().setComponent(LAUNCHING_ACTIVITY) }); assertNotEquals("The activity with different task affinity started by flag NEW_TASK" + " should be in a different task", taskIds[0], taskIds[1]); assertEquals("The activity started without flag NEW_TASK should be put in the same task", taskIds[1], taskIds[2]); } @Test public void testNormalActivityCanNotSetActivityType() { // Activities should not be started if the launch activity type is set. boolean useShellPermission = false; startingActivityWithType(ACTIVITY_TYPE_STANDARD, useShellPermission); startingActivityWithType(ACTIVITY_TYPE_HOME, useShellPermission); startingActivityWithType(ACTIVITY_TYPE_RECENTS, useShellPermission); startingActivityWithType(ACTIVITY_TYPE_ASSISTANT, useShellPermission); startingActivityWithType(ACTIVITY_TYPE_DREAM, useShellPermission); // Activities can be started because they are started with shell permissions. useShellPermission = true; startingActivityWithType(ACTIVITY_TYPE_STANDARD, useShellPermission); startingActivityWithType(ACTIVITY_TYPE_HOME, useShellPermission); startingActivityWithType(ACTIVITY_TYPE_RECENTS, useShellPermission); startingActivityWithType(ACTIVITY_TYPE_ASSISTANT, useShellPermission); startingActivityWithType(ACTIVITY_TYPE_DREAM, useShellPermission); } private void startingActivityWithType(int type, boolean useShellPermission) { separateTestJournal(); getLaunchActivityBuilder() .setTargetActivity(BROADCAST_RECEIVER_ACTIVITY) .setUseInstrumentation() .setWithShellPermission(useShellPermission) .setActivityType(type) .setWaitForLaunched(false) .setMultipleTask(true) .execute(); mWmState.computeState(); if (useShellPermission) { waitAndAssertResumedActivity(BROADCAST_RECEIVER_ACTIVITY, "Activity should be started and resumed"); mWmState.assertFrontStackActivityType("The activity type should be same as requested.", type); mBroadcastActionTrigger.finishBroadcastReceiverActivity(); mWmState.waitAndAssertActivityRemoved(BROADCAST_RECEIVER_ACTIVITY); } else { assertSecurityExceptionFromActivityLauncher(); } } /** * Assume there are 3 activities (A1, A2, B1) with default launch mode. The uid of B1 is * different from A1 and A2. After A1 called {@link Activity#startActivities} to start B1 and * A2, the result should be 3 tasks. */ @Test public void testStartActivitiesWithDiffUidNotInSameTask() { final int[] taskIds = startActivitiesAndGetTaskIds(new Intent[] { new Intent().setComponent(SECOND_ACTIVITY) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), new Intent().setComponent(LAUNCHING_ACTIVITY) }); assertNotEquals("The activity in a different application (uid) started by flag NEW_TASK" + " should be in a different task", taskIds[0], taskIds[1]); assertWithMessage("The last started activity should be in a different task because " + SECOND_ACTIVITY + " has a different uid from the source caller") .that(taskIds[2]).isNotIn(Arrays.asList(taskIds[0], taskIds[1])); } /** * Test the activity launched with ActivityOptions#setTaskOverlay should remain on top of the * task after start another activity. */ @Test public void testStartActivitiesTaskOverlayStayOnTop() { final Intent baseIntent = new Intent(mContext, Activities.RegularActivity.class); final String regularActivityName = Activities.RegularActivity.class.getName(); final TestActivitySession activitySession = createManagedTestActivitySession(); activitySession.launchTestActivityOnDisplaySync(regularActivityName, baseIntent, DEFAULT_DISPLAY); mWmState.computeState(baseIntent.getComponent()); final int taskId = mWmState.getTaskByActivity(baseIntent.getComponent()).getTaskId(); final Activity baseActivity = activitySession.getActivity(); final ActivityOptions overlayOptions = ActivityOptions.makeBasic(); overlayOptions.setTaskOverlay(true, true); overlayOptions.setLaunchTaskId(taskId); final Intent taskOverlay = new Intent().setComponent(SECOND_ACTIVITY); runWithShellPermission(() -> baseActivity.startActivity(taskOverlay, overlayOptions.toBundle())); waitAndAssertResumedActivity(taskOverlay.getComponent(), "taskOverlay activity on top"); final Intent behindOverlay = new Intent().setComponent(TEST_ACTIVITY); baseActivity.startActivity(behindOverlay); waitAndAssertActivityState(TEST_ACTIVITY, STATE_INITIALIZING, "Activity behind taskOverlay should not resumed"); // check order: SecondActivity(top) -> TestActivity -> RegularActivity(base) final List activitiesOrder = mWmState.getTaskByActivity(baseIntent.getComponent()) .mActivities .stream() .map(WindowManagerState.Activity::getName) .collect(Collectors.toList()); final List expectedOrder = Stream.of( SECOND_ACTIVITY, TEST_ACTIVITY, baseIntent.getComponent()) .map(c -> c.flattenToShortString()) .collect(Collectors.toList()); assertEquals(activitiesOrder, expectedOrder); mWmState.assertResumedActivity("TaskOverlay activity should be remained on top and " + "resumed", taskOverlay.getComponent()); } /** * Invokes {@link android.app.Activity#startActivities} from {@link #TEST_ACTIVITY} and returns * the task id of each started activity (the index 0 will be the caller {@link #TEST_ACTIVITY}). */ private int[] startActivitiesAndGetTaskIds(Intent[] intents) { final ActivitySession activity = createManagedActivityClientSession() .startActivity(getLaunchActivityBuilder().setUseInstrumentation()); final Bundle intentBundle = new Bundle(); intentBundle.putParcelableArray(EXTRA_INTENTS, intents); // The {@link Activity#startActivities} cannot be called from the instrumentation // package because the implementation (given by test runner) may be overridden. activity.sendCommand(COMMAND_START_ACTIVITIES, intentBundle); final int[] taskIds = new int[intents.length + 1]; // The {@code intents} are started, wait for the last (top) activity to be ready and then // verify their task ids. mWmState.computeState(intents[intents.length - 1].getComponent()); taskIds[0] = mWmState.getTaskByActivity(TEST_ACTIVITY).getTaskId(); for (int i = 0; i < intents.length; i++) { taskIds[i + 1] = mWmState.getTaskByActivity(intents[i].getComponent()).getTaskId(); } return taskIds; } }