blob: e408ba73ffe79526ac8b74d4e7617e9ea7a1a32d [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.CollectingOutputReceiver;
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.NullOutputReceiver;
import com.android.ddmlib.RawImage;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.SyncException;
import com.android.ddmlib.SyncException.SyncError;
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.build.IBuildInfo;
import com.android.tradefed.device.WifiHelper.WifiState;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.StubTestRunListener;
import com.android.tradefed.targetprep.TargetSetupError;
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 com.android.tradefed.util.StreamUtil;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
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.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
/**
* Default implementation of a {@link ITestDevice}
*/
class TestDevice implements IManagedTestDevice {
/** the default number of command retry attempts to perform */
static final int MAX_RETRY_ATTEMPTS = 2;
private static final String BUGREPORT_CMD = "bugreport";
static final String LIST_PACKAGES_CMD = "pm list packages -f";
private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)");
/**
* 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 password for encrypting and decrypting the device. */
private static final String ENCRYPTION_PASSWORD = "android";
/** Encrypting with inplace can take up to 2 hours. */
private static final int ENCRYPTION_INPLACE_TIMEOUT = 2 * 60 * 60 * 1000;
/** Encrypting with wipe can take up to 5 minutes. */
private static final int ENCRYPTION_WIPE_TIMEOUT = 5 * 60 * 1000;
/** Beginning of the string returned by vdc for "vdc cryptfs enablecrypto". */
private static final String ENCRYPTION_SUPPORTED_CODE = "500";
/** Message in the string returned by vdc for "vdc cryptfs enablecrypto". */
private static final String ENCRYPTION_SUPPORTED_USAGE = "Usage: ";
/** 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 become unavailable. Should usually be short */
private static final int DEFAULT_UNAVAILABLE_TIMEOUT = 20 * 1000;
/** The time in ms to wait for a recovery that we skip because of the NONE mode */
static final int NONE_RECOVERY_MODE_DELAY = 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 = new WaitDeviceRecovery();
private final IDeviceStateMonitor mMonitor;
private TestDeviceState mState = TestDeviceState.ONLINE;
private final ReentrantLock mFastbootLock = new ReentrantLock();
private LogcatReceiver mLogcatReceiver;
private IFileEntry mRootFile = null;
private boolean mFastbootEnabled = true;
private TestDeviceOptions mOptions = new TestDeviceOptions();
private Process mEmulatorProcess;
private RecoveryMode mRecoveryMode = RecoveryMode.AVAILABLE;
private Boolean mIsEncryptionSupported = null;
/**
* 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;
}
/**
* A {@link DeviceAction} for running a OS 'adb ....' command.
*/
private class AdbAction implements DeviceAction {
/** the output from the command */
String mOutput = null;
private String[] mCmd;
AdbAction(String[] cmd) {
mCmd = cmd;
}
@Override
public boolean run() throws TimeoutException, IOException {
CommandResult result = getRunUtil().runTimedCmd(getCommandTimeout(), mCmd);
// TODO: how to determine device not present with command failing for other reasons
if (result.getStatus() == CommandStatus.EXCEPTION) {
throw new IOException();
} else if (result.getStatus() == CommandStatus.TIMED_OUT) {
throw new TimeoutException();
} else if (result.getStatus() == CommandStatus.FAILED) {
// interpret as communication failure
throw new IOException();
}
mOutput = result.getStdout();
return true;
}
}
/**
* Creates a {@link TestDevice}.
*
* @param device the associated {@link IDevice}
* @param monitor the {@link IDeviceStateMonitor} mechanism to use
*/
TestDevice(IDevice device, IDeviceStateMonitor monitor) {
throwIfNull(device);
throwIfNull(monitor);
mIDevice = device;
mMonitor = monitor;
}
/**
* Get the {@link RunUtil} instance to use.
* <p/>
* Exposed for unit testing.
*/
IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
/**
* {@inheritDoc}
*/
@Override
public void setOptions(TestDeviceOptions options) {
throwIfNull(options);
mOptions = options;
mMonitor.setDefaultOnlineTimeout(options.getOnlineTimeout());
mMonitor.setDefaultAvailableTimeout(options.getAvailableTimeout());
}
/**
* Sets the max size of a tmp logcat file.
*
* @param size max byte size of tmp file
*/
void setTmpLogcatSize(long size) {
mOptions.setMaxLogcatFileSize(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}
*/
@Override
public IDevice getIDevice() {
synchronized (mIDevice) {
return mIDevice;
}
}
/**
* {@inheritDoc}
*/
@Override
public void setIDevice(IDevice newDevice) {
throwIfNull(newDevice);
IDevice currentDevice = mIDevice;
if (!getIDevice().equals(newDevice)) {
synchronized (currentDevice) {
mIDevice = newDevice;
}
mMonitor.setIDevice(mIDevice);
}
}
/**
* {@inheritDoc}
*/
@Override
public String getSerialNumber() {
return getIDevice().getSerialNumber();
}
private boolean nullOrEmpty(String string) {
return string == null || string.isEmpty();
}
/**
* Fetch a device property, from the ddmlib cache by default, and falling back to either
* `adb shell getprop` or `fastboot getvar` depending on whether the device is in Fastboot or
* not.
*
* @param prop The name of the device property as returned by `adb shell getprop`
* @param fastbootVar The name of the equivalent fastboot variable to query. if {@code null},
* fastboot query will not be attempted
* @param description A simple description of the variable. First letter should be capitalized.
* @return A string, possibly {@code null} or empty, containing the value of the given property
*/
private String internalGetProperty(String prop, String fastbootVar, String description)
throws DeviceNotAvailableException, UnsupportedOperationException {
if (getIDevice().arePropertiesSet()) {
return getIDevice().getProperty(prop);
} else if (TestDeviceState.FASTBOOT.equals(getDeviceState()) &&
fastbootVar != null) {
CLog.i("%s for device %s is null, re-querying in fastboot", description,
getSerialNumber());
return getFastbootVariable(fastbootVar);
} else {
CLog.d("property collection for device %s is null, re-querying for prop %s",
getSerialNumber(), description);
return getProperty(prop);
}
}
/**
* {@inheritDoc}
*/
@Override
public String getProperty(final String name) throws DeviceNotAvailableException {
final String[] result = new String[1];
DeviceAction propAction = new DeviceAction() {
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
result[0] = getIDevice().getPropertyCacheOrSync(name);
return true;
}
};
performDeviceAction("getprop", propAction, MAX_RETRY_ATTEMPTS);
return result[0];
}
/**
* {@inheritDoc}
*/
@Override
public String getPropertySync(final String name) throws DeviceNotAvailableException {
final String[] result = new String[1];
DeviceAction propAction = new DeviceAction() {
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
result[0] = getIDevice().getPropertySync(name);
return true;
}
};
performDeviceAction("getprop", propAction, MAX_RETRY_ATTEMPTS);
return result[0];
}
/**
* {@inheritDoc}
*/
@Override
public String getBootloaderVersion() throws UnsupportedOperationException,
DeviceNotAvailableException {
return internalGetProperty("ro.bootloader", "version-bootloader", "Bootloader");
}
/**
* {@inheritDoc}
*/
@Override
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 = internalGetProperty("ro.hardware", "product", "Product type");
// 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 (nullOrEmpty(productType)) {
if (retryAttempts > 0) {
recoverDevice();
productType = internalGetProductType(retryAttempts - 1);
}
if (nullOrEmpty(productType)) {
throw new DeviceNotAvailableException(String.format(
"Could not determine product type for device %s.", getSerialNumber()));
}
}
return productType;
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootProductType()
throws DeviceNotAvailableException, UnsupportedOperationException {
return getFastbootVariable("product");
}
/**
* {@inheritDoc}
*/
@Override
public String getProductVariant() throws DeviceNotAvailableException {
return internalGetProperty("ro.product.device", "variant", "Product variant");
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootProductVariant()
throws DeviceNotAvailableException, UnsupportedOperationException {
return getFastbootVariable("variant");
}
private String getFastbootVariable(String variableName)
throws DeviceNotAvailableException, UnsupportedOperationException {
CommandResult result = executeFastbootCommand("getvar", variableName);
if (result.getStatus() == CommandStatus.SUCCESS) {
Pattern fastbootProductPattern = Pattern.compile(variableName + ":\\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}
*/
@Override
public String getBuildId() {
String bid = getIDevice().getProperty(BUILD_ID_PROP);
if (bid == null) {
CLog.w("Could not get device %s build id.", getSerialNumber());
return IBuildInfo.UNKNOWN_BUILD_ID;
}
return bid;
}
/**
* {@inheritDoc}
*/
@Override
public void executeShellCommand(final String command, final IShellOutputReceiver receiver)
throws DeviceNotAvailableException {
DeviceAction action = new DeviceAction() {
@Override
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() {
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
ShellCommandUnresponsiveException {
getIDevice().executeShellCommand(command, receiver, maxTimeToOutputShellResponse);
return true;
}
};
performDeviceAction(String.format("shell %s", command), action, retryAttempts);
}
/**
* {@inheritDoc}
*/
@Override
public String executeShellCommand(String command) throws DeviceNotAvailableException {
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
executeShellCommand(command, receiver);
String output = receiver.getOutput();
CLog.v("%s on %s returned %s", command, getSerialNumber(), output);
return output;
}
/**
* {@inheritDoc}
*/
@Override
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.isRunFailure()) {
// 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 StubTestRunListener {
private boolean mIsRunFailure = false;
@Override
public void testRunFailed(String message) {
mIsRunFailure = true;
}
public boolean isRunFailure() {
return mIsRunFailure;
}
}
/**
* {@inheritDoc}
*/
@Override
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}
*/
@Override
public String installPackage(final File packageFile, final boolean reinstall,
final String... extraArgs) throws DeviceNotAvailableException {
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction installAction = new DeviceAction() {
@Override
public boolean run() throws InstallException {
String result = getIDevice().installPackage(packageFile.getAbsolutePath(),
reinstall, extraArgs);
response[0] = result;
return result == null;
}
};
performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
installAction, MAX_RETRY_ATTEMPTS);
return response[0];
}
/**
* {@inheritDoc}
*/
public String installPackage(final File packageFile, final File certFile,
final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction installAction = new DeviceAction() {
@Override
public boolean run() throws InstallException, SyncException, IOException,
TimeoutException, AdbCommandRejectedException {
// TODO: create a getIDevice().installPackage(File, File...) method when the dist
// cert functionality is ready to be open sourced
String remotePackagePath = getIDevice().syncPackageToDevice(
packageFile.getAbsolutePath());
String remoteCertPath = getIDevice().syncPackageToDevice(
certFile.getAbsolutePath());
// trick installRemotePackage into issuing a 'pm install <apk> <cert>' command,
// by adding apk path to extraArgs, and using cert as the 'apk file'
String[] newExtraArgs = new String[extraArgs.length + 1];
System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
newExtraArgs[newExtraArgs.length - 1] = String.format("\"%s\"", remotePackagePath);
try {
response[0] = getIDevice().installRemotePackage(remoteCertPath, reinstall,
newExtraArgs);
} finally {
getIDevice().removeRemotePackage(remotePackagePath);
getIDevice().removeRemotePackage(remoteCertPath);
}
return true;
}
};
performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
installAction, MAX_RETRY_ATTEMPTS);
return response[0];
}
/**
* {@inheritDoc}
*/
@Override
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() {
@Override
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}
*/
@Override
public boolean pullFile(final String remoteFilePath, final File localFile)
throws DeviceNotAvailableException {
DeviceAction pullAction = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
SyncException {
SyncService syncService = null;
boolean status = false;
try {
syncService = getIDevice().getSyncService();
syncService.pullFile(interpolatePathVariables(remoteFilePath),
localFile.getAbsolutePath(), SyncService.getNullProgressMonitor());
status = true;
} catch (SyncException e) {
CLog.w("Failed to pull %s from %s to %s. Message %s", remoteFilePath,
getSerialNumber(), localFile.getAbsolutePath(), e.getMessage());
throw e;
} finally {
if (syncService != null) {
syncService.close();
}
}
return status;
}
};
return performDeviceAction(String.format("pull %s to %s", remoteFilePath,
localFile.getAbsolutePath()), pullAction, MAX_RETRY_ATTEMPTS);
}
/**
* {@inheridDoc}
*/
@Override
public File pullFile(String remoteFilePath) throws DeviceNotAvailableException {
try {
File localFile = FileUtil.createTempFileForRemote(remoteFilePath, null);
if (pullFile(remoteFilePath, localFile)) {
return localFile;
}
} catch (IOException e) {
CLog.w("Encountered IOException while trying to pull '%s': %s", remoteFilePath, e);
}
return null;
}
/**
* {@inheridDoc}
*/
@Override
public File pullFileFromExternal(String remoteFilePath) throws DeviceNotAvailableException {
String externalPath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
String fullPath = (new File(externalPath, remoteFilePath)).getPath();
return pullFile(fullPath);
}
/**
* Helper function that watches for the string "${EXTERNAL_STORAGE}" and replaces it with the
* pathname of the EXTERNAL_STORAGE mountpoint. Specifically intended to be used for pathnames
* that are being passed to SyncService, which does not support variables inside of filenames.
*/
String interpolatePathVariables(String path) {
final String esString = "${EXTERNAL_STORAGE}";
if (path.contains(esString)) {
final String esPath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
path = path.replace(esString, esPath);
}
return path;
}
/**
* {@inheritDoc}
*/
@Override
public boolean pushFile(final File localFile, final String remoteFilePath)
throws DeviceNotAvailableException {
DeviceAction pushAction = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
SyncException {
SyncService syncService = null;
boolean status = false;
try {
syncService = getIDevice().getSyncService();
syncService.pushFile(localFile.getAbsolutePath(),
interpolatePathVariables(remoteFilePath),
SyncService.getNullProgressMonitor());
status = true;
} catch (SyncException e) {
CLog.w("Failed to push %s to %s on device %s. Message %s",
localFile.getAbsolutePath(), remoteFilePath, getSerialNumber(),
e.getMessage());
throw e;
} finally {
if (syncService != null) {
syncService.close();
}
}
return status;
}
};
return performDeviceAction(String.format("push %s to %s", localFile.getAbsolutePath(),
remoteFilePath), pushAction, MAX_RETRY_ATTEMPTS);
}
/**
* {@inheritDoc}
*/
@Override
public boolean pushString(final String contents, final String remoteFilePath)
throws DeviceNotAvailableException {
File tmpFile = null;
try {
tmpFile = FileUtil.createTempFile("temp", ".txt");
FileUtil.writeToFile(contents, tmpFile);
return pushFile(tmpFile, remoteFilePath);
} catch (IOException e) {
CLog.e(e);
return false;
} finally {
if (tmpFile != null) {
tmpFile.delete();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean doesFileExist(String destPath) throws DeviceNotAvailableException {
String lsGrep = executeShellCommand(String.format("ls \"%s\"", destPath));
return !lsGrep.contains("No such file or directory");
}
/**
* {@inheritDoc}
*/
@Override
public long getExternalStoreFreeSpace() throws DeviceNotAvailableException {
CLog.i("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;
}
CLog.e("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}
*/
@Override
public String getMountPoint(String mountName) {
return mMonitor.getMountPoint(mountName);
}
/**
* {@inheritDoc}
*/
@Override
public List<MountPointInfo> getMountPointInfo() throws DeviceNotAvailableException {
final String mountInfo = executeShellCommand("cat /proc/mounts");
final String[] mountInfoLines = mountInfo.split("\r\n");
List<MountPointInfo> list = new ArrayList<MountPointInfo>(mountInfoLines.length);
for (String line : mountInfoLines) {
// We ignore the last two fields
// /dev/block/mtdblock4 /cache yaffs2 rw,nosuid,nodev,relatime 0 0
final String[] parts = line.split("\\s+", 5);
list.add(new MountPointInfo(parts[0], parts[1], parts[2], parts[3]));
}
return list;
}
/**
* {@inheritDoc}
*/
@Override
public MountPointInfo getMountPointInfo(String mountpoint) throws DeviceNotAvailableException {
// The overhead of parsing all of the lines should be minimal
List<MountPointInfo> mountpoints = getMountPointInfo();
for (MountPointInfo info : mountpoints) {
if (mountpoint.equals(info.mountpoint)) return info;
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public IFileEntry getFileEntry(String path) throws DeviceNotAvailableException {
String[] pathComponents = path.split(FileListingService.FILE_SEPARATOR);
if (mRootFile == null) {
FileListingService service = getFileListingService();
mRootFile = new FileEntryWrapper(this, service.getRoot());
}
return FileEntryWrapper.getDescendant(mRootFile, Arrays.asList(pathComponents));
}
/**
* Retrieve the {@link FileListingService} for the {@link IDevice}, making multiple attempts
* and recovery operations if necessary.
* <p/>
* This is necessary because {@link IDevice#getFileListingService()} can return
* <code>null</code> if device is in fastboot. The symptom of this condition is that the
* current {@link #getIDevice()} is a {@link StubDevice}.
*
* @return the {@link FileListingService}
* @throws DeviceNotAvailableException if device communication is lost.
*/
private FileListingService getFileListingService() throws DeviceNotAvailableException {
final FileListingService[] service = new FileListingService[1];
DeviceAction serviceAction = new DeviceAction() {
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
service[0] = getIDevice().getFileListingService();
if (service[0] == null) {
// could not get file listing service - must be a stub device - enter recovery
throw new IOException("Could not get file listing service");
}
return true;
}
};
performDeviceAction("getFileListingService", serviceAction, MAX_RETRY_ATTEMPTS);
return service[0];
}
/**
* {@inheritDoc}
*/
@Override
public boolean pushDir(File localFileDir, String deviceFilePath)
throws DeviceNotAvailableException {
if (!localFileDir.isDirectory()) {
CLog.e("file %s is not a directory", localFileDir.getAbsolutePath());
return false;
}
File[] childFiles = localFileDir.listFiles();
if (childFiles == null) {
CLog.e("Could not read files in %s", localFileDir.getAbsolutePath());
return false;
}
for (File childFile : childFiles) {
String remotePath = String.format("%s/%s", deviceFilePath, childFile.getName());
if (childFile.isDirectory()) {
executeShellCommand(String.format("mkdir %s", remotePath));
if (!pushDir(childFile, remotePath)) {
return false;
}
} else if (childFile.isFile()) {
if (!pushFile(childFile, remotePath)) {
return false;
}
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean syncFiles(File localFileDir, String deviceFilePath)
throws DeviceNotAvailableException {
CLog.i("Syncing %s to %s on device %s",
localFileDir.getAbsolutePath(), deviceFilePath, getSerialNumber());
if (!localFileDir.isDirectory()) {
CLog.e("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", interpolatePathVariables(deviceFilePath),
localFileDir.getName());
if (!doesFileExist(deviceFilePath)) {
executeShellCommand(String.format("mkdir %s", deviceFilePath));
}
IFileEntry remoteFileEntry = getFileEntry(deviceFilePath);
if (remoteFileEntry == null) {
CLog.e("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 {
CLog.d("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) {
CLog.d("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)) {
CLog.d("Detected newer file %s", localFile.getAbsolutePath());
filePathsToSync.add(localFile.getAbsolutePath());
}
}
if (filePathsToSync.size() == 0) {
CLog.d("No files to sync");
return true;
}
final String files[] = filePathsToSync.toArray(new String[filePathsToSync.size()]);
DeviceAction syncAction = new DeviceAction() {
@Override
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) {
CLog.w("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 final FileEntry mRemoteFileEntry;
private final FileListingService mService;
FileQueryAction(FileEntry remoteFileEntry, FileListingService service) {
throwIfNull(remoteFileEntry);
throwIfNull(service);
mRemoteFileEntry = remoteFileEntry;
mService = service;
}
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
ShellCommandUnresponsiveException {
mFileContents = mService.getChildrenSync(mRemoteFileEntry);
return true;
}
}
/**
* A {@link FilenameFilter} that rejects hidden (ie starts with ".") files.
*/
private static class NoHiddenFilesFilter implements FilenameFilter {
/**
* {@inheritDoc}
*/
@Override
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) {
CLog.e("Error converting remote time stamp %s for %s on device %s", entryTimeString,
entry.getFullPath(), getSerialNumber());
}
// sync file by default
return true;
}
/**
* {@inheritDoc}
*/
@Override
public String executeAdbCommand(String... cmdArgs) throws DeviceNotAvailableException {
final String[] fullCmd = buildAdbCommand(cmdArgs);
AdbAction adbAction = new AdbAction(fullCmd);
performDeviceAction(String.format("adb %s", cmdArgs[0]), adbAction, MAX_RETRY_ATTEMPTS);
return adbAction.mOutput;
}
/**
* {@inheritDoc}
*/
@Override
public CommandResult executeFastbootCommand(String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
return doFastbootCommand(getCommandTimeout(), cmdArgs);
}
/**
* {@inheritDoc}
*/
@Override
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++) {
CommandResult result = new CommandResult(CommandStatus.EXCEPTION);
// block state changes while executing a fastboot command, since
// device will disappear from fastboot devices while command is being executed
mFastbootLock.lock();
try {
result = getRunUtil().runTimedCmd(timeout, fullCmd);
} finally {
mFastbootLock.unlock();
}
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()));
}
/**
* {@inheritDoc}
*/
@Override
public boolean getUseFastbootErase() {
return mOptions.getUseFastbootErase();
}
/**
* {@inheritDoc}
*/
@Override
public void setUseFastbootErase(boolean useFastbootErase) {
mOptions.setUseFastbootErase(useFastbootErase);
}
/**
* {@inheritDoc}
*/
@Override
public CommandResult fastbootWipePartition(String partition)
throws DeviceNotAvailableException {
if (mOptions.getUseFastbootErase()) {
return executeLongFastbootCommand("erase", partition);
} else {
return executeLongFastbootCommand("format", partition);
}
}
/**
* 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() == null ||
fastbootResult.getStderr().contains("data transfer failure (Protocol error)") ||
fastbootResult.getStderr().contains("status read failed (No such device)")) {
CLog.w("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(new String[] {"adb", "-s", getSerialNumber()},
commandArgs);
}
/**
* Builds the OS command for the given fastboot command and args
*/
private String[] buildFastbootCommand(String... commandArgs) {
return ArrayUtil.buildArray(new String[] {"fastboot", "-s", getSerialNumber()},
commandArgs);
}
/**
* 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) {
logDeviceActionException(actionDescription, e);
} catch (IOException e) {
logDeviceActionException(actionDescription, e);
} catch (InstallException e) {
logDeviceActionException(actionDescription, e);
} catch (SyncException e) {
logDeviceActionException(actionDescription, e);
// 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) {
logDeviceActionException(actionDescription, e);
} catch (ShellCommandUnresponsiveException e) {
CLog.w("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;
}
/**
* Log an entry for given exception
*
* @param actionDescription the action's description
* @param e the exception
*/
private void logDeviceActionException(String actionDescription, Exception e) {
CLog.w("%s (%s) when attempting %s on device %s", e.getClass().getSimpleName(),
getExceptionMessage(e), actionDescription, getSerialNumber());
}
/**
* Make a best effort attempt to retrieve a meaningful short descriptive message for given
* {@link Exception}
*
* @param e the {@link Exception}
* @return a short message
*/
private String getExceptionMessage(Exception e) {
StringBuilder msgBuilder = new StringBuilder();
if (e.getMessage() != null) {
msgBuilder.append(e.getMessage());
}
if (e.getCause() != null) {
msgBuilder.append(" cause: ");
msgBuilder.append(e.getCause().getClass().getSimpleName());
if (e.getCause().getMessage() != null) {
msgBuilder.append(" (");
msgBuilder.append(e.getCause().getMessage());
msgBuilder.append(")");
}
}
return msgBuilder.toString();
}
/**
* Attempts to recover device communication.
*
* @throws DeviceNotAvailableException if device is not longer available
*/
@Override
public void recoverDevice() throws DeviceNotAvailableException {
if (mRecoveryMode.equals(RecoveryMode.NONE)) {
CLog.i("Skipping recovery on %s", getSerialNumber());
getRunUtil().sleep(NONE_RECOVERY_MODE_DELAY);
return;
}
CLog.i("Attempting recovery on %s", getSerialNumber());
mRecovery.recoverDevice(mMonitor, mRecoveryMode.equals(RecoveryMode.ONLINE));
if (mRecoveryMode.equals(RecoveryMode.AVAILABLE)) {
// turn off recovery mode to prevent reentrant recovery
// TODO: look for a better way to handle this, such as doing postBootUp steps in
// recovery itself
mRecoveryMode = RecoveryMode.NONE;
// this might be a runtime reset - still need to run post boot setup steps
if (isEncryptionSupported() && isDeviceEncrypted()) {
unlockDevice();
}
postBootSetup();
mRecoveryMode = RecoveryMode.AVAILABLE;
} else if (mRecoveryMode.equals(RecoveryMode.ONLINE)) {
// turn off recovery mode to prevent reentrant recovery
// TODO: look for a better way to handle this, such as doing postBootUp steps in
// recovery itself
mRecoveryMode = RecoveryMode.NONE;
postOnlineSetup();
mRecoveryMode = RecoveryMode.ONLINE;
}
CLog.i("Recovery successful for %s", getSerialNumber());
}
/**
* Attempts to recover device fastboot communication.
*
* @throws DeviceNotAvailableException if device is not longer available
*/
private void recoverDeviceFromBootloader() throws DeviceNotAvailableException {
CLog.i("Attempting recovery on %s in bootloader", getSerialNumber());
mRecovery.recoverDeviceBootloader(mMonitor);
CLog.i("Bootloader recovery successful for %s", getSerialNumber());
}
private void recoverDeviceInRecovery() throws DeviceNotAvailableException {
CLog.i("Attempting recovery on %s in recovery", getSerialNumber());
mRecovery.recoverDeviceRecovery(mMonitor);
CLog.i("Recovery mode recovery successful for %s", getSerialNumber());
}
/**
* {@inheritDoc}
*/
@Override
public void startLogcat() {
if (mLogcatReceiver != null) {
CLog.d("Already capturing logcat for %s, ignoring", getSerialNumber());
return;
}
mLogcatReceiver = createLogcatReceiver();
mLogcatReceiver.start();
}
/**
* {@inheritDoc}
*/
@Override
public void clearLogcat() {
if (mLogcatReceiver != null) {
mLogcatReceiver.clear();
}
}
/**
* {@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
*/
@Override
public InputStreamSource getLogcat() {
if (mLogcatReceiver == null) {
CLog.w("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(LogcatReceiver.LOGCAT_CMD + " -d", receiver);
output = receiver.getOutput();
} catch (IOException e) {
CLog.w("Failed to get logcat dump from %s: ", getSerialNumber(), e.getMessage());
} catch (TimeoutException e) {
CLog.w("Failed to get logcat dump from %s: timeout", getSerialNumber());
} catch (AdbCommandRejectedException e) {
CLog.w("Failed to get logcat dump from %s: ", getSerialNumber(), e.getMessage());
} catch (ShellCommandUnresponsiveException e) {
CLog.w("Failed to get logcat dump from %s: ", getSerialNumber(), e.getMessage());
}
return new ByteArrayInputStreamSource(output);
}
/**
* {@inheritDoc}
*/
@Override
public void stopLogcat() {
if (mLogcatReceiver != null) {
mLogcatReceiver.stop();
mLogcatReceiver = null;
} else {
CLog.w("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(this, mOptions.getMaxLogcatFileSize(), mLogStartDelay);
}
/**
* {@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
CLog.e("Device %s became unresponsive while retrieving bugreport", getSerialNumber());
}
return new ByteArrayInputStreamSource(receiver.getOutput());
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getScreenshot() throws DeviceNotAvailableException {
ScreenshotAction action = new ScreenshotAction();
if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) {
byte[] pngData = compressRawImageAsPng(action.mRawScreenshot);
if (pngData != null) {
return new ByteArrayInputStreamSource(pngData);
}
}
return null;
}
private class ScreenshotAction implements DeviceAction {
RawImage mRawScreenshot;
/**
* {@inheritDoc}
*/
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
mRawScreenshot = getIDevice().getScreenshot();
return mRawScreenshot != null;
}
}
private byte[] compressRawImageAsPng(RawImage rawImage) {
BufferedImage image = new BufferedImage(rawImage.width, rawImage.height,
BufferedImage.TYPE_INT_ARGB);
// borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java
int index = 0;
int IndexInc = rawImage.bpp >> 3;
for (int y = 0 ; y < rawImage.height ; y++) {
for (int x = 0 ; x < rawImage.width ; x++) {
int value = rawImage.getARGB(index);
index += IndexInc;
image.setRGB(x, y, value);
}
}
// store compressed image in memory, and let callers write to persistent storage
// use initial buffer size of 128K
byte[] pngData = null;
ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024);
try {
if (ImageIO.write(image, "png", imageOut)) {
pngData = imageOut.toByteArray();
} else {
CLog.e("Failed to compress screenshot to png");
}
} catch (IOException e) {
CLog.e("Failed to compress screenshot to png");
CLog.e(e);
}
StreamUtil.closeStream(imageOut);
return pngData;
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk)
throws DeviceNotAvailableException {
CLog.i("Connecting to wifi network %s on %s", wifiSsid, getSerialNumber());
try {
IWifiHelper wifi = createWifiHelper();
wifi.enableWifi();
// TODO: return false here if failed?
wifi.waitForWifiState(WifiState.SCANNING, WifiState.COMPLETED);
boolean added = false;
if (wifiPsk != null) {
added = wifi.addWpaPskNetwork(wifiSsid, wifiPsk);
} else {
added = wifi.addOpenNetwork(wifiSsid);
}
if (!added) {
CLog.e("Failed to add wifi network %s on %s", wifiSsid, getSerialNumber());
return false;
}
if (!wifi.waitForWifiState(WifiState.COMPLETED)) {
CLog.e("wifi network %s failed to associate on %s", wifiSsid, getSerialNumber());
return false;
}
// TODO: make timeout configurable
if (!wifi.waitForIp(30 * 1000)) {
CLog.e("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);
}
CLog.e("ping unsuccessful after connecting to wifi network %s on %s", wifiSsid,
getSerialNumber());
return false;
} catch (TargetSetupError e) {
CLog.e(e);
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean disconnectFromWifi() throws DeviceNotAvailableException {
try {
IWifiHelper wifi = createWifiHelper();
wifi.removeAllNetworks();
wifi.disableWifi();
return true;
} catch (TargetSetupError e) {
CLog.e(e);
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public String getIpAddress() throws DeviceNotAvailableException {
try {
IWifiHelper wifi = createWifiHelper();
return wifi.getIpAddress();
} catch (TargetSetupError e) {
CLog.e(e);
return null;
}
}
/**
* Create a {@link WifiHelper} to use
* <p/>
* Exposed so unit tests can mock
*/
IWifiHelper createWifiHelper() throws TargetSetupError, DeviceNotAvailableException {
return new WifiHelper(this);
}
/**
* {@inheritDoc}
*/
@Override
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
CLog.e("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 {
CLog.i("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}
*/
@Override
public void postBootSetup() throws DeviceNotAvailableException {
postOnlineSetup();
if (mOptions.isDisableKeyguard()) {
CLog.i("Attempting to disable keyguard on %s using %s", getSerialNumber(),
getDisableKeyguardCmd());
executeShellCommand(getDisableKeyguardCmd());
}
}
// TODO: consider exposing this method
private void postOnlineSetup() throws DeviceNotAvailableException {
if (isEnableAdbRoot()) {
enableAdbRoot();
}
}
/**
* Gets the adb shell command to disable the keyguard for this device.
* <p/>
* Exposed for unit testing.
*/
String getDisableKeyguardCmd() {
return mOptions.getDisableKeyguardCmd();
}
/**
* {@inheritDoc}
*/
@Override
public void rebootIntoBootloader()
throws DeviceNotAvailableException, UnsupportedOperationException {
if (!mFastbootEnabled) {
throw new UnsupportedOperationException(
"Fastboot is not available and cannot reboot into bootloader");
}
CLog.i("Rebooting device %s in state %s into bootloader", getSerialNumber(),
getDeviceState());
if (TestDeviceState.FASTBOOT.equals(getDeviceState())) {
CLog.i("device %s already in fastboot. Rebooting anyway", getSerialNumber());
executeFastbootCommand("reboot-bootloader");
} else {
CLog.i("Booting device %s into bootloader", getSerialNumber());
doAdbRebootBootloader();
}
if (!mMonitor.waitForDeviceBootloader(mOptions.getFastbootTimeout())) {
recoverDeviceFromBootloader();
}
}
private void doAdbRebootBootloader() throws DeviceNotAvailableException {
try {
getIDevice().reboot("bootloader");
return;
} catch (IOException e) {
CLog.w("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) {
CLog.w("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) {
CLog.w("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}
*/
@Override
public void reboot() throws DeviceNotAvailableException {
rebootUntilOnline();
RecoveryMode cachedRecoveryMode = getRecoveryMode();
setRecoveryMode(RecoveryMode.ONLINE);
if (isEncryptionSupported() && isDeviceEncrypted()) {
unlockDevice();
}
setRecoveryMode(cachedRecoveryMode);
if (mMonitor.waitForDeviceAvailable(mOptions.getRebootTimeout()) != null) {
postBootSetup();
return;
} else {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public void rebootUntilOnline() throws DeviceNotAvailableException {
doReboot();
RecoveryMode cachedRecoveryMode = getRecoveryMode();
setRecoveryMode(RecoveryMode.ONLINE);
if (mMonitor.waitForDeviceOnline() != null) {
if (isEnableAdbRoot()) {
enableAdbRoot();
}
} else {
recoverDevice();
}
setRecoveryMode(cachedRecoveryMode);
}
/**
* {@inheritDoc}
*/
@Override
public void rebootIntoRecovery() throws DeviceNotAvailableException {
if (TestDeviceState.FASTBOOT == getDeviceState()) {
CLog.w("device %s in fastboot when requesting boot to recovery. " +
"Rebooting to userspace first.", getSerialNumber());
rebootUntilOnline();
}
doAdbReboot("recovery");
if (!waitForDeviceInRecovery(mOptions.getAdbRecoveryTimeout())) {
recoverDeviceInRecovery();
}
}
/**
* {@inheritDoc}
*/
@Override
public void nonBlockingReboot() throws DeviceNotAvailableException {
doReboot();
}
/**
* Exposed for unit testing.
*
* @throws DeviceNotAvailableException
*/
void doReboot() throws DeviceNotAvailableException, UnsupportedOperationException {
if (TestDeviceState.FASTBOOT == getDeviceState()) {
CLog.i("device %s in fastboot. Rebooting to userspace.", getSerialNumber());
executeFastbootCommand("reboot");
} else {
CLog.i("Rebooting device %s", getSerialNumber());
doAdbReboot(null);
waitForDeviceNotAvailable("reboot", DEFAULT_UNAVAILABLE_TIMEOUT);
}
}
/**
* 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() {
@Override
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
// before the operation
if (!mMonitor.waitForDeviceNotAvailable(time)) {
// above check is flaky, ignore till better solution is found
CLog.w("Did not detect device %s becoming unavailable after %s", getSerialNumber(),
operationDesc);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean enableAdbRoot() throws DeviceNotAvailableException {
// adb root is a relatively intensive command, so do a brief check first to see
// if its necessary or not
if (isAdbRoot()) {
CLog.i("adb is already running as root on %s", getSerialNumber());
return true;
}
CLog.i("adb root on device %s", getSerialNumber());
int attempts = MAX_RETRY_ATTEMPTS + 1;
for (int i=1; i <= attempts; i++) {
String output = executeAdbCommand("root");
// wait for device to disappear from adb
waitForDeviceNotAvailable("root", 20 * 1000);
// wait for device to be back online
waitForDeviceOnline();
if (isAdbRoot()) {
return true;
}
CLog.w("'adb root' on %s unsuccessful on attempt %d of %d. Output: '%s'",
getSerialNumber(), i, attempts, output);
}
return false;
}
/**
* @{inheritDoc}
*/
@Override
public boolean isAdbRoot() throws DeviceNotAvailableException {
String output = executeShellCommand("id");
return output.contains("uid=0(root)");
}
/**
* {@inheritDoc}
*/
@Override
public boolean encryptDevice(boolean inplace) throws DeviceNotAvailableException,
UnsupportedOperationException {
if (!isEncryptionSupported()) {
throw new UnsupportedOperationException(String.format("Can't encrypt device %s: "
+ "encryption not supported", getSerialNumber()));
}
if (isDeviceEncrypted()) {
CLog.d("Device %s is already encrypted, skipping", getSerialNumber());
return true;
}
enableAdbRoot();
String encryptMethod;
int timeout;
if (inplace) {
encryptMethod = "inplace";
timeout = ENCRYPTION_INPLACE_TIMEOUT;
} else {
encryptMethod = "wipe";
timeout = ENCRYPTION_WIPE_TIMEOUT;
}
CLog.i("Encrypting device %s via %s", getSerialNumber(), encryptMethod);
executeShellCommand(String.format("vdc cryptfs enablecrypto %s \"%s\"", encryptMethod,
ENCRYPTION_PASSWORD), new NullOutputReceiver(), timeout, 1);
waitForDeviceNotAvailable("reboot", getCommandTimeout());
waitForDeviceOnline(); // Device will not become available until the user data is unlocked.
return isDeviceEncrypted();
}
/**
* {@inheritDoc}
*/
@Override
public boolean unencryptDevice() throws DeviceNotAvailableException,
UnsupportedOperationException {
if (!isEncryptionSupported()) {
throw new UnsupportedOperationException(String.format("Can't unencrypt device %s: "
+ "encryption not supported", getSerialNumber()));
}
if (!isDeviceEncrypted()) {
CLog.d("Device %s is already unencrypted, skipping", getSerialNumber());
return true;
}
CLog.i("Unencrypting device %s", getSerialNumber());
// If the device supports fastboot format, then we're done.
if (!mOptions.getUseFastbootErase()) {
rebootIntoBootloader();
fastbootWipePartition("userdata");
reboot();
return true;
}
// Determine if we need to format partition instead of wipe.
boolean format = false;
String output = executeShellCommand("vdc volume list");
String[] splitOutput;
if (output != null) {
splitOutput = output.split("\r\n");
for (String line : splitOutput) {
if (line.startsWith("110 ") && line.contains("sdcard /mnt/sdcard") &&
!line.endsWith("0")) {
format = true;
}
}
}
rebootIntoBootloader();
fastbootWipePartition("userdata");
// If the device requires time to format the filesystem after fastboot erase userdata, wait
// for the device to reboot a second time.
if (mOptions.getUnencryptRebootTimeout() > 0) {
rebootUntilOnline();
if (waitForDeviceNotAvailable(mOptions.getUnencryptRebootTimeout())) {
waitForDeviceOnline();
}
}
if (format) {
CLog.d("Need to format sdcard for device %s", getSerialNumber());
RecoveryMode cachedRecoveryMode = getRecoveryMode();
setRecoveryMode(RecoveryMode.ONLINE);
output = executeShellCommand("vdc volume format sdcard");
if (output == null) {
CLog.e("Command vdc volume format sdcard failed will no output for device %s:\n%s",
getSerialNumber());
setRecoveryMode(cachedRecoveryMode);
return false;
}
splitOutput = output.split("\r\n");
if (!splitOutput[splitOutput.length - 1].startsWith("200 ")) {
CLog.e("Command vdc volume format sdcard failed for device %s:\n%s",
getSerialNumber(), output);
setRecoveryMode(cachedRecoveryMode);
return false;
}
setRecoveryMode(cachedRecoveryMode);
}
reboot();
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean unlockDevice() throws DeviceNotAvailableException,
UnsupportedOperationException {
if (!isEncryptionSupported()) {
throw new UnsupportedOperationException(String.format("Can't unlock device %s: "
+ "encryption not supported", getSerialNumber()));
}
if (!isDeviceEncrypted()) {
CLog.d("Device %s is not encrypted, skipping", getSerialNumber());
return true;
}
CLog.i("Unlocking device %s", getSerialNumber());
enableAdbRoot();
// FIXME: currently, vcd checkpw can return an empty string when it never should. Try 3
// times.
String output;
int i = 0;
do {
// Enter the password. Output will be:
// "200 [X] -1" if the password has already been entered correctly,
// "200 [X] 0" if the password is entered correctly,
// "200 [X] N" where N is any positive number if the password is incorrect,
// any other string if there is an error.
output = executeShellCommand(String.format("vdc cryptfs checkpw \"%s\"",
ENCRYPTION_PASSWORD)).trim();
if (output.startsWith("200 ") && output.endsWith(" -1")) {
return true;
}
if (!output.isEmpty() && !(output.startsWith("200 ") && output.endsWith(" 0"))) {
CLog.e("checkpw gave output '%s' while trying to unlock device %s",
output, getSerialNumber());
return false;
}
getRunUtil().sleep(500);
} while (output.isEmpty() && ++i < 3);
if (output.isEmpty()) {
CLog.e("checkpw gave no output while trying to unlock device %s");
}
// Restart the framework. Output will be:
// "200 [X] 0" if the user data partition can be mounted,
// "200 [X] -1" if the user data partition can not be mounted (no correct password given),
// any other string if there is an error.
output = executeShellCommand("vdc cryptfs restart").trim();
if (!(output.startsWith("200 ") && output.endsWith(" 0"))) {
CLog.e("restart gave output '%s' while trying to unlock device %s", output,
getSerialNumber());
return false;
}
waitForDeviceAvailable();
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDeviceEncrypted() throws DeviceNotAvailableException {
String output = getPropertySync("ro.crypto.state");
if (output == null && isEncryptionSupported()) {
CLog.e("Property ro.crypto.state is null on device %s", getSerialNumber());
}
return "encrypted".equals(output);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isEncryptionSupported() throws DeviceNotAvailableException {
if (!isEnableAdbRoot()) {
CLog.i("root is required for encryption");
mIsEncryptionSupported = false;
return mIsEncryptionSupported;
}
if (mIsEncryptionSupported != null) {
return mIsEncryptionSupported.booleanValue();
}
enableAdbRoot();
String output = executeShellCommand("vdc cryptfs enablecrypto").trim();
mIsEncryptionSupported = (output != null && output.startsWith(ENCRYPTION_SUPPORTED_CODE) &&
output.contains(ENCRYPTION_SUPPORTED_USAGE));
return mIsEncryptionSupported;
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceOnline(long waitTime) throws DeviceNotAvailableException {
if (mMonitor.waitForDeviceOnline(waitTime) == null) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceOnline() throws DeviceNotAvailableException {
if (mMonitor.waitForDeviceOnline() == null) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceAvailable(long waitTime) throws DeviceNotAvailableException {
if (mMonitor.waitForDeviceAvailable(waitTime) == null) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceAvailable() throws DeviceNotAvailableException {
if (mMonitor.waitForDeviceAvailable() == null) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForDeviceNotAvailable(long waitTime) {
return mMonitor.waitForDeviceNotAvailable(waitTime);
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForDeviceInRecovery(long waitTime) {
return mMonitor.waitForDeviceInRecovery(waitTime);
}
/**
* Small helper function to throw an NPE if the passed arg is null. This should be used when
* some value will be stored and used later, in which case it'll avoid hard-to-trace
* asynchronous NullPointerExceptions by throwing the exception synchronously. This is not
* intended to be used where the NPE would be thrown synchronously -- just let the jvm take care
* of it in that case.
*/
private void throwIfNull(Object obj) {
if (obj == null) throw new NullPointerException();
}
/**
* Retrieve this device's recovery mechanism.
* <p/>
* Exposed for unit testing.
*/
IDeviceRecovery getRecovery() {
return mRecovery;
}
/**
* {@inheritDoc}
*/
@Override
public void setRecovery(IDeviceRecovery recovery) {
throwIfNull(recovery);
mRecovery = recovery;
}
/**
* {@inheritDoc}
*/
@Override
public void setRecoveryMode(RecoveryMode mode) {
throwIfNull(mRecoveryMode);
mRecoveryMode = mode;
}
/**
* {@inheritDoc}
*/
@Override
public RecoveryMode getRecoveryMode() {
return mRecoveryMode;
}
/**
* {@inheritDoc}
*/
@Override
public void setFastbootEnabled(boolean fastbootEnabled) {
mFastbootEnabled = fastbootEnabled;
}
/**
* {@inheritDoc}
*/
@Override
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.isLocked()) {
return;
}
mState = deviceState;
CLog.d("Device %s state is now %s", getSerialNumber(), deviceState);
mMonitor.setState(deviceState);
}
}
/**
* {@inheritDoc}
*/
@Override
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) {
CLog.e("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;
}
/**
* {@inheritDoc}
*/
@Override
public void setEmulatorProcess(Process p) {
mEmulatorProcess = p;
}
/**
* {@inheritDoc}
*/
@Override
public Process getEmulatorProcess() {
return mEmulatorProcess;
}
/**
* @return <code>true</code> if adb root should be enabled on device
*/
public boolean isEnableAdbRoot() {
return mOptions.isEnableAdbRoot();
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException {
return getInstalledPackageNames(new PkgFilter() {
@Override
public boolean accept(String pkgName, String apkPath) {
return true;
}
});
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getInstalledNonSystemPackageNames() throws DeviceNotAvailableException {
return getInstalledPackageNames(new PkgFilter() {
@Override
public boolean accept(String pkgName, String apkPath) {
return !apkPath.startsWith("/system");
}
});
}
private static interface PkgFilter {
boolean accept(String pkgName, String apkPath);
}
private Set<String> getInstalledPackageNames(PkgFilter filter)
throws DeviceNotAvailableException {
Set<String> packages= new HashSet<String>();
String output = executeShellCommand(LIST_PACKAGES_CMD);
if (output != null) {
Matcher m = PACKAGE_REGEX.matcher(output);
while (m.find()) {
String packagePath = m.group(1);
String packageName = m.group(2);
if (filter.accept(packageName, packagePath)) {
packages.add(packageName);
}
}
}
return packages;
}
/**
* {@inheritDoc}
*/
@Override
public TestDeviceOptions getOptions() {
return mOptions;
}
}