blob: b1cf2a2953ef59e6f17bda1b58c589a2e4fb894b [file] [log] [blame]
/*
* 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.cts;
import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.device.CollectingOutputReceiver;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.testtype.DeviceTestCase;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.Exception;
import java.lang.Integer;
import java.lang.String;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static android.server.cts.StateLogger.log;
import static android.server.cts.StateLogger.logE;
import android.server.cts.ActivityManagerState.ActivityStack;
import javax.imageio.ImageIO;
public abstract class ActivityManagerTestBase extends DeviceTestCase {
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";
// Constants copied from ActivityManager.StackId. If they are changed there, these must be
// updated.
/** Invalid stack ID. */
public static final int INVALID_STACK_ID = -1;
/** First static stack ID. */
public static final int FIRST_STATIC_STACK_ID = 0;
/** Home activity stack ID. */
public static final int HOME_STACK_ID = FIRST_STATIC_STACK_ID;
/** ID of stack where fullscreen activities are normally launched into. */
public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;
/** ID of stack where freeform/resized activities are normally launched into. */
public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1;
/** ID of stack that occupies a dedicated region of the screen. */
public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1;
/** ID of stack that always on top (always visible) when it exist. */
public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1;
/** Recents activity stack ID. */
public static final int RECENTS_STACK_ID = PINNED_STACK_ID + 1;
/** Assistant activity stack ID. This stack is fullscreen and non-resizeable. */
public static final int ASSISTANT_STACK_ID = RECENTS_STACK_ID + 1;
protected static final int[] ALL_STACK_IDS_BUT_HOME = {
FULLSCREEN_WORKSPACE_STACK_ID, FREEFORM_WORKSPACE_STACK_ID, DOCKED_STACK_ID,
PINNED_STACK_ID, ASSISTANT_STACK_ID
};
protected static final int[] ALL_STACK_IDS_BUT_HOME_AND_FULLSCREEN = {
FREEFORM_WORKSPACE_STACK_ID, DOCKED_STACK_ID, PINNED_STACK_ID, ASSISTANT_STACK_ID
};
private static final String TASK_ID_PREFIX = "taskId";
private static final String AM_STACK_LIST = "am stack list";
private static final String AM_FORCE_STOP_TEST_PACKAGE = "am force-stop android.server.cts";
private static final String AM_FORCE_STOP_SECOND_TEST_PACKAGE
= "am force-stop android.server.cts.second";
private static final String AM_FORCE_STOP_THIRD_TEST_PACKAGE
= "am force-stop android.server.cts.third";
private static final String AM_REMOVE_STACK = "am stack remove ";
protected static final String AM_START_HOME_ACTIVITY_COMMAND =
"am start -a android.intent.action.MAIN -c android.intent.category.HOME";
protected static final String AM_MOVE_TOP_ACTIVITY_TO_PINNED_STACK_COMMAND =
"am stack move-top-activity-to-pinned-stack 1 0 0 500 500";
static final String LAUNCHING_ACTIVITY = "LaunchingActivity";
static final String ALT_LAUNCHING_ACTIVITY = "AltLaunchingActivity";
static final String BROADCAST_RECEIVER_ACTIVITY = "BroadcastReceiverActivity";
/** Broadcast shell command for finishing {@link BroadcastReceiverActivity}. */
static final String FINISH_ACTIVITY_BROADCAST
= "am broadcast -a trigger_broadcast --ez finish true";
/** Broadcast shell command for finishing {@link BroadcastReceiverActivity}. */
static final String MOVE_TASK_TO_BACK_BROADCAST
= "am broadcast -a trigger_broadcast --ez moveToBack true";
private static final String AM_RESIZE_DOCKED_STACK = "am stack resize-docked-stack ";
private static final String AM_RESIZE_STACK = "am stack resize ";
static final String AM_MOVE_TASK = "am stack move-task ";
private static final String AM_SUPPORTS_SPLIT_SCREEN_MULTIWINDOW =
"am supports-split-screen-multiwindow";
private static final String AM_NO_HOME_SCREEN = "am no-home-screen";
private static final String INPUT_KEYEVENT_HOME = "input keyevent 3";
private static final String INPUT_KEYEVENT_BACK = "input keyevent 4";
private static final String INPUT_KEYEVENT_APP_SWITCH = "input keyevent 187";
public static final String INPUT_KEYEVENT_WINDOW = "input keyevent 171";
private static final String LOCK_CREDENTIAL = "1234";
private static final int INVALID_DISPLAY_ID = -1;
private static final String DEFAULT_COMPONENT_NAME = "android.server.cts";
static String componentName = DEFAULT_COMPONENT_NAME;
protected static final int INVALID_DEVICE_ROTATION = -1;
/** A reference to the device under test. */
protected ITestDevice mDevice;
private HashSet<String> mAvailableFeatures;
protected static String getAmStartCmd(final String activityName) {
return "am start -n " + getActivityComponentName(activityName);
}
/**
* @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.
*/
protected static String getAmStartCmd(final String activityName,
final String... keyValuePairs) {
String base = getAmStartCmd(activityName);
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) {
base += " --es " + keyValuePairs[i] + " " + keyValuePairs[i + 1];
}
return base;
}
protected static String getAmStartCmd(final String activityName, final int displayId) {
return "am start -n " + getActivityComponentName(activityName) + " -f 0x18000000"
+ " --display " + displayId;
}
protected static String getAmStartCmdInNewTask(final String activityName) {
return "am start -n " + getActivityComponentName(activityName) + " -f 0x18000000";
}
protected static String getAmStartCmdOverHome(final String activityName) {
return "am start --activity-task-on-home -n " + getActivityComponentName(activityName);
}
protected static String getOrientationBroadcast(int orientation) {
return "am broadcast -a trigger_broadcast --ei orientation " + orientation;
}
static String getActivityComponentName(final String activityName) {
return getActivityComponentName(componentName, activityName);
}
private static boolean isFullyQualifiedActivityName(String name) {
return name != null && name.contains(".");
}
static String getActivityComponentName(final String packageName, final String activityName) {
return packageName + "/" + (isFullyQualifiedActivityName(activityName) ? "" : ".") +
activityName;
}
// A little ugly, but lets avoid having to strip static everywhere for
// now.
public static void setComponentName(String name) {
componentName = name;
}
static String getBaseWindowName() {
return getBaseWindowName(componentName);
}
static String getBaseWindowName(final String packageName) {
return getBaseWindowName(packageName, true /*prependPackageName*/);
}
static String getBaseWindowName(final String packageName, boolean prependPackageName) {
return packageName + "/" + (prependPackageName ? packageName + "." : "");
}
static String getWindowName(final String activityName) {
return getWindowName(componentName, activityName);
}
static String getWindowName(final String packageName, final String activityName) {
return getBaseWindowName(packageName, !isFullyQualifiedActivityName(activityName))
+ activityName;
}
protected ActivityAndWindowManagersState mAmWmState = new ActivityAndWindowManagersState();
private int mInitialAccelerometerRotation;
private int mUserRotation;
private float mFontScale;
private SurfaceTraceReceiver mSurfaceTraceReceiver;
private Thread mSurfaceTraceThread;
void installSurfaceObserver(SurfaceTraceReceiver.SurfaceObserver observer) {
mSurfaceTraceReceiver = new SurfaceTraceReceiver(observer);
mSurfaceTraceThread = new Thread() {
@Override
public void run() {
try {
mDevice.executeShellCommand("wm surface-trace", mSurfaceTraceReceiver);
} catch (DeviceNotAvailableException e) {
logE("Device not available: " + e.toString());
}
}
};
mSurfaceTraceThread.start();
}
void removeSurfaceObserver() {
mSurfaceTraceReceiver.cancel();
mSurfaceTraceThread.interrupt();
}
@Override
protected void setUp() throws Exception {
super.setUp();
setComponentName(DEFAULT_COMPONENT_NAME);
// Get the device, this gives a handle to run commands and install APKs.
mDevice = getDevice();
wakeUpAndUnlockDevice();
// Remove special stacks.
removeStacks(ALL_STACK_IDS_BUT_HOME_AND_FULLSCREEN);
// Store rotation settings.
mInitialAccelerometerRotation = getAccelerometerRotation();
mUserRotation = getUserRotation();
mFontScale = getFontScale();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
try {
executeShellCommand(AM_FORCE_STOP_TEST_PACKAGE);
executeShellCommand(AM_FORCE_STOP_SECOND_TEST_PACKAGE);
executeShellCommand(AM_FORCE_STOP_THIRD_TEST_PACKAGE);
// Restore rotation settings to the state they were before test.
setAccelerometerRotation(mInitialAccelerometerRotation);
setUserRotation(mUserRotation);
setFontScale(mFontScale);
// Remove special stacks.
removeStacks(ALL_STACK_IDS_BUT_HOME_AND_FULLSCREEN);
wakeUpAndUnlockDevice();
} catch (DeviceNotAvailableException e) {
}
}
protected void removeStacks(int... stackIds) {
try {
for (Integer stackId : stackIds) {
executeShellCommand(AM_REMOVE_STACK + stackId);
}
} catch (DeviceNotAvailableException e) {
}
}
protected String executeShellCommand(String command) throws DeviceNotAvailableException {
return executeShellCommand(mDevice, command);
}
protected static String executeShellCommand(ITestDevice device, String command)
throws DeviceNotAvailableException {
log("adb shell " + command);
return device.executeShellCommand(command);
}
protected void executeShellCommand(String command, CollectingOutputReceiver outputReceiver)
throws DeviceNotAvailableException {
log("adb shell " + command);
mDevice.executeShellCommand(command, outputReceiver);
}
protected BufferedImage takeScreenshot() throws Exception {
final InputStreamSource stream = mDevice.getScreenshot("PNG", false /* rescale */);
if (stream == null) {
fail("Failed to take screenshot of device");
}
return ImageIO.read(stream.createInputStream());
}
protected void launchActivityInComponent(final String componentName,
final String targetActivityName, final String... keyValuePairs) throws Exception {
final String originalComponentName = ActivityManagerTestBase.componentName;
setComponentName(componentName);
launchActivity(targetActivityName, keyValuePairs);
setComponentName(originalComponentName);
}
protected void launchActivity(final String targetActivityName, final String... keyValuePairs)
throws Exception {
executeShellCommand(getAmStartCmd(targetActivityName, keyValuePairs));
mAmWmState.waitForValidState(mDevice, targetActivityName);
}
protected void launchActivityNoWait(final String targetActivityName,
final String... keyValuePairs) throws Exception {
executeShellCommand(getAmStartCmd(targetActivityName, keyValuePairs));
}
protected void launchActivityInNewTask(final String targetActivityName) throws Exception {
executeShellCommand(getAmStartCmdInNewTask(targetActivityName));
mAmWmState.waitForValidState(mDevice, targetActivityName);
}
/**
* Starts an activity in a new stack.
* @return the stack id of the newly created stack.
*/
protected int launchActivityInNewDynamicStack(final String activityName) throws Exception {
HashSet<Integer> stackIds = getStackIds();
executeShellCommand("am stack start " + ActivityAndWindowManagersState.DEFAULT_DISPLAY_ID
+ " " + getActivityComponentName(activityName));
HashSet<Integer> newStackIds = getStackIds();
newStackIds.removeAll(stackIds);
if (newStackIds.isEmpty()) {
return INVALID_STACK_ID;
} else {
assertTrue(newStackIds.size() == 1);
return newStackIds.iterator().next();
}
}
/**
* Returns the set of stack ids.
*/
private HashSet<Integer> getStackIds() throws Exception {
mAmWmState.computeState(mDevice, null);
final List<ActivityStack> stacks = mAmWmState.getAmState().getStacks();
final HashSet<Integer> stackIds = new HashSet<>();
for (ActivityStack s : stacks) {
stackIds.add(s.mStackId);
}
return stackIds;
}
protected void launchHomeActivity()
throws Exception {
executeShellCommand(AM_START_HOME_ACTIVITY_COMMAND);
mAmWmState.waitForHomeActivityVisible(mDevice);
}
protected void launchActivityOnDisplay(String targetActivityName, int displayId)
throws Exception {
executeShellCommand(getAmStartCmd(targetActivityName, displayId));
mAmWmState.waitForValidState(mDevice, targetActivityName);
}
/**
* Launch specific target activity. It uses existing instance of {@link #LAUNCHING_ACTIVITY}, so
* that one should be started first.
* @param toSide Launch to side in split-screen.
* @param randomData Make intent URI random by generating random data.
* @param multipleTask Allow multiple task launch.
* @param targetActivityName Target activity to be launched. Only class name should be provided,
* package name of {@link #LAUNCHING_ACTIVITY} will be added
* automatically.
* @param displayId Display id where target activity should be launched.
* @throws Exception
*/
protected void launchActivityFromLaunching(boolean toSide, boolean randomData,
boolean multipleTask, String targetActivityName, int displayId) throws Exception {
StringBuilder commandBuilder = new StringBuilder(getAmStartCmd(LAUNCHING_ACTIVITY));
commandBuilder.append(" -f 0x20000000");
if (toSide) {
commandBuilder.append(" --ez launch_to_the_side true");
}
if (randomData) {
commandBuilder.append(" --ez random_data true");
}
if (multipleTask) {
commandBuilder.append(" --ez multiple_task true");
}
if (targetActivityName != null) {
commandBuilder.append(" --es target_activity ").append(targetActivityName);
}
if (displayId != INVALID_DISPLAY_ID) {
commandBuilder.append(" --ei display_id ").append(displayId);
}
executeShellCommand(commandBuilder.toString());
mAmWmState.waitForValidState(mDevice, targetActivityName);
}
protected void launchActivityInStack(String activityName, int stackId,
final String... keyValuePairs) throws Exception {
executeShellCommand(getAmStartCmd(activityName, keyValuePairs) + " --stack " + stackId);
mAmWmState.waitForValidState(mDevice, activityName, stackId);
}
protected void launchActivityInDockStack(String activityName) throws Exception {
launchActivity(activityName);
// TODO(b/36279415): The way we launch an activity into the docked stack is different from
// what the user actually does. Long term we should use
// "adb shell input keyevent --longpress _app_swich_key_code_" to trigger a long press on
// the recents button which is consistent with what the user does. However, currently sys-ui
// does handle FLAG_LONG_PRESS for the app switch key. It just listens for long press on the
// view. We need to fix that in sys-ui before we can change this.
moveActivityToDockStack(activityName);
mAmWmState.waitForValidState(mDevice, activityName, DOCKED_STACK_ID);
}
protected void launchActivityToSide(boolean randomData, boolean multipleTaskFlag,
String targetActivity) throws Exception {
final String activityToLaunch = targetActivity != null ? targetActivity : "TestActivity";
getLaunchActivityBuilder().setToSide(true).setRandomData(randomData)
.setMultipleTask(multipleTaskFlag).setTargetActivityName(activityToLaunch)
.execute();
mAmWmState.waitForValidState(mDevice, activityToLaunch, FULLSCREEN_WORKSPACE_STACK_ID);
}
protected void moveActivityToDockStack(String activityName) throws Exception {
moveActivityToStack(activityName, DOCKED_STACK_ID);
}
protected void moveActivityToStack(String activityName, int stackId) throws Exception {
final int taskId = getActivityTaskId(activityName);
final String cmd = AM_MOVE_TASK + taskId + " " + stackId + " true";
executeShellCommand(cmd);
mAmWmState.waitForValidState(mDevice, activityName, stackId);
}
protected void resizeActivityTask(String activityName, int left, int top, int right, int bottom)
throws Exception {
final int taskId = getActivityTaskId(activityName);
final String cmd = "am task resize "
+ taskId + " " + left + " " + top + " " + right + " " + bottom;
executeShellCommand(cmd);
}
protected void resizeDockedStack(
int stackWidth, int stackHeight, int taskWidth, int taskHeight)
throws DeviceNotAvailableException {
executeShellCommand(AM_RESIZE_DOCKED_STACK
+ "0 0 " + stackWidth + " " + stackHeight
+ " 0 0 " + taskWidth + " " + taskHeight);
}
protected void resizeStack(int stackId, int stackLeft, int stackTop, int stackWidth,
int stackHeight) throws DeviceNotAvailableException {
executeShellCommand(AM_RESIZE_STACK + String.format("%d %d %d %d %d", stackId, stackLeft,
stackTop, stackWidth, stackHeight));
}
protected void pressHomeButton() throws DeviceNotAvailableException {
executeShellCommand(INPUT_KEYEVENT_HOME);
}
protected void pressBackButton() throws DeviceNotAvailableException {
executeShellCommand(INPUT_KEYEVENT_BACK);
}
protected void pressAppSwitchButton() throws DeviceNotAvailableException {
executeShellCommand(INPUT_KEYEVENT_APP_SWITCH);
}
// Utility method for debugging, not used directly here, but useful, so kept around.
protected void printStacksAndTasks() throws DeviceNotAvailableException {
CollectingOutputReceiver outputReceiver = new CollectingOutputReceiver();
executeShellCommand(AM_STACK_LIST, outputReceiver);
String output = outputReceiver.getOutput();
for (String line : output.split("\\n")) {
CLog.logAndDisplay(LogLevel.INFO, line);
}
}
protected int getActivityTaskId(String name) throws DeviceNotAvailableException {
CollectingOutputReceiver outputReceiver = new CollectingOutputReceiver();
executeShellCommand(AM_STACK_LIST, outputReceiver);
final String output = outputReceiver.getOutput();
final Pattern activityPattern = Pattern.compile("(.*) " + getWindowName(name) + " (.*)");
for (String line : output.split("\\n")) {
Matcher matcher = activityPattern.matcher(line);
if (matcher.matches()) {
for (String word : line.split("\\s+")) {
if (word.startsWith(TASK_ID_PREFIX)) {
final String withColon = word.split("=")[1];
return Integer.parseInt(withColon.substring(0, withColon.length() - 1));
}
}
}
}
return -1;
}
protected boolean supportsVrMode() throws DeviceNotAvailableException {
return hasDeviceFeature("android.software.vr.mode") &&
hasDeviceFeature("android.hardware.vr.high_performance");
}
protected boolean supportsPip() throws DeviceNotAvailableException {
return hasDeviceFeature("android.software.picture_in_picture")
|| PRETEND_DEVICE_SUPPORTS_PIP;
}
protected boolean supportsFreeform() throws DeviceNotAvailableException {
return hasDeviceFeature("android.software.freeform_window_management")
|| PRETEND_DEVICE_SUPPORTS_FREEFORM;
}
protected boolean isHandheld() throws DeviceNotAvailableException {
return !hasDeviceFeature("android.software.leanback")
&& !hasDeviceFeature("android.software.watch");
}
protected boolean supportsSplitScreenMultiWindow() throws DeviceNotAvailableException {
CollectingOutputReceiver outputReceiver = new CollectingOutputReceiver();
executeShellCommand(AM_SUPPORTS_SPLIT_SCREEN_MULTIWINDOW, outputReceiver);
String output = outputReceiver.getOutput();
return !output.startsWith("false");
}
protected boolean noHomeScreen() throws DeviceNotAvailableException {
CollectingOutputReceiver outputReceiver = new CollectingOutputReceiver();
executeShellCommand(AM_NO_HOME_SCREEN, outputReceiver);
String output = outputReceiver.getOutput();
return output.startsWith("true");
}
protected boolean hasDeviceFeature(String requiredFeature) throws DeviceNotAvailableException {
if (mAvailableFeatures == null) {
// TODO: Move this logic to ITestDevice.
final String output = runCommandAndPrintOutput("pm list features");
// Extract the id of the new user.
mAvailableFeatures = new HashSet<>();
for (String feature: output.split("\\s+")) {
// Each line in the output of the command has the format "feature:{FEATURE_VALUE}".
String[] tokens = feature.split(":");
assertTrue("\"" + feature + "\" expected to have format feature:{FEATURE_VALUE}",
tokens.length > 1);
assertEquals(feature, "feature", tokens[0]);
mAvailableFeatures.add(tokens[1]);
}
}
boolean result = mAvailableFeatures.contains(requiredFeature);
if (!result) {
CLog.logAndDisplay(LogLevel.INFO, "Device doesn't support " + requiredFeature);
}
return result;
}
private boolean isDisplayOn() throws DeviceNotAvailableException {
final CollectingOutputReceiver outputReceiver = new CollectingOutputReceiver();
mDevice.executeShellCommand("dumpsys power", outputReceiver);
for (String line : outputReceiver.getOutput().split("\\n")) {
line = line.trim();
final Matcher matcher = sDisplayStatePattern.matcher(line);
if (matcher.matches()) {
final String state = matcher.group(1);
log("power state=" + state);
return "ON".equals(state);
}
}
log("power state :(");
return false;
}
protected void sleepDevice() throws DeviceNotAvailableException {
int retriesLeft = 5;
runCommandAndPrintOutput("input keyevent 26");
do {
if (isDisplayOn()) {
log("***Waiting for display to turn off...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log(e.toString());
// Well I guess we are not waiting...
}
} else {
break;
}
} while (retriesLeft-- > 0);
}
protected void wakeUpAndUnlockDevice() throws DeviceNotAvailableException {
wakeUpDevice();
unlockDevice();
}
protected void wakeUpDevice() throws DeviceNotAvailableException {
runCommandAndPrintOutput("input keyevent 224");
}
protected void unlockDevice() throws DeviceNotAvailableException {
runCommandAndPrintOutput("input keyevent 82");
}
protected void unlockDeviceWithCredential() throws Exception {
runCommandAndPrintOutput("input keyevent 82");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
//ignored
}
enterAndConfirmLockCredential();
}
protected void enterAndConfirmLockCredential() throws Exception {
// TODO: This should use waitForIdle..but there ain't such a thing on hostside tests, boo :(
Thread.sleep(500);
runCommandAndPrintOutput("input text " + LOCK_CREDENTIAL);
runCommandAndPrintOutput("input keyevent KEYCODE_ENTER");
}
protected void gotoKeyguard() throws DeviceNotAvailableException {
sleepDevice();
wakeUpDevice();
}
protected void setLockCredential() throws DeviceNotAvailableException {
runCommandAndPrintOutput("locksettings set-pin " + LOCK_CREDENTIAL);
}
protected void removeLockCredential() throws DeviceNotAvailableException {
runCommandAndPrintOutput("locksettings clear --old " + LOCK_CREDENTIAL);
}
/**
* Sets the device rotation, value corresponds to one of {@link Surface.ROTATION_0},
* {@link Surface.ROTATION_90}, {@link Surface.ROTATION_180}, {@link Surface.ROTATION_270}.
*/
protected void setDeviceRotation(int rotation) throws Exception {
setAccelerometerRotation(0);
setUserRotation(rotation);
mAmWmState.waitForRotation(mDevice, rotation);
}
protected int getDeviceRotation(int displayId) throws DeviceNotAvailableException {
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);
while (matcher.find()) {
final String match = matcher.group(7);
return Integer.parseInt(match);
}
return INVALID_DEVICE_ROTATION;
}
private int getAccelerometerRotation() throws DeviceNotAvailableException {
final String rotation =
runCommandAndPrintOutput("settings get system accelerometer_rotation");
return Integer.parseInt(rotation.trim());
}
private void setAccelerometerRotation(int rotation) throws DeviceNotAvailableException {
runCommandAndPrintOutput(
"settings put system accelerometer_rotation " + rotation);
}
protected int getUserRotation() throws DeviceNotAvailableException {
final String rotation =
runCommandAndPrintOutput("settings get system user_rotation").trim();
if ("null".equals(rotation)) {
return -1;
}
return Integer.parseInt(rotation);
}
private void setUserRotation(int rotation) throws DeviceNotAvailableException {
if (rotation == -1) {
runCommandAndPrintOutput(
"settings delete system user_rotation");
} else {
runCommandAndPrintOutput(
"settings put system user_rotation " + rotation);
}
}
protected void setFontScale(float fontScale) throws DeviceNotAvailableException {
if (fontScale == 0.0f) {
runCommandAndPrintOutput(
"settings delete system font_scale");
} else {
runCommandAndPrintOutput(
"settings put system font_scale " + fontScale);
}
}
protected float getFontScale() throws DeviceNotAvailableException {
try {
final String fontScale =
runCommandAndPrintOutput("settings get system font_scale").trim();
return Float.parseFloat(fontScale);
} catch (NumberFormatException e) {
// If we don't have a valid font scale key, return 0.0f now so
// that we delete the key in tearDown().
return 0.0f;
}
}
protected String runCommandAndPrintOutput(String command) throws DeviceNotAvailableException {
final String output = executeShellCommand(command);
log(output);
return output;
}
/**
* Tries to clear logcat and inserts log separator in case clearing didn't succeed, so we can
* always find the starting point from where to evaluate following logs.
* @return Unique log separator.
*/
protected String clearLogcat() throws DeviceNotAvailableException {
mDevice.executeAdbCommand("logcat", "-c");
final String uniqueString = UUID.randomUUID().toString();
executeShellCommand("log -t " + LOG_SEPARATOR + " " + uniqueString);
return uniqueString;
}
void assertActivityLifecycle(String activityName, boolean relaunched,
String logSeparator) throws DeviceNotAvailableException {
int retriesLeft = 5;
String resultString;
do {
resultString = verifyLifecycleCondition(activityName, logSeparator, relaunched);
if (resultString != null) {
log("***Waiting for valid lifecycle state: " + resultString);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log(e.toString());
}
} else {
break;
}
} while (retriesLeft-- > 0);
assertNull(resultString, resultString);
}
/** @return Error string if lifecycle counts don't match, null if everything is fine. */
private String verifyLifecycleCondition(String activityName, String logSeparator,
boolean relaunched) throws DeviceNotAvailableException {
final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(activityName,
logSeparator);
if (relaunched) {
if (lifecycleCounts.mDestroyCount < 1) {
return activityName + " must have been destroyed. mDestroyCount="
+ lifecycleCounts.mDestroyCount;
}
if (lifecycleCounts.mCreateCount < 1) {
return activityName + " must have been (re)created. mCreateCount="
+ lifecycleCounts.mCreateCount;
}
} else {
if (lifecycleCounts.mDestroyCount > 0) {
return activityName + " must *NOT* have been destroyed. mDestroyCount="
+ lifecycleCounts.mDestroyCount;
}
if (lifecycleCounts.mCreateCount > 0) {
return activityName + " must *NOT* have been (re)created. mCreateCount="
+ lifecycleCounts.mCreateCount;
}
if (lifecycleCounts.mConfigurationChangedCount < 1) {
return activityName + " must have received configuration changed. "
+ "mConfigurationChangedCount="
+ lifecycleCounts.mConfigurationChangedCount;
}
}
return null;
}
protected void assertRelaunchOrConfigChanged(
String activityName, int numRelaunch, int numConfigChange, String logSeparator)
throws DeviceNotAvailableException {
int retriesLeft = 5;
String resultString;
do {
resultString = verifyRelaunchOrConfigChanged(activityName, numRelaunch, numConfigChange,
logSeparator);
if (resultString != null) {
log("***Waiting for relaunch or config changed: " + resultString);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log(e.toString());
}
} else {
break;
}
} while (retriesLeft-- > 0);
assertNull(resultString, resultString);
}
/** @return Error string if lifecycle counts don't match, null if everything is fine. */
private String verifyRelaunchOrConfigChanged(String activityName, int numRelaunch,
int numConfigChange, String logSeparator) throws DeviceNotAvailableException {
final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(activityName,
logSeparator);
if (lifecycleCounts.mDestroyCount != numRelaunch) {
return activityName + " has been destroyed " + lifecycleCounts.mDestroyCount
+ " time(s), expecting " + numRelaunch;
} else if (lifecycleCounts.mCreateCount != numRelaunch) {
return activityName + " has been (re)created " + lifecycleCounts.mCreateCount
+ " time(s), expecting " + numRelaunch;
} else if (lifecycleCounts.mConfigurationChangedCount != numConfigChange) {
return activityName + " has received " + lifecycleCounts.mConfigurationChangedCount
+ " onConfigurationChanged() calls, expecting " + numConfigChange;
}
return null;
}
protected void assertActivityDestroyed(String activityName, String logSeparator)
throws DeviceNotAvailableException {
int retriesLeft = 5;
String resultString;
do {
resultString = verifyActivityDestroyed(activityName, logSeparator);
if (resultString != null) {
log("***Waiting for activity destroyed: " + resultString);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log(e.toString());
}
} else {
break;
}
} while (retriesLeft-- > 0);
assertNull(resultString, resultString);
}
/** @return Error string if lifecycle counts don't match, null if everything is fine. */
private String verifyActivityDestroyed(String activityName, String logSeparator)
throws DeviceNotAvailableException {
final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(activityName,
logSeparator);
if (lifecycleCounts.mDestroyCount != 1) {
return activityName + " has been destroyed " + lifecycleCounts.mDestroyCount
+ " time(s), expecting single destruction.";
} else if (lifecycleCounts.mCreateCount != 0) {
return activityName + " has been (re)created " + lifecycleCounts.mCreateCount
+ " time(s), not expecting any.";
} else if (lifecycleCounts.mConfigurationChangedCount != 0) {
return activityName + " has received " + lifecycleCounts.mConfigurationChangedCount
+ " onConfigurationChanged() calls, not expecting any.";
}
return null;
}
protected String[] getDeviceLogsForComponent(String componentName, String logSeparator)
throws DeviceNotAvailableException {
return getDeviceLogsForComponents(new String[]{componentName}, logSeparator);
}
protected String[] getDeviceLogsForComponents(final String[] componentNames,
String logSeparator) throws DeviceNotAvailableException {
String filters = LOG_SEPARATOR + ":I ";
for (String component : componentNames) {
filters += component + ":I ";
}
final String[] result = mDevice.executeAdbCommand(
"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)) {
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;
}
private static final Pattern sCreatePattern = Pattern.compile("(.+): onCreate");
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 sNewConfigPattern = Pattern.compile(
"(.+): config size=\\((\\d+),(\\d+)\\) displaySize=\\((\\d+),(\\d+)\\)"
+ " metricsSize=\\((\\d+),(\\d+)\\) smallestScreenWidth=(\\d+) densityDpi=(\\d+)"
+ " orientation=(\\d+)");
private static final Pattern sDisplayStatePattern =
Pattern.compile("Display Power: state=(.+)");
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;
}
}
ReportedSizes getLastReportedSizesForActivity(String activityName, String logSeparator)
throws DeviceNotAvailableException {
int retriesLeft = 5;
ReportedSizes result;
do {
result = readLastReportedSizes(activityName, logSeparator);
if (result == null) {
log("***Waiting for sizes to be reported...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log(e.toString());
// Well I guess we are not waiting...
}
} else {
break;
}
} while (retriesLeft-- > 0);
return result;
}
private ReportedSizes readLastReportedSizes(String activityName, String logSeparator)
throws DeviceNotAvailableException {
final String[] lines = getDeviceLogsForComponent(activityName, logSeparator);
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;
}
class ActivityLifecycleCounts {
int mCreateCount;
int mResumeCount;
int mConfigurationChangedCount;
int mLastConfigurationChangedLineIndex;
int mMovedToDisplayCount;
int mMultiWindowModeChangedCount;
int mLastMultiWindowModeChangedLineIndex;
int mPictureInPictureModeChangedCount;
int mLastPictureInPictureModeChangedLineIndex;
int mPauseCount;
int mStopCount;
int mLastStopLineIndex;
int mDestroyCount;
public ActivityLifecycleCounts(String activityName, String logSeparator)
throws DeviceNotAvailableException {
int lineIndex = 0;
for (String line : getDeviceLogsForComponent(activityName, logSeparator)) {
line = line.trim();
lineIndex++;
Matcher matcher = sCreatePattern.matcher(line);
if (matcher.matches()) {
mCreateCount++;
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 = 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;
}
}
}
}
protected void stopTestCase() throws Exception {
executeShellCommand("am force-stop " + componentName);
}
protected LaunchActivityBuilder getLaunchActivityBuilder() {
return new LaunchActivityBuilder(mAmWmState, mDevice);
}
protected static class LaunchActivityBuilder {
private final ActivityAndWindowManagersState mAmWmState;
private final ITestDevice mDevice;
private String mTargetActivityName;
private String mTargetPackage = componentName;
private boolean mToSide;
private boolean mRandomData;
private boolean mMultipleTask;
private int mDisplayId = INVALID_DISPLAY_ID;
private String mLaunchingActivityName = LAUNCHING_ACTIVITY;
private boolean mReorderToFront;
public LaunchActivityBuilder(ActivityAndWindowManagersState amWmState,
ITestDevice device) {
mAmWmState = amWmState;
mDevice = device;
}
public LaunchActivityBuilder setToSide(boolean toSide) {
mToSide = toSide;
return this;
}
public LaunchActivityBuilder setRandomData(boolean randomData) {
mRandomData = randomData;
return this;
}
public LaunchActivityBuilder setMultipleTask(boolean multipleTask) {
mMultipleTask = multipleTask;
return this;
}
public LaunchActivityBuilder setReorderToFront(boolean reorderToFront) {
mReorderToFront = reorderToFront;
return this;
}
public LaunchActivityBuilder setTargetActivityName(String name) {
mTargetActivityName = name;
return this;
}
public LaunchActivityBuilder setTargetPackage(String pkg) {
mTargetPackage = pkg;
return this;
}
public LaunchActivityBuilder setDisplayId(int id) {
mDisplayId = id;
return this;
}
public LaunchActivityBuilder setLaunchingActivityName(String name) {
mLaunchingActivityName = name;
return this;
}
public void execute() throws Exception {
StringBuilder commandBuilder = new StringBuilder(getAmStartCmd(mLaunchingActivityName));
commandBuilder.append(" -f 0x20000000");
// Add a flag to ensure we actually mean to launch an activity.
commandBuilder.append(" --ez launch_activity true");
if (mToSide) {
commandBuilder.append(" --ez launch_to_the_side true");
}
if (mRandomData) {
commandBuilder.append(" --ez random_data true");
}
if (mMultipleTask) {
commandBuilder.append(" --ez multiple_task true");
}
if (mReorderToFront) {
commandBuilder.append(" --ez reorder_to_front true");
}
if (mTargetActivityName != null) {
commandBuilder.append(" --es target_activity ").append(mTargetActivityName);
commandBuilder.append(" --es package_name ").append(mTargetPackage);
}
if (mDisplayId != INVALID_DISPLAY_ID) {
commandBuilder.append(" --ei display_id ").append(mDisplayId);
}
executeShellCommand(mDevice, commandBuilder.toString());
mAmWmState.waitForValidState(mDevice, new String[]{mTargetActivityName},
null /* stackIds */, false /* compareTaskAndStackBounds */, mTargetPackage);
}
}
}