blob: 6e8547bc81b1add1c83ef75406296a11844f0042 [file] [log] [blame]
/*
* Copyright (C) 2022 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.car.cts.builtin.app;
import static android.car.builtin.app.ActivityManagerHelper.ProcessObserverCallback;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.Instrumentation;
import android.app.TaskInfo;
import android.car.builtin.app.ActivityManagerHelper;
import android.car.cts.builtin.activity.ActivityManagerTestActivityBase;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.os.Process;
import android.os.UserHandle;
import android.server.wm.ActivityManagerTestBase;
import android.server.wm.WindowManagerState;
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.PollingCheck;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RunWith(AndroidJUnit4.class)
public final class ActivityManagerHelperTest extends ActivityManagerTestBase {
// type values from frameworks/base/core/java/android/app/WindowConfiguration
enum ActivityType {
ACTIVITY_TYPE_UNDEFINED,
ACTIVITY_TYPE_STANDARD
}
private static final String TAG = ActivityManagerHelperTest.class.getSimpleName();
private static final String PERMISSION_SET_ACTIVITY_WATCHER =
"android.permission.SET_ACTIVITY_WATCHER";
private static final String NOT_REQUESTED_PERMISSION_CAR_MILEAGE =
"android.car.permission.CAR_MILEAGE";
private static final String NOT_REQUESTED_PERMISSION_READ_CAR_POWER_POLICY =
"android.car.permission.READ_CAR_POWER_POLICY";
private static final String GRANTED_PERMISSION_INTERACT_ACROSS_USERS =
"android.permission.INTERACT_ACROSS_USERS";
// ActivityManagerHelper.removeTask needs this permission
private static final String PERMISSION_REMOVE_TASKS = "android.permission.REMOVE_TASKS";
// IActivityManager.getAllRootTaskInfos called in ActivityManagerHelper.stopAllTaskForUser
// needs this permission.
private static final String PERMISSION_MANAGE_ACTIVITY_TASKS =
"android.permission.MANAGE_ACTIVITY_TASKS";
// ActivityManager.getRunningAppProcess called in isAppRunning needs this permission
private static final String PERMISSION_INTERACT_ACROSS_USERS_FULL =
"android.permission.INTERACT_ACROSS_USERS_FULL";
private static final String SIMPLE_APP_PACKAGE_NAME = "android.car.cts.builtin.apps.simple";
private static final String SIMPLE_ACTIVITY_RELATIVE_NAME = ".SimpleActivity";
private static final String SIMPLE_ACTIVITY_NAME = SIMPLE_APP_PACKAGE_NAME
+ SIMPLE_ACTIVITY_RELATIVE_NAME;
private static final String START_SIMPLE_ACTIVITY_COMMAND = "am start -W -n "
+ SIMPLE_APP_PACKAGE_NAME + "/" + SIMPLE_ACTIVITY_RELATIVE_NAME;
private static final ComponentName SIMPLE_ACTIVITY_COMPONENT_NAME =
new ComponentName(SIMPLE_APP_PACKAGE_NAME, SIMPLE_ACTIVITY_RELATIVE_NAME);
// TODO(b/230757942): replace following shell commands with direct API calls
private static final String CREATE_USER_COMMAND = "cmd car_service create-user ";
private static final String SWITCH_USER_COMMAND = "cmd car_service switch-user ";
private static final String REMOVE_USER_COMMAND = "cmd car_service remove-user ";
private static final String START_USER_COMMAND = "am start-user -w ";
private static final String GET_CURRENT_USER_COMMAND = "am get-current-user ";
private static final String CTS_CAR_TEST_USER_NAME = "CtsCarTestUser";
// the value from UserHandle.USER_NULL
private static final int INVALID_USER_ID = -10_000;
private static final int OWNING_UID = UserHandle.ALL.getIdentifier();
private static final int MAX_NUM_TASKS = 1_000;
private static final int TIMEOUT_MS = 4_000;
// x coordinate of the left boundary line of the animation rectangle
private static final int ANIMATION_RECT_LEFT = 0;
// y coordinate of the top boundary line of the animation rectangle
private static final int ANIMATION_RECT_TOP = 200;
// x coordinate of the right boundary line of the animation rectangle
private static final int ANIMATION_RECT_RIGHT = 400;
// y coordinate of the bottom boundary line of the animation rectangle
private static final int ANIMATION_RECT_BOTTOM = 0;
private static final int RANDOM_NON_DEFAULT_DISPLAY_ID = 1;
private static final boolean NON_DEFAULT_LOCK_TASK_MODE = true;
private static final boolean
NON_DEFAULT_PENDING_INTENT_BACKGROUND_ACTIVITY_LAUNCH_ALLOWED = true;
private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
private final Context mContext = mInstrumentation.getContext();
@Before
public void setUp() throws Exception {
// Home was launched in ActivityManagerTestBase#setUp, wait until it is stable,
// in order not to mix the event of its TaskView Activity with the TestActivity.
mWmState.waitForHomeActivityVisible();
}
@Test
public void testCheckComponentPermission() throws Exception {
// not requested from Manifest
assertComponentPermissionNotGranted(NOT_REQUESTED_PERMISSION_CAR_MILEAGE);
assertComponentPermissionNotGranted(NOT_REQUESTED_PERMISSION_READ_CAR_POWER_POLICY);
// requested from Manifest and granted
assertComponentPermissionGranted(GRANTED_PERMISSION_INTERACT_ACROSS_USERS);
}
@Test
public void testSetFocusedRootTask() throws Exception {
// setup
ActivityA task1BottomActivity = launchTestActivity(ActivityA.class);
ActivityB task1TopActivity = launchTestActivity(ActivityB.class);
ActivityC task2TopActivity = launchTestActivity(ActivityC.class);
logActivityStack("amTestActivitys ",
task1BottomActivity, task1TopActivity, task2TopActivity);
assertWithMessage("bottom activity is the task root")
.that(task1BottomActivity.isTaskRoot()).isTrue();
assertWithMessage("task id of the top activity in the task1")
.that(task1TopActivity.getTaskId()).isEqualTo(task1BottomActivity.getTaskId());
assertWithMessage("task id of the top activity in the task2")
.that(task2TopActivity.getTaskId()).isNotEqualTo(task1TopActivity.getTaskId());
assertWithMessage("task1 top activity is visible")
.that(task1TopActivity.isVisible()).isFalse();
assertWithMessage("task2 top activity is visible")
.that(task2TopActivity.isVisible()).isTrue();
// execute
try {
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(
PERMISSION_MANAGE_ACTIVITY_TASKS);
ActivityManagerHelper.setFocusedRootTask(task1BottomActivity.getTaskId());
} finally {
mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
}
// assert
ComponentName activityName = task1TopActivity.getComponentName();
waitAndAssertTopResumedActivity(activityName, DEFAULT_DISPLAY,
"Activity must be resumed");
waitAndAssertFocusStatusChanged(task1TopActivity, true);
assertWithMessage("task1 top activity is visible")
.that(task1TopActivity.isVisible()).isTrue();
// teardown
task1TopActivity.finish();
task1BottomActivity.finish();
task2TopActivity.finish();
}
@Test
public void testRemoveTask() throws Exception {
// setup
ActivityC testActivity = launchTestActivity(ActivityC.class);
int taskId = testActivity.getTaskId();
assertThat(doesTaskExist(taskId)).isTrue();
// execute
try {
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(
PERMISSION_REMOVE_TASKS);
ActivityManagerHelper.removeTask(taskId);
} finally {
mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
}
// assert
PollingCheck.waitFor(TIMEOUT_MS, () -> testActivity.isDestroyed());
assertThat(doesTaskExist(taskId)).isFalse();
}
@Test
public void testProcessObserverCallback() throws Exception {
// setup
ProcessObserverCallbackTestImpl callbackImpl = new ProcessObserverCallbackTestImpl();
// execute
try {
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(
PERMISSION_SET_ACTIVITY_WATCHER); // for registerProcessObserverCallback
ActivityManagerHelper.registerProcessObserverCallback(callbackImpl);
launchSimpleActivity();
// assert
assertThat(callbackImpl.waitForForegroundActivitiesChanged()).isTrue();
} finally {
// teardown
ActivityManagerHelper.unregisterProcessObserverCallback(callbackImpl);
mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
}
}
@Test
public void testCreateActivityOptions() {
Rect expectedRect = new Rect(ANIMATION_RECT_LEFT,
ANIMATION_RECT_TOP,
ANIMATION_RECT_RIGHT,
ANIMATION_RECT_BOTTOM);
int expectedDisplayId = RANDOM_NON_DEFAULT_DISPLAY_ID;
boolean expectedLockTaskMode = NON_DEFAULT_LOCK_TASK_MODE;
boolean expectedLaunchAllowed =
NON_DEFAULT_PENDING_INTENT_BACKGROUND_ACTIVITY_LAUNCH_ALLOWED;
ActivityOptions originalOptions =
ActivityOptions.makeCustomAnimation(mContext,
/* entResId= */ android.R.anim.fade_in,
/* exitResId= */ android.R.anim.fade_out);
originalOptions.setLaunchBounds(expectedRect);
originalOptions.setLaunchDisplayId(expectedDisplayId);
originalOptions.setLockTaskEnabled(expectedLockTaskMode);
originalOptions.setPendingIntentBackgroundActivityLaunchAllowed(expectedLaunchAllowed);
ActivityOptions createdOptions =
ActivityManagerHelper.createActivityOptions(originalOptions.toBundle());
assertThat(createdOptions.getLaunchBounds()).isEqualTo(expectedRect);
assertThat(createdOptions.getLaunchDisplayId()).isEqualTo(expectedDisplayId);
assertThat(createdOptions.getLockTaskMode()).isEqualTo(expectedLockTaskMode);
assertThat(createdOptions.isPendingIntentBackgroundActivityLaunchAllowed())
.isEqualTo(expectedLaunchAllowed);
}
@Ignore("b/232432706")
@Test
public void testStopAllTasksForUser() throws Exception {
int initialCurrentUserId = getCurrentUserId();
int testUserId = INVALID_USER_ID;
try {
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(
PERMISSION_MANAGE_ACTIVITY_TASKS,
PERMISSION_REMOVE_TASKS,
PERMISSION_INTERACT_ACROSS_USERS_FULL);
testUserId = createUser(CTS_CAR_TEST_USER_NAME);
startUser(testUserId);
switchUser(testUserId);
waitUntilUserCurrent(testUserId);
installPackageForUser(testUserId);
launchSimpleActivityInCurrentUser();
waitUntilSimpleActivityExistenceStatusIs(true);
assertThat(isAppRunning(SIMPLE_APP_PACKAGE_NAME)).isTrue();
switchUser(initialCurrentUserId);
waitUntilUserCurrent(initialCurrentUserId);
stopAllTasksForUser(testUserId);
waitUntilSimpleActivityExistenceStatusIs(false);
assertThat(isAppRunning(SIMPLE_APP_PACKAGE_NAME)).isFalse();
removeUser(testUserId);
testUserId = INVALID_USER_ID;
} finally {
mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
deepCleanTestStopAllTasksForUser(testUserId, initialCurrentUserId);
}
}
private void deepCleanTestStopAllTasksForUser(int testUserId, int initialCurrentUserId)
throws Exception {
try {
if (initialCurrentUserId != getCurrentUserId()) {
switchUser(initialCurrentUserId);
waitUntilUserCurrent(initialCurrentUserId);
}
} finally {
if (testUserId != INVALID_USER_ID) {
removeUser(testUserId);
}
}
}
private void assertComponentPermissionGranted(String permission) throws Exception {
assertThat(ActivityManagerHelper.checkComponentPermission(permission,
Process.myUid(), /* owningUid= */ OWNING_UID, /* exported= */ true))
.isEqualTo(PackageManager.PERMISSION_GRANTED);
}
private void assertComponentPermissionNotGranted(String permission) throws Exception {
assertThat(ActivityManagerHelper.checkComponentPermission(permission,
Process.myUid(), /* owningUid= */ OWNING_UID, /* exported= */ true))
.isEqualTo(PackageManager.PERMISSION_DENIED);
}
private static final class ProcessObserverCallbackTestImpl extends ProcessObserverCallback {
private final CountDownLatch mLatch = new CountDownLatch(1);
// Use onForegroundActivitiesChanged(), because onProcessDied() can be called
// in very long time later even if the task was removed.
@Override
public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
Log.d(TAG, "onForegroundActivitiesChanged: pid " + pid + " uid " + uid);
mLatch.countDown();
}
public boolean waitForForegroundActivitiesChanged() throws Exception {
return mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
}
private void launchSimpleActivity() {
ComponentName simpleActivity = new ComponentName(
SIMPLE_APP_PACKAGE_NAME, SIMPLE_ACTIVITY_NAME);
Intent intent = new Intent()
.setComponent(simpleActivity)
.addFlags(FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent, /* options = */ null);
waitAndAssertTopResumedActivity(simpleActivity, DEFAULT_DISPLAY, "Activity isn't resumed");
}
// launchSimpleActivity in the current user space via the car shell instead of the calling user.
// The calling user could be in the background.
private static void launchSimpleActivityInCurrentUser() {
Log.d(TAG, "launchSimpleActivityInCurrentUser: " + START_SIMPLE_ACTIVITY_COMMAND);
String retStr = SystemUtil.runShellCommand(START_SIMPLE_ACTIVITY_COMMAND);
Log.d(TAG, "launchSimpleActivityInCurrentUser return: " + retStr);
}
private static void installPackageForUser(int userId) {
String fullCommand = String.format("pm install-existing --user %d %s",
userId, SIMPLE_APP_PACKAGE_NAME);
Log.d(TAG, "installPackageForUser: " + fullCommand);
String retStr = SystemUtil.runShellCommand(fullCommand);
Log.d(TAG, "installPackageForUser return: " + retStr);
}
private boolean isAppRunning(String pkgName) {
ActivityManager am = mContext.getSystemService(ActivityManager.class);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses =
am.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo procInfo : runningAppProcesses) {
if (pkgName.equals(procInfo.processName)) {
return true;
}
}
return false;
}
private <T> T launchTestActivity(Class<T> type) {
Intent startIntent = new Intent(mContext, type)
.addFlags(FLAG_ACTIVITY_NEW_TASK);
Activity testActivity = (Activity) mInstrumentation
.startActivitySync(startIntent, /* options = */ null);
ComponentName testActivityName = testActivity.getComponentName();
waitAndAssertTopResumedActivity(testActivityName, DEFAULT_DISPLAY,
"Activity must be resumed");
return type.cast(testActivity);
}
// The logging order of the Activities follows the stack order. The first Activity
// in the parameter list is logged at last.
private static void logActivityStack(String msg, Activity... activityStack) {
for (int index = activityStack.length - 1; index >= 0; index--) {
String logMsg = String.format("%s\tindex=%d taskId=%d",
msg, index, activityStack[index].getTaskId());
Log.d(TAG, logMsg);
}
}
private boolean doesTaskExist(int taskId) {
boolean retVal = false;
try {
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(
PERMISSION_REMOVE_TASKS);
ActivityManager am = mContext.getSystemService(ActivityManager.class);
List<ActivityManager.RunningTaskInfo> taskList = am.getRunningTasks(MAX_NUM_TASKS);
for (TaskInfo taskInfo : taskList) {
if (taskInfo.taskId == taskId) {
retVal = true;
break;
}
}
} finally {
mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
}
return retVal;
}
public static final class ActivityA extends ActivityManagerTestActivityBase {
}
public static final class ActivityB extends ActivityManagerTestActivityBase {
}
public static final class ActivityC extends ActivityManagerTestActivityBase {
}
private static void waitAndAssertFocusStatusChanged(ActivityManagerTestActivityBase activity,
boolean expectedStatus) throws Exception {
PollingCheck.waitFor(TIMEOUT_MS, () -> activity.hasFocus() == expectedStatus);
}
private static int createUser(String userName) throws Exception {
Log.d(TAG, "createUser: " + userName);
String retStr = SystemUtil.runShellCommand(CREATE_USER_COMMAND + userName);
Pattern userIdPattern = Pattern.compile("id=(\\d+)");
Matcher matcher = userIdPattern.matcher(retStr);
if (!matcher.find()) {
throw new Exception("failed to create user: " + userName);
}
return Integer.parseInt(matcher.group(1));
}
private static void switchUser(int userId) throws Exception {
Log.d(TAG, "switchUser: " + userId);
String retStr = SystemUtil.runShellCommand(SWITCH_USER_COMMAND + userId);
if (!retStr.contains("STATUS_SUCCESSFUL")) {
throw new Exception("failed to switch to user: " + userId);
}
Log.d(TAG, "switchUser: " + retStr);
}
private static void removeUser(int userId) throws Exception {
Log.d(TAG, "removeUser: " + userId);
String retStr = SystemUtil.runShellCommand(REMOVE_USER_COMMAND + userId);
if (!retStr.contains("STATUS_SUCCESSFUL")) {
throw new Exception("failed to remove user: " + userId);
}
Log.d(TAG, "removeUser: " + retStr);
}
private static void startUser(int userId) throws Exception {
String retStr = SystemUtil.runShellCommand(START_USER_COMMAND + userId);
if (!retStr.contains("Success: user started")) {
throw new Exception("failed to start user: " + userId + " with return: " + retStr);
}
Log.d(TAG, "startUser: " + retStr);
}
private static int getCurrentUserId() {
String retStr = SystemUtil.runShellCommand(GET_CURRENT_USER_COMMAND);
Log.d(TAG, "getCurrentUserId: " + retStr);
return Integer.parseInt(retStr.trim());
}
private static void waitUntilUserCurrent(int userId) throws Exception {
PollingCheck.waitFor(TIMEOUT_MS, () -> userId == getCurrentUserId());
}
// need to get the permission in the same user
private static void stopAllTasksForUser(int userId) {
ActivityManagerHelper.stopAllTasksForUser(userId);
}
private static void waitUntilSimpleActivityExistenceStatusIs(boolean expectedStatus) {
PollingCheck.waitFor(TIMEOUT_MS,
() -> (checkSimpleActivityExistence() == expectedStatus));
}
private static boolean checkSimpleActivityExistence() {
boolean foundSimpleActivity = false;
Log.d(TAG, "checkSimpleActivityExistence --- Begin");
WindowManagerState wmState = new WindowManagerState();
wmState.computeState();
for (ActivityType activityType : ActivityType.values()) {
if (findSimpleActivityInType(activityType, wmState)) {
foundSimpleActivity = true;
break;
}
}
Log.d(TAG, "checkSimpleActivityExistence --- End with --- " + foundSimpleActivity);
return foundSimpleActivity;
}
private static boolean findSimpleActivityInType(ActivityType activityType,
WindowManagerState wmState) {
boolean foundRootTask = false;
boolean foundSimpleActivity = false;
WindowManagerState.Task rootTask =
wmState.getRootTaskByActivityType(activityType.ordinal());
if (rootTask != null) {
foundRootTask = true;
List<WindowManagerState.Activity> allActivities = rootTask.getActivities();
if (rootTask.getActivity(SIMPLE_ACTIVITY_COMPONENT_NAME) != null) {
foundSimpleActivity = true;
}
// for debugging purpose only
for (WindowManagerState.Activity act : allActivities) {
Log.d(TAG, activityType.name() + ": activity name -- " + act.getName());
}
}
Log.d(TAG, activityType.name() + " has simple activity root task:" + foundRootTask);
Log.d(TAG, activityType.name() + " has simple activity: " + foundSimpleActivity);
return foundSimpleActivity;
}
}