| /* |
| * Copyright (C) 2016 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.am; |
| |
| import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT; |
| import static android.app.Instrumentation.ActivityMonitor; |
| import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; |
| import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; |
| import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; |
| import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; |
| import static android.content.Intent.ACTION_MAIN; |
| import static android.content.Intent.CATEGORY_HOME; |
| import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; |
| import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; |
| import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; |
| import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; |
| import static android.content.pm.PackageManager.FEATURE_EMBEDDED; |
| import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; |
| import static android.content.pm.PackageManager.FEATURE_LEANBACK; |
| import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; |
| import static android.content.pm.PackageManager.FEATURE_SCREEN_LANDSCAPE; |
| import static android.content.pm.PackageManager.FEATURE_SCREEN_PORTRAIT; |
| import static android.content.pm.PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE; |
| import static android.content.pm.PackageManager.FEATURE_WATCH; |
| import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; |
| import static android.server.am.ActivityLauncher.KEY_ACTIVITY_TYPE; |
| import static android.server.am.ActivityLauncher.KEY_DISPLAY_ID; |
| import static android.server.am.ActivityLauncher.KEY_INTENT_FLAGS; |
| import static android.server.am.ActivityLauncher.KEY_LAUNCH_ACTIVITY; |
| import static android.server.am.ActivityLauncher.KEY_LAUNCH_TO_SIDE; |
| import static android.server.am.ActivityLauncher.KEY_MULTIPLE_INSTANCES; |
| import static android.server.am.ActivityLauncher.KEY_MULTIPLE_TASK; |
| import static android.server.am.ActivityLauncher.KEY_NEW_TASK; |
| import static android.server.am.ActivityLauncher.KEY_RANDOM_DATA; |
| import static android.server.am.ActivityLauncher.KEY_REORDER_TO_FRONT; |
| import static android.server.am.ActivityLauncher.KEY_SUPPRESS_EXCEPTIONS; |
| import static android.server.am.ActivityLauncher.KEY_TARGET_COMPONENT; |
| import static android.server.am.ActivityLauncher.KEY_USE_APPLICATION_CONTEXT; |
| import static android.server.am.ActivityLauncher.KEY_USE_INSTRUMENTATION; |
| import static android.server.am.ActivityLauncher.launchActivityFromExtras; |
| import static android.server.am.ActivityManagerState.STATE_RESUMED; |
| import static android.server.am.ComponentNameUtils.getActivityName; |
| import static android.server.am.ComponentNameUtils.getLogTag; |
| import static android.server.am.Components.BROADCAST_RECEIVER_ACTIVITY; |
| import static android.server.am.Components.BroadcastReceiverActivity.ACTION_TRIGGER_BROADCAST; |
| import static android.server.am.Components.BroadcastReceiverActivity.EXTRA_BROADCAST_ORIENTATION; |
| import static android.server.am.Components.BroadcastReceiverActivity.EXTRA_DISMISS_KEYGUARD; |
| import static android.server.am.Components.BroadcastReceiverActivity.EXTRA_DISMISS_KEYGUARD_METHOD; |
| import static android.server.am.Components.BroadcastReceiverActivity.EXTRA_FINISH_BROADCAST; |
| import static android.server.am.Components.BroadcastReceiverActivity.EXTRA_MOVE_BROADCAST_TO_BACK; |
| import static android.server.am.Components.LAUNCHING_ACTIVITY; |
| import static android.server.am.Components.PipActivity.ACTION_EXPAND_PIP; |
| import static android.server.am.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION; |
| import static android.server.am.Components.PipActivity.EXTRA_PIP_ORIENTATION; |
| import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR; |
| import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR; |
| import static android.server.am.Components.TEST_ACTIVITY; |
| import static android.server.am.StateLogger.log; |
| import static android.server.am.StateLogger.logAlways; |
| import static android.server.am.StateLogger.logE; |
| import static android.server.am.UiDeviceUtils.pressAppSwitchButton; |
| import static android.server.am.UiDeviceUtils.pressBackButton; |
| import static android.server.am.UiDeviceUtils.pressEnterButton; |
| import static android.server.am.UiDeviceUtils.pressHomeButton; |
| import static android.server.am.UiDeviceUtils.pressSleepButton; |
| import static android.server.am.UiDeviceUtils.pressUnlockButton; |
| import static android.server.am.UiDeviceUtils.pressWakeupButton; |
| import static android.server.am.UiDeviceUtils.waitForDeviceIdle; |
| import static android.support.test.InstrumentationRegistry.getContext; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| import static android.view.Display.INVALID_DISPLAY; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import static java.lang.Integer.toHexString; |
| |
| import android.accessibilityservice.AccessibilityService; |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.app.ActivityOptions; |
| import android.app.ActivityTaskManager; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.Rect; |
| import android.hardware.display.DisplayManager; |
| import android.os.Bundle; |
| import android.os.SystemClock; |
| import android.provider.Settings; |
| import android.server.am.CommandSession.ActivityCallback; |
| import android.server.am.CommandSession.ActivitySession; |
| import android.server.am.CommandSession.LaunchInjector; |
| import android.server.am.CommandSession.LaunchProxy; |
| import android.server.am.settings.SettingsSession; |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.rule.ActivityTestRule; |
| import android.util.EventLog; |
| import android.util.EventLog.Event; |
| import android.view.Display; |
| import android.view.InputDevice; |
| import android.view.MotionEvent; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.compatibility.common.util.SystemUtil; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.rules.TestRule; |
| import org.junit.runner.Description; |
| import org.junit.runners.model.Statement; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.UUID; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.TimeUnit; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import java.util.stream.IntStream; |
| |
| public abstract class ActivityManagerTestBase { |
| private static final boolean PRETEND_DEVICE_SUPPORTS_PIP = false; |
| private static final boolean PRETEND_DEVICE_SUPPORTS_FREEFORM = false; |
| private static final String LOG_SEPARATOR = "LOG_SEPARATOR"; |
| // Use one of the test tags as a separator |
| private static final int EVENT_LOG_SEPARATOR_TAG = 42; |
| |
| protected static final int[] ALL_ACTIVITY_TYPE_BUT_HOME = { |
| ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_ASSISTANT, ACTIVITY_TYPE_RECENTS, |
| ACTIVITY_TYPE_UNDEFINED |
| }; |
| |
| private static final String TEST_PACKAGE = "android.server.am"; |
| private static final String SECOND_TEST_PACKAGE = "android.server.am.second"; |
| private static final String THIRD_TEST_PACKAGE = "android.server.am.third"; |
| private static final List<String> TEST_PACKAGES; |
| static { |
| final List<String> testPackages = new ArrayList<>(3); |
| testPackages.add(TEST_PACKAGE); |
| testPackages.add(SECOND_TEST_PACKAGE); |
| testPackages.add(THIRD_TEST_PACKAGE); |
| TEST_PACKAGES = Collections.unmodifiableList(testPackages); |
| } |
| |
| protected static final String AM_START_HOME_ACTIVITY_COMMAND = |
| "am start -a android.intent.action.MAIN -c android.intent.category.HOME"; |
| |
| private static final String LOCK_CREDENTIAL = "1234"; |
| |
| private static final int UI_MODE_TYPE_MASK = 0x0f; |
| private static final int UI_MODE_TYPE_VR_HEADSET = 0x07; |
| |
| private static Boolean sHasHomeScreen = null; |
| |
| protected static final int INVALID_DEVICE_ROTATION = -1; |
| |
| protected Context mContext; |
| protected ActivityManager mAm; |
| protected ActivityTaskManager mAtm; |
| |
| /** |
| * Callable to clear launch params for all test packages. |
| */ |
| private final Callable<Void> mClearLaunchParamsCallable = () -> { |
| mAtm.clearLaunchParamsForPackages(TEST_PACKAGES); |
| return null; |
| }; |
| |
| @Rule |
| public final ActivityTestRule<SideActivity> mSideActivityRule = |
| new ActivityTestRule<>(SideActivity.class, true /* initialTouchMode */, |
| false /* launchActivity */); |
| |
| /** |
| * @return the am command to start the given activity with the following extra key/value pairs. |
| * {@param keyValuePairs} must be a list of arguments defining each key/value extra. |
| */ |
| // TODO: Make this more generic, for instance accepting flags or extras of other types. |
| protected static String getAmStartCmd(final ComponentName activityName, |
| final String... keyValuePairs) { |
| return getAmStartCmdInternal(getActivityName(activityName), keyValuePairs); |
| } |
| |
| private static String getAmStartCmdInternal(final String activityName, |
| final String... keyValuePairs) { |
| return appendKeyValuePairs( |
| new StringBuilder("am start -n ").append(activityName), |
| keyValuePairs); |
| } |
| |
| private static String appendKeyValuePairs( |
| final StringBuilder cmd, final String... keyValuePairs) { |
| if (keyValuePairs.length % 2 != 0) { |
| throw new RuntimeException("keyValuePairs must be pairs of key/value arguments"); |
| } |
| for (int i = 0; i < keyValuePairs.length; i += 2) { |
| final String key = keyValuePairs[i]; |
| final String value = keyValuePairs[i + 1]; |
| cmd.append(" --es ") |
| .append(key) |
| .append(" ") |
| .append(value); |
| } |
| return cmd.toString(); |
| } |
| |
| protected static String getAmStartCmd(final ComponentName activityName, final int displayId, |
| final String... keyValuePair) { |
| return getAmStartCmdInternal(getActivityName(activityName), displayId, keyValuePair); |
| } |
| |
| private static String getAmStartCmdInternal(final String activityName, final int displayId, |
| final String... keyValuePairs) { |
| return appendKeyValuePairs( |
| new StringBuilder("am start -n ") |
| .append(activityName) |
| .append(" -f 0x") |
| .append(toHexString(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK)) |
| .append(" --display ") |
| .append(displayId), |
| keyValuePairs); |
| } |
| |
| protected static String getAmStartCmdInNewTask(final ComponentName activityName) { |
| return "am start -n " + getActivityName(activityName) + " -f 0x18000000"; |
| } |
| |
| protected static String getAmStartCmdOverHome(final ComponentName activityName) { |
| return "am start --activity-task-on-home -n " + getActivityName(activityName); |
| } |
| |
| protected ActivityAndWindowManagersState mAmWmState = new ActivityAndWindowManagersState(); |
| |
| protected BroadcastActionTrigger mBroadcastActionTrigger = new BroadcastActionTrigger(); |
| |
| /** |
| * Helper class to process test actions by broadcast. |
| */ |
| protected class BroadcastActionTrigger { |
| |
| private Intent createIntentWithAction(String broadcastAction) { |
| return new Intent(broadcastAction) |
| .setFlags(Intent.FLAG_RECEIVER_FOREGROUND); |
| } |
| |
| void doAction(String broadcastAction) { |
| mContext.sendBroadcast(createIntentWithAction(broadcastAction)); |
| } |
| |
| void finishBroadcastReceiverActivity() { |
| mContext.sendBroadcast(createIntentWithAction(ACTION_TRIGGER_BROADCAST) |
| .putExtra(EXTRA_FINISH_BROADCAST, true)); |
| } |
| |
| void launchActivityNewTask(String launchComponent) { |
| mContext.sendBroadcast(createIntentWithAction(ACTION_TRIGGER_BROADCAST) |
| .putExtra(KEY_LAUNCH_ACTIVITY, true) |
| .putExtra(KEY_NEW_TASK, true) |
| .putExtra(KEY_TARGET_COMPONENT, launchComponent)); |
| } |
| |
| void moveTopTaskToBack() { |
| mContext.sendBroadcast(createIntentWithAction(ACTION_TRIGGER_BROADCAST) |
| .putExtra(EXTRA_MOVE_BROADCAST_TO_BACK, true)); |
| } |
| |
| void requestOrientation(int orientation) { |
| mContext.sendBroadcast(createIntentWithAction(ACTION_TRIGGER_BROADCAST) |
| .putExtra(EXTRA_BROADCAST_ORIENTATION, orientation)); |
| } |
| |
| void dismissKeyguardByFlag() { |
| mContext.sendBroadcast(createIntentWithAction(ACTION_TRIGGER_BROADCAST) |
| .putExtra(EXTRA_DISMISS_KEYGUARD, true)); |
| } |
| |
| void dismissKeyguardByMethod() { |
| mContext.sendBroadcast(createIntentWithAction(ACTION_TRIGGER_BROADCAST) |
| .putExtra(EXTRA_DISMISS_KEYGUARD_METHOD, true)); |
| } |
| |
| void expandPipWithAspectRatio(String extraNum, String extraDenom) { |
| mContext.sendBroadcast(createIntentWithAction(ACTION_EXPAND_PIP) |
| .putExtra(EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR, extraNum) |
| .putExtra(EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR, extraDenom)); |
| } |
| |
| void requestOrientationForPip(int orientation) { |
| mContext.sendBroadcast(createIntentWithAction(ACTION_SET_REQUESTED_ORIENTATION) |
| .putExtra(EXTRA_PIP_ORIENTATION, String.valueOf(orientation))); |
| } |
| } |
| |
| /** |
| * Helper class to launch / close test activity by instrumentation way. |
| */ |
| protected class TestActivitySession<T extends Activity> implements AutoCloseable { |
| private T mTestActivity; |
| boolean mFinishAfterClose; |
| private static final int ACTIVITY_LAUNCH_TIMEOUT = 10000; |
| |
| void launchTestActivityOnDisplaySync(Class<T> activityClass, int displayId) { |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| final Bundle bundle = ActivityOptions.makeBasic() |
| .setLaunchDisplayId(displayId).toBundle(); |
| final ActivityMonitor monitor = InstrumentationRegistry.getInstrumentation() |
| .addMonitor((String) null, null, false); |
| mContext.startActivity(new Intent(mContext, activityClass) |
| .addFlags(FLAG_ACTIVITY_NEW_TASK), bundle); |
| // Wait for activity launch with timeout. |
| mTestActivity = (T) monitor.waitForActivityWithTimeout(ACTIVITY_LAUNCH_TIMEOUT); |
| assertNotNull(mTestActivity); |
| // Check activity is launched and resumed. |
| final ComponentName testActivityName = mTestActivity.getComponentName(); |
| waitAndAssertTopResumedActivity(testActivityName, displayId, |
| "Activity must be resumed"); |
| }); |
| } |
| |
| void finishCurrentActivityNoWait() { |
| if (mTestActivity != null) { |
| mTestActivity.finishAndRemoveTask(); |
| mTestActivity = null; |
| } |
| } |
| |
| void runOnMainSyncAndWait(Runnable runnable) { |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); |
| InstrumentationRegistry.getInstrumentation().waitForIdleSync(); |
| } |
| |
| T getActivity() { |
| return mTestActivity; |
| } |
| |
| @Override |
| public void close() throws Exception { |
| if (mTestActivity != null && mFinishAfterClose) { |
| mTestActivity.finishAndRemoveTask(); |
| } |
| } |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| mContext = InstrumentationRegistry.getContext(); |
| mAm = mContext.getSystemService(ActivityManager.class); |
| mAtm = mContext.getSystemService(ActivityTaskManager.class); |
| |
| pressWakeupButton(); |
| pressUnlockButton(); |
| pressHomeButton(); |
| removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME); |
| |
| // Clear launch params for all test packages to make sure each test is run in a clean state. |
| SystemUtil.callWithShellPermissionIdentity(mClearLaunchParamsCallable); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| // Synchronous execution of removeStacksWithActivityTypes() ensures that all activities but |
| // home are cleaned up from the stack at the end of each test. Am force stop shell commands |
| // might be asynchronous and could interrupt the stack cleanup process if executed first. |
| removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME); |
| stopTestPackage(TEST_PACKAGE); |
| stopTestPackage(SECOND_TEST_PACKAGE); |
| stopTestPackage(THIRD_TEST_PACKAGE); |
| pressHomeButton(); |
| |
| } |
| |
| protected void moveTopActivityToPinnedStack(int stackId) { |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> mAtm.moveTopActivityToPinnedStack(stackId, new Rect(0, 0, 500, 500)) |
| ); |
| } |
| |
| protected void startActivityOnDisplay(int displayId, ComponentName component) { |
| final ActivityOptions options = ActivityOptions.makeBasic(); |
| options.setLaunchDisplayId(displayId); |
| |
| mContext.startActivity(new Intent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
| .setComponent(component), options.toBundle()); |
| } |
| |
| protected boolean noHomeScreen() { |
| try { |
| return mContext.getResources().getBoolean( |
| Resources.getSystem().getIdentifier("config_noHomeScreen", "bool", |
| "android")); |
| } catch (Resources.NotFoundException e) { |
| // Assume there's a home screen. |
| return false; |
| } |
| } |
| |
| protected void tapOnDisplay(int x, int y, int displayId) { |
| final long downTime = SystemClock.uptimeMillis(); |
| injectMotion(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, displayId); |
| |
| final long upTime = SystemClock.uptimeMillis(); |
| injectMotion(downTime, upTime, MotionEvent.ACTION_UP, x, y, displayId); |
| } |
| |
| private static void injectMotion(long downTime, long eventTime, int action, |
| int x, int y, int displayId) { |
| final MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, |
| x, y, 0 /* metaState */); |
| event.setSource(InputDevice.SOURCE_TOUCHSCREEN); |
| event.setDisplayId(displayId); |
| InstrumentationRegistry.getInstrumentation().getUiAutomation().injectInputEvent( |
| event, true /* sync */); |
| } |
| |
| protected void removeStacksWithActivityTypes(int... activityTypes) { |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> mAtm.removeStacksWithActivityTypes(activityTypes)); |
| waitForIdle(); |
| } |
| |
| protected void removeStacksInWindowingModes(int... windowingModes) { |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> mAtm.removeStacksInWindowingModes(windowingModes) |
| ); |
| waitForIdle(); |
| } |
| |
| public static String executeShellCommand(String command) { |
| log("Shell command: " + command); |
| try { |
| return SystemUtil |
| .runShellCommand(InstrumentationRegistry.getInstrumentation(), command); |
| } catch (IOException e) { |
| //bubble it up |
| logE("Error running shell command: " + command); |
| throw new RuntimeException(e); |
| } |
| } |
| |
| protected Bitmap takeScreenshot() { |
| return InstrumentationRegistry.getInstrumentation().getUiAutomation().takeScreenshot(); |
| } |
| |
| protected void launchActivity(final ComponentName activityName, final String... keyValuePairs) { |
| launchActivityNoWait(activityName, keyValuePairs); |
| mAmWmState.waitForValidState(activityName); |
| } |
| |
| protected void launchActivityNoWait(final ComponentName activityName, |
| final String... keyValuePairs) { |
| executeShellCommand(getAmStartCmd(activityName, keyValuePairs)); |
| } |
| |
| protected void launchActivityInNewTask(final ComponentName activityName) { |
| executeShellCommand(getAmStartCmdInNewTask(activityName)); |
| mAmWmState.waitForValidState(activityName); |
| } |
| |
| private static void waitForIdle() { |
| InstrumentationRegistry.getInstrumentation().waitForIdleSync(); |
| } |
| |
| /** Returns the set of stack ids. */ |
| private HashSet<Integer> getStackIds() { |
| mAmWmState.computeState(true); |
| final List<ActivityManagerState.ActivityStack> stacks = mAmWmState.getAmState().getStacks(); |
| final HashSet<Integer> stackIds = new HashSet<>(); |
| for (ActivityManagerState.ActivityStack s : stacks) { |
| stackIds.add(s.mStackId); |
| } |
| return stackIds; |
| } |
| |
| protected void launchHomeActivity() { |
| executeShellCommand(AM_START_HOME_ACTIVITY_COMMAND); |
| mAmWmState.waitForHomeActivityVisible(); |
| } |
| |
| protected void launchActivity(ComponentName activityName, int windowingMode, |
| final String... keyValuePairs) { |
| executeShellCommand(getAmStartCmd(activityName, keyValuePairs) |
| + " --windowingMode " + windowingMode); |
| mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName) |
| .setWindowingMode(windowingMode) |
| .build()); |
| } |
| |
| protected void launchActivityOnDisplay(ComponentName activityName, int displayId, |
| String... keyValuePairs) { |
| launchActivityOnDisplayNoWait(activityName, displayId, keyValuePairs); |
| mAmWmState.waitForValidState(activityName); |
| } |
| |
| protected void launchActivityOnDisplayNoWait(ComponentName activityName, int displayId, |
| String... keyValuePairs) { |
| executeShellCommand(getAmStartCmd(activityName, displayId, keyValuePairs)); |
| } |
| |
| /** |
| * Launches {@param activityName} into split-screen primary windowing mode and also makes |
| * the recents activity visible to the side of it. |
| * NOTE: Recents view may be combined with home screen on some devices, so using this to wait |
| * for Recents only makes sense when {@link ActivityManagerState#isHomeRecentsComponent()} is |
| * {@code false}. |
| */ |
| protected void launchActivityInSplitScreenWithRecents(ComponentName activityName) { |
| launchActivityInSplitScreenWithRecents(activityName, SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT); |
| } |
| |
| protected void launchActivityInSplitScreenWithRecents(ComponentName activityName, |
| int createMode) { |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| launchActivity(activityName); |
| final int taskId = mAmWmState.getAmState().getTaskByActivity(activityName).mTaskId; |
| mAtm.setTaskWindowingModeSplitScreenPrimary(taskId, createMode, |
| true /* onTop */, false /* animate */, |
| null /* initialBounds */, true /* showRecents */); |
| |
| mAmWmState.waitForValidState( |
| new WaitForValidActivityState.Builder(activityName) |
| .setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) |
| .setActivityType(ACTIVITY_TYPE_STANDARD) |
| .build()); |
| mAmWmState.waitForRecentsActivityVisible(); |
| }); |
| } |
| |
| public void moveTaskToPrimarySplitScreen(int taskId) { |
| moveTaskToPrimarySplitScreen(taskId, false /* showRecents */); |
| } |
| |
| /** |
| * Moves the device into split-screen with the specified task into the primary stack. |
| * @param taskId The id of the task to move into the primary stack. |
| * @param showRecents Whether to show the recents activity (or a placeholder activity in |
| * place of the Recents activity if home is the recents component) |
| */ |
| public void moveTaskToPrimarySplitScreen(int taskId, boolean showRecents) { |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| mAtm.setTaskWindowingModeSplitScreenPrimary(taskId, |
| SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT, true /* onTop */, |
| false /* animate */, |
| null /* initialBounds */, showRecents); |
| mAmWmState.waitForRecentsActivityVisible(); |
| |
| if (mAmWmState.getAmState().isHomeRecentsComponent() && showRecents) { |
| // Launch Placeholder Recents |
| final Activity recentsActivity = mSideActivityRule.launchActivity( |
| new Intent()); |
| mAmWmState.waitForActivityState(recentsActivity.getComponentName(), STATE_RESUMED); |
| } |
| }); |
| } |
| |
| /** |
| * Launches {@param primaryActivity} into split-screen primary windowing mode |
| * and {@param secondaryActivity} to the side in split-screen secondary windowing mode. |
| */ |
| protected void launchActivitiesInSplitScreen(LaunchActivityBuilder primaryActivity, |
| LaunchActivityBuilder secondaryActivity) { |
| // Launch split-screen primary. |
| primaryActivity |
| .setUseInstrumentation() |
| .setWaitForLaunched(true) |
| .execute(); |
| |
| final int taskId = mAmWmState.getAmState().getTaskByActivity( |
| primaryActivity.mTargetActivity).mTaskId; |
| moveTaskToPrimarySplitScreen(taskId); |
| |
| // Launch split-screen secondary |
| // Recents become focused, so we can just launch new task in focused stack |
| secondaryActivity |
| .setUseInstrumentation() |
| .setWaitForLaunched(true) |
| .setNewTask(true) |
| .setMultipleTask(true) |
| .execute(); |
| } |
| |
| protected void setActivityTaskWindowingMode(ComponentName activityName, int windowingMode) { |
| mAmWmState.computeState(activityName); |
| final int taskId = mAmWmState.getAmState().getTaskByActivity(activityName).mTaskId; |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> mAtm.setTaskWindowingMode(taskId, windowingMode, true /* toTop */)); |
| mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName) |
| .setActivityType(ACTIVITY_TYPE_STANDARD) |
| .setWindowingMode(windowingMode) |
| .build()); |
| } |
| |
| protected void moveActivityToStack(ComponentName activityName, int stackId) { |
| mAmWmState.computeState(activityName); |
| final int taskId = mAmWmState.getAmState().getTaskByActivity(activityName).mTaskId; |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> mAtm.moveTaskToStack(taskId, stackId, true)); |
| |
| mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName) |
| .setStackId(stackId) |
| .build()); |
| } |
| |
| protected void resizeActivityTask( |
| ComponentName activityName, int left, int top, int right, int bottom) { |
| mAmWmState.computeState(activityName); |
| final int taskId = mAmWmState.getAmState().getTaskByActivity(activityName).mTaskId; |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> mAtm.resizeTask(taskId, new Rect(left, top, right, bottom))); |
| } |
| |
| protected void resizeDockedStack( |
| int stackWidth, int stackHeight, int taskWidth, int taskHeight) { |
| SystemUtil.runWithShellPermissionIdentity(() -> |
| mAtm.resizeDockedStack(new Rect(0, 0, stackWidth, stackHeight), |
| new Rect(0, 0, taskWidth, taskHeight))); |
| } |
| |
| protected void resizeStack(int stackId, int stackLeft, int stackTop, int stackWidth, |
| int stackHeight) { |
| SystemUtil.runWithShellPermissionIdentity(() -> mAtm.resizeStack(stackId, |
| new Rect(stackLeft, stackTop, stackWidth, stackHeight))); |
| } |
| |
| protected void pressAppSwitchButtonAndWaitForRecents() { |
| pressAppSwitchButton(); |
| mAmWmState.waitForRecentsActivityVisible(); |
| mAmWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY); |
| } |
| |
| // Utility method for debugging, not used directly here, but useful, so kept around. |
| protected void printStacksAndTasks() { |
| SystemUtil.runWithShellPermissionIdentity(() -> { |
| final String output = mAtm.listAllStacks(); |
| for (String line : output.split("\\n")) { |
| log(line); |
| } |
| }); |
| } |
| |
| protected boolean supportsVrMode() { |
| return hasDeviceFeature(FEATURE_VR_MODE_HIGH_PERFORMANCE); |
| } |
| |
| protected boolean supportsPip() { |
| return hasDeviceFeature(FEATURE_PICTURE_IN_PICTURE) |
| || PRETEND_DEVICE_SUPPORTS_PIP; |
| } |
| |
| protected boolean supportsFreeform() { |
| return hasDeviceFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT) |
| || PRETEND_DEVICE_SUPPORTS_FREEFORM; |
| } |
| |
| /** Whether or not the device pin/pattern/password lock. */ |
| protected boolean supportsSecureLock() { |
| return !hasDeviceFeature(FEATURE_LEANBACK) |
| && !hasDeviceFeature(FEATURE_EMBEDDED); |
| } |
| |
| /** Whether or not the device supports "swipe" lock. */ |
| protected boolean supportsInsecureLock() { |
| return !hasDeviceFeature(FEATURE_LEANBACK) |
| && !hasDeviceFeature(FEATURE_WATCH) |
| && !hasDeviceFeature(FEATURE_EMBEDDED); |
| } |
| |
| protected boolean isWatch() { |
| return hasDeviceFeature(FEATURE_WATCH); |
| } |
| |
| protected boolean isTablet() { |
| // Larger than approx 7" tablets |
| return mContext.getResources().getConfiguration().smallestScreenWidthDp >= 600; |
| } |
| |
| protected void waitAndAssertActivityState(ComponentName activityName, |
| String state, String message) { |
| mAmWmState.waitForActivityState(activityName, state); |
| |
| assertTrue(message, mAmWmState.getAmState().hasActivityState(activityName, state)); |
| } |
| |
| protected void waitAndAssertTopResumedActivity(ComponentName activityName, int displayId, |
| String message) throws Exception { |
| mAmWmState.waitForValidState(activityName); |
| mAmWmState.waitForActivityState(activityName, STATE_RESUMED); |
| final String activityClassName = getActivityName(activityName); |
| mAmWmState.waitForWithAmState(state -> |
| activityClassName.equals(state.getFocusedActivity()), |
| "Waiting for activity to be on top"); |
| |
| mAmWmState.assertSanity(); |
| mAmWmState.assertFocusedActivity(message, activityName); |
| assertTrue("Activity must be resumed", |
| mAmWmState.getAmState().hasActivityState(activityName, STATE_RESUMED)); |
| final int frontStackId = mAmWmState.getAmState().getFrontStackId(displayId); |
| ActivityManagerState.ActivityStack frontStackOnDisplay = |
| mAmWmState.getAmState().getStackById(frontStackId); |
| assertEquals("Resumed activity of front stack of the target display must match. " + message, |
| activityClassName, frontStackOnDisplay.mResumedActivity); |
| mAmWmState.assertFocusedStack("Top activity's stack must also be on top", frontStackId); |
| mAmWmState.assertVisibility(activityName, true /* visible */); |
| } |
| |
| // TODO: Switch to using a feature flag, when available. |
| protected static boolean isUiModeLockedToVrHeadset() { |
| final String output = runCommandAndPrintOutput("dumpsys uimode"); |
| |
| Integer curUiMode = null; |
| Boolean uiModeLocked = null; |
| for (String line : output.split("\\n")) { |
| line = line.trim(); |
| Matcher matcher = sCurrentUiModePattern.matcher(line); |
| if (matcher.find()) { |
| curUiMode = Integer.parseInt(matcher.group(1), 16); |
| } |
| matcher = sUiModeLockedPattern.matcher(line); |
| if (matcher.find()) { |
| uiModeLocked = matcher.group(1).equals("true"); |
| } |
| } |
| |
| boolean uiModeLockedToVrHeadset = (curUiMode != null) && (uiModeLocked != null) |
| && ((curUiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_VR_HEADSET) && uiModeLocked; |
| |
| if (uiModeLockedToVrHeadset) { |
| log("UI mode is locked to VR headset"); |
| } |
| |
| return uiModeLockedToVrHeadset; |
| } |
| |
| protected boolean supportsSplitScreenMultiWindow() { |
| return ActivityTaskManager.supportsSplitScreenMultiWindow(mContext); |
| } |
| |
| protected boolean hasHomeScreen() { |
| if (sHasHomeScreen == null) { |
| sHasHomeScreen = !noHomeScreen(); |
| } |
| return sHasHomeScreen; |
| } |
| |
| /** |
| * Rotation support is indicated by explicitly having both landscape and portrait |
| * features or not listing either at all. |
| */ |
| protected boolean supportsRotation() { |
| final boolean supportsLandscape = hasDeviceFeature(FEATURE_SCREEN_LANDSCAPE); |
| final boolean supportsPortrait = hasDeviceFeature(FEATURE_SCREEN_PORTRAIT); |
| return (supportsLandscape && supportsPortrait) |
| || (!supportsLandscape && !supportsPortrait); |
| } |
| |
| protected boolean hasDeviceFeature(final String requiredFeature) { |
| return InstrumentationRegistry.getContext() |
| .getPackageManager() |
| .hasSystemFeature(requiredFeature); |
| } |
| |
| protected static boolean isDisplayOn(int displayId) { |
| final DisplayManager displayManager = getContext().getSystemService(DisplayManager.class); |
| final Display display = displayManager.getDisplay(displayId); |
| return display != null && display.getState() == Display.STATE_ON; |
| } |
| |
| /** |
| * Test @Rule class that disables screen doze settings before each test method running and |
| * restoring to initial values after test method finished. |
| */ |
| protected static class DisableScreenDozeRule implements TestRule { |
| |
| /** Copied from android.provider.Settings.Secure since these keys are hiden. */ |
| private static final String[] DOZE_SETTINGS = { |
| "doze_enabled", |
| "doze_always_on", |
| "doze_pulse_on_pick_up", |
| "doze_pulse_on_long_press", |
| "doze_pulse_on_double_tap" |
| }; |
| |
| private String get(String key) { |
| return executeShellCommand("settings get secure " + key).trim(); |
| } |
| |
| private void put(String key, String value) { |
| executeShellCommand("settings put secure " + key + " " + value); |
| } |
| |
| @Override |
| public Statement apply(final Statement base, final Description description) { |
| return new Statement() { |
| @Override |
| public void evaluate() throws Throwable { |
| final Map<String, String> initialValues = new HashMap<>(); |
| Arrays.stream(DOZE_SETTINGS).forEach(k -> initialValues.put(k, get(k))); |
| try { |
| Arrays.stream(DOZE_SETTINGS).forEach(k -> put(k, "0")); |
| base.evaluate(); |
| } finally { |
| Arrays.stream(DOZE_SETTINGS).forEach(k -> put(k, initialValues.get(k))); |
| } |
| } |
| }; |
| } |
| } |
| |
| /** |
| * HomeActivitySession is used to replace the default home component, so that you can use |
| * your preferred home for testing within the session. The original default home will be |
| * restored automatically afterward. |
| */ |
| protected class HomeActivitySession implements AutoCloseable { |
| private PackageManager mPackageManager; |
| private ComponentName mOrigHome; |
| private ComponentName mSessionHome; |
| |
| public HomeActivitySession(ComponentName sessionHome) { |
| mSessionHome = sessionHome; |
| mPackageManager = mContext.getPackageManager(); |
| |
| final Intent intent = new Intent(ACTION_MAIN); |
| intent.addCategory(CATEGORY_HOME); |
| intent.addFlags(FLAG_ACTIVITY_NEW_TASK); |
| final ResolveInfo resolveInfo = |
| mPackageManager.resolveActivity(intent, MATCH_DEFAULT_ONLY); |
| if (resolveInfo != null) { |
| mOrigHome = new ComponentName(resolveInfo.activityInfo.packageName, |
| resolveInfo.activityInfo.name); |
| } |
| |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> mPackageManager.setComponentEnabledSetting(mSessionHome, |
| COMPONENT_ENABLED_STATE_ENABLED, 0 /* flags */)); |
| setDefaultHome(mSessionHome); |
| } |
| |
| @Override |
| public void close() { |
| SystemUtil.runWithShellPermissionIdentity( |
| () -> mPackageManager.setComponentEnabledSetting(mSessionHome, |
| COMPONENT_ENABLED_STATE_DISABLED, 0 /* flags */)); |
| if (mOrigHome != null) { |
| setDefaultHome(mOrigHome); |
| } |
| } |
| |
| private void setDefaultHome(ComponentName componentName) { |
| executeShellCommand("cmd package set-home-activity --user " |
| + android.os.Process.myUserHandle().getIdentifier() + " " |
| + componentName.flattenToString()); |
| } |
| } |
| |
| protected class LockScreenSession implements AutoCloseable { |
| private static final boolean DEBUG = false; |
| |
| private final boolean mIsLockDisabled; |
| private boolean mLockCredentialSet; |
| |
| public LockScreenSession() { |
| mIsLockDisabled = isLockDisabled(); |
| mLockCredentialSet = false; |
| // Enable lock screen (swipe) by default. |
| setLockDisabled(false); |
| } |
| |
| public LockScreenSession setLockCredential() { |
| mLockCredentialSet = true; |
| runCommandAndPrintOutput("locksettings set-pin " + LOCK_CREDENTIAL); |
| return this; |
| } |
| |
| public LockScreenSession enterAndConfirmLockCredential() { |
| // Ensure focus will switch to default display. Meanwhile we cannot tap on center area, |
| // which may tap on input credential area. |
| tapOnDisplay(10, 10, DEFAULT_DISPLAY); |
| |
| waitForDeviceIdle(3000); |
| SystemUtil.runWithShellPermissionIdentity(() -> |
| InstrumentationRegistry.getInstrumentation().sendStringSync(LOCK_CREDENTIAL)); |
| pressEnterButton(); |
| return this; |
| } |
| |
| private void removeLockCredential() { |
| runCommandAndPrintOutput("locksettings clear --old " + LOCK_CREDENTIAL); |
| mLockCredentialSet = false; |
| } |
| |
| LockScreenSession disableLockScreen() { |
| setLockDisabled(true); |
| return this; |
| } |
| |
| LockScreenSession sleepDevice() { |
| pressSleepButton(); |
| // Not all device variants lock when we go to sleep, so we need to explicitly lock the |
| // device. Note that pressSleepButton() above is redundant because the action also |
| // puts the device to sleep, but kept around for clarity. |
| InstrumentationRegistry.getInstrumentation().getUiAutomation().performGlobalAction( |
| AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN); |
| for (int retry = 1; isDisplayOn(DEFAULT_DISPLAY) && retry <= 5; retry++) { |
| logAlways("***Waiting for display to turn off... retry=" + retry); |
| SystemClock.sleep(TimeUnit.SECONDS.toMillis(1)); |
| } |
| return this; |
| } |
| |
| LockScreenSession wakeUpDevice() { |
| pressWakeupButton(); |
| return this; |
| } |
| |
| LockScreenSession unlockDevice() { |
| pressUnlockButton(); |
| return this; |
| } |
| |
| public LockScreenSession gotoKeyguard(ComponentName... showWhenLockedActivities) { |
| if (DEBUG && isLockDisabled()) { |
| logE("LockScreenSession.gotoKeyguard() is called without lock enabled."); |
| } |
| sleepDevice(); |
| wakeUpDevice(); |
| if (showWhenLockedActivities.length == 0) { |
| mAmWmState.waitForKeyguardShowingAndNotOccluded(); |
| } else { |
| mAmWmState.waitForValidState(showWhenLockedActivities); |
| } |
| return this; |
| } |
| |
| @Override |
| public void close() { |
| setLockDisabled(mIsLockDisabled); |
| if (mLockCredentialSet) { |
| removeLockCredential(); |
| } |
| |
| // Dismiss active keyguard after credential is cleared, so keyguard doesn't ask for |
| // the stale credential. |
| // TODO (b/112015010) If keyguard is occluded, credential cannot be removed as expected. |
| // LockScreenSession#close is always calls before stop all test activities, |
| // which could cause keyguard stay at occluded after wakeup. |
| // If Keyguard is occluded, press back key can close ShowWhenLocked activity. |
| pressBackButton(); |
| |
| // If device is unlocked, there might have ShowWhenLocked activity runs on, |
| // use home key to clear all activity at foreground. |
| pressHomeButton(); |
| sleepDevice(); |
| wakeUpDevice(); |
| unlockDevice(); |
| } |
| |
| /** |
| * Returns whether the lock screen is disabled. |
| * |
| * @return true if the lock screen is disabled, false otherwise. |
| */ |
| private boolean isLockDisabled() { |
| final String isLockDisabled = runCommandAndPrintOutput( |
| "locksettings get-disabled").trim(); |
| return !"null".equals(isLockDisabled) && Boolean.parseBoolean(isLockDisabled); |
| } |
| |
| /** |
| * Disable the lock screen. |
| * |
| * @param lockDisabled true if should disable, false otherwise. |
| */ |
| protected void setLockDisabled(boolean lockDisabled) { |
| runCommandAndPrintOutput("locksettings set-disabled " + lockDisabled); |
| } |
| } |
| |
| /** Helper class to save, set & wait, and restore rotation related preferences. */ |
| protected class RotationSession extends SettingsSession<Integer> { |
| private final SettingsSession<Integer> mUserRotation; |
| |
| public RotationSession() throws Exception { |
| // Save accelerometer_rotation preference. |
| super(Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), |
| Settings.System::getInt, Settings.System::putInt); |
| mUserRotation = new SettingsSession<>( |
| Settings.System.getUriFor(Settings.System.USER_ROTATION), |
| Settings.System::getInt, Settings.System::putInt); |
| // Disable accelerometer_rotation. |
| super.set(0); |
| } |
| |
| @Override |
| public void set(@NonNull Integer value) throws Exception { |
| mUserRotation.set(value); |
| // Wait for settling rotation. |
| mAmWmState.waitForRotation(value); |
| } |
| |
| @Override |
| public void close() throws Exception { |
| mUserRotation.close(); |
| // Restore accelerometer_rotation preference. |
| super.close(); |
| } |
| } |
| |
| /** |
| * Returns whether the test device respects settings of locked user rotation mode. |
| * |
| * The method sets the locked user rotation settings to the rotation that rotates the display by |
| * 180 degrees and checks if the actual display rotation changes after that. |
| * |
| * This is a necessary assumption check before leveraging user rotation mode to force display |
| * rotation, because there is no requirement that an Android device that supports both |
| * orientations needs to support user rotation mode. |
| * |
| * @param session the rotation session used to set user rotation |
| * @param displayId the display ID to check rotation against |
| * @return {@code true} if test device respects settings of locked user rotation mode; |
| * {@code false} if not. |
| */ |
| protected boolean supportsLockedUserRotation(RotationSession session, int displayId) |
| throws Exception { |
| final int origRotation = getDeviceRotation(displayId); |
| // Use the same orientation as target rotation to avoid affect of app-requested orientation. |
| final int targetRotation = (origRotation + 2) % 4; |
| session.set(targetRotation); |
| final boolean result = (getDeviceRotation(displayId) == targetRotation); |
| session.set(origRotation); |
| return result; |
| } |
| |
| protected int getDeviceRotation(int displayId) { |
| final String displays = runCommandAndPrintOutput("dumpsys display displays").trim(); |
| Pattern pattern = Pattern.compile( |
| "(mDisplayId=" + displayId + ")([\\s\\S]*)(mOverrideDisplayInfo)(.*)" |
| + "(rotation)(\\s+)(\\d+)"); |
| Matcher matcher = pattern.matcher(displays); |
| if (matcher.find()) { |
| final String match = matcher.group(7); |
| return Integer.parseInt(match); |
| } |
| |
| return INVALID_DEVICE_ROTATION; |
| } |
| |
| protected static String runCommandAndPrintOutput(String command) { |
| final String output = executeShellCommand(command); |
| log(output); |
| return output; |
| } |
| |
| protected static class LogSeparator { |
| private final String mUniqueString; |
| |
| private LogSeparator() { |
| mUniqueString = UUID.randomUUID().toString(); |
| } |
| |
| @Override |
| public String toString() { |
| return mUniqueString; |
| } |
| } |
| |
| /** |
| * Inserts a log separator so we can always find the starting point from where to evaluate |
| * following logs. |
| * @return Unique log separator. |
| */ |
| protected LogSeparator separateLogs() { |
| final LogSeparator logSeparator = new LogSeparator(); |
| executeShellCommand("log -t " + LOG_SEPARATOR + " " + logSeparator); |
| EventLog.writeEvent(EVENT_LOG_SEPARATOR_TAG, logSeparator.mUniqueString); |
| return logSeparator; |
| } |
| |
| protected static String[] getDeviceLogsForComponents( |
| LogSeparator logSeparator, String... logTags) { |
| String filters = LOG_SEPARATOR + ":I "; |
| for (String component : logTags) { |
| filters += component + ":I "; |
| } |
| final String[] result = executeShellCommand("logcat -v brief -d " + filters + " *:S") |
| .split("\\n"); |
| if (logSeparator == null) { |
| return result; |
| } |
| |
| // Make sure that we only check logs after the separator. |
| int i = 0; |
| boolean lookingForSeparator = true; |
| while (i < result.length && lookingForSeparator) { |
| if (result[i].contains(logSeparator.toString())) { |
| lookingForSeparator = false; |
| } |
| i++; |
| } |
| final String[] filteredResult = new String[result.length - i]; |
| for (int curPos = 0; i < result.length; curPos++, i++) { |
| filteredResult[curPos] = result[i]; |
| } |
| return filteredResult; |
| } |
| |
| protected static List<Event> getEventLogsForComponents(LogSeparator logSeparator, int... tags) { |
| List<Event> events = new ArrayList<>(); |
| |
| int[] searchTags = Arrays.copyOf(tags, tags.length + 1); |
| searchTags[searchTags.length - 1] = EVENT_LOG_SEPARATOR_TAG; |
| |
| try { |
| EventLog.readEvents(searchTags, events); |
| } catch (IOException e) { |
| fail("Could not read from event log." + e); |
| } |
| |
| for (Iterator<Event> itr = events.iterator(); itr.hasNext(); ) { |
| Event event = itr.next(); |
| itr.remove(); |
| if (event.getTag() == EVENT_LOG_SEPARATOR_TAG && |
| logSeparator.mUniqueString.equals(event.getData())) { |
| break; |
| } |
| } |
| return events; |
| } |
| |
| /** |
| * Base helper class for retrying validator success. |
| */ |
| private abstract static class RetryValidator { |
| |
| private static final int RETRY_LIMIT = 5; |
| private static final long RETRY_INTERVAL = TimeUnit.SECONDS.toMillis(1); |
| |
| /** |
| * @return Error string if validation is failed, null if everything is fine. |
| **/ |
| @Nullable |
| protected abstract String validate(); |
| |
| /** |
| * Executes {@link #validate()}. Retries {@link #RETRY_LIMIT} times with |
| * {@link #RETRY_INTERVAL} interval. |
| * |
| * @param waitingMessage logging message while waiting validation. |
| */ |
| void assertValidator(String waitingMessage) { |
| String resultString = null; |
| for (int retry = 1; retry <= RETRY_LIMIT; retry++) { |
| resultString = validate(); |
| if (resultString == null) { |
| return; |
| } |
| logAlways(waitingMessage + ": " + resultString); |
| SystemClock.sleep(RETRY_INTERVAL); |
| } |
| fail(resultString); |
| } |
| } |
| |
| private static class ActivityLifecycleCountsValidator extends RetryValidator { |
| private final ComponentName mActivityName; |
| private final LogSeparator mLogSeparator; |
| private final int mCreateCount; |
| private final int mStartCount; |
| private final int mResumeCount; |
| private final int mPauseCount; |
| private final int mStopCount; |
| private final int mDestroyCount; |
| |
| ActivityLifecycleCountsValidator(ComponentName activityName, LogSeparator logSeparator, |
| int createCount, int startCount, int resumeCount, int pauseCount, int stopCount, |
| int destroyCount) { |
| mActivityName = activityName; |
| mLogSeparator = logSeparator; |
| mCreateCount = createCount; |
| mStartCount = startCount; |
| mResumeCount = resumeCount; |
| mPauseCount = pauseCount; |
| mStopCount = stopCount; |
| mDestroyCount = destroyCount; |
| } |
| |
| @Override |
| @Nullable |
| protected String validate() { |
| final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts( |
| mActivityName, mLogSeparator); |
| if (lifecycleCounts.mCreateCount == mCreateCount |
| && lifecycleCounts.mStartCount == mStartCount |
| && lifecycleCounts.mResumeCount == mResumeCount |
| && lifecycleCounts.mPauseCount == mPauseCount |
| && lifecycleCounts.mStopCount == mStopCount |
| && lifecycleCounts.mDestroyCount == mDestroyCount) { |
| return null; |
| } |
| final String expected = IntStream.of(mCreateCount, mStopCount, mResumeCount, |
| mPauseCount, mStopCount, mDestroyCount) |
| .mapToObj(Integer::toString) |
| .collect(Collectors.joining("/")); |
| return getActivityName(mActivityName) + " lifecycle count mismatched:" |
| + " expected=" + expected |
| + " actual=" + lifecycleCounts.counters(); |
| } |
| } |
| |
| void assertActivityLifecycle(ComponentName activityName, boolean relaunched, |
| LogSeparator logSeparator) { |
| new RetryValidator() { |
| |
| @Nullable |
| @Override |
| protected String validate() { |
| final ActivityLifecycleCounts lifecycleCounts = |
| new ActivityLifecycleCounts(activityName, logSeparator); |
| final String logTag = getLogTag(activityName); |
| if (relaunched) { |
| if (lifecycleCounts.mDestroyCount < 1) { |
| return logTag + " must have been destroyed. mDestroyCount=" |
| + lifecycleCounts.mDestroyCount; |
| } |
| if (lifecycleCounts.mCreateCount < 1) { |
| return logTag + " must have been (re)created. mCreateCount=" |
| + lifecycleCounts.mCreateCount; |
| } |
| return null; |
| } |
| if (lifecycleCounts.mDestroyCount > 0) { |
| return logTag + " must *NOT* have been destroyed. mDestroyCount=" |
| + lifecycleCounts.mDestroyCount; |
| } |
| if (lifecycleCounts.mCreateCount > 0) { |
| return logTag + " must *NOT* have been (re)created. mCreateCount=" |
| + lifecycleCounts.mCreateCount; |
| } |
| if (lifecycleCounts.mConfigurationChangedCount < 1) { |
| return logTag + " must have received configuration changed. " |
| + "mConfigurationChangedCount=" |
| + lifecycleCounts.mConfigurationChangedCount; |
| } |
| return null; |
| } |
| }.assertValidator("***Waiting for valid lifecycle state"); |
| } |
| |
| protected void assertRelaunchOrConfigChanged(ComponentName activityName, int numRelaunch, |
| int numConfigChange, LogSeparator logSeparator) { |
| new RetryValidator() { |
| |
| @Nullable |
| @Override |
| protected String validate() { |
| final ActivityLifecycleCounts lifecycleCounts = |
| new ActivityLifecycleCounts(activityName, logSeparator); |
| final String logTag = getLogTag(activityName); |
| if (lifecycleCounts.mDestroyCount != numRelaunch) { |
| return logTag + " has been destroyed " + lifecycleCounts.mDestroyCount |
| + " time(s), expecting " + numRelaunch; |
| } else if (lifecycleCounts.mCreateCount != numRelaunch) { |
| return logTag + " has been (re)created " + lifecycleCounts.mCreateCount |
| + " time(s), expecting " + numRelaunch; |
| } else if (lifecycleCounts.mConfigurationChangedCount != numConfigChange) { |
| return logTag + " has received " |
| + lifecycleCounts.mConfigurationChangedCount |
| + " onConfigurationChanged() calls, expecting " + numConfigChange; |
| } |
| return null; |
| } |
| }.assertValidator("***Waiting for relaunch or config changed"); |
| } |
| |
| protected void assertActivityDestroyed(ComponentName activityName, LogSeparator logSeparator) { |
| new RetryValidator() { |
| |
| @Nullable |
| @Override |
| protected String validate() { |
| final ActivityLifecycleCounts lifecycleCounts = |
| new ActivityLifecycleCounts(activityName, logSeparator); |
| final String logTag = getLogTag(activityName); |
| if (lifecycleCounts.mDestroyCount != 1) { |
| return logTag + " has been destroyed " + lifecycleCounts.mDestroyCount |
| + " time(s), expecting single destruction."; |
| } |
| if (lifecycleCounts.mCreateCount != 0) { |
| return logTag + " has been (re)created " + lifecycleCounts.mCreateCount |
| + " time(s), not expecting any."; |
| } |
| if (lifecycleCounts.mConfigurationChangedCount != 0) { |
| return logTag + " has received " + lifecycleCounts.mConfigurationChangedCount |
| + " onConfigurationChanged() calls, not expecting any."; |
| } |
| return null; |
| } |
| }.assertValidator("***Waiting for activity destroyed"); |
| } |
| |
| void assertLifecycleCounts(ComponentName activityName, LogSeparator logSeparator, |
| int createCount, int startCount, int resumeCount, int pauseCount, int stopCount, |
| int destroyCount, int configurationChangeCount) { |
| new RetryValidator() { |
| @Override |
| protected String validate() { |
| final ActivityLifecycleCounts lifecycleCounts = |
| new ActivityLifecycleCounts(activityName, logSeparator); |
| final String logTag = getLogTag(activityName); |
| if (createCount != lifecycleCounts.mCreateCount) { |
| return logTag + " has been created " + lifecycleCounts.mCreateCount |
| + " time(s), expecting " + createCount; |
| } |
| if (startCount != lifecycleCounts.mStartCount) { |
| return logTag + " has been started " + lifecycleCounts.mStartCount |
| + " time(s), expecting " + startCount; |
| } |
| if (resumeCount != lifecycleCounts.mResumeCount) { |
| return logTag + " has been resumed " + lifecycleCounts.mResumeCount |
| + " time(s), expecting " + resumeCount; |
| } |
| if (pauseCount != lifecycleCounts.mPauseCount) { |
| return logTag + " has been paused " + lifecycleCounts.mPauseCount |
| + " time(s), expecting " + pauseCount; |
| } |
| if (stopCount != lifecycleCounts.mStopCount) { |
| return logTag + " has been stopped " + lifecycleCounts.mStopCount |
| + " time(s), expecting " + stopCount; |
| } |
| if (destroyCount != lifecycleCounts.mDestroyCount) { |
| return logTag + " has been destroyed " + lifecycleCounts.mDestroyCount |
| + " time(s), expecting " + destroyCount; |
| } |
| if (configurationChangeCount != lifecycleCounts.mConfigurationChangedCount) { |
| return logTag + " has received config changes " |
| + lifecycleCounts.mConfigurationChangedCount |
| + " time(s), expecting " + configurationChangeCount; |
| } |
| return null; |
| } |
| }.assertValidator("***Waiting for activity lifecycle counts"); |
| } |
| |
| void assertSingleLaunch(ComponentName activityName, LogSeparator logSeparator) { |
| new ActivityLifecycleCountsValidator(activityName, logSeparator, 1 /* createCount */, |
| 1 /* startCount */, 1 /* resumeCount */, 0 /* pauseCount */, 0 /* stopCount */, |
| 0 /* destroyCount */) |
| .assertValidator("***Waiting for activity create, start, and resume"); |
| } |
| |
| void assertSingleLaunchAndStop(ComponentName activityName, LogSeparator logSeparator) { |
| new ActivityLifecycleCountsValidator(activityName, logSeparator, 1 /* createCount */, |
| 1 /* startCount */, 1 /* resumeCount */, 1 /* pauseCount */, 1 /* stopCount */, |
| 0 /* destroyCount */) |
| .assertValidator("***Waiting for activity create, start, resume, pause, and stop"); |
| } |
| |
| void assertSingleStartAndStop(ComponentName activityName, LogSeparator logSeparator) { |
| new ActivityLifecycleCountsValidator(activityName, logSeparator, 0 /* createCount */, |
| 1 /* startCount */, 1 /* resumeCount */, 1 /* pauseCount */, 1 /* stopCount */, |
| 0 /* destroyCount */) |
| .assertValidator("***Waiting for activity start, resume, pause, and stop"); |
| } |
| |
| void assertSingleStart(ComponentName activityName, LogSeparator logSeparator) { |
| new ActivityLifecycleCountsValidator(activityName, logSeparator, 0 /* createCount */, |
| 1 /* startCount */, 1 /* resumeCount */, 0 /* pauseCount */, 0 /* stopCount */, |
| 0 /* destroyCount */) |
| .assertValidator("***Waiting for activity start and resume"); |
| } |
| |
| // TODO: Now that our test are device side, we can convert these to a more direct communication |
| // channel vs. depending on logs. |
| private static final Pattern sCreatePattern = Pattern.compile("(.+): onCreate"); |
| private static final Pattern sStartPattern = Pattern.compile("(.+): onStart"); |
| private static final Pattern sResumePattern = Pattern.compile("(.+): onResume"); |
| private static final Pattern sPausePattern = Pattern.compile("(.+): onPause"); |
| private static final Pattern sConfigurationChangedPattern = |
| Pattern.compile("(.+): onConfigurationChanged"); |
| private static final Pattern sMovedToDisplayPattern = |
| Pattern.compile("(.+): onMovedToDisplay"); |
| private static final Pattern sStopPattern = Pattern.compile("(.+): onStop"); |
| private static final Pattern sDestroyPattern = Pattern.compile("(.+): onDestroy"); |
| private static final Pattern sMultiWindowModeChangedPattern = |
| Pattern.compile("(.+): onMultiWindowModeChanged"); |
| private static final Pattern sPictureInPictureModeChangedPattern = |
| Pattern.compile("(.+): onPictureInPictureModeChanged"); |
| private static final Pattern sUserLeaveHintPattern = Pattern.compile("(.+): onUserLeaveHint"); |
| private static final Pattern sNewConfigPattern = Pattern.compile( |
| "(.+): config size=\\((\\d+),(\\d+)\\) displaySize=\\((\\d+),(\\d+)\\)" |
| + " metricsSize=\\((\\d+),(\\d+)\\) smallestScreenWidth=(\\d+) densityDpi=(\\d+)" |
| + " orientation=(\\d+)"); |
| private static final Pattern sDisplayCutoutPattern = Pattern.compile( |
| "(.+): cutout=(true|false)"); |
| private static final Pattern sDisplayStatePattern = |
| Pattern.compile("Display Power: state=(.+)"); |
| private static final Pattern sCurrentUiModePattern = Pattern.compile("mCurUiMode=0x(\\d+)"); |
| private static final Pattern sUiModeLockedPattern = |
| Pattern.compile("mUiModeLocked=(true|false)"); |
| |
| static class ReportedSizes { |
| int widthDp; |
| int heightDp; |
| int displayWidth; |
| int displayHeight; |
| int metricsWidth; |
| int metricsHeight; |
| int smallestWidthDp; |
| int densityDpi; |
| int orientation; |
| |
| @Override |
| public String toString() { |
| return "ReportedSizes: {widthDp=" + widthDp + " heightDp=" + heightDp |
| + " displayWidth=" + displayWidth + " displayHeight=" + displayHeight |
| + " metricsWidth=" + metricsWidth + " metricsHeight=" + metricsHeight |
| + " smallestWidthDp=" + smallestWidthDp + " densityDpi=" + densityDpi |
| + " orientation=" + orientation + "}"; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if ( this == obj ) return true; |
| if ( !(obj instanceof ReportedSizes) ) return false; |
| ReportedSizes that = (ReportedSizes) obj; |
| return widthDp == that.widthDp |
| && heightDp == that.heightDp |
| && displayWidth == that.displayWidth |
| && displayHeight == that.displayHeight |
| && metricsWidth == that.metricsWidth |
| && metricsHeight == that.metricsHeight |
| && smallestWidthDp == that.smallestWidthDp |
| && densityDpi == that.densityDpi |
| && orientation == that.orientation; |
| } |
| } |
| |
| @Nullable |
| ReportedSizes getLastReportedSizesForActivity( |
| ComponentName activityName, LogSeparator logSeparator) { |
| final String logTag = getLogTag(activityName); |
| for (int retry = 1; retry <= 5; retry++ ) { |
| final ReportedSizes result = readLastReportedSizes(logSeparator, logTag); |
| if (result != null) { |
| return result; |
| } |
| logAlways("***Waiting for sizes to be reported... retry=" + retry); |
| SystemClock.sleep(1000); |
| } |
| logE("***Waiting for activity size failed: activityName=" + logTag); |
| return null; |
| } |
| |
| private ReportedSizes readLastReportedSizes(LogSeparator logSeparator, String logTag) { |
| final String[] lines = getDeviceLogsForComponents(logSeparator, logTag); |
| for (int i = lines.length - 1; i >= 0; i--) { |
| final String line = lines[i].trim(); |
| final Matcher matcher = sNewConfigPattern.matcher(line); |
| if (matcher.matches()) { |
| ReportedSizes details = new ReportedSizes(); |
| details.widthDp = Integer.parseInt(matcher.group(2)); |
| details.heightDp = Integer.parseInt(matcher.group(3)); |
| details.displayWidth = Integer.parseInt(matcher.group(4)); |
| details.displayHeight = Integer.parseInt(matcher.group(5)); |
| details.metricsWidth = Integer.parseInt(matcher.group(6)); |
| details.metricsHeight = Integer.parseInt(matcher.group(7)); |
| details.smallestWidthDp = Integer.parseInt(matcher.group(8)); |
| details.densityDpi = Integer.parseInt(matcher.group(9)); |
| details.orientation = Integer.parseInt(matcher.group(10)); |
| return details; |
| } |
| } |
| return null; |
| } |
| |
| /** Check if a device has display cutout. */ |
| boolean hasDisplayCutout() { |
| // Launch an activity to report cutout state |
| final LogSeparator logSeparator = separateLogs(); |
| launchActivity(BROADCAST_RECEIVER_ACTIVITY); |
| |
| // Read the logs to check if cutout is present |
| final Boolean displayCutoutPresent = |
| getCutoutStateForActivity(BROADCAST_RECEIVER_ACTIVITY, logSeparator); |
| assertNotNull("The activity should report cutout state", displayCutoutPresent); |
| |
| // Finish activity |
| mBroadcastActionTrigger.finishBroadcastReceiverActivity(); |
| mAmWmState.waitForWithAmState( |
| (state) -> !state.containsActivity(BROADCAST_RECEIVER_ACTIVITY), |
| "Waiting for activity to be removed"); |
| |
| return displayCutoutPresent; |
| } |
| |
| /** |
| * Wait for activity to report cutout state in logs and return it. Will return {@code null} |
| * after timeout. |
| */ |
| @Nullable |
| private Boolean getCutoutStateForActivity(ComponentName activityName, |
| LogSeparator logSeparator) { |
| final String logTag = getLogTag(activityName); |
| for (int retry = 1; retry <= 5; retry++ ) { |
| final Boolean result = readLastReportedCutoutState(logSeparator, logTag); |
| if (result != null) { |
| return result; |
| } |
| logAlways("***Waiting for cutout state to be reported... retry=" + retry); |
| SystemClock.sleep(1000); |
| } |
| logE("***Waiting for activity cutout state failed: activityName=" + logTag); |
| return null; |
| } |
| |
| /** Read display cutout state from device logs. */ |
| private Boolean readLastReportedCutoutState(LogSeparator logSeparator, String logTag) { |
| final String[] lines = getDeviceLogsForComponents(logSeparator, logTag); |
| for (int i = lines.length - 1; i >= 0; i--) { |
| final String line = lines[i].trim(); |
| final Matcher matcher = sDisplayCutoutPattern.matcher(line); |
| if (matcher.matches()) { |
| return "true".equals(matcher.group(2)); |
| } |
| } |
| return null; |
| } |
| |
| /** Waits for at least one onMultiWindowModeChanged event. */ |
| ActivityLifecycleCounts waitForOnMultiWindowModeChanged(ComponentName activityName, |
| LogSeparator logSeparator) { |
| int retry = 1; |
| ActivityLifecycleCounts result; |
| do { |
| result = new ActivityLifecycleCounts(activityName, logSeparator); |
| if (result.mMultiWindowModeChangedCount >= 1) { |
| return result; |
| } |
| logAlways("***waitForOnMultiWindowModeChanged... retry=" + retry); |
| SystemClock.sleep(TimeUnit.SECONDS.toMillis(1)); |
| } while (retry++ <= 5); |
| return result; |
| } |
| |
| // TODO: Now that our test are device side, we can convert these to a more direct communication |
| // channel vs. depending on logs. |
| static class ActivityLifecycleCounts { |
| int mCreateCount; |
| int mStartCount; |
| int mResumeCount; |
| int mConfigurationChangedCount; |
| int mLastConfigurationChangedLineIndex; |
| int mMovedToDisplayCount; |
| int mMultiWindowModeChangedCount; |
| int mLastMultiWindowModeChangedLineIndex; |
| int mPictureInPictureModeChangedCount; |
| int mLastPictureInPictureModeChangedLineIndex; |
| int mUserLeaveHintCount; |
| int mPauseCount; |
| int mStopCount; |
| int mLastStopLineIndex; |
| int mDestroyCount; |
| |
| ActivityLifecycleCounts(ComponentName componentName, LogSeparator logSeparator) { |
| int lineIndex = 0; |
| waitForIdle(); |
| for (String line : getDeviceLogsForComponents(logSeparator, getLogTag(componentName))) { |
| line = line.trim(); |
| lineIndex++; |
| |
| Matcher matcher = sCreatePattern.matcher(line); |
| if (matcher.matches()) { |
| mCreateCount++; |
| continue; |
| } |
| |
| matcher = sStartPattern.matcher(line); |
| if (matcher.matches()) { |
| mStartCount++; |
| continue; |
| } |
| |
| matcher = sResumePattern.matcher(line); |
| if (matcher.matches()) { |
| mResumeCount++; |
| continue; |
| } |
| |
| matcher = sConfigurationChangedPattern.matcher(line); |
| if (matcher.matches()) { |
| mConfigurationChangedCount++; |
| mLastConfigurationChangedLineIndex = lineIndex; |
| continue; |
| } |
| |
| matcher = sMovedToDisplayPattern.matcher(line); |
| if (matcher.matches()) { |
| mMovedToDisplayCount++; |
| continue; |
| } |
| |
| matcher = sMultiWindowModeChangedPattern.matcher(line); |
| if (matcher.matches()) { |
| mMultiWindowModeChangedCount++; |
| mLastMultiWindowModeChangedLineIndex = lineIndex; |
| continue; |
| } |
| |
| matcher = sPictureInPictureModeChangedPattern.matcher(line); |
| if (matcher.matches()) { |
| mPictureInPictureModeChangedCount++; |
| mLastPictureInPictureModeChangedLineIndex = lineIndex; |
| continue; |
| } |
| |
| matcher = sUserLeaveHintPattern.matcher(line); |
| if (matcher.matches()) { |
| mUserLeaveHintCount++; |
| continue; |
| } |
| |
| matcher = sPausePattern.matcher(line); |
| if (matcher.matches()) { |
| mPauseCount++; |
| continue; |
| } |
| |
| matcher = sStopPattern.matcher(line); |
| if (matcher.matches()) { |
| mStopCount++; |
| mLastStopLineIndex = lineIndex; |
| continue; |
| } |
| |
| matcher = sDestroyPattern.matcher(line); |
| if (matcher.matches()) { |
| mDestroyCount++; |
| continue; |
| } |
| } |
| } |
| |
| String counters() { |
| return IntStream.of(mCreateCount, mStartCount, mResumeCount, mPauseCount, mStopCount, |
| mDestroyCount) |
| .mapToObj(Integer::toString) |
| .collect(Collectors.joining("/")); |
| } |
| } |
| |
| /** Assert the activity is either relaunched or received configuration changed. */ |
| List<ActivityCallback> assertActivityLifecycle(ActivitySession activitySession, |
| boolean relaunched) { |
| final String name = activitySession.getName(); |
| final List<ActivityCallback> callbackHistory = activitySession.takeCallbackHistory(); |
| final int[] lifecycleCounts = getActivityLifecycleCounts(callbackHistory); |
| if (relaunched) { |
| if (lifecycleCounts[ActivityCallback.ON_DESTROY.ordinal()] < 1) { |
| fail(name + " must have been destroyed. callbacks=" + callbackHistory); |
| } |
| if (lifecycleCounts[ActivityCallback.ON_CREATE.ordinal()] < 1) { |
| fail(name + " must have been (re)created. callbacks=" + callbackHistory); |
| } |
| return callbackHistory; |
| } |
| if (lifecycleCounts[ActivityCallback.ON_DESTROY.ordinal()] > 0) { |
| fail(name + " must *NOT* have been destroyed. callbacks=" + callbackHistory); |
| } |
| if (lifecycleCounts[ActivityCallback.ON_CREATE.ordinal()] > 0) { |
| fail(name + " must *NOT* have been (re)created. callbacks=" + callbackHistory); |
| } |
| if (lifecycleCounts[ActivityCallback.ON_CONFIGURATION_CHANGED.ordinal()] < 1) { |
| fail(name + " must have received configuration changed. callbacks=" + callbackHistory); |
| } |
| return callbackHistory; |
| } |
| |
| /** @return A array contains the lifecycle count by the ordinal of {@link ActivityCallback}. */ |
| int[] getActivityLifecycleCounts(List<ActivityCallback> lifecycleCallbacks) { |
| final int[] counts = new int[ActivityCallback.SIZE]; |
| for (ActivityCallback callback : lifecycleCallbacks) { |
| counts[callback.ordinal()]++; |
| } |
| return counts; |
| } |
| |
| protected void stopTestPackage(final String packageName) { |
| SystemUtil.runWithShellPermissionIdentity(() -> mAm.forceStopPackage(packageName)); |
| } |
| |
| protected LaunchActivityBuilder getLaunchActivityBuilder() { |
| return new LaunchActivityBuilder(mAmWmState); |
| } |
| |
| protected static class LaunchActivityBuilder implements LaunchProxy { |
| private final ActivityAndWindowManagersState mAmWmState; |
| |
| // The activity to be launched |
| private ComponentName mTargetActivity = TEST_ACTIVITY; |
| private boolean mUseApplicationContext; |
| private boolean mToSide; |
| private boolean mRandomData; |
| private boolean mNewTask; |
| private boolean mMultipleTask; |
| private boolean mAllowMultipleInstances = true; |
| private int mDisplayId = INVALID_DISPLAY; |
| private int mActivityType = ACTIVITY_TYPE_UNDEFINED; |
| // A proxy activity that launches other activities including mTargetActivityName |
| private ComponentName mLaunchingActivity = LAUNCHING_ACTIVITY; |
| private boolean mReorderToFront; |
| private boolean mWaitForLaunched; |
| private boolean mSuppressExceptions; |
| private boolean mWithShellPermission; |
| // Use of the following variables indicates that a broadcast receiver should be used instead |
| // of a launching activity; |
| private ComponentName mBroadcastReceiver; |
| private String mBroadcastReceiverAction; |
| private int mIntentFlags; |
| private LaunchInjector mLaunchInjector; |
| |
| private enum LauncherType { |
| INSTRUMENTATION, LAUNCHING_ACTIVITY, BROADCAST_RECEIVER |
| } |
| private LauncherType mLauncherType = LauncherType.LAUNCHING_ACTIVITY; |
| |
| public LaunchActivityBuilder(ActivityAndWindowManagersState amWmState) { |
| mAmWmState = amWmState; |
| mWaitForLaunched = true; |
| } |
| |
| public LaunchActivityBuilder setToSide(boolean toSide) { |
| mToSide = toSide; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setRandomData(boolean randomData) { |
| mRandomData = randomData; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setNewTask(boolean newTask) { |
| mNewTask = newTask; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setMultipleTask(boolean multipleTask) { |
| mMultipleTask = multipleTask; |
| return this; |
| } |
| |
| public LaunchActivityBuilder allowMultipleInstances(boolean allowMultipleInstances) { |
| mAllowMultipleInstances = allowMultipleInstances; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setReorderToFront(boolean reorderToFront) { |
| mReorderToFront = reorderToFront; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setUseApplicationContext(boolean useApplicationContext) { |
| mUseApplicationContext = useApplicationContext; |
| return this; |
| } |
| |
| public ComponentName getTargetActivity() { |
| return mTargetActivity; |
| } |
| |
| public boolean isTargetActivityTranslucent() { |
| return mAmWmState.getAmState().isActivityTranslucent(mTargetActivity); |
| } |
| |
| public LaunchActivityBuilder setTargetActivity(ComponentName targetActivity) { |
| mTargetActivity = targetActivity; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setDisplayId(int id) { |
| mDisplayId = id; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setActivityType(int type) { |
| mActivityType = type; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setLaunchingActivity(ComponentName launchingActivity) { |
| mLaunchingActivity = launchingActivity; |
| mLauncherType = LauncherType.LAUNCHING_ACTIVITY; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setWaitForLaunched(boolean shouldWait) { |
| mWaitForLaunched = shouldWait; |
| return this; |
| } |
| |
| /** Use broadcast receiver as a launchpad for activities. */ |
| public LaunchActivityBuilder setUseBroadcastReceiver(final ComponentName broadcastReceiver, |
| final String broadcastAction) { |
| mBroadcastReceiver = broadcastReceiver; |
| mBroadcastReceiverAction = broadcastAction; |
| mLauncherType = LauncherType.BROADCAST_RECEIVER; |
| return this; |
| } |
| |
| /** Use {@link android.app.Instrumentation} as a launchpad for activities. */ |
| public LaunchActivityBuilder setUseInstrumentation() { |
| mLauncherType = LauncherType.INSTRUMENTATION; |
| // Calling startActivity() from outside of an Activity context requires the |
| // FLAG_ACTIVITY_NEW_TASK flag. |
| setNewTask(true); |
| return this; |
| } |
| |
| public LaunchActivityBuilder setSuppressExceptions(boolean suppress) { |
| mSuppressExceptions = suppress; |
| return this; |
| } |
| |
| public LaunchActivityBuilder setWithShellPermission(boolean withShellPermission) { |
| mWithShellPermission = withShellPermission; |
| return this; |
| } |
| |
| @Override |
| public boolean shouldWaitForLaunched() { |
| return mWaitForLaunched; |
| } |
| |
| public LaunchActivityBuilder setIntentFlags(int flags) { |
| mIntentFlags = flags; |
| return this; |
| } |
| |
| @Override |
| public void setLaunchInjector(LaunchInjector injector) { |
| mLaunchInjector = injector; |
| } |
| |
| @Override |
| public void execute() { |
| switch (mLauncherType) { |
| case INSTRUMENTATION: |
| if (mWithShellPermission) { |
| SystemUtil.runWithShellPermissionIdentity(this::launchUsingInstrumentation); |
| } else { |
| launchUsingInstrumentation(); |
| } |
| break; |
| case LAUNCHING_ACTIVITY: |
| case BROADCAST_RECEIVER: |
| launchUsingShellCommand(); |
| } |
| |
| if (mWaitForLaunched) { |
| mAmWmState.waitForValidState(mTargetActivity); |
| } |
| } |
| |
| /** Launch an activity using instrumentation. */ |
| private void launchUsingInstrumentation() { |
| final Bundle b = new Bundle(); |
| b.putBoolean(KEY_USE_INSTRUMENTATION, true); |
| b.putBoolean(KEY_LAUNCH_ACTIVITY, true); |
| b.putBoolean(KEY_LAUNCH_TO_SIDE, mToSide); |
| b.putBoolean(KEY_RANDOM_DATA, mRandomData); |
| b.putBoolean(KEY_NEW_TASK, mNewTask); |
| b.putBoolean(KEY_MULTIPLE_TASK, mMultipleTask); |
| b.putBoolean(KEY_MULTIPLE_INSTANCES, mAllowMultipleInstances); |
| b.putBoolean(KEY_REORDER_TO_FRONT, mReorderToFront); |
| b.putInt(KEY_DISPLAY_ID, mDisplayId); |
| b.putInt(KEY_ACTIVITY_TYPE, mActivityType); |
| b.putBoolean(KEY_USE_APPLICATION_CONTEXT, mUseApplicationContext); |
| b.putString(KEY_TARGET_COMPONENT, getActivityName(mTargetActivity)); |
| b.putBoolean(KEY_SUPPRESS_EXCEPTIONS, mSuppressExceptions); |
| b.putInt(KEY_INTENT_FLAGS, mIntentFlags); |
| final Context context = InstrumentationRegistry.getContext(); |
| launchActivityFromExtras(context, b, mLaunchInjector); |
| } |
| |
| /** Build and execute a shell command to launch an activity. */ |
| private void launchUsingShellCommand() { |
| StringBuilder commandBuilder = new StringBuilder(); |
| if (mBroadcastReceiver != null && mBroadcastReceiverAction != null) { |
| // Use broadcast receiver to launch the target. |
| commandBuilder.append("am broadcast -a ").append(mBroadcastReceiverAction) |
| .append(" -p ").append(mBroadcastReceiver.getPackageName()) |
| // Include stopped packages |
| .append(" -f 0x00000020"); |
| } else { |
| // Use launching activity to launch the target. |
| commandBuilder.append(getAmStartCmd(mLaunchingActivity)) |
| .append(" -f 0x20000020"); |
| } |
| |
| // Add a flag to ensure we actually mean to launch an activity. |
| commandBuilder.append(" --ez " + KEY_LAUNCH_ACTIVITY + " true"); |
| |
| if (mToSide) { |
| commandBuilder.append(" --ez " + KEY_LAUNCH_TO_SIDE + " true"); |
| } |
| if (mRandomData) { |
| commandBuilder.append(" --ez " + KEY_RANDOM_DATA + " true"); |
| } |
| if (mNewTask) { |
| commandBuilder.append(" --ez " + KEY_NEW_TASK + " true"); |
| } |
| if (mMultipleTask) { |
| commandBuilder.append(" --ez " + KEY_MULTIPLE_TASK + " true"); |
| } |
| if (mAllowMultipleInstances) { |
| commandBuilder.append(" --ez " + KEY_MULTIPLE_INSTANCES + " true"); |
| } |
| if (mReorderToFront) { |
| commandBuilder.append(" --ez " + KEY_REORDER_TO_FRONT + " true"); |
| } |
| if (mDisplayId != INVALID_DISPLAY) { |
| commandBuilder.append(" --ei " + KEY_DISPLAY_ID + " ").append(mDisplayId); |
| } |
| if (mActivityType != ACTIVITY_TYPE_UNDEFINED) { |
| commandBuilder.append(" --ei " + KEY_ACTIVITY_TYPE + " ").append(mActivityType); |
| } |
| |
| if (mUseApplicationContext) { |
| commandBuilder.append(" --ez " + KEY_USE_APPLICATION_CONTEXT + " true"); |
| } |
| |
| if (mTargetActivity != null) { |
| // {@link ActivityLauncher} parses this extra string by |
| // {@link ComponentName#unflattenFromString(String)}. |
| commandBuilder.append(" --es " + KEY_TARGET_COMPONENT + " ") |
| .append(getActivityName(mTargetActivity)); |
| } |
| |
| if (mSuppressExceptions) { |
| commandBuilder.append(" --ez " + KEY_SUPPRESS_EXCEPTIONS + " true"); |
| } |
| |
| if (mIntentFlags != 0) { |
| commandBuilder.append(" --ei " + KEY_INTENT_FLAGS + " ").append(mIntentFlags); |
| } |
| |
| if (mLaunchInjector != null) { |
| mLaunchInjector.setupShellCommand(commandBuilder); |
| } |
| executeShellCommand(commandBuilder.toString()); |
| } |
| } |
| |
| // Activity used in place of recents when home is the recents component. |
| public static class SideActivity extends Activity { |
| } |
| } |