--- /dev/null
+/*
+ * Copyright (C) 2010 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 com.android.tradefed.device;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.FileListingService;
+import com.android.ddmlib.FileListingService.FileEntry;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.InstallException;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.SyncException;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.WifiHelper.WifiState;
+import com.android.tradefed.result.StubTestListener;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.SequenceInputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Default implementation of a {@link ITestDevice}
+ */
+class TestDevice implements IManagedTestDevice {
+
+ private static final String LOG_TAG = "TestDevice";
+ /** the default number of command retry attempts to perform */
+ static final int MAX_RETRY_ATTEMPTS = 3;
+ /** the max number of bytes to store in logcat tmp buffer */
+ private static final int LOGCAT_BUFF_SIZE = 32 * 1024;
+ private static final String LOGCAT_CMD = "logcat -v threadtime";
+
+ /** The time in ms to wait before starting logcat for a device */
+ private int mLogStartDelay = 5*1000;
+ /** The time in ms to wait for a device to boot into fastboot. */
+ private static final int FASTBOOT_TIMEOUT = 1 * 60 * 1000;
+ /** number of attempts made to clear dialogs */
+ private static final int NUM_CLEAR_ATTEMPTS = 5;
+ /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
+ static final String DISMISS_DIALOG_CMD = "input keyevent 23";
+ /** The time in ms to wait for a command to complete. */
+ private int mCmdTimeout = 2 * 60 * 1000;
+ /** The time in ms to wait for a 'long' command to complete. */
+ private long mLongCmdTimeout = 12 * 60 * 1000;
+
+ private IDevice mIDevice;
+ private IDeviceRecovery mRecovery;
+ private final IDeviceStateMonitor mMonitor;
+ private TestDeviceState mState = TestDeviceState.ONLINE;
+ private Semaphore mFastbootLock = new Semaphore(1);
+ private LogCatReceiver mLogcatReceiver;
+ private IFileEntry mRootFile = null;
+
+ // TODO: TestDevice is not loaded from configuration yet, so these options are currently fixed
+
+ @Option(name="enable-root", description="enable adb root on boot")
+ private boolean mEnableAdbRoot = true;
+
+ @Option(name="disable-keyguard", description="attempt to disable keyguard once complete")
+ private boolean mDisableKeyguard = true;
+
+ @Option(name="disable-keyguard-cmd", description="shell command to disable keyguard")
+ private String mDisableKeyguardCmd = "input keyevent 82";
+
+ /**
+ * The maximum size of a tmp logcat file, in bytes.
+ * <p/>
+ * The actual size of the log info stored will be up to twice this number, as two logcat files
+ * are stored.
+ */
+ @Option(name="max-tmp-logcat-file", description="The maximum size of a tmp logcat file, in bytes")
+ private long mMaxLogcatFileSize = 10 * 1024 * 1024;
+
+ /**
+ * Interface for a generic device communication attempt.
+ */
+ private abstract interface DeviceAction {
+
+ /**
+ * Execute the device operation.
+ *
+ * @return <code>true</code> if operation is performed successfully, <code>false</code>
+ * otherwise
+ * @throws Exception if operation terminated abnormally
+ */
+ public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
+ ShellCommandUnresponsiveException, InstallException, SyncException;
+ }
+
+ /**
+ * Creates a {@link TestDevice}.
+ *
+ * @param device the associated {@link IDevice}
+ * @param recovery the {@link IDeviceRecovery} mechanism to use
+ */
+ TestDevice(IDevice device, IDeviceStateMonitor monitor) {
+ mIDevice = device;
+ mMonitor = monitor;
+ }
+
+ /**
+ * Get the {@link RunUtil} instance to use.
+ * <p/>
+ * Exposed for unit testing.
+ */
+ IRunUtil getRunUtil() {
+ return RunUtil.getInstance();
+ }
+
+ /**
+ * Sets the max size of a tmp logcat file.
+ *
+ * @param size max byte size of tmp file
+ */
+ void setTmpLogcatSize(long size) {
+ mMaxLogcatFileSize = size;
+ }
+
+ /**
+ * Sets the time in ms to wait before starting logcat capture for a online device.
+ *
+ * @param delay the delay in ms
+ */
+ void setLogStartDelay(int delay) {
+ mLogStartDelay = delay;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public IDevice getIDevice() {
+ synchronized (mIDevice) {
+ return mIDevice;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setIDevice(IDevice newDevice) {
+ IDevice currentDevice = mIDevice;
+ if (!getIDevice().equals(newDevice)) {
+ synchronized (currentDevice) {
+ mIDevice = newDevice;
+ }
+ mMonitor.setIDevice(mIDevice);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getSerialNumber() {
+ return getIDevice().getSerialNumber();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getProductType() throws DeviceNotAvailableException {
+ return internalGetProductType(MAX_RETRY_ATTEMPTS);
+ }
+
+ /**
+ * {@see getProductType()}
+ *
+ * @param retryAttempts The number of times to try calling {@see recoverDevice()} if the
+ * device's product type cannot be found.
+ */
+ private String internalGetProductType(int retryAttempts)
+ throws DeviceNotAvailableException {
+ String productType = getIDevice().getProperty("ro.product.board");
+ if (productType == null || productType.isEmpty()) {
+ /* DDMS may not have processes all of the properties yet, or the device may be in
+ * fastboot, or the device may simply be misconfigured or malfunctioning. Try querying
+ * directly.
+ */
+ if (getDeviceState() == TestDeviceState.FASTBOOT) {
+ Log.w(LOG_TAG, String.format(
+ "Product type for device %s is null, re-querying in fastboot",
+ getSerialNumber()));
+ productType = getFastbootProduct();
+ } else {
+ Log.w(LOG_TAG, String.format(
+ "Product type for device %s is null, re-querying", getSerialNumber()));
+ productType = executeShellCommand("getprop ro.product.board").trim();
+
+ if (productType.isEmpty()) {
+ // last ditch effort; try ro.product.device
+ productType = executeShellCommand("getprop ro.product.device").trim();
+ Log.w(LOG_TAG, String.format("Fell back to ro.product.device because " +
+ "ro.product.board is unset. product type is %s.", productType));
+ }
+ }
+ }
+
+ // Things will likely break if we don't have a valid product type. Try recovery (in case
+ // the device is only partially booted for some reason), and if that doesn't help, bail.
+ if (productType == null || productType.isEmpty()) {
+ if (retryAttempts > 0) {
+ recoverDevice();
+ productType = internalGetProductType(retryAttempts - 1);
+ }
+
+ if (productType == null || productType.isEmpty()) {
+ throw new DeviceNotAvailableException(String.format(
+ "Could not determine product type for device %s.", getSerialNumber()));
+ }
+ }
+
+ return productType;
+ }
+
+ private String getFastbootProduct() throws DeviceNotAvailableException {
+ CommandResult result = executeFastbootCommand("getvar", "product");
+ if (result.getStatus() == CommandStatus.SUCCESS) {
+ Pattern fastbootProductPattern = Pattern.compile("product:[ ]+(\\w+)");
+ // fastboot is weird, and may dump the output on stderr instead of stdout
+ String resultText = result.getStdout();
+ if (resultText == null || resultText.length() < 1) {
+ resultText = result.getStderr();
+ }
+ Matcher matcher = fastbootProductPattern.matcher(resultText);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void executeShellCommand(final String command, final IShellOutputReceiver receiver)
+ throws DeviceNotAvailableException {
+ DeviceAction action = new DeviceAction() {
+ public boolean run() throws TimeoutException, IOException,
+ AdbCommandRejectedException, ShellCommandUnresponsiveException {
+ getIDevice().executeShellCommand(command, receiver, mCmdTimeout);
+ return true;
+ }
+ };
+ performDeviceAction(String.format("shell %s", command), action, MAX_RETRY_ATTEMPTS);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void executeShellCommand(final String command, final IShellOutputReceiver receiver,
+ final int maxTimeToOutputShellResponse, int retryAttempts)
+ throws DeviceNotAvailableException {
+ DeviceAction action = new DeviceAction() {
+ public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
+ ShellCommandUnresponsiveException {
+ getIDevice().executeShellCommand(command, receiver, maxTimeToOutputShellResponse);
+ return true;
+ }
+ };
+ performDeviceAction(String.format("shell %s", command), action, retryAttempts);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String executeShellCommand(String command) throws DeviceNotAvailableException {
+ CollectingOutputReceiver receiver = new CollectingOutputReceiver();
+ executeShellCommand(command, receiver);
+ String output = receiver.getOutput();
+ Log.v(LOG_TAG, String.format("%s on %s returned %s", command, getSerialNumber(), output));
+ return output;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void runInstrumentationTests(IRemoteAndroidTestRunner runner,
+ Collection<ITestRunListener> listeners) throws DeviceNotAvailableException {
+ try {
+ RunFailureListener failureListener = new RunFailureListener();
+ listeners.add(failureListener);
+ // TODO: query current maxTimeToOutputResponse and only override if not set
+ runner.setMaxtimeToOutputResponse((int)mLongCmdTimeout);
+ runner.run(listeners);
+ if (failureListener.mIsRunFailure) {
+ // run failed, might be system crash. Ensure device is up
+ if (mMonitor.waitForDeviceAvailable(5*1000) == null) {
+ // device isn't up, recover
+ recoverDevice();
+ }
+ }
+ } catch (IOException e) {
+ // TODO: no attempt tracking for exceptions. Would be good to catch scenario where
+ // repeated test runs fail even though recovery is succeeding
+ recoverDevice();
+ } catch (ShellCommandUnresponsiveException e) {
+ recoverDevice();
+ } catch (TimeoutException e) {
+ recoverDevice();
+ } catch (AdbCommandRejectedException e) {
+ recoverDevice();
+ }
+ }
+
+ private static class RunFailureListener extends StubTestListener {
+ private boolean mIsRunFailure = false;
+
+ @Override
+ public void testRunFailed(String message) {
+ mIsRunFailure = true;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void runInstrumentationTests(IRemoteAndroidTestRunner runner,
+ ITestRunListener... listeners) throws DeviceNotAvailableException {
+ List<ITestRunListener> listenerList = new ArrayList<ITestRunListener>();
+ listenerList.addAll(Arrays.asList(listeners));
+ runInstrumentationTests(runner, listenerList);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String installPackage(final File packageFile, final boolean reinstall)
+ throws DeviceNotAvailableException {
+ // use array to store response, so it can be returned to caller
+ final String[] response = new String[1];
+ DeviceAction installAction = new DeviceAction() {
+ public boolean run() throws InstallException {
+ String result = getIDevice().installPackage(packageFile.getAbsolutePath(),
+ reinstall);
+ response[0] = result;
+ return result == null;
+ }
+ };
+ performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
+ installAction, MAX_RETRY_ATTEMPTS);
+ return response[0];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
+ // use array to store response, so it can be returned to caller
+ final String[] response = new String[1];
+ DeviceAction uninstallAction = new DeviceAction() {
+ public boolean run() throws InstallException {
+ String result = getIDevice().uninstallPackage(packageName);
+ response[0] = result;
+ return result == null;
+ }
+ };
+ performDeviceAction(String.format("uninstall %s", packageName), uninstallAction,
+ MAX_RETRY_ATTEMPTS);
+ return response[0];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean pullFile(final String remoteFilePath, final File localFile)
+ throws DeviceNotAvailableException {
+
+ DeviceAction pullAction = new DeviceAction() {
+ public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException, SyncException {
+ SyncService syncService = null;
+ boolean status = false;
+ try {
+ syncService = getIDevice().getSyncService();
+ syncService.pullFile(remoteFilePath,
+ localFile.getAbsolutePath(), SyncService.getNullProgressMonitor());
+ status = true;
+ } catch (SyncException e) {
+ Log.w(LOG_TAG, String.format(
+ "Failed to pull %s from %s. Message %s",
+ remoteFilePath, getSerialNumber(), e.getMessage()));
+ throw e;
+ } finally {
+ if (syncService != null) {
+ syncService.close();
+ }
+ }
+ return status;
+ }
+ };
+ return performDeviceAction(String.format("pull %s", remoteFilePath), pullAction,
+ MAX_RETRY_ATTEMPTS);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean pushFile(final File localFile, final String remoteFilePath)
+ throws DeviceNotAvailableException {
+ DeviceAction pushAction = new DeviceAction() {
+ public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
+ SyncException {
+ SyncService syncService = null;
+ boolean status = false;
+ try {
+ syncService = getIDevice().getSyncService();
+ syncService.pushFile(localFile.getAbsolutePath(),
+ remoteFilePath, SyncService.getNullProgressMonitor());
+ status = true;
+ } catch (SyncException e) {
+ Log.w(LOG_TAG, String.format(
+ "Failed to push to %s on device %s. Message %s",
+ remoteFilePath, getSerialNumber(), e.getMessage()));
+ throw e;
+ } finally {
+ if (syncService != null) {
+ syncService.close();
+ }
+ }
+ return status;
+ }
+ };
+ return performDeviceAction(String.format("push %s", remoteFilePath), pushAction,
+ MAX_RETRY_ATTEMPTS);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean doesFileExist(String destPath) throws DeviceNotAvailableException {
+ String lsGrep = executeShellCommand(String.format("ls \"%s\"", destPath));
+ return !lsGrep.contains("No such file or directory");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public long getExternalStoreFreeSpace() throws DeviceNotAvailableException {
+ Log.i(LOG_TAG, String.format("Checking free space for %s", getSerialNumber()));
+ String externalStorePath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
+ String output = executeShellCommand(String.format("df %s", externalStorePath));
+ Long available = parseFreeSpaceFromAvailable(output);
+ if (available != null) {
+ return available;
+ }
+ available = parseFreeSpaceFromFree(externalStorePath, output);
+ if (available != null) {
+ return available;
+ }
+
+ Log.e(LOG_TAG, String.format(
+ "free space command output \"%s\" did not match expected patterns", output));
+ return 0;
+ }
+
+ /**
+ * Parses a partitions available space from the legacy output of a 'df' command.
+ * <p/>
+ * Assumes output format of:
+ * <br>/
+ * <code>
+ * [partition]: 15659168K total, 51584K used, 15607584K available (block size 32768)
+ * </code>
+ * @param dfOutput the output of df command to parse
+ * @return the available space in kilobytes or <code>null</code> if output could not be parsed
+ */
+ private Long parseFreeSpaceFromAvailable(String dfOutput) {
+ final Pattern freeSpacePattern = Pattern.compile("(\\d+)K available");
+ Matcher patternMatcher = freeSpacePattern.matcher(dfOutput);
+ if (patternMatcher.find()) {
+ String freeSpaceString = patternMatcher.group(1);
+ try {
+ return Long.parseLong(freeSpaceString);
+ } catch (NumberFormatException e) {
+ // fall through
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Parses a partitions available space from the 'table-formatted' output of a 'df' command.
+ * <p/>
+ * Assumes output format of:
+ * <br/>
+ * <code>
+ * Filesystem Size Used Free Blksize
+ * <br/>
+ * [partition]: 3G 790M 2G 4096
+ * </code>
+ * @param dfOutput the output of df command to parse
+ * @return the available space in kilobytes or <code>null</code> if output could not be parsed
+ */
+ private Long parseFreeSpaceFromFree(String externalStorePath, String dfOutput) {
+ Long freeSpace = null;
+ final Pattern freeSpaceTablePattern = Pattern.compile(String.format(
+ //fs Size Used Free
+ "%s\\s+[\\w\\d]+\\s+[\\w\\d]+\\s+(\\d+)(\\w)", externalStorePath));
+ Matcher tablePatternMatcher = freeSpaceTablePattern.matcher(dfOutput);
+ if (tablePatternMatcher.find()) {
+ String numericValueString = tablePatternMatcher.group(1);
+ String unitType = tablePatternMatcher.group(2);
+ try {
+ freeSpace = Long.parseLong(numericValueString);
+ if (unitType.equals("M")) {
+ freeSpace = freeSpace * 1024;
+ } else if (unitType.equals("G")) {
+ freeSpace = freeSpace * 1024 * 1024;
+ }
+ } catch (NumberFormatException e) {
+ // fall through
+ }
+ }
+ return freeSpace;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getMountPoint(String mountName) {
+ return mMonitor.getMountPoint(mountName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public IFileEntry getFileEntry(String path) throws DeviceNotAvailableException {
+ String[] pathComponents = path.split(FileListingService.FILE_SEPARATOR);
+ if (mRootFile == null) {
+ FileListingService service = getIDevice().getFileListingService();
+ mRootFile = new FileEntryWrapper(this, service.getRoot());
+ }
+ return FileEntryWrapper.getDescendant(mRootFile, Arrays.asList(pathComponents));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean syncFiles(File localFileDir, String deviceFilePath)
+ throws DeviceNotAvailableException {
+ Log.i(LOG_TAG, String.format("Syncing %s to %s on device %s",
+ localFileDir.getAbsolutePath(), deviceFilePath, getSerialNumber()));
+ if (!localFileDir.isDirectory()) {
+ Log.e(LOG_TAG, String.format("file %s is not a directory",
+ localFileDir.getAbsolutePath()));
+ return false;
+ }
+ // get the real destination path. This is done because underlying syncService.push
+ // implementation will add localFileDir.getName() to destination path
+ deviceFilePath = String.format("%s/%s", deviceFilePath, localFileDir.getName());
+ if (!doesFileExist(deviceFilePath)) {
+ executeShellCommand(String.format("mkdir %s", deviceFilePath));
+ }
+ IFileEntry remoteFileEntry = getFileEntry(deviceFilePath);
+ if (remoteFileEntry == null) {
+ Log.e(LOG_TAG, String.format("Could not find remote file entry %s ", deviceFilePath));
+ return false;
+ }
+
+ return syncFiles(localFileDir, remoteFileEntry);
+ }
+
+ /**
+ * Recursively sync newer files.
+ *
+ * @param localFileDir the local {@link File} directory to sync
+ * @param remoteFileEntry the remote destination {@link IFileEntry}
+ * @return <code>true</code> if files were synced successfully
+ * @throws DeviceNotAvailableException
+ */
+ private boolean syncFiles(File localFileDir, final IFileEntry remoteFileEntry)
+ throws DeviceNotAvailableException {
+ Log.d(LOG_TAG, String.format("Syncing %s to %s on %s", localFileDir.getAbsolutePath(),
+ remoteFileEntry.getFullPath(), getSerialNumber()));
+ // find newer files to sync
+ File[] localFiles = localFileDir.listFiles(new NoHiddenFilesFilter());
+ ArrayList<String> filePathsToSync = new ArrayList<String>();
+ for (File localFile : localFiles) {
+ IFileEntry entry = remoteFileEntry.findChild(localFile.getName());
+ if (entry == null) {
+ Log.d(LOG_TAG, String.format("Detected missing file path %s",
+ localFile.getAbsolutePath()));
+ filePathsToSync.add(localFile.getAbsolutePath());
+ } else if (localFile.isDirectory()) {
+ // This directory exists remotely. recursively sync it to sync only its newer files
+ // contents
+ if (!syncFiles(localFile, entry)) {
+ return false;
+ }
+ } else if (isNewer(localFile, entry)) {
+ Log.d(LOG_TAG, String.format("Detected newer file %s",
+ localFile.getAbsolutePath()));
+ filePathsToSync.add(localFile.getAbsolutePath());
+ }
+ }
+
+ if (filePathsToSync.size() == 0) {
+ Log.d(LOG_TAG, "No files to sync");
+ return true;
+ }
+ final String files[] = filePathsToSync.toArray(new String[filePathsToSync.size()]);
+ DeviceAction syncAction = new DeviceAction() {
+ public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
+ SyncException {
+ SyncService syncService = null;
+ boolean status = false;
+ try {
+ syncService = getIDevice().getSyncService();
+ syncService.push(files, remoteFileEntry.getFileEntry(),
+ SyncService.getNullProgressMonitor());
+ status = true;
+ } catch (SyncException e) {
+ Log.w(LOG_TAG, String.format(
+ "Failed to sync files to %s on device %s. Message %s",
+ remoteFileEntry.getFullPath(), getSerialNumber(), e.getMessage()));
+ throw e;
+ } finally {
+ if (syncService != null) {
+ syncService.close();
+ }
+ }
+ return status;
+ }
+ };
+ return performDeviceAction(String.format("sync files %s", remoteFileEntry.getFullPath()),
+ syncAction, MAX_RETRY_ATTEMPTS);
+ }
+
+ /**
+ * Queries the file listing service for a given directory
+ *
+ * @param remoteFileEntry
+ * @param service
+ * @throws DeviceNotAvailableException
+ */
+ FileEntry[] getFileChildren(final FileEntry remoteFileEntry)
+ throws DeviceNotAvailableException {
+ // time this operation because its known to hang
+ FileQueryAction action = new FileQueryAction(remoteFileEntry,
+ getIDevice().getFileListingService());
+ performDeviceAction("buildFileCache", action, MAX_RETRY_ATTEMPTS);
+ return action.mFileContents;
+ }
+
+ private class FileQueryAction implements DeviceAction {
+
+ FileEntry[] mFileContents = null;
+ private FileEntry mRemoteFileEntry;
+ private FileListingService mService;
+
+ FileQueryAction(FileEntry remoteFileEntry, FileListingService service) {
+ mRemoteFileEntry = remoteFileEntry;
+ mService = service;
+ }
+
+ public boolean run() throws TimeoutException, IOException {
+ mFileContents = mService.getChildren(mRemoteFileEntry, false, null);
+ return true;
+ }
+ }
+
+ /**
+ * A {@link FilenameFilter} that rejects hidden (ie starts with ".") files.
+ */
+ private static class NoHiddenFilesFilter implements FilenameFilter {
+ /**
+ * {@inheritDoc}
+ */
+ public boolean accept(File dir, String name) {
+ return !name.startsWith(".");
+ }
+ }
+
+ /**
+ * Return <code>true</code> if local file is newer than remote file.
+ */
+ private boolean isNewer(File localFile, IFileEntry entry) {
+ // remote times are in GMT timezone
+ final String entryTimeString = String.format("%s %s GMT", entry.getDate(), entry.getTime());
+ try {
+ // expected format of a FileEntry's date and time
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm zzz");
+ Date remoteDate = format.parse(entryTimeString);
+ // localFile.lastModified has granularity of ms, but remoteDate.getTime only has
+ // granularity of minutes. Shift remoteDate.getTime() backward by one minute so newly
+ // modified files get synced
+ return localFile.lastModified() > (remoteDate.getTime() - 60 * 1000);
+ } catch (ParseException e) {
+ Log.e(LOG_TAG, String.format(
+ "Error converting remote time stamp %s for %s on device %s", entryTimeString,
+ entry.getFullPath(), getSerialNumber()));
+ }
+ // sync file by default
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String executeAdbCommand(String... cmdArgs) throws DeviceNotAvailableException {
+ final String[] fullCmd = buildAdbCommand(cmdArgs);
+ final String[] output = new String[1];
+ DeviceAction adbAction = new DeviceAction() {
+ public boolean run() throws TimeoutException, IOException {
+ CommandResult result = getRunUtil().runTimedCmd(getCommandTimeout(), fullCmd);
+ // TODO: how to determine device not present with command failing for other reasons
+ if (result.getStatus() != CommandStatus.SUCCESS) {
+ // interpret this as device offline??
+ throw new IOException();
+ } else if (result.getStatus() == CommandStatus.EXCEPTION) {
+ throw new IOException();
+ } else if (result.getStatus() == CommandStatus.TIMED_OUT) {
+ throw new TimeoutException();
+ }
+ output[0] = result.getStdout();
+ return true;
+ }
+ };
+ performDeviceAction(String.format("adb %s", cmdArgs[0]), adbAction, MAX_RETRY_ATTEMPTS);
+ return output[0];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public CommandResult executeFastbootCommand(String... cmdArgs)
+ throws DeviceNotAvailableException {
+ return doFastbootCommand(getCommandTimeout(), cmdArgs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public CommandResult executeLongFastbootCommand(String... cmdArgs)
+ throws DeviceNotAvailableException {
+ return doFastbootCommand(getLongCommandTimeout(), cmdArgs);
+ }
+
+ /**
+ * @param cmdArgs
+ * @throws DeviceNotAvailableException
+ */
+ private CommandResult doFastbootCommand(final long timeout, String... cmdArgs)
+ throws DeviceNotAvailableException {
+ final String[] fullCmd = buildFastbootCommand(cmdArgs);
+ for (int i = 0; i < MAX_RETRY_ATTEMPTS; i++) {
+ // block state changes while executing a fastboot command, since
+ // device will disappear from fastboot devices while command is being executed
+ try {
+ mFastbootLock.acquire();
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ CommandResult result = getRunUtil().runTimedCmd(timeout, fullCmd);
+ mFastbootLock.release();
+ if (!isRecoveryNeeded(result)) {
+ return result;
+ }
+ recoverDeviceFromBootloader();
+ }
+ throw new DeviceUnresponsiveException(String.format("Attempted fastboot %s multiple "
+ + "times on device %s without communication success. Aborting.", cmdArgs[0],
+ getSerialNumber()));
+ }
+
+ /**
+ * Evaluate the given fastboot result to determine if recovery mode needs to be entered
+ *
+ * @param fastbootResult the {@link CommandResult} from a fastboot command
+ * @return <code>true</code> if recovery mode should be entered, <code>false</code> otherwise.
+ */
+ private boolean isRecoveryNeeded(CommandResult fastbootResult) {
+ if (fastbootResult.getStatus().equals(CommandStatus.TIMED_OUT)) {
+ // fastboot commands always time out if devices is not present
+ return true;
+ } else {
+ // check for specific error messages in result that indicate bad device communication
+ // and recovery mode is needed
+ if (fastbootResult.getStderr().contains("data transfer failure (Protocol error)") ||
+ fastbootResult.getStderr().contains("status read failed (No such device)")) {
+ Log.w(LOG_TAG, String.format(
+ "Bad fastboot response from device %s. stderr: %s. Entering recovery",
+ getSerialNumber(), fastbootResult.getStderr()));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the max time allowed in ms for commands.
+ */
+ int getCommandTimeout() {
+ return mCmdTimeout;
+ }
+
+ /**
+ * Set the max time allowed in ms for commands.
+ */
+ void setLongCommandTimeout(long timeout) {
+ mLongCmdTimeout = timeout;
+ }
+
+ /**
+ * Get the max time allowed in ms for commands.
+ */
+ long getLongCommandTimeout() {
+ return mLongCmdTimeout;
+ }
+
+ /**
+ * Set the max time allowed in ms for commands.
+ */
+ void setCommandTimeout(int timeout) {
+ mCmdTimeout = timeout;
+ }
+
+ /**
+ * Builds the OS command for the given adb command and args
+ */
+ private String[] buildAdbCommand(String... commandArgs) {
+ final int numAdbArgs = 3;
+ String[] newCmdArgs = new String[commandArgs.length + numAdbArgs];
+ // TODO: use full adb path
+ newCmdArgs[0] = "adb";
+ newCmdArgs[1] = "-s";
+ newCmdArgs[2] = getSerialNumber();
+ System.arraycopy(commandArgs, 0, newCmdArgs, numAdbArgs, commandArgs.length);
+ return newCmdArgs;
+ }
+
+ /**
+ * Builds the OS command for the given fastboot command and args
+ */
+ private String[] buildFastbootCommand(String... commandArgs) {
+ final int numAdbArgs = 3;
+ String[] newCmdArgs = new String[commandArgs.length + numAdbArgs];
+ // TODO: use full fastboot path
+ newCmdArgs[0] = "fastboot";
+ newCmdArgs[1] = "-s";
+ newCmdArgs[2] = getSerialNumber();
+ System.arraycopy(commandArgs, 0, newCmdArgs, numAdbArgs, commandArgs.length);
+ return newCmdArgs;
+ }
+
+ /**
+ * Performs an action on this device. Attempts to recover device and retry command
+ * if action fails.
+ *
+ * @param actionDescription a short description of action to be performed. Used for logging
+ * purposes only.
+ * @param action the action to be performed
+ * @param callback optional action to perform if action fails but recovery succeeds. If no post
+ * recovery action needs to be taken pass in <code>null</code>
+ * @param attempts the retry attempts to make for action if it fails but
+ * recovery succeeds
+ * @returns <code>true</code> if action was performed successfully
+ * @throws DeviceNotAvailableException if recovery attempt fails or max attempts done without
+ * success
+ */
+ private boolean performDeviceAction(String actionDescription, final DeviceAction action,
+ int attempts) throws DeviceNotAvailableException {
+
+ for (int i=0; i < attempts; i++) {
+ try {
+ return action.run();
+ } catch (TimeoutException e) {
+ Log.w(LOG_TAG, String.format("'%s' timed out on device %s",
+ actionDescription, getSerialNumber()));
+ } catch (IOException e) {
+ Log.w(LOG_TAG, String.format("Exception when attempting %s on device %s",
+ actionDescription, getSerialNumber()));
+ } catch (InstallException e) {
+ Log.w(LOG_TAG, String.format("InstallException when attempting %s on device %s",
+ actionDescription, getSerialNumber()));
+ } catch (SyncException e) {
+ Log.w(LOG_TAG, String.format("SyncException when attempting %s on device %s",
+ actionDescription, getSerialNumber()));
+ } catch (AdbCommandRejectedException e) {
+ Log.w(LOG_TAG, String.format("AdbCommandRejectedException when attempting %s on device %s",
+ actionDescription, getSerialNumber()));
+ } catch (ShellCommandUnresponsiveException e) {
+ Log.w(LOG_TAG, String.format("Device %s stopped responding when attempting %s",
+ getSerialNumber(), actionDescription));
+ }
+ // TODO: currently treat all exceptions the same. In future consider different recovery
+ // mechanisms for time out's vs IOExceptions
+ recoverDevice();
+ }
+ throw new DeviceUnresponsiveException(String.format("Attempted %s multiple times "
+ + "on device %s without communication success. Aborting.", actionDescription,
+ getSerialNumber()));
+
+ }
+
+ /**
+ * Attempts to recover device communication.
+ * <p/>
+ * Exposed for testing.
+ *
+ * @throws DeviceNotAvailableException if device is not longer available
+ */
+ void recoverDevice() throws DeviceNotAvailableException {
+ Log.i(LOG_TAG, String.format("Attempting recovery on %s", getSerialNumber()));
+ mRecovery.recoverDevice(mMonitor);
+ Log.i(LOG_TAG, String.format("Recovery successful for %s", getSerialNumber()));
+ // this might be a runtime reset - still need to run post boot setup steps
+ postBootSetup();
+ }
+
+ /**
+ * Attempts to recover device fastboot communication.
+ *
+ * @throws DeviceNotAvailableException if device is not longer available
+ */
+ private void recoverDeviceFromBootloader() throws DeviceNotAvailableException {
+ Log.i(LOG_TAG, String.format("Attempting recovery on %s in bootloader", getSerialNumber()));
+ mRecovery.recoverDeviceBootloader(mMonitor);
+ Log.i(LOG_TAG, String.format("Bootloader recovery successful for %s", getSerialNumber()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void startLogcat() {
+ if (mLogcatReceiver != null) {
+ Log.d(LOG_TAG, String.format("Already capturing logcat for %s, ignoring",
+ getSerialNumber()));
+ return;
+ }
+ mLogcatReceiver = new LogCatReceiver();
+ mLogcatReceiver.start();
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p/>
+ * Works in two modes:
+ * <li>If the logcat is currently being captured in the background (i.e. the manager of this
+ * device is calling startLogcat and stopLogcat as appropriate), will return the current
+ * contents of the background logcat capture.
+ * <li>Otherwise, will return a static dump of the logcat data if device is currently responding
+ */
+ public InputStream getLogcat() {
+ if (mLogcatReceiver == null) {
+ Log.w(LOG_TAG, String.format("Not capturing logcat for %s in background, " +
+ "returning a logcat dump", getSerialNumber()));
+ return getLogcatDump();
+ } else {
+ return mLogcatReceiver.getLogcatData();
+ }
+ }
+
+ /**
+ * Get a dump of the current logcat for device.
+ *
+ * @return a {@link InputStream} of the logcat data. An empty stream is returned if fail to
+ * capture logcat data.
+ */
+ private InputStream getLogcatDump() {
+ String output = "";
+ try {
+ // use IDevice directly because we don't want callers to handle
+ // DeviceNotAvailableException for this method
+ CollectingOutputReceiver receiver = new CollectingOutputReceiver();
+ // add -d parameter to make this a non blocking call
+ getIDevice().executeShellCommand(LOGCAT_CMD + " -d", receiver);
+ output = receiver.getOutput();
+ } catch (IOException e) {
+ Log.w(LOG_TAG, String.format("Failed to get logcat dump from %s: ", getSerialNumber(),
+ e.getMessage()));
+ } catch (TimeoutException e) {
+ Log.w(LOG_TAG, String.format("Failed to get logcat dump from %s: timeout",
+ getSerialNumber()));
+ } catch (AdbCommandRejectedException e) {
+ Log.w(LOG_TAG, String.format("Failed to get logcat dump from %s: ", getSerialNumber(),
+ e.getMessage()));
+ } catch (ShellCommandUnresponsiveException e) {
+ Log.w(LOG_TAG, String.format("Failed to get logcat dump from %s: ", getSerialNumber(),
+ e.getMessage()));
+ }
+ return new ByteArrayInputStream(output.getBytes());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void stopLogcat() {
+ if (mLogcatReceiver != null) {
+ synchronized(mLogcatReceiver) {
+ mLogcatReceiver.cancel();
+ mLogcatReceiver = null;
+ }
+ } else {
+ Log.w(LOG_TAG, String.format("Attempting to stop logcat when not capturing for %s",
+ getSerialNumber()));
+ }
+ }
+
+ /**
+ * Factory method to create a {@link LogCatReceiver}.
+ * <p/>
+ * Exposed for unit testing.
+ */
+ LogCatReceiver createLogcatReceiver() {
+ return new LogCatReceiver();
+ }
+
+ /**
+ * A background thread that captures logcat data into a temporary host file.
+ * <p/>
+ * This is done so:
+ * <li>if device goes permanently offline during a test, the log data is retained.
+ * <li>to capture more data than may fit in device's circular log.
+ * <p/>
+ * The maximum size of the tmp file is limited to approximately mMaxLogcatFileSize.
+ * To prevent data loss when the limit has been reached, this file keeps two tmp host
+ * files.
+ */
+ class LogCatReceiver extends Thread implements IShellOutputReceiver {
+
+ private boolean mIsCancelled = false;
+ private OutputStream mOutStream;
+ /** the archived previous tmp file */
+ private File mPreviousTmpFile = null;
+ /** the current temp file which logcat data will be streamed into */
+ private File mTmpFile = null;
+ private long mTmpBytesStored = 0;
+
+ /**
+ * {@inheritDoc}
+ */
+ public synchronized void addOutput(byte[] data, int offset, int length) {
+ if (mOutStream == null) {
+ return;
+ }
+ try {
+ mOutStream.write(data, offset, length);
+ mTmpBytesStored += length;
+ if (mTmpBytesStored > mMaxLogcatFileSize) {
+ Log.i(LOG_TAG, String.format(
+ "Max tmp logcat file size reached for %s, swapping",
+ getSerialNumber()));
+ createTmpFile();
+ mTmpBytesStored = 0;
+ }
+ } catch (IOException e) {
+ Log.w(LOG_TAG, String.format("failed to write logcat data for %s.",
+ getSerialNumber()));
+ }
+ }
+
+ public synchronized InputStream getLogcatData() {
+ if (mTmpFile != null) {
+ flush();
+ try {
+ FileInputStream fileStream = new FileInputStream(mTmpFile);
+ if (mPreviousTmpFile != null) {
+ // return a input stream that first reads from mPreviousTmpFile, then reads
+ // from mTmpFile
+ return new SequenceInputStream(new FileInputStream(mPreviousTmpFile),
+ fileStream);
+ } else {
+ // no previous file, just return mTmpFile's stream
+ return fileStream;
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG,
+ String.format("failed to get logcat data for %s.", getSerialNumber()));
+ Log.e(LOG_TAG, e);
+ }
+ }
+ // return an empty input stream
+ return new ByteArrayInputStream(new byte[0]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public synchronized void flush() {
+ if (mOutStream == null) {
+ return;
+ }
+ try {
+ mOutStream.flush();
+ } catch (IOException e) {
+ Log.w(LOG_TAG, String.format("failed to flush logcat data for %s.",
+ getSerialNumber()));
+ }
+ }
+
+ public synchronized void cancel() {
+ mIsCancelled = true;
+ interrupt();
+ closeLogStream();
+ if (mTmpFile != null) {
+ mTmpFile.delete();
+ mTmpFile = null;
+ }
+ if (mPreviousTmpFile != null) {
+ mPreviousTmpFile.delete();
+ mPreviousTmpFile = null;
+ }
+ }
+
+ /**
+ * Closes the stream to tmp log file
+ */
+ private void closeLogStream() {
+ try {
+ if (mOutStream != null) {
+ mOutStream.flush();
+ mOutStream.close();
+ mOutStream = null;
+ }
+
+ } catch (IOException e) {
+ Log.w(LOG_TAG, String.format("failed to close logcat stream for %s.",
+ getSerialNumber()));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public synchronized boolean isCancelled() {
+ return mIsCancelled;
+ }
+
+ @Override
+ public void run() {
+ try {
+ createTmpFile();
+ } catch (IOException e) {
+ Log.e(LOG_TAG, String.format("failed to create tmp logcat file for %s.",
+ getSerialNumber()));
+ Log.e(LOG_TAG, e);
+ return;
+ }
+
+ // continually run in a loop attempting to grab logcat data, skipping recovery
+ // this is done so logcat data can continue to be captured even if device goes away and
+ // then comes back online
+ while (!isCancelled()) {
+ try {
+ // FIXME: Disgusting hack alert! Sleep for a small amount before starting
+ // logcat, as starting logcat immediately after a device comes online has caused
+ // adb instability
+ if (mLogStartDelay > 0) {
+ Log.d(LOG_TAG, String.format("Sleep for %d before starting logcat for %s.",
+ mLogStartDelay, getSerialNumber()));
+ getRunUtil().sleep(mLogStartDelay);
+ }
+ Log.d(LOG_TAG, String.format("Starting logcat for %s.", getSerialNumber()));
+ getIDevice().executeShellCommand(LOGCAT_CMD, this, 0);
+ } catch (Exception e) {
+ final String msg = String.format("logcat capture interrupted for %s. Waiting"
+ + " for device to be back online. May see duplicate content in log.",
+ getSerialNumber());
+ Log.d(LOG_TAG, msg);
+ appendDeviceLogMsg(msg);
+ // sleep a small amount for device to settle
+ getRunUtil().sleep(5 * 1000);
+ // wait a long time for device to be online
+ mMonitor.waitForDeviceOnline(10 * 60 * 1000);
+ }
+ }
+ }
+
+ /**
+ * Creates a new tmp file, closing the old one as necessary
+ * @throws IOException
+ * @throws FileNotFoundException
+ */
+ private synchronized void createTmpFile() throws IOException, FileNotFoundException {
+ closeLogStream();
+ if (mPreviousTmpFile != null) {
+ mPreviousTmpFile.delete();
+ }
+ mPreviousTmpFile = mTmpFile;
+ mTmpFile = FileUtil.createTempFile(String.format("logcat_%s_", getSerialNumber()),
+ ".txt");
+ Log.i(LOG_TAG, String.format("Created tmp logcat file %s",
+ mTmpFile.getAbsolutePath()));
+ mOutStream = new BufferedOutputStream(new FileOutputStream(mTmpFile),
+ LOGCAT_BUFF_SIZE);
+ }
+
+ /**
+ * Adds a message to the captured device log.
+ *
+ * @param msg
+ */
+ private synchronized void appendDeviceLogMsg(String msg) {
+ if (mOutStream == null) {
+ return;
+ }
+ // add the msg to device tmp log, so readers will know logcat was interrupted
+ try {
+ mOutStream.write("\n*******************\n".getBytes());
+ mOutStream.write(msg.getBytes());
+ mOutStream.write("\n*******************\n".getBytes());
+ } catch (IOException e) {
+ Log.w(LOG_TAG, String.format("failed to write logcat data for %s.",
+ getSerialNumber()));
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk)
+ throws DeviceNotAvailableException {
+ Log.i(LOG_TAG, String.format("Connecting to wifi network %s on %s", wifiSsid,
+ getSerialNumber()));
+ WifiHelper wifi = new WifiHelper(this);
+ wifi.enableWifi();
+ // TODO: return false here if failed?
+ wifi.waitForWifiState(WifiState.SCANNING, WifiState.COMPLETED);
+
+ Integer networkId = null;
+ if (wifiPsk != null) {
+ networkId = wifi.addWpaPskNetwork(wifiSsid, wifiPsk);
+ } else {
+ networkId = wifi.addOpenNetwork(wifiSsid);
+ }
+
+ if (networkId == null) {
+ Log.e(LOG_TAG, String.format("Failed to add wifi network %s on %s", wifiSsid,
+ getSerialNumber()));
+ return false;
+ }
+ if (!wifi.associateNetwork(networkId)) {
+ Log.e(LOG_TAG, String.format("Failed to enable wifi network %s on %s", wifiSsid,
+ getSerialNumber()));
+ return false;
+ }
+ if (!wifi.waitForWifiState(WifiState.COMPLETED)) {
+ Log.e(LOG_TAG, String.format("wifi network %s failed to associate on %s", wifiSsid,
+ getSerialNumber()));
+ return false;
+ }
+ // TODO: make timeout configurable
+ if (!wifi.waitForDhcp(30 * 1000)) {
+ Log.e(LOG_TAG, String.format("dhcp timeout when connecting to wifi network %s on %s",
+ wifiSsid, getSerialNumber()));
+ return false;
+ }
+ // wait for ping success
+ for (int i = 0; i < 10; i++) {
+ String pingOutput = executeShellCommand("ping -c 1 -w 5 www.google.com");
+ if (pingOutput.contains("1 packets transmitted, 1 received")) {
+ return true;
+ }
+ getRunUtil().sleep(1 * 1000);
+ }
+ Log.e(LOG_TAG, String.format("ping unsuccessful after connecting to wifi network %s on %s",
+ wifiSsid, getSerialNumber()));
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean disconnectFromWifi() throws DeviceNotAvailableException {
+ WifiHelper wifi = new WifiHelper(this);
+ wifi.removeAllNetworks();
+ wifi.disableWifi();
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean clearErrorDialogs() throws DeviceNotAvailableException {
+ // attempt to clear error dialogs multiple times
+ for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) {
+ int numErrorDialogs = getErrorDialogCount();
+ if (numErrorDialogs == 0) {
+ return true;
+ }
+ doClearDialogs(numErrorDialogs);
+ }
+ if (getErrorDialogCount() > 0) {
+ // at this point, all attempts to clear error dialogs completely have failed
+ // it might be the case that the process keeps showing new dialogs immediately after
+ // clearing. There's really no workaround, but to dump an error
+ Log.e(LOG_TAG, String.format("error dialogs still exist on %s.", getSerialNumber()));
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Detects the number of crash or ANR dialogs currently displayed.
+ * <p/>
+ * Parses output of 'dump activity processes'
+ *
+ * @return count of dialogs displayed
+ * @throws DeviceNotAvailableException
+ */
+ private int getErrorDialogCount() throws DeviceNotAvailableException {
+ int errorDialogCount = 0;
+ Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*");
+ Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*");
+ String systemStatusOutput = executeShellCommand("dumpsys activity processes");
+ Matcher crashMatcher = crashPattern.matcher(systemStatusOutput);
+ while (crashMatcher.find()) {
+ errorDialogCount++;
+ }
+ Matcher anrMatcher = anrPattern.matcher(systemStatusOutput);
+ while (anrMatcher.find()) {
+ errorDialogCount++;
+ }
+
+ return errorDialogCount;
+ }
+
+ private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException {
+ Log.i(LOG_TAG, String.format("Attempted to clear %d dialogs on %s", numDialogs,
+ getSerialNumber()));
+ for (int i=0; i < numDialogs; i++) {
+ // send DPAD_CENTER
+ executeShellCommand(DISMISS_DIALOG_CMD);
+ }
+ }
+
+ IDeviceStateMonitor getDeviceStateMonitor() {
+ return mMonitor;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void postBootSetup() throws DeviceNotAvailableException {
+ if (mEnableAdbRoot) {
+ enableAdbRoot();
+ }
+ if (mDisableKeyguard) {
+ Log.i(LOG_TAG, String.format("Attempting to disable keyguard on %s using %s",
+ getSerialNumber(), mDisableKeyguardCmd));
+ executeShellCommand(mDisableKeyguardCmd);
+ }
+ }
+
+ /**
+ * Gets the adb shell command to disable the keyguard for this device.
+ * <p/>
+ * Exposed for unit testing.
+ */
+ String getDisableKeyguardCmd() {
+ return mDisableKeyguardCmd;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void rebootIntoBootloader() throws DeviceNotAvailableException {
+ if (TestDeviceState.FASTBOOT == mMonitor.getDeviceState()) {
+ Log.i(LOG_TAG, String.format("device %s already in fastboot. Rebooting anyway",
+ getSerialNumber()));
+ executeFastbootCommand("reboot-bootloader");
+ } else {
+ Log.i(LOG_TAG, String.format("Booting device %s into bootloader", getSerialNumber()));
+ doAdbRebootBootloader();
+ }
+ if (!mMonitor.waitForDeviceBootloader(FASTBOOT_TIMEOUT)) {
+ recoverDeviceFromBootloader();
+ }
+ }
+
+ private void doAdbRebootBootloader() throws DeviceNotAvailableException {
+ try {
+ getIDevice().reboot("bootloader");
+ return;
+ } catch (IOException e) {
+ Log.w(LOG_TAG, String.format("IOException '%s' when rebooting %s into bootloader",
+ e.getMessage(), getSerialNumber()));
+ recoverDeviceFromBootloader();
+ // no need to try multiple times - if recoverDeviceFromBootloader() succeeds device is
+ // successfully in bootloader mode
+
+ } catch (TimeoutException e) {
+ Log.w(LOG_TAG, String.format("TimeoutException when rebooting %s into bootloader",
+ getSerialNumber()));
+ recoverDeviceFromBootloader();
+ // no need to try multiple times - if recoverDeviceFromBootloader() succeeds device is
+ // successfully in bootloader mode
+
+ } catch (AdbCommandRejectedException e) {
+ Log.w(LOG_TAG, String.format(
+ "AdbCommandRejectedException '%s' when rebooting %s into bootloader",
+ e.getMessage(), getSerialNumber()));
+ recoverDeviceFromBootloader();
+ // no need to try multiple times - if recoverDeviceFromBootloader() succeeds device is
+ // successfully in bootloader mode
+
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void reboot() throws DeviceNotAvailableException {
+ doReboot();
+ if (mMonitor.waitForDeviceAvailable() != null) {
+ postBootSetup();
+ return;
+ } else {
+ recoverDevice();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void rebootUntilOnline() throws DeviceNotAvailableException {
+ doReboot();
+ if (mMonitor.waitForDeviceOnline() != null) {
+ if (mEnableAdbRoot) {
+ enableAdbRoot();
+ }
+ return;
+ } else {
+ // TODO: change this into a recoverDeviceUntilOnline type method
+ recoverDevice();
+ }
+ }
+
+ /**
+ * @throws DeviceNotAvailableException
+ */
+ private void doReboot() throws DeviceNotAvailableException {
+ if (TestDeviceState.FASTBOOT == getDeviceState()) {
+ Log.i(LOG_TAG, String.format("device %s in fastboot. Rebooting to userspace.",
+ getSerialNumber()));
+ executeFastbootCommand("reboot");
+ } else {
+ Log.i(LOG_TAG, String.format("Rebooting device %s", getSerialNumber()));
+ doAdbReboot(null);
+ waitForDeviceNotAvailable("reboot", getCommandTimeout());
+ }
+ }
+
+ /**
+ * Perform a adb reboot.
+ *
+ * @param into the bootloader name to reboot into, or <code>null</code> to just reboot the
+ * device.
+ * @throws DeviceNotAvailableException
+ */
+ private void doAdbReboot(final String into) throws DeviceNotAvailableException {
+ DeviceAction rebootAction = new DeviceAction() {
+ public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException {
+ getIDevice().reboot(into);
+ return true;
+ }
+ };
+ performDeviceAction("reboot", rebootAction, MAX_RETRY_ATTEMPTS);
+ }
+
+ private void waitForDeviceNotAvailable(String operationDesc, long time) {
+ // TODO: a bit of a race condition here. Would be better to start a device listener
+ // before the operation
+ if (!mMonitor.waitForDeviceNotAvailable(time)) {
+ // above check is flaky, ignore till better solution is found
+ Log.w(LOG_TAG, String.format("Did not detect device %s becoming unavailable after %s",
+ getSerialNumber(), operationDesc));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean enableAdbRoot() throws DeviceNotAvailableException {
+ Log.i(LOG_TAG, String.format("adb root on device %s", getSerialNumber()));
+
+ String output = executeAdbCommand("root");
+ if (output.contains("adbd is already running as root")) {
+ return true;
+ } else if (output.contains("restarting adbd as root")) {
+ // wait for device to disappear from adb
+ waitForDeviceNotAvailable("root", 30 * 1000);
+ // wait for device to be back online
+ waitForDeviceOnline();
+ return true;
+
+ } else {
+ Log.e(LOG_TAG, String.format("Unrecognized output from adb root: %s", output));
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void waitForDeviceOnline(long waitTime) throws DeviceNotAvailableException {
+ if (mMonitor.waitForDeviceOnline(waitTime) == null) {
+ recoverDevice();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void waitForDeviceOnline() throws DeviceNotAvailableException {
+ if (mMonitor.waitForDeviceOnline() == null) {
+ recoverDevice();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void waitForDeviceAvailable(long waitTime) throws DeviceNotAvailableException {
+ if (mMonitor.waitForDeviceAvailable(waitTime) == null) {
+ recoverDevice();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void waitForDeviceAvailable() throws DeviceNotAvailableException {
+ if (mMonitor.waitForDeviceAvailable() == null) {
+ recoverDevice();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean waitForDeviceNotAvailable(long waitTime) {
+ return mMonitor.waitForDeviceNotAvailable(waitTime);
+ }
+
+ void setEnableAdbRoot(boolean enable) {
+ mEnableAdbRoot = enable;
+ }
+
+ /**
+ * Retrieve this device's recovery mechanism.
+ * <p/>
+ * Exposed for unit testing.
+ */
+ IDeviceRecovery getRecovery() {
+ return mRecovery;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setRecovery(IDeviceRecovery recovery) {
+ mRecovery = recovery;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setDeviceState(final TestDeviceState deviceState) {
+ if (!deviceState.equals(getDeviceState())) {
+ // disable state changes while fastboot lock is held, because issuing fastboot command
+ // will disrupt state
+ if (getDeviceState().equals(TestDeviceState.FASTBOOT) && !mFastbootLock.tryAcquire()) {
+ return;
+ }
+ Log.d(LOG_TAG, String.format("Device %s state is now %s", getSerialNumber(),
+ deviceState));
+ mState = deviceState;
+ mFastbootLock.release();
+ mMonitor.setState(deviceState);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public TestDeviceState getDeviceState() {
+ return mState;
+ }
+}