blob: e84d932f850760c0e24858335df892cbaf6e9b66 [file] [log] [blame]
/*
* 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.SyncException.SyncError;
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.ByteArrayInputStreamSource;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.StubTestListener;
import com.android.tradefed.result.SnapshotInputStreamSource;
import com.android.tradefed.util.ArrayUtil;
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.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 = 2;
/** 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";
private static final String BUGREPORT_CMD = "bugreport";
/**
* Allow pauses of up to 2 minutes while receiving bugreport. Note that dumpsys may pause up to
* a minute while waiting for unresponsive components, but should bail after that minute, if it
* will ever terminate on its own.
*/
private static final int BUGREPORT_TIMEOUT = 2 * 60 * 1000;
/** 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;
/** The time in ms to wait for a device to boot into recovery. */
private static final int ADB_RECOVERY_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";
private static final String BUILD_ID_PROP = "ro.build.version.incremental";
/** 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;
private boolean mFastbootEnabled = true;
// 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);
}
/**
* {@inheritDoc}
*/
public int getBuildId() {
String stringBuild = getIDevice().getProperty(BUILD_ID_PROP);
try {
int currentBuildId = Integer.parseInt(stringBuild);
return currentBuildId;
} catch (NumberFormatException e) {
Log.w(LOG_TAG, String.format("Could not get device %s build id. Received %s",
getSerialNumber(), stringBuild));
}
return -1;
}
/**
* {@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.i(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.i(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, UnsupportedOperationException {
// TODO: extract this into a common getFastbootVariable() method
CommandResult result = executeFastbootCommand("getvar", "product");
if (result.getStatus() == CommandStatus.SUCCESS) {
Pattern fastbootProductPattern = Pattern.compile("product:\\s(.*)\\s");
// 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 boolean runInstrumentationTests(final IRemoteAndroidTestRunner runner,
final Collection<ITestRunListener> listeners) throws DeviceNotAvailableException {
RunFailureListener failureListener = new RunFailureListener();
listeners.add(failureListener);
DeviceAction runTestsAction = new DeviceAction() {
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
runner.run(listeners);
return true;
}
};
boolean result = performDeviceAction(String.format("run %s instrumentation tests",
runner.getPackageName()), runTestsAction, 0);
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();
}
}
return result;
}
private static class RunFailureListener extends StubTestListener {
private boolean mIsRunFailure = false;
@Override
public void testRunFailed(String message) {
mIsRunFailure = true;
}
}
/**
* {@inheritDoc}
*/
public boolean runInstrumentationTests(IRemoteAndroidTestRunner runner,
ITestRunListener... listeners) throws DeviceNotAvailableException {
List<ITestRunListener> listenerList = new ArrayList<ITestRunListener>();
listenerList.addAll(Arrays.asList(listeners));
return 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);
}
/**
* {@inheridDoc}
*/
public File pullFile(String remoteFilePath) throws DeviceNotAvailableException {
try {
File localFile = FileUtil.createTempFileForRemote(remoteFilePath, null);
if (pullFile(remoteFilePath, localFile)) {
return localFile;
}
} catch (IOException e) {
Log.w(LOG_TAG, String.format("Encountered IOException while trying to pull '%s': %s",
remoteFilePath, e));
}
return null;
}
/**
* {@inheridDoc}
*/
public File pullFileFromExternal(String remoteFilePath) throws DeviceNotAvailableException {
String externalPath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
String fullPath = (new File(externalPath, remoteFilePath)).getPath();
return pullFile(fullPath);
}
/**
* {@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, UnsupportedOperationException {
return doFastbootCommand(getCommandTimeout(), cmdArgs);
}
/**
* {@inheritDoc}
*/
public CommandResult executeLongFastbootCommand(String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
return doFastbootCommand(getLongCommandTimeout(), cmdArgs);
}
/**
* @param cmdArgs
* @throws DeviceNotAvailableException
*/
private CommandResult doFastbootCommand(final long timeout, String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
if (!mFastbootEnabled) {
throw new UnsupportedOperationException(String.format(
"Attempted to fastboot on device %s , but fastboot is not available. Aborting.",
getSerialNumber()));
}
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) {
return ArrayUtil.buildArray(commandArgs, "adb", "-s", getSerialNumber());
}
/**
* Builds the OS command for the given fastboot command and args
*/
private String[] buildFastbootCommand(String... commandArgs) {
return ArrayUtil.buildArray(commandArgs, "fastboot", "-s", getSerialNumber());
}
/**
* Performs an action on this device. Attempts to recover device and optionally 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 retryAttempts 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 retryAttempts) throws DeviceNotAvailableException {
for (int i=0; i < retryAttempts +1; 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()));
// a SyncException is not necessarily a device communication problem
// do additional diagnosis
if (!e.getErrorCode().equals(SyncError.BUFFER_OVERRUN) &&
!e.getErrorCode().equals(SyncError.TRANSFER_PROTOCOL_ERROR)) {
// this is a logic problem, doesn't need recovery or to be retried
return false;
}
} 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();
}
if (retryAttempts > 0) {
throw new DeviceUnresponsiveException(String.format("Attempted %s multiple times "
+ "on device %s without communication success. Aborting.", actionDescription,
getSerialNumber()));
}
return false;
}
/**
* Attempts to recover device communication.
*
* @throws DeviceNotAvailableException if device is not longer available
*/
public 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 InputStreamSource 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 InputStreamSource getLogcatDump() {
byte[] output = new byte[0];
try {
// use IDevice directly because we don't want callers to handle
// DeviceNotAvailableException for this method
CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
// 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 ByteArrayInputStreamSource(output);
}
/**
* {@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();
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getBugreport() {
CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
try {
executeShellCommand(BUGREPORT_CMD, receiver, BUGREPORT_TIMEOUT, 0 /* don't retry */);
} catch (DeviceNotAvailableException e) {
// Log, but don't throw, so the caller can get the bugreport contents even if the device
// goes away
Log.e(LOG_TAG, String.format("Device %s became unresponsive while retrieving bugreport",
getSerialNumber()));
}
return new ByteArrayInputStreamSource(receiver.getOutput());
}
/**
* 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 InputStreamSource getLogcatData() {
if (mTmpFile != null) {
flush();
try {
FileInputStream fileStream = new FileInputStream(mTmpFile);
if (mPreviousTmpFile != null) {
// return an input stream that first reads from mPreviousTmpFile, then reads
// from mTmpFile
InputStream stream = new SequenceInputStream(
new FileInputStream(mPreviousTmpFile), fileStream);
return new SnapshotInputStreamSource(stream);
} else {
// no previous file, just return a wrapper around mTmpFile's stream
return new SnapshotInputStreamSource(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 InputStreamSource
return new ByteArrayInputStreamSource(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);
// add an initial message to log, to give info to viewer
if (mPreviousTmpFile == null) {
// first log!
appendDeviceLogMsg(String.format("Logcat for device %s running system build %d",
getSerialNumber(), getBuildId()));
} else {
appendDeviceLogMsg(String.format(
"Continuing logcat capture for device %s running system build %d. " +
"Previous content may have been truncated.", getSerialNumber(),
getBuildId()));
}
}
/**
* 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()));
}
}
/**
* Override finalize to determine when a LogcatReceiver is being destroyed without having
* been cleaned up. This is temporary as we try to hunt down file leaks.
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
String name = String.format("%s[%s]", LOG_TAG, getSerialNumber());
Log.d(LOG_TAG, String.format("%s in finalizer", name));
if (!mIsCancelled) {
Log.e(LOG_TAG, String.format("%s was not cancelled!", name));
cancel();
}
if (mIsCancelled && (mTmpFile != null || mPreviousTmpFile != null)) {
Log.e(LOG_TAG, String.format("%s was cancelled, but tmpfile(%s) or " +
"prevtmpfile(%s) wasn't closed!", mTmpFile, mPreviousTmpFile));
if (mTmpFile != null) {
mTmpFile.delete();
mTmpFile = null;
}
if (mPreviousTmpFile != null) {
mPreviousTmpFile.delete();
mPreviousTmpFile = null;
}
}
}
}
/**
* {@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()));
IWifiHelper wifi = createWifiHelper();
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.waitForIp(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 {
IWifiHelper wifi = createWifiHelper();
wifi.removeAllNetworks();
wifi.disableWifi();
return true;
}
/**
* {@inheritDoc}
*/
@Override
public String getIpAddress() throws DeviceNotAvailableException {
IWifiHelper wifi = createWifiHelper();
return wifi.getIpAddress(null);
}
/**
* Create a {@link WifiHelper} to use
* <p/>
* Exposed so unit tests can mock
*/
IWifiHelper createWifiHelper() {
return new WifiHelper(this);
}
/**
* {@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, UnsupportedOperationException {
if (!mFastbootEnabled) {
throw new UnsupportedOperationException(
"Fastboot is not available and cannot reboot into bootloader");
}
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();
}
}
/**
* {@inheritDoc}
*/
public void rebootIntoRecovery() throws DeviceNotAvailableException {
if (TestDeviceState.FASTBOOT == getDeviceState()) {
Log.w(LOG_TAG, String.format(
"device %s in fastboot when requesting boot to recovery. " +
"Rebooting to userspace first.", getSerialNumber()));
rebootUntilOnline();
}
doAdbReboot("recovery");
if (!waitForDeviceInRecovery(ADB_RECOVERY_TIMEOUT)) {
// TODO: add a recoverDeviceInRecovery() type method
throw new DeviceNotAvailableException();
}
}
/**
* @throws DeviceNotAvailableException
*/
private void doReboot() throws DeviceNotAvailableException, UnsupportedOperationException {
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);
}
/**
* {@inheritDoc}
*/
public boolean waitForDeviceInRecovery(long waitTime) {
return mMonitor.waitForDeviceInRecovery(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 setFastbootEnabled(boolean fastbootEnabled) {
mFastbootEnabled = fastbootEnabled;
}
/**
* {@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;
}
@Override
public boolean isAdbTcp() {
return mMonitor.isAdbTcp();
}
/**
* {@inheritDoc}
*/
@Override
public String switchToAdbTcp() throws DeviceNotAvailableException {
String ipAddress = getIpAddress();
if (ipAddress == null) {
Log.e(LOG_TAG, String.format("connectToTcp failed: Device %s doesn't have an IP",
getSerialNumber()));
return null;
}
String port = "5555";
executeAdbCommand("tcpip", port);
// TODO: analyze result? wait for device offline?
return String.format("%s:%s", ipAddress, port);
}
/**
* {@inheritDoc}
*/
@Override
public boolean switchToAdbUsb() throws DeviceNotAvailableException {
executeAdbCommand("usb");
// TODO: analyze result? wait for device offline?
return true;
}
}