blob: da887cf0b45d2712e791039e523518089c792c57 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.device;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.FileListingService;
import com.android.ddmlib.FileListingService.FileEntry;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IDevice.DeviceState;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.InstallException;
import com.android.ddmlib.Log.LogLevel;
import com.android.ddmlib.NullOutputReceiver;
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.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.command.remote.DeviceDescriptor;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.device.contentprovider.ContentProviderHandler;
import com.android.tradefed.host.IHostOptions;
import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestLifeCycleReceiver;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.SnapshotInputStreamSource;
import com.android.tradefed.result.StubTestRunListener;
import com.android.tradefed.result.ddmlib.TestRunToTestInvocationForwarder;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.Bugreport;
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.KeyguardControllerState;
import com.android.tradefed.util.ProcessInfo;
import com.android.tradefed.util.PsParser;
import com.android.tradefed.util.QuotationAwareTokenizer;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.SizeLimitedOutputStream;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.ZipUtil;
import com.android.tradefed.util.ZipUtil2;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.concurrent.GuardedBy;
/**
* Default implementation of a {@link ITestDevice}
* Non-full stack android devices.
*/
public class NativeDevice implements IManagedTestDevice {
private static final String SD_CARD = "/sdcard/";
/**
* Allow pauses of up to 2 minutes while receiving bugreport.
* <p/>
* Note that dumpsys may pause up to a minute while waiting for unresponsive components.
* It still should bail after that minute, if it will ever terminate on its own.
*/
private static final int BUGREPORT_TIMEOUT = 2 * 60 * 1000;
/**
* Allow a little more time for bugreportz because there are extra steps.
*/
private static final int BUGREPORTZ_TIMEOUT = 5 * 60 * 1000;
private static final String BUGREPORT_CMD = "bugreport";
private static final String BUGREPORTZ_CMD = "bugreportz";
private static final String BUGREPORTZ_TMP_PATH = "/bugreports/";
/**
* Allow up to 2 minutes to receives the full logcat dump.
*/
private static final int LOGCAT_DUMP_TIMEOUT = 2 * 60 * 1000;
/** the default number of command retry attempts to perform */
protected static final int MAX_RETRY_ATTEMPTS = 2;
/** Value returned for any invalid/not found user id: UserHandle defined the -10000 value **/
protected static final int INVALID_USER_ID = -10000;
/** regex to match input dispatch readiness line **/
static final Pattern INPUT_DISPATCH_STATE_REGEX =
Pattern.compile("DispatchEnabled:\\s?([01])");
/** regex to match build signing key type */
private static final Pattern KEYS_PATTERN = Pattern.compile("^.*-keys$");
private static final Pattern DF_PATTERN = Pattern.compile(
//Fs 1K-blks Used Available Use% Mounted on
"^/\\S+\\s+\\d+\\s+\\d+\\s+(\\d+)\\s+\\d+%\\s+/\\S*$", Pattern.MULTILINE);
private static final Pattern BUGREPORTZ_RESPONSE_PATTERN = Pattern.compile("(OK:)(.*)");
protected static final long MAX_HOST_DEVICE_TIME_OFFSET = 5 * 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_MIN = 2 * 60;
/** Encrypting with wipe can take up to 20 minutes. */
private static final long ENCRYPTION_WIPE_TIMEOUT_MIN = 20;
/** 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;
static final String BUILD_ID_PROP = "ro.build.version.incremental";
private static final String PRODUCT_NAME_PROP = "ro.product.name";
private static final String BUILD_TYPE_PROP = "ro.build.type";
private static final String BUILD_ALIAS_PROP = "ro.build.id";
private static final String BUILD_FLAVOR = "ro.build.flavor";
private static final String HEADLESS_PROP = "ro.build.headless";
static final String BUILD_CODENAME_PROP = "ro.build.version.codename";
static final String BUILD_TAGS = "ro.build.tags";
private static final String PS_COMMAND = "ps -A || ps";
private static final String SIM_STATE_PROP = "gsm.sim.state";
private static final String SIM_OPERATOR_PROP = "gsm.operator.alpha";
static final String MAC_ADDRESS_PATTERN = "([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}";
static final String MAC_ADDRESS_COMMAND = "su root cat /sys/class/net/wlan0/address";
/** The network monitoring interval in ms. */
private static final int NETWORK_MONITOR_INTERVAL = 10 * 1000;
/** Wifi reconnect check interval in ms. */
private static final int WIFI_RECONNECT_CHECK_INTERVAL = 1 * 1000;
/** Wifi reconnect timeout in ms. */
private static final int WIFI_RECONNECT_TIMEOUT = 60 * 1000;
/** The time in ms to wait for a command to complete. */
private long mCmdTimeout = 2 * 60 * 1000L;
/** The time in ms to wait for a 'long' command to complete. */
private long mLongCmdTimeout = 25 * 60 * 1000L;
private IDevice mIDevice;
private IDeviceRecovery mRecovery = new WaitDeviceRecovery();
protected final IDeviceStateMonitor mStateMonitor;
private TestDeviceState mState = TestDeviceState.ONLINE;
private final ReentrantLock mFastbootLock = new ReentrantLock();
private LogcatReceiver mLogcatReceiver;
private boolean mFastbootEnabled = true;
private String mFastbootPath = "fastboot";
protected TestDeviceOptions mOptions = new TestDeviceOptions();
private Process mEmulatorProcess;
private SizeLimitedOutputStream mEmulatorOutput;
private Clock mClock = Clock.systemUTC();
private RecoveryMode mRecoveryMode = RecoveryMode.AVAILABLE;
private Boolean mIsEncryptionSupported = null;
private ReentrantLock mAllocationStateLock = new ReentrantLock();
@GuardedBy("mAllocationStateLock")
private DeviceAllocationState mAllocationState = DeviceAllocationState.Unknown;
private IDeviceMonitor mAllocationMonitor = null;
private String mLastConnectedWifiSsid = null;
private String mLastConnectedWifiPsk = null;
private boolean mNetworkMonitorEnabled = false;
private ContentProviderHandler mContentProvider = null;
private boolean mShouldSkipContentProviderSetup = false;
/**
* Interface for a generic device communication attempt.
*/
abstract interface DeviceAction {
/**
* Execute the device operation.
*
* @return <code>true</code> if operation is performed successfully, <code>false</code>
* otherwise
* @throws IOException, TimeoutException, AdbCommandRejectedException,
* ShellCommandUnresponsiveException, InstallException,
* SyncException if operation terminated abnormally
*/
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException;
}
/**
* A {@link DeviceAction} for running a OS 'adb ....' command.
*/
protected 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;
}
}
protected class AdbShellAction implements DeviceAction {
/** the output from the command */
CommandResult mResult = null;
private String[] mCmd;
private long mTimeout;
private File mPipeAsInput;
AdbShellAction(String[] cmd, File pipeAsInput, long timeout) {
mCmd = cmd;
mPipeAsInput = pipeAsInput;
mTimeout = timeout;
}
@Override
public boolean run() throws TimeoutException, IOException {
if (mPipeAsInput != null) {
mResult = getRunUtil().runTimedCmdWithInputRedirect(mTimeout, mPipeAsInput, mCmd);
} else {
mResult = getRunUtil().runTimedCmd(mTimeout, mCmd);
}
if (mResult.getStatus() == CommandStatus.EXCEPTION) {
throw new IOException(mResult.getStderr());
} else if (mResult.getStatus() == CommandStatus.TIMED_OUT) {
throw new TimeoutException(mResult.getStderr());
}
// If it's not some issue with running the adb command, then we return the CommandResult
// which will contain all the infos.
return true;
}
}
/**
* Creates a {@link TestDevice}.
*
* @param device the associated {@link IDevice}
* @param stateMonitor the {@link IDeviceStateMonitor} mechanism to use
* @param allocationMonitor the {@link IDeviceMonitor} to inform of allocation state changes.
* Can be null
*/
public NativeDevice(IDevice device, IDeviceStateMonitor stateMonitor,
IDeviceMonitor allocationMonitor) {
throwIfNull(device);
throwIfNull(stateMonitor);
mIDevice = device;
mStateMonitor = stateMonitor;
mAllocationMonitor = allocationMonitor;
}
/** Get the {@link RunUtil} instance to use. */
@VisibleForTesting
protected IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
/** Set the Clock instance to use. */
@VisibleForTesting
protected void setClock(Clock clock) {
mClock = clock;
}
/**
* {@inheritDoc}
*/
@Override
public void setOptions(TestDeviceOptions options) {
throwIfNull(options);
mOptions = options;
mStateMonitor.setDefaultOnlineTimeout(options.getOnlineTimeout());
mStateMonitor.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.setMaxLogcatDataSize(size);
}
/**
* Sets the time in ms to wait before starting logcat capture for a online device.
*
* @param delay the delay in ms
*/
protected 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;
}
mStateMonitor.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 propName 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
*/
protected String internalGetProperty(String propName, String fastbootVar, String description)
throws DeviceNotAvailableException, UnsupportedOperationException {
String propValue = getIDevice().getProperty(propName);
if (propValue != null) {
return propValue;
} 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(propName);
}
}
/**
* {@inheritDoc}
*/
@Override
public String getProperty(final String name) throws DeviceNotAvailableException {
if (getIDevice() instanceof StubDevice) {
return null;
}
if (!DeviceState.ONLINE.equals(getIDevice().getState())) {
CLog.d("Device %s is not online cannot get property %s.", getSerialNumber(), name);
return null;
}
final String[] result = new String[1];
DeviceAction propAction = new DeviceAction() {
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
try {
result[0] = getIDevice().getSystemProperty(name).get();
} catch (InterruptedException | ExecutionException e) {
// getProperty will stash the original exception inside
// ExecutionException.getCause
// throw the specific original exception if available in case TF ever does
// specific handling for different exceptions
if (e.getCause() instanceof IOException) {
throw (IOException)e.getCause();
} else if (e.getCause() instanceof TimeoutException) {
throw (TimeoutException)e.getCause();
} else if (e.getCause() instanceof AdbCommandRejectedException) {
throw (AdbCommandRejectedException)e.getCause();
} else if (e.getCause() instanceof ShellCommandUnresponsiveException) {
throw (ShellCommandUnresponsiveException)e.getCause();
}
else {
throw new IOException(e);
}
}
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");
}
@Override
public String getBasebandVersion() throws DeviceNotAvailableException {
return internalGetProperty("gsm.version.baseband", "version-baseband", "Baseband");
}
/**
* {@inheritDoc}
*/
@Override
public String getProductType() throws DeviceNotAvailableException {
return internalGetProductType(MAX_RETRY_ATTEMPTS);
}
/**
* {@link #getProductType()}
*
* @param retryAttempts The number of times to try calling {@link #recoverDevice()} if the
* device's product type cannot be found.
*/
private String internalGetProductType(int retryAttempts) throws DeviceNotAvailableException {
String productType = internalGetProperty(DeviceProperties.BOARD, "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()),
getSerialNumber());
}
}
return productType.toLowerCase();
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootProductType()
throws DeviceNotAvailableException, UnsupportedOperationException {
String prop = getFastbootVariable("product");
if (prop != null) {
prop = prop.toLowerCase();
}
return prop;
}
/**
* {@inheritDoc}
*/
@Override
public String getProductVariant() throws DeviceNotAvailableException {
String prop = internalGetProperty(DeviceProperties.VARIANT, "variant", "Product variant");
if (prop == null) {
prop =
internalGetProperty(
DeviceProperties.VARIANT_LEGACY, "variant", "Product variant");
}
if (prop != null) {
prop = prop.toLowerCase();
}
return prop;
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootProductVariant()
throws DeviceNotAvailableException, UnsupportedOperationException {
String prop = getFastbootVariable("variant");
if (prop != null) {
prop = prop.toLowerCase();
}
return prop;
}
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 getBuildAlias() throws DeviceNotAvailableException {
String alias = getProperty(BUILD_ALIAS_PROP);
if (alias == null || alias.isEmpty()) {
return getBuildId();
}
return alias;
}
/**
* {@inheritDoc}
*/
@Override
public String getBuildId() throws DeviceNotAvailableException {
String bid = 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 String getBuildFlavor() throws DeviceNotAvailableException {
String buildFlavor = getProperty(BUILD_FLAVOR);
if (buildFlavor != null && !buildFlavor.isEmpty()) {
return buildFlavor;
}
String productName = getProperty(PRODUCT_NAME_PROP);
String buildType = getProperty(BUILD_TYPE_PROP);
if (productName == null || buildType == null) {
CLog.w("Could not get device %s build flavor.", getSerialNumber());
return null;
}
return String.format("%s-%s", productName, buildType);
}
/**
* {@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, TimeUnit.MILLISECONDS);
return true;
}
};
performDeviceAction(String.format("shell %s", command), action, MAX_RETRY_ATTEMPTS);
}
/**
* {@inheritDoc}
*/
@Override
public void executeShellCommand(final String command, final IShellOutputReceiver receiver,
final long maxTimeToOutputShellResponse, final TimeUnit timeUnit,
final int retryAttempts) throws DeviceNotAvailableException {
DeviceAction action = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
ShellCommandUnresponsiveException {
getIDevice().executeShellCommand(command, receiver,
maxTimeToOutputShellResponse, timeUnit);
return true;
}
};
performDeviceAction(String.format("shell %s", command), action, retryAttempts);
}
/** {@inheritDoc} */
@Override
public void executeShellCommand(
final String command,
final IShellOutputReceiver receiver,
final long maxTimeoutForCommand,
final long maxTimeToOutputShellResponse,
final TimeUnit timeUnit,
final int retryAttempts)
throws DeviceNotAvailableException {
DeviceAction action =
new DeviceAction() {
@Override
public boolean run()
throws TimeoutException, IOException, AdbCommandRejectedException,
ShellCommandUnresponsiveException {
getIDevice()
.executeShellCommand(
command,
receiver,
maxTimeoutForCommand,
maxTimeToOutputShellResponse,
timeUnit);
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 CommandResult executeShellV2Command(String cmd) throws DeviceNotAvailableException {
return executeShellV2Command(cmd, getCommandTimeout(), TimeUnit.MILLISECONDS);
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(String cmd, File pipeAsInput)
throws DeviceNotAvailableException {
return executeShellV2Command(
cmd, pipeAsInput, getCommandTimeout(), TimeUnit.MILLISECONDS, MAX_RETRY_ATTEMPTS);
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(
String cmd, final long maxTimeoutForCommand, final TimeUnit timeUnit)
throws DeviceNotAvailableException {
return executeShellV2Command(cmd, null, maxTimeoutForCommand, timeUnit, MAX_RETRY_ATTEMPTS);
}
/** {@inheritDoc} */
@Override
public CommandResult executeShellV2Command(
String cmd, final long maxTimeoutForCommand, final TimeUnit timeUnit, int retryAttempts)
throws DeviceNotAvailableException {
return executeShellV2Command(cmd, null, maxTimeoutForCommand, timeUnit, retryAttempts);
}
private CommandResult executeShellV2Command(
String cmd,
File pipeAsInput,
final long maxTimeoutForCommand,
final TimeUnit timeUnit,
int retryAttempts)
throws DeviceNotAvailableException {
final String[] fullCmd = buildAdbShellCommand(cmd);
AdbShellAction adbActionV2 =
new AdbShellAction(fullCmd, pipeAsInput, timeUnit.toMillis(maxTimeoutForCommand));
performDeviceAction(String.format("adb %s", fullCmd[4]), adbActionV2, retryAttempts);
return adbActionV2.mResult;
}
/** {@inheritDoc} */
@Override
public boolean runInstrumentationTests(
final IRemoteAndroidTestRunner runner,
final Collection<ITestLifeCycleReceiver> listeners)
throws DeviceNotAvailableException {
RunFailureListener failureListener = new RunFailureListener();
List<ITestRunListener> runListeners = new ArrayList<>();
runListeners.add(failureListener);
runListeners.add(new TestRunToTestInvocationForwarder(listeners));
DeviceAction runTestsAction =
new DeviceAction() {
@Override
public boolean run()
throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException,
SyncException {
runner.run(runListeners);
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 (mStateMonitor.waitForDeviceAvailable(5 * 1000) == null) {
// device isn't up, recover
recoverDevice();
}
}
return result;
}
/** {@inheritDoc} */
@Override
public boolean runInstrumentationTestsAsUser(
final IRemoteAndroidTestRunner runner,
int userId,
final Collection<ITestLifeCycleReceiver> listeners)
throws DeviceNotAvailableException {
String oldRunTimeOptions = appendUserRunTimeOptionToRunner(runner, userId);
boolean result = runInstrumentationTests(runner, listeners);
resetUserRunTimeOptionToRunner(runner, oldRunTimeOptions);
return result;
}
/**
* Helper method to add user run time option to {@link RemoteAndroidTestRunner}
*
* @param runner {@link IRemoteAndroidTestRunner}
* @param userId the integer of the user id to run as.
* @return original run time options.
*/
private String appendUserRunTimeOptionToRunner(final IRemoteAndroidTestRunner runner, int userId) {
if (runner instanceof RemoteAndroidTestRunner) {
String original = ((RemoteAndroidTestRunner) runner).getRunOptions();
String userRunTimeOption = String.format("--user %s", Integer.toString(userId));
((RemoteAndroidTestRunner) runner).setRunOptions(userRunTimeOption);
return original;
} else {
throw new IllegalStateException(String.format("%s runner does not support multi-user",
runner.getClass().getName()));
}
}
/**
* Helper method to reset the run time options to {@link RemoteAndroidTestRunner}
*
* @param runner {@link IRemoteAndroidTestRunner}
* @param oldRunTimeOptions
*/
private void resetUserRunTimeOptionToRunner(final IRemoteAndroidTestRunner runner,
String oldRunTimeOptions) {
if (runner instanceof RemoteAndroidTestRunner) {
if (oldRunTimeOptions != null) {
((RemoteAndroidTestRunner) runner).setRunOptions(oldRunTimeOptions);
}
} else {
throw new IllegalStateException(String.format("%s runner does not support multi-user",
runner.getClass().getName()));
}
}
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, ITestLifeCycleReceiver... listeners)
throws DeviceNotAvailableException {
List<ITestLifeCycleReceiver> listenerList = new ArrayList<>();
listenerList.addAll(Arrays.asList(listeners));
return runInstrumentationTests(runner, listenerList);
}
/** {@inheritDoc} */
@Override
public boolean runInstrumentationTestsAsUser(
IRemoteAndroidTestRunner runner, int userId, ITestLifeCycleReceiver... listeners)
throws DeviceNotAvailableException {
String oldRunTimeOptions = appendUserRunTimeOptionToRunner(runner, userId);
boolean result = runInstrumentationTests(runner, listeners);
resetUserRunTimeOptionToRunner(runner, oldRunTimeOptions);
return result;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isRuntimePermissionSupported() throws DeviceNotAvailableException {
return getApiLevel() > 22;
}
/**
* helper method to throw exception if runtime permission isn't supported
* @throws DeviceNotAvailableException
*/
protected void ensureRuntimePermissionSupported() throws DeviceNotAvailableException {
boolean runtimePermissionSupported = isRuntimePermissionSupported();
if (!runtimePermissionSupported) {
throw new UnsupportedOperationException(
"platform on device does not support runtime permission granting!");
}
}
/**
* {@inheritDoc}
*/
@Override
public String installPackage(final File packageFile, final boolean reinstall,
final String... extraArgs) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/**
* {@inheritDoc}
*/
@Override
public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
String... extraArgs) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/**
* {@inheritDoc}
*/
@Override
public String installPackageForUser(File packageFile, boolean reinstall, int userId,
String... extraArgs) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/**
* {@inheritDoc}
*/
@Override
public String installPackageForUser(File packageFile, boolean reinstall,
boolean grantPermissions, int userId, String... extraArgs)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/**
* {@inheritDoc}
*/
@Override
public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package Manager's features");
}
/**
* {@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);
}
/**
* {@inheritDoc}
*/
@Override
public File pullFile(String remoteFilePath) throws DeviceNotAvailableException {
File localFile = null;
boolean success = false;
try {
localFile = FileUtil.createTempFileForRemote(remoteFilePath, null);
if (pullFile(remoteFilePath, localFile)) {
success = true;
return localFile;
}
} catch (IOException e) {
CLog.w("Encountered IOException while trying to pull '%s':", remoteFilePath);
CLog.e(e);
} finally {
if (!success) {
FileUtil.deleteFile(localFile);
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String pullFileContents(String remoteFilePath) throws DeviceNotAvailableException {
File temp = pullFile(remoteFilePath);
if (temp != null) {
try {
return FileUtil.readStringFromFile(temp);
} catch (IOException e) {
CLog.e(String.format("Could not pull file: %s", remoteFilePath));
} finally {
FileUtil.deleteFile(temp);
}
}
return null;
}
/**
* {@inheritDoc}
*/
@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 {
if (remoteFilePath.startsWith(SD_CARD)) {
ContentProviderHandler handler = getContentProvider();
if (handler != null) {
mShouldSkipContentProviderSetup = true;
return handler.pushFile(localFile, remoteFilePath);
}
}
DeviceAction pushAction =
new DeviceAction() {
@Override
public boolean run()
throws TimeoutException, IOException, AdbCommandRejectedException,
SyncException {
SyncService syncService = null;
boolean status = false;
try {
syncService = getIDevice().getSyncService();
if (syncService == null) {
throw new IOException("SyncService returned null.");
}
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'. "
+ "Error code: %s",
localFile.getAbsolutePath(),
remoteFilePath,
getSerialNumber(),
e.getMessage(),
e.getErrorCode());
// TODO: check if ddmlib can report a better error
if (SyncError.TRANSFER_PROTOCOL_ERROR.equals(e.getErrorCode())) {
if (e.getMessage().contains("Permission denied")) {
return false;
}
}
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 {
FileUtil.deleteFile(tmpFile);
}
}
/**
* {@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 {
String externalStorePath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
return getPartitionFreeSpace(externalStorePath);
}
/** {@inheritDoc} */
@Override
public long getPartitionFreeSpace(String partition) throws DeviceNotAvailableException {
CLog.i("Checking free space for %s on partition %s", getSerialNumber(), partition);
String output = getDfOutput(partition);
// Try coreutils/toybox style output first.
Long available = parseFreeSpaceFromModernOutput(output);
if (available != null) {
return available;
}
// Then the two legacy toolbox formats.
available = parseFreeSpaceFromAvailable(output);
if (available != null) {
return available;
}
available = parseFreeSpaceFromFree(partition, output);
if (available != null) {
return available;
}
CLog.e("free space command output \"%s\" did not match expected patterns", output);
return 0;
}
/**
* Run the 'df' shell command and return output, making multiple attempts if necessary.
*
* @param externalStorePath the path to check
* @return the output from 'shell df path'
* @throws DeviceNotAvailableException
*/
private String getDfOutput(String externalStorePath) throws DeviceNotAvailableException {
for (int i=0; i < MAX_RETRY_ATTEMPTS; i++) {
String output = executeShellCommand(String.format("df %s", externalStorePath));
if (output.trim().length() > 0) {
return output;
}
}
throw new DeviceUnresponsiveException(String.format(
"Device %s not returning output from df command after %d attempts",
getSerialNumber(), MAX_RETRY_ATTEMPTS), getSerialNumber());
}
/**
* Parses a partition's available space from the legacy output of a 'df' command, used
* pre-gingerbread.
* <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 partition's available space from the 'table-formatted' output of a toolbox 'df'
* command, used from gingerbread to lollipop.
* <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
*/
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 {
Float freeSpaceFloat = Float.parseFloat(numericValueString);
if (unitType.equals("M")) {
freeSpaceFloat = freeSpaceFloat * 1024;
} else if (unitType.equals("G")) {
freeSpaceFloat = freeSpaceFloat * 1024 * 1024;
}
freeSpace = freeSpaceFloat.longValue();
} catch (NumberFormatException e) {
// fall through
}
}
return freeSpace;
}
/**
* Parses a partition's available space from the modern coreutils/toybox 'df' output, used
* after lollipop.
* <p/>
* Assumes output format of:
* <br/>
* <code>
* Filesystem 1K-blocks Used Available Use% Mounted on
* <br/>
* /dev/fuse 11585536 1316348 10269188 12% /mnt/shell/emulated
* </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
*/
Long parseFreeSpaceFromModernOutput(String dfOutput) {
Matcher matcher = DF_PATTERN.matcher(dfOutput);
if (matcher.find()) {
try {
return Long.parseLong(matcher.group(1));
} catch (NumberFormatException e) {
// fall through
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String getMountPoint(String mountName) {
return mStateMonitor.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<>(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 {
path = interpolatePathVariables(path);
String[] pathComponents = path.split(FileListingService.FILE_SEPARATOR);
FileListingService service = getFileListingService();
IFileEntry rootFile = new FileEntryWrapper(this, service.getRoot());
return FileEntryWrapper.getDescendant(rootFile, Arrays.asList(pathComponents));
}
/**
* Unofficial helper to get a {@link FileEntry} from a non-root path. FIXME: Refactor the
* FileEntry system to have it available from any path. (even non root).
*
* @param entry a {@link FileEntry} not necessarily root as Ddmlib requires.
* @return a {@link FileEntryWrapper} representing the FileEntry.
* @throws DeviceNotAvailableException
*/
public IFileEntry getFileEntry(FileEntry entry) throws DeviceNotAvailableException {
// FileEntryWrapper is going to construct the list of child fild internally.
return new FileEntryWrapper(this, entry);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isDirectory(String path) throws DeviceNotAvailableException {
return executeShellCommand(String.format("ls -ld %s", path)).charAt(0) == 'd';
}
/**
* {@inheritDoc}
*/
@Override
public String[] getChildren(String path) throws DeviceNotAvailableException {
String lsOutput = executeShellCommand(String.format("ls -A1 %s", path));
if (lsOutput.trim().isEmpty()) {
return new String[0];
}
return lsOutput.split("\r?\n");
}
/**
* 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 -p \"%s\"", remotePath));
if (!pushDir(childFile, remotePath)) {
return false;
}
} else if (childFile.isFile()) {
if (!pushFile(childFile, remotePath)) {
return false;
}
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean pullDir(String deviceFilePath, File localDir)
throws DeviceNotAvailableException {
if (!localDir.isDirectory()) {
CLog.e("Local path %s is not a directory", localDir.getAbsolutePath());
return false;
}
if (!isDirectory(deviceFilePath)) {
CLog.e("Device path %s is not a directory", deviceFilePath);
return false;
}
FileEntry entryRoot =
new FileEntry(null, deviceFilePath, FileListingService.TYPE_DIRECTORY, false);
IFileEntry entry = getFileEntry(entryRoot);
Collection<IFileEntry> children = entry.getChildren(false);
if (children.isEmpty()) {
CLog.i("Device path is empty, nothing to do.");
return true;
}
for (IFileEntry item : children) {
if (item.isDirectory()) {
// handle sub dir
File subDir = new File(localDir, item.getName());
if (!subDir.mkdir()) {
CLog.w("Failed to create sub directory %s, aborting.",
subDir.getAbsolutePath());
return false;
}
String deviceSubDir = item.getFullPath();
if (!pullDir(deviceSubDir, subDir)) {
CLog.w("Failed to pull sub directory %s from device, aborting", deviceSubDir);
return false;
}
} else {
// handle regular file
File localFile = new File(localDir, item.getName());
String fullPath = item.getFullPath();
if (!pullFile(fullPath, localFile)) {
CLog.w("Failed to pull file %s from device, aborting", fullPath);
return false;
}
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean syncFiles(File localFileDir, String deviceFilePath)
throws DeviceNotAvailableException {
if (localFileDir == null || deviceFilePath == null) {
throw new IllegalArgumentException("syncFiles does not take null arguments");
}
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 -p \"%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<>();
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
* @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(".");
}
}
/**
* helper to get the timezone from the device. Example: "Europe/London"
*/
private String getDeviceTimezone() {
try {
// This may not be set at first, default to GMT in this case.
String timezone = getProperty("persist.sys.timezone");
if (timezone != null) {
return timezone.trim();
}
} catch (DeviceNotAvailableException e) {
// Fall through on purpose
}
return "GMT";
}
/**
* Return <code>true</code> if local file is newer than remote file. {@link IFileEntry} being
* accurate to the minute, in case of equal times, the file will be considered newer.
*/
@VisibleForTesting
protected boolean isNewer(File localFile, IFileEntry entry) {
final String entryTimeString = String.format("%s %s", entry.getDate(), entry.getTime());
try {
String timezone = getDeviceTimezone();
// expected format of a FileEntry's date and time
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
format.setTimeZone(TimeZone.getTimeZone(timezone));
Date remoteDate = format.parse(entryTimeString);
long offset = 0;
try {
offset = getDeviceTimeOffset(null);
} catch (DeviceNotAvailableException e) {
offset = 0;
}
CLog.i("Device offset time: %s", offset);
// 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 + offset);
} 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 executeFastbootCommand(long timeout, String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
return doFastbootCommand(timeout, 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++) {
File fastbootTmpDir = getHostOptions().getFastbootTmpDir();
IRunUtil runUtil = null;
if (fastbootTmpDir != null) {
runUtil = new RunUtil();
runUtil.setEnvVariable("TMPDIR", fastbootTmpDir.getAbsolutePath());
} else {
runUtil = getRunUtil();
}
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 = runUtil.runTimedCmd(timeout, fullCmd);
} finally {
mFastbootLock.unlock();
}
if (!isRecoveryNeeded(result)) {
return result;
}
CLog.w("Recovery needed after executing fastboot command");
if (result != null) {
CLog.v("fastboot command output:\nstdout: %s\nstderr:%s",
result.getStdout(), result.getStderr());
}
recoverDeviceFromBootloader();
}
throw new DeviceUnresponsiveException(String.format("Attempted fastboot %s multiple "
+ "times on device %s without communication success. Aborting.", cmdArgs[0],
getSerialNumber()), 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. */
long 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(long 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 adb shell command session and args */
private String[] buildAdbShellCommand(String command) {
// TODO: implement the shell v2 support in ddmlib itself.
String[] commandArgs =
QuotationAwareTokenizer.tokenizeLine(
command,
/** No logging */
false);
return ArrayUtil.buildArray(
new String[] {"adb", "-s", getSerialNumber(), "shell"}, commandArgs);
}
/**
* Builds the OS command for the given fastboot command and args
*/
private String[] buildFastbootCommand(String... commandArgs) {
return ArrayUtil.buildArray(new String[] {getFastbootPath(), "-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 retryAttempts the retry attempts to make for action if it fails but
* recovery succeeds
* @return <code>true</code> if action was performed successfully
* @throws DeviceNotAvailableException if recovery attempt fails or max attempts done without
* success
*/
protected 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()), 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());
try {
mRecovery.recoverDevice(mStateMonitor, mRecoveryMode.equals(RecoveryMode.ONLINE));
} catch (DeviceUnresponsiveException due) {
RecoveryMode previousRecoveryMode = mRecoveryMode;
mRecoveryMode = RecoveryMode.NONE;
boolean enabled = enableAdbRoot();
CLog.d("Device Unresponsive during recovery, is root still enabled: %s", enabled);
mRecoveryMode = previousRecoveryMode;
throw due;
}
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;
enableAdbRoot();
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(mStateMonitor);
CLog.i("Bootloader recovery successful for %s", getSerialNumber());
}
private void recoverDeviceInRecovery() throws DeviceNotAvailableException {
CLog.i("Attempting recovery on %s in recovery", getSerialNumber());
mRecovery.recoverDeviceRecovery(mStateMonitor);
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} */
@Override
@SuppressWarnings("MustBeClosedChecker")
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();
}
}
/** {@inheritDoc} */
@Override
@SuppressWarnings("MustBeClosedChecker")
public InputStreamSource getLogcat(int maxBytes) {
if (mLogcatReceiver == null) {
CLog.w("Not capturing logcat for %s in background, returning a logcat dump "
+ "ignoring size", getSerialNumber());
return getLogcatDump();
} else {
return mLogcatReceiver.getLogcatData(maxBytes);
}
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getLogcatSince(long date) {
try {
if (getApiLevel() <= 22) {
CLog.i("Api level too low to use logcat -t 'time' reverting to dump");
return getLogcatDump();
}
} catch (DeviceNotAvailableException e) {
// For convenience of interface, we catch the DNAE here.
CLog.e(e);
return getLogcatDump();
}
// Convert date to format needed by the command:
// 'MM-DD HH:mm:ss.mmm' or 'YYYY-MM-DD HH:mm:ss.mmm'
SimpleDateFormat format = new SimpleDateFormat("MM-dd HH:mm:ss.mmm");
String dateFormatted = format.format(new Date(date));
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();
String command = String.format("%s -t '%s'", LogcatReceiver.LOGCAT_CMD, dateFormatted);
getIDevice().executeShellCommand(command, receiver);
output = receiver.getOutput();
} catch (IOException|AdbCommandRejectedException|
ShellCommandUnresponsiveException|TimeoutException e) {
CLog.w("Failed to get logcat dump from %s: %s", getSerialNumber(), e.getMessage());
CLog.e(e);
}
return new ByteArrayInputStreamSource(output);
}
/**
* {@inheritDoc}
*/
@Override
public 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,
LOGCAT_DUMP_TIMEOUT, TimeUnit.MILLISECONDS);
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}. */
@VisibleForTesting
LogcatReceiver createLogcatReceiver() {
String logcatOptions = mOptions.getLogcatOptions();
if (logcatOptions == null) {
return new LogcatReceiver(this, mOptions.getMaxLogcatDataSize(), mLogStartDelay);
} else {
return new LogcatReceiver(this,
String.format("%s %s", LogcatReceiver.LOGCAT_CMD, logcatOptions),
mOptions.getMaxLogcatDataSize(), mLogStartDelay);
}
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getBugreport() {
if (getApiLevelSafe() < 24) {
InputStreamSource bugreport = getBugreportInternal();
if (bugreport == null) {
// Safe call so we don't return null but an empty resource.
return new ByteArrayInputStreamSource("".getBytes());
}
return bugreport;
}
CLog.d("Api level above 24, using bugreportz instead.");
File mainEntry = null;
File bugreportzFile = null;
try {
bugreportzFile = getBugreportzInternal();
if (bugreportzFile == null) {
bugreportzFile = bugreportzFallback();
}
if (bugreportzFile == null) {
// return empty buffer
return new ByteArrayInputStreamSource("".getBytes());
}
try (ZipFile zip = new ZipFile(bugreportzFile)) {
// We get the main_entry.txt that contains the bugreport name.
mainEntry = ZipUtil2.extractFileFromZip(zip, "main_entry.txt");
String bugreportName = FileUtil.readStringFromFile(mainEntry).trim();
CLog.d("bugreport name: '%s'", bugreportName);
File bugreport = ZipUtil2.extractFileFromZip(zip, bugreportName);
return new FileInputStreamSource(bugreport, true);
}
} catch (IOException e) {
CLog.e("Error while unzipping bugreportz");
CLog.e(e);
return new ByteArrayInputStreamSource("corrupted bugreport.".getBytes());
} finally {
FileUtil.deleteFile(bugreportzFile);
FileUtil.deleteFile(mainEntry);
}
}
/**
* If first bugreportz collection was interrupted for any reasons, the temporary file where the
* dumpstate is redirected could exists if it started. We attempt to get it to have some partial
* data.
*/
private File bugreportzFallback() {
try {
IFileEntry entries = getFileEntry(BUGREPORTZ_TMP_PATH);
if (entries != null) {
for (IFileEntry f : entries.getChildren(false)) {
String name = f.getName();
CLog.d("bugreport entry: %s", name);
// Only get left-over zipped data to avoid confusing data types.
if (name.endsWith(".zip")) {
File pulledZip = pullFile(BUGREPORTZ_TMP_PATH + name);
try {
// Validate the zip before returning it.
if (ZipUtil.isZipFileValid(pulledZip, false)) {
return pulledZip;
}
} catch (IOException e) {
CLog.e(e);
}
CLog.w("Failed to get a valid bugreportz.");
// if zip validation failed, delete it and return null.
FileUtil.deleteFile(pulledZip);
return null;
}
}
CLog.w("Could not find a tmp bugreport file in the directory.");
} else {
CLog.w("Could not find the file entry: '%s' on the device.", BUGREPORTZ_TMP_PATH);
}
} catch (DeviceNotAvailableException e) {
CLog.e(e);
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean logBugreport(String dataName, ITestLogger listener) {
InputStreamSource bugreport = null;
LogDataType type = null;
try {
bugreport = getBugreportz();
type = LogDataType.BUGREPORTZ;
if (bugreport == null) {
CLog.d("Bugreportz failed, attempting bugreport collection instead.");
bugreport = getBugreportInternal();
type = LogDataType.BUGREPORT;
}
// log what we managed to capture.
if (bugreport != null) {
listener.testLog(dataName, type, bugreport);
return true;
}
} finally {
StreamUtil.cancel(bugreport);
}
CLog.d(
"logBugreport() was not successful in collecting and logging the bugreport "
+ "for device %s",
getSerialNumber());
return false;
}
/**
* {@inheritDoc}
*/
@Override
public Bugreport takeBugreport() {
File bugreportFile = null;
int apiLevel = getApiLevelSafe();
if (apiLevel == UNKNOWN_API_LEVEL) {
return null;
}
if (apiLevel >= 24) {
CLog.d("Api level above 24, using bugreportz.");
bugreportFile = getBugreportzInternal();
if (bugreportFile != null) {
return new Bugreport(bugreportFile, true);
}
return null;
}
// fall back to regular bugreport
InputStreamSource bugreport = getBugreportInternal();
if (bugreport == null) {
CLog.e("Error when collecting the bugreport.");
return null;
}
try {
bugreportFile = FileUtil.createTempFile("bugreport", ".txt");
FileUtil.writeToFile(bugreport.createInputStream(), bugreportFile);
return new Bugreport(bugreportFile, false);
} catch (IOException e) {
CLog.e("Error when writing the bugreport file");
CLog.e(e);
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getBugreportz() {
if (getApiLevelSafe() < 24) {
return null;
}
File bugreportZip = getBugreportzInternal();
if (bugreportZip == null) {
bugreportZip = bugreportzFallback();
}
if (bugreportZip != null) {
return new FileInputStreamSource(bugreportZip, true);
}
return null;
}
/** Internal Helper method to get the bugreportz zip file as a {@link File}. */
@VisibleForTesting
protected File getBugreportzInternal() {
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
// Does not rely on {@link ITestDevice#executeAdbCommand(String...)} because it does not
// provide a timeout.
try {
executeShellCommand(BUGREPORTZ_CMD, receiver,
BUGREPORTZ_TIMEOUT, TimeUnit.MILLISECONDS, 0 /* don't retry */);
String output = receiver.getOutput().trim();
Matcher match = BUGREPORTZ_RESPONSE_PATTERN.matcher(output);
if (!match.find()) {
CLog.e("Something went went wrong during bugreportz collection: '%s'", output);
return null;
} else {
String remoteFilePath = match.group(2);
File zipFile = null;
try {
if (!doesFileExist(remoteFilePath)) {
CLog.e("Did not find bugreportz at: %s", remoteFilePath);
return null;
}
// Create a placeholder to replace the file
zipFile = FileUtil.createTempFile("bugreportz", ".zip");
pullFile(remoteFilePath, zipFile);
String bugreportDir =
remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/'));
if (!bugreportDir.isEmpty()) {
// clean bugreport files directory on device
executeShellCommand(String.format("rm %s/*", bugreportDir));
}
return zipFile;
} catch (IOException e) {
CLog.e("Failed to create the temporary file.");
return null;
}
}
} catch (DeviceNotAvailableException e) {
CLog.e("Device %s became unresponsive while retrieving bugreportz", getSerialNumber());
CLog.e(e);
}
return null;
}
protected InputStreamSource getBugreportInternal() {
CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
try {
executeShellCommand(
BUGREPORT_CMD,
receiver,
BUGREPORT_TIMEOUT,
TimeUnit.MILLISECONDS,
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 null;
}
return new ByteArrayInputStreamSource(receiver.getOutput());
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getScreenshot() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Screenshot");
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Screenshot");
}
/** {@inheritDoc} */
@Override
public InputStreamSource getScreenshot(String format, boolean rescale)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Screenshot");
}
/** {@inheritDoc} */
@Override
public void clearLastConnectedWifiNetwork() {
mLastConnectedWifiSsid = null;
mLastConnectedWifiPsk = null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk)
throws DeviceNotAvailableException {
return connectToWifiNetwork(wifiSsid, wifiPsk, false);
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk, boolean scanSsid)
throws DeviceNotAvailableException {
// Clears the last connected wifi network.
mLastConnectedWifiSsid = null;
mLastConnectedWifiPsk = null;
// Connects to wifi network. It retries up to {@link TestDeviceOptions@getWifiAttempts()}
// times
Random rnd = new Random();
int backoffSlotCount = 2;
int slotTime = mOptions.getWifiRetryWaitTime();
int waitTime = 0;
IWifiHelper wifi = createWifiHelper();
long startTime = mClock.millis();
for (int i = 1; i <= mOptions.getWifiAttempts(); i++) {
CLog.i("Connecting to wifi network %s on %s", wifiSsid, getSerialNumber());
boolean success =
wifi.connectToNetwork(wifiSsid, wifiPsk, mOptions.getConnCheckUrl(), scanSsid);
final Map<String, String> wifiInfo = wifi.getWifiInfo();
if (success) {
CLog.i(
"Successfully connected to wifi network %s(%s) on %s",
wifiSsid, wifiInfo.get("bssid"), getSerialNumber());
mLastConnectedWifiSsid = wifiSsid;
mLastConnectedWifiPsk = wifiPsk;
return true;
} else {
CLog.w(
"Failed to connect to wifi network %s(%s) on %s on attempt %d of %d",
wifiSsid,
wifiInfo.get("bssid"),
getSerialNumber(),
i,
mOptions.getWifiAttempts());
}
if (mClock.millis() - startTime >= mOptions.getMaxWifiConnectTime()) {
CLog.e(
"Failed to connect to wifi after %d ms. Aborting.",
mOptions.getMaxWifiConnectTime());
break;
}
if (i < mOptions.getWifiAttempts()) {
if (mOptions.isWifiExpoRetryEnabled()) {
// use binary exponential back-offs when retrying.
waitTime = rnd.nextInt(backoffSlotCount) * slotTime;
backoffSlotCount *= 2;
}
CLog.e("Waiting for %d ms before reconnecting to %s...", waitTime, wifiSsid);
getRunUtil().sleep(waitTime);
}
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean checkConnectivity() throws DeviceNotAvailableException {
IWifiHelper wifi = createWifiHelper();
return wifi.checkConnectivity(mOptions.getConnCheckUrl());
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetworkIfNeeded(String wifiSsid, String wifiPsk)
throws DeviceNotAvailableException {
return connectToWifiNetworkIfNeeded(wifiSsid, wifiPsk, false);
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetworkIfNeeded(String wifiSsid, String wifiPsk, boolean scanSsid)
throws DeviceNotAvailableException {
if (!checkConnectivity()) {
return connectToWifiNetwork(wifiSsid, wifiPsk, scanSsid);
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isWifiEnabled() throws DeviceNotAvailableException {
final IWifiHelper wifi = createWifiHelper();
try {
return wifi.isWifiEnabled();
} catch (RuntimeException e) {
CLog.w("Failed to create WifiHelper: %s", e.getMessage());
return false;
}
}
/**
* Checks that the device is currently successfully connected to given wifi SSID.
*
* @param wifiSSID the wifi ssid
* @return <code>true</code> if device is currently connected to wifiSSID and has network
* connectivity. <code>false</code> otherwise
* @throws DeviceNotAvailableException if connection with device was lost
*/
boolean checkWifiConnection(String wifiSSID) throws DeviceNotAvailableException {
CLog.i("Checking connection with wifi network %s on %s", wifiSSID, getSerialNumber());
final IWifiHelper wifi = createWifiHelper();
// getSSID returns SSID as "SSID"
final String quotedSSID = String.format("\"%s\"", wifiSSID);
boolean test = wifi.isWifiEnabled();
CLog.v("%s: wifi enabled? %b", getSerialNumber(), test);
if (test) {
final String actualSSID = wifi.getSSID();
test = quotedSSID.equals(actualSSID);
CLog.v("%s: SSID match (%s, %s, %b)", getSerialNumber(), quotedSSID, actualSSID, test);
}
if (test) {
test = wifi.hasValidIp();
CLog.v("%s: validIP? %b", getSerialNumber(), test);
}
if (test) {
test = checkConnectivity();
CLog.v("%s: checkConnectivity returned %b", getSerialNumber(), test);
}
return test;
}
/**
* {@inheritDoc}
*/
@Override
public boolean disconnectFromWifi() throws DeviceNotAvailableException {
CLog.i("Disconnecting from wifi on %s", getSerialNumber());
// Clears the last connected wifi network.
mLastConnectedWifiSsid = null;
mLastConnectedWifiPsk = null;
IWifiHelper wifi = createWifiHelper();
return wifi.disconnectFromNetwork();
}
/**
* {@inheritDoc}
*/
@Override
public String getIpAddress() throws DeviceNotAvailableException {
IWifiHelper wifi = createWifiHelper();
return wifi.getIpAddress();
}
/**
* {@inheritDoc}
*/
@Override
public boolean enableNetworkMonitor() throws DeviceNotAvailableException {
mNetworkMonitorEnabled = false;
IWifiHelper wifi = createWifiHelper();
wifi.stopMonitor();
if (wifi.startMonitor(NETWORK_MONITOR_INTERVAL, mOptions.getConnCheckUrl())) {
mNetworkMonitorEnabled = true;
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean disableNetworkMonitor() throws DeviceNotAvailableException {
mNetworkMonitorEnabled = false;
IWifiHelper wifi = createWifiHelper();
List<Long> samples = wifi.stopMonitor();
if (!samples.isEmpty()) {
int failures = 0;
long totalLatency = 0;
for (Long sample : samples) {
if (sample < 0) {
failures += 1;
} else {
totalLatency += sample;
}
}
double failureRate = failures * 100.0 / samples.size();
double avgLatency = 0.0;
if (failures < samples.size()) {
avgLatency = totalLatency / (samples.size() - failures);
}
CLog.d("[metric] url=%s, window=%ss, failure_rate=%.2f%%, latency_avg=%.2f",
mOptions.getConnCheckUrl(), samples.size() * NETWORK_MONITOR_INTERVAL / 1000,
failureRate, avgLatency);
}
return true;
}
/**
* Create a {@link WifiHelper} to use
*
* <p>
*
* @throws DeviceNotAvailableException
*/
@VisibleForTesting
IWifiHelper createWifiHelper() throws DeviceNotAvailableException {
// current wifi helper won't work on AndroidNativeDevice
// TODO: create a new Wifi helper with supported feature of AndroidNativeDevice when
// we learn what is available.
throw new UnsupportedOperationException("Wifi helper is not supported.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean clearErrorDialogs() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Screen's features");
}
/** {@inheritDoc} */
@Override
public KeyguardControllerState getKeyguardState() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for keyguard querying.");
}
IDeviceStateMonitor getDeviceStateMonitor() {
return mStateMonitor;
}
/**
* {@inheritDoc}
*/
@Override
public void postBootSetup() throws DeviceNotAvailableException {
enableAdbRoot();
prePostBootSetup();
for (String command : mOptions.getPostBootCommands()) {
executeShellCommand(command);
}
}
/**
* Allows each device type (AndroidNativeDevice, TestDevice) to override this method for
* specific post boot setup.
* @throws DeviceNotAvailableException
*/
protected void prePostBootSetup() throws DeviceNotAvailableException {
// Empty on purpose.
}
/**
* Ensure wifi connection is re-established after boot. This is intended to be called after TF
* initiated reboots(ones triggered by {@link #reboot()}) only.
*
* @throws DeviceNotAvailableException
*/
void postBootWifiSetup() throws DeviceNotAvailableException {
if (mLastConnectedWifiSsid != null) {
reconnectToWifiNetwork();
}
if (mNetworkMonitorEnabled) {
if (!enableNetworkMonitor()) {
CLog.w("Failed to enable network monitor on %s after reboot", getSerialNumber());
}
}
}
void reconnectToWifiNetwork() throws DeviceNotAvailableException {
// First, wait for wifi to re-connect automatically.
long startTime = System.currentTimeMillis();
boolean isConnected = checkConnectivity();
while (!isConnected && (System.currentTimeMillis() - startTime) < WIFI_RECONNECT_TIMEOUT) {
getRunUtil().sleep(WIFI_RECONNECT_CHECK_INTERVAL);
isConnected = checkConnectivity();
}
if (isConnected) {
return;
}
// If wifi is still not connected, try to re-connect on our own.
final String wifiSsid = mLastConnectedWifiSsid;
if (!connectToWifiNetworkIfNeeded(mLastConnectedWifiSsid, mLastConnectedWifiPsk)) {
throw new NetworkNotAvailableException(
String.format("Failed to connect to wifi network %s on %s after reboot",
wifiSsid, getSerialNumber()));
}
}
/**
* {@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 (!mStateMonitor.waitForDeviceBootloader(mOptions.getFastbootTimeout())) {
recoverDeviceFromBootloader();
}
}
private void doAdbRebootBootloader() throws DeviceNotAvailableException {
doAdbReboot("bootloader");
}
/**
* {@inheritDoc}
*/
@Override
public void reboot() throws DeviceNotAvailableException {
rebootUntilOnline();
RecoveryMode cachedRecoveryMode = getRecoveryMode();
setRecoveryMode(RecoveryMode.ONLINE);
if (isEncryptionSupported() && isDeviceEncrypted()) {
unlockDevice();
}
setRecoveryMode(cachedRecoveryMode);
if (mStateMonitor.waitForDeviceAvailable(mOptions.getRebootTimeout()) != null) {
postBootSetup();
postBootWifiSetup();
return;
} else {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public void rebootUntilOnline() throws DeviceNotAvailableException {
doReboot();
RecoveryMode cachedRecoveryMode = getRecoveryMode();
setRecoveryMode(RecoveryMode.ONLINE);
if (mStateMonitor.waitForDeviceOnline() != null) {
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();
}
@VisibleForTesting
void doReboot() throws DeviceNotAvailableException, UnsupportedOperationException {
if (TestDeviceState.FASTBOOT == getDeviceState()) {
CLog.i("device %s in fastboot. Rebooting to userspace.", getSerialNumber());
executeFastbootCommand("reboot");
} else {
if (mOptions.shouldDisableReboot()) {
CLog.i("Device reboot disabled by options, skipped.");
return;
}
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
*/
protected 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);
}
protected void waitForDeviceNotAvailable(String operationDesc, long time) {
// TODO: a bit of a race condition here. Would be better to start a
// before the operation
if (!mStateMonitor.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());
// Still check for online, in some case we could see the root, but device could be
// very early in its cycle.
waitForDeviceOnline();
return true;
}
// Don't enable root if user requested no root
if (!isEnableAdbRoot()) {
CLog.i("\"enable-root\" set to false; ignoring 'adb root' request");
return false;
}
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);
postAdbRootAction();
// 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 disableAdbRoot() throws DeviceNotAvailableException {
if (!isAdbRoot()) {
CLog.i("adb is already unroot on %s", getSerialNumber());
return true;
}
CLog.i("adb unroot on device %s", getSerialNumber());
int attempts = MAX_RETRY_ATTEMPTS + 1;
for (int i=1; i <= attempts; i++) {
String output = executeAdbCommand("unroot");
// wait for device to disappear from adb
waitForDeviceNotAvailable("unroot", 5 * 1000);
postAdbUnrootAction();
// wait for device to be back online
waitForDeviceOnline();
if (!isAdbRoot()) {
return true;
}
CLog.w("'adb unroot' on %s unsuccessful on attempt %d of %d. Output: '%s'",
getSerialNumber(), i, attempts, output);
}
return false;
}
/**
* Override if the device needs some specific actions to be taken after adb root and before the
* device is back online.
* Default implementation doesn't include any addition actions.
* adb root is not guaranteed to be enabled at this stage.
* @throws DeviceNotAvailableException
*/
public void postAdbRootAction() throws DeviceNotAvailableException {
// Empty on purpose.
}
/**
* Override if the device needs some specific actions to be taken after adb unroot and before
* the device is back online.
* Default implementation doesn't include any additional actions.
* adb root is not guaranteed to be disabled at this stage.
* @throws DeviceNotAvailableException
*/
public void postAdbUnrootAction() throws DeviceNotAvailableException {
// Empty on purpose.
}
/**
* {@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;
long timeout;
if (inplace) {
encryptMethod = "inplace";
timeout = ENCRYPTION_INPLACE_TIMEOUT_MIN;
} else {
encryptMethod = "wipe";
timeout = ENCRYPTION_WIPE_TIMEOUT_MIN;
}
CLog.i("Encrypting device %s via %s", getSerialNumber(), encryptMethod);
// enable crypto takes one of the following formats:
// cryptfs enablecrypto <wipe|inplace> <passwd>
// cryptfs enablecrypto <wipe|inplace> default|password|pin|pattern [passwd]
// Try the first one first, if it outputs "500 0 Usage: ...", try the second.
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
String command = String.format("vdc cryptfs enablecrypto %s \"%s\"", encryptMethod,
ENCRYPTION_PASSWORD);
executeShellCommand(command, receiver, timeout, TimeUnit.MINUTES, 1);
if (receiver.getOutput().split(":")[0].matches("500 \\d+ Usage")) {
command = String.format("vdc cryptfs enablecrypto %s default", encryptMethod);
executeShellCommand(command, new NullOutputReceiver(), timeout, TimeUnit.MINUTES, 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");
rebootUntilOnline();
waitForDeviceAvailable(ENCRYPTION_WIPE_TIMEOUT_MIN * 60 * 1000);
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 = getProperty("ro.crypto.state");
if (output == null && isEncryptionSupported()) {
CLog.w("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
&& Pattern.matches("(500)(\\s+)(\\d+)(\\s+)(Usage)(.*)(:)(.*)", output));
return mIsEncryptionSupported;
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceOnline(long waitTime) throws DeviceNotAvailableException {
if (mStateMonitor.waitForDeviceOnline(waitTime) == null) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceOnline() throws DeviceNotAvailableException {
if (mStateMonitor.waitForDeviceOnline() == null) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceAvailable(long waitTime) throws DeviceNotAvailableException {
if (mStateMonitor.waitForDeviceAvailable(waitTime) == null) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public void waitForDeviceAvailable() throws DeviceNotAvailableException {
if (mStateMonitor.waitForDeviceAvailable() == null) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForDeviceNotAvailable(long waitTime) {
return mStateMonitor.waitForDeviceNotAvailable(waitTime);
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForDeviceInRecovery(long waitTime) {
return mStateMonitor.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. */
@VisibleForTesting
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 boolean isFastbootEnabled() {
return mFastbootEnabled;
}
/**
* {@inheritDoc}
*/
@Override
public void setFastbootPath(String fastbootPath) {
mFastbootPath = fastbootPath;
// ensure the device and its associated recovery use the same fastboot version.
mRecovery.setFastbootPath(fastbootPath);
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootPath() {
return mFastbootPath;
}
/**
* {@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);
mStateMonitor.setState(deviceState);
}
}
/**
* {@inheritDoc}
*/
@Override
public TestDeviceState getDeviceState() {
return mState;
}
@Override
public boolean isAdbTcp() {
return mStateMonitor.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;
}
/**
* For emulator set {@link SizeLimitedOutputStream} to log output
* @param output to log the output
*/
public void setEmulatorOutputStream(SizeLimitedOutputStream output) {
mEmulatorOutput = output;
}
/**
* {@inheritDoc}
*/
@Override
public void stopEmulatorOutput() {
if (mEmulatorOutput != null) {
mEmulatorOutput.delete();
mEmulatorOutput = null;
}
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getEmulatorOutput() {
if (getIDevice().isEmulator()) {
if (mEmulatorOutput == null) {
CLog.w("Emulator output for %s was not captured in background",
getSerialNumber());
} else {
try {
return new SnapshotInputStreamSource(
"getEmulatorOutput", mEmulatorOutput.getData());
} catch (IOException e) {
CLog.e("Failed to get %s data.", getSerialNumber());
CLog.e(e);
}
}
}
return new ByteArrayInputStreamSource(new byte[0]);
}
/**
* {@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 {
throw new UnsupportedOperationException("No support for Package's feature");
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/**
* {@inheritDoc}
*/
@Override
public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Package's feature");
}
/**
* {@inheritDoc}
*/
@Override
public TestDeviceOptions getOptions() {
return mOptions;
}
/**
* {@inheritDoc}
*/
@Override
public int getApiLevel() throws DeviceNotAvailableException {
int apiLevel = UNKNOWN_API_LEVEL;
try {
String prop = getProperty("ro.build.version.sdk");
apiLevel = Integer.parseInt(prop);
} catch (NumberFormatException nfe) {
// ignore, return unknown instead
}
return apiLevel;
}
private int getApiLevelSafe() {
try {
return getApiLevel();
} catch (DeviceNotAvailableException e) {
CLog.e(e);
return UNKNOWN_API_LEVEL;
}
}
@Override
public IDeviceStateMonitor getMonitor() {
return mStateMonitor;
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForDeviceShell(long waitTime) {
return mStateMonitor.waitForDeviceShell(waitTime);
}
@Override
public DeviceAllocationState getAllocationState() {
return mAllocationState;
}
/**
* {@inheritDoc}
* <p>
* Process the DeviceEvent, which may or may not transition this device to a new allocation
* state.
* </p>
*/
@Override
public DeviceEventResponse handleAllocationEvent(DeviceEvent event) {
// keep track of whether state has actually changed or not
boolean stateChanged = false;
DeviceAllocationState newState;
DeviceAllocationState oldState = mAllocationState;
mAllocationStateLock.lock();
try {
// update oldState here, just in case in changed before we got lock
oldState = mAllocationState;
newState = mAllocationState.handleDeviceEvent(event);
if (oldState != newState) {
// state has changed! record this fact, and store the new state
stateChanged = true;
mAllocationState = newState;
}
} finally {
mAllocationStateLock.unlock();
}
if (stateChanged && mAllocationMonitor != null) {
// state has changed! Lets inform the allocation monitor listener
mAllocationMonitor.notifyDeviceStateChange(getSerialNumber(), oldState, newState);
}
return new DeviceEventResponse(newState, stateChanged);
}
/** {@inheritDoc} */
@Override
public long getDeviceTimeOffset(Date date) throws DeviceNotAvailableException {
Long deviceTime = getDeviceDate();
long offset = 0;
if (date == null) {
date = new Date();
}
offset = date.getTime() - deviceTime;
CLog.d("Time offset = %d ms", offset);
return offset;
}
/**
* {@inheritDoc}
*/
@Override
public void setDate(Date date) throws DeviceNotAvailableException {
if (date == null) {
date = new Date();
}
long timeOffset = getDeviceTimeOffset(date);
// no need to set date
if (Math.abs(timeOffset) <= MAX_HOST_DEVICE_TIME_OFFSET) {
return;
}
String dateString = null;
if (getApiLevel() < 23) {
// set date in epoch format
dateString = Long.toString(date.getTime() / 1000); //ms to s
} else {
// set date with POSIX like params
SimpleDateFormat sdf = new java.text.SimpleDateFormat(
"MMddHHmmyyyy.ss");
sdf.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
dateString = sdf.format(date);
}
// best effort, no verification
executeShellCommand("date -u " + dateString);
}
/**
* {@inheritDoc}
*/
@Override
public long getDeviceDate() throws DeviceNotAvailableException {
String deviceTimeString = executeShellCommand("date +%s");
Long deviceTime = null;
try {
deviceTime = Long.valueOf(deviceTimeString.trim());
} catch (NumberFormatException nfe) {
CLog.i("Invalid device time: \"%s\", ignored.", nfe);
return 0;
}
// Convert from seconds to milliseconds
return deviceTime * 1000L;
}
/**
* {@inheritDoc}
*/
@Override
public boolean waitForBootComplete(long timeOut) throws DeviceNotAvailableException {
return mStateMonitor.waitForBootComplete(timeOut);
}
/**
* {@inheritDoc}
*/
@Override
public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
@Override
public int getMaxNumberOfRunningUsersSupported() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean isMultiUserSupported() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public int createUserNoThrow(String name) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int createUser(String name, boolean guest, boolean ephemeral)
throws DeviceNotAvailableException, IllegalStateException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeUser(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean startUser(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean stopUser(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public void remountSystemWritable() throws DeviceNotAvailableException {
String verity = getProperty("partition.system.verified");
// have the property set (regardless state) implies verity is enabled, so we send adb
// command to disable verity
if (verity != null && !verity.isEmpty()) {
executeAdbCommand("disable-verity");
reboot();
}
executeAdbCommand("remount");
waitForDeviceAvailable();
}
/**
* {@inheritDoc}
*/
@Override
public Integer getPrimaryUserId() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int getCurrentUser() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public boolean isUserSecondary(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int getUserFlags(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public int getUserSerialNumber(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean switchUser(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean isUserRunning(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasFeature(String feature) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support pm's features.");
}
/**
* {@inheritDoc}
*/
@Override
public String getSetting(String namespace, String key)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public String getSetting(int userId, String namespace, String key)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/** {@inheritDoc} */
@Override
public Map<String, String> getAllSettings(String namespace) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public void setSetting(String namespace, String key, String value)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public void setSetting(int userId, String namespace, String key, String value)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for setting's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public String getBuildSigningKeys() throws DeviceNotAvailableException {
String buildTags = getProperty(BUILD_TAGS);
if (buildTags != null) {
String[] tags = buildTags.split(",");
for (String tag : tags) {
Matcher m = KEYS_PATTERN.matcher(tag);
if (m.matches()) {
return tag;
}
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String getAndroidId(int userId) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public boolean setDeviceOwner(String componentName, int userId)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public boolean removeAdmin(String componentName, int userId)
throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/** {@inheritDoc} */
@Override
public void removeOwners() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for user's feature.");
}
/**
* {@inheritDoc}
*/
@Override
public void disableKeyguard() throws DeviceNotAvailableException {
throw new UnsupportedOperationException("No support for Window Manager's features");
}
/** {@inheritDoc} */
@Override
public String getDeviceClass() {
IDevice device = getIDevice();
if (device == null) {
CLog.w("No IDevice instance, cannot determine device class.");
return "";
}
return device.getClass().getSimpleName();
}
/**
* {@inheritDoc}
*/
@Override
public void preInvocationSetup(IBuildInfo info)
throws TargetSetupError, DeviceNotAvailableException {
// Default implementation empty on purpose
}
/**
* {@inheritDoc}
*/
@Override
public void postInvocationTearDown() {
// Default implementation
if (getIDevice() instanceof StubDevice) {
return;
}
try {
// If we never installed it, don't even bother checking for it during tear down.
if (mContentProvider == null) {
return;
}
ContentProviderHandler handler = getContentProvider();
if (handler != null) {
handler.tearDown();
}
} catch (DeviceNotAvailableException e) {
CLog.e(e);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isHeadless() throws DeviceNotAvailableException {
if (getProperty(HEADLESS_PROP) != null) {
return true;
}
return false;
}
protected void checkApiLevelAgainst(String feature, int strictMinLevel) {
try {
if (getApiLevel() < strictMinLevel){
throw new IllegalArgumentException(String.format("%s not supported on %s. "
+ "Must be API %d.", feature, getSerialNumber(), strictMinLevel));
}
} catch (DeviceNotAvailableException e) {
throw new RuntimeException("Device became unavailable while checking API level", e);
}
}
/**
* {@inheritDoc}
*/
@Override
public DeviceDescriptor getDeviceDescriptor() {
IDeviceSelection selector = new DeviceSelectionOptions();
IDevice idevice = getIDevice();
return new DeviceDescriptor(
idevice.getSerialNumber(),
idevice instanceof StubDevice,
getAllocationState(),
getDisplayString(selector.getDeviceProductType(idevice)),
getDisplayString(selector.getDeviceProductVariant(idevice)),
getDisplayString(idevice.getProperty("ro.build.version.sdk")),
getDisplayString(idevice.getProperty("ro.build.id")),
getDisplayString(selector.getBatteryLevel(idevice)),
getDeviceClass(),
getDisplayString(getMacAddress()),
getDisplayString(getSimState()),
getDisplayString(getSimOperator()));
}
/**
* Return the displayable string for given object
*/
private String getDisplayString(Object o) {
return o == null ? "unknown" : o.toString();
}
/**
* {@inheritDoc}
*/
@Override
public List<ProcessInfo> getProcesses() throws DeviceNotAvailableException {
return PsParser.getProcesses(executeShellCommand(PS_COMMAND));
}
/**
* {@inheritDoc}
*/
@Override
public ProcessInfo getProcessByName(String processName) throws DeviceNotAvailableException {
List<ProcessInfo> processList = getProcesses();
for (ProcessInfo processInfo : processList) {
if (processName.equals(processInfo.getName())) {
return processInfo;
}
}
return null;
}
/**
* Validates that the given input is a valid MAC address
*
* @param address input to validate
* @return true if the input is a valid MAC address
*/
boolean isMacAddress(String address) {
Pattern macPattern = Pattern.compile(MAC_ADDRESS_PATTERN);
Matcher macMatcher = macPattern.matcher(address);
return macMatcher.find();
}
/**
* {@inheritDoc}
*/
@Override
public String getMacAddress() {
if (mIDevice instanceof StubDevice) {
// Do not query MAC addresses from stub devices.
return null;
}
if (!TestDeviceState.ONLINE.equals(mState)) {
// Only query MAC addresses from online devices.
return null;
}
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
try {
mIDevice.executeShellCommand(MAC_ADDRESS_COMMAND, receiver);
} catch (IOException | TimeoutException | AdbCommandRejectedException |
ShellCommandUnresponsiveException e) {
CLog.w("Failed to query MAC address for %s", mIDevice.getSerialNumber());
CLog.w(e);
}
String output = receiver.getOutput().trim();
if (isMacAddress(output)) {
return output;
}
CLog.d("No valid MAC address queried from device %s", mIDevice.getSerialNumber());
return null;
}
/** {@inheritDoc} */
@Override
public String getSimState() {
try {
return getProperty(SIM_STATE_PROP);
} catch (DeviceNotAvailableException e) {
CLog.w("Failed to query SIM state for %s", mIDevice.getSerialNumber());
CLog.w(e);
return null;
}
}
/** {@inheritDoc} */
@Override
public String getSimOperator() {
try {
return getProperty(SIM_OPERATOR_PROP);
} catch (DeviceNotAvailableException e) {
CLog.w("Failed to query SIM operator for %s", mIDevice.getSerialNumber());
CLog.w(e);
return null;
}
}
/** {@inheritDoc} */
@Override
public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException {
throw new UnsupportedOperationException("dumpHeap is not supported.");
}
/** {@inheritDoc} */
@Override
public String getProcessPid(String process) throws DeviceNotAvailableException {
String output = executeShellCommand(String.format("pidof %s", process)).trim();
if (checkValidPid(output)) {
return output;
}
CLog.e("Failed to find a valid pid for process.");
return null;
}
/** {@inheritDoc} */
@Override
public void logOnDevice(String tag, LogLevel level, String format, Object... args) {
String message = String.format(format, args);
try {
String levelLetter = logLevelToLogcatLevel(level);
String command = String.format("log -t %s -p %s '%s'", tag, levelLetter, message);
executeShellCommand(command);
} catch (DeviceNotAvailableException e) {
CLog.e("Device went not available when attempting to log '%s'", message);
CLog.e(e);
}
}
/** Convert the {@link LogLevel} to the letter used in log (see 'adb shell log --help'). */
private String logLevelToLogcatLevel(LogLevel level) {
switch (level) {
case DEBUG:
return "d";
case ERROR:
return "e";
case INFO:
return "i";
case VERBOSE:
return "v";
case WARN:
return "w";
default:
return "i";
}
}
/** {@inheritDoc} */
@Override
public long getTotalMemory() {
// "/proc/meminfo" always returns value in kilobytes.
long totalMemory = 0;
String output = null;
try {
output = executeShellCommand("cat /proc/meminfo | grep MemTotal");
} catch (DeviceNotAvailableException e) {
CLog.e(e);
return -1;
}
if (output.isEmpty()) {
return -1;
}
String[] results = output.split("\\s+");
try {
totalMemory = Long.parseLong(results[1].replaceAll("\\D+", ""));
} catch (ArrayIndexOutOfBoundsException | NumberFormatException e) {
CLog.e(e);
return -1;
}
return totalMemory * 1024;
}
/** Validate that pid is an integer and not empty. */
private boolean checkValidPid(String output) {
if (output.isEmpty()) {
return false;
}
try {
Integer.parseInt(output);
} catch (NumberFormatException e) {
CLog.e(e);
return false;
}
return true;
}
/** Gets the {@link IHostOptions} instance to use. */
@VisibleForTesting
IHostOptions getHostOptions() {
return GlobalConfiguration.getInstance().getHostOptions();
}
/** Returns the {@link ContentProviderHandler} or null if not available. */
@VisibleForTesting
ContentProviderHandler getContentProvider() throws DeviceNotAvailableException {
if (mContentProvider == null) {
mContentProvider = new ContentProviderHandler(this);
}
if (!mShouldSkipContentProviderSetup) {
boolean res = mContentProvider.setUp();
if (!res) {
return null;
}
}
return mContentProvider;
}
}