blob: b91c9034d938ebae82e2454f4a165b61d666aa55 [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.IShellOutputReceiver;
import com.android.ddmlib.InstallException;
import com.android.ddmlib.NullOutputReceiver;
import com.android.ddmlib.RawImage;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.SyncException;
import com.android.ddmlib.SyncException.SyncError;
import com.android.ddmlib.SyncService;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.ITestRunListener;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.SnapshotInputStreamSource;
import com.android.tradefed.result.StubTestRunListener;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.SizeLimitedOutputStream;
import com.android.tradefed.util.StreamUtil;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
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;
import javax.imageio.ImageIO;
/**
* Default implementation of a {@link ITestDevice}
* Non-full stack android devices.
*/
public class AndroidNativeDevice implements IManagedTestDevice {
/** the default number of command retry attempts to perform */
static final int MAX_RETRY_ATTEMPTS = 2;
private static final String BUGREPORT_CMD = "bugreport";
/** command to test input dispatch readiness **/
private static final String TEST_INPUT_CMD = "dumpsys input";
static final String LIST_PACKAGES_CMD = "pm list packages -f";
private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)");
/** 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
"^[/a-z]+\\s+\\d+\\s+\\d+\\s+(\\d+)\\s+\\d+%\\s+[/a-z]+$", Pattern.MULTILINE);
/**
* Allow pauses of up to 2 minutes while receiving bugreport. Note that dumpsys may pause up to
* a minute while waiting for unresponsive components, but should bail after that minute, if it
* will ever terminate on its own.
*/
private static final int BUGREPORT_TIMEOUT = 2 * 60 * 1000;
private 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;
/** Timeout to wait for input dispatch to become ready **/
private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000;
/** Beginning of the string returned by vdc for "vdc cryptfs enablecrypto". */
private static final String ENCRYPTION_SUPPORTED_CODE = "500";
/** Message in the string returned by vdc for "vdc cryptfs enablecrypto". */
private static final String ENCRYPTION_SUPPORTED_USAGE = "Usage: ";
/** The time in ms to wait before starting logcat for a device */
private int mLogStartDelay = 5*1000;
/** The time in ms to wait for a device to become unavailable. Should usually be short */
private static final int DEFAULT_UNAVAILABLE_TIMEOUT = 20 * 1000;
/** The time in ms to wait for a recovery that we skip because of the NONE mode */
static final int NONE_RECOVERY_MODE_DELAY = 1000;
/** number of attempts made to clear dialogs */
private static final int NUM_CLEAR_ATTEMPTS = 5;
/** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
static final String DISMISS_DIALOG_CMD = "input keyevent 23";
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";
static final String BUILD_CODENAME_PROP = "ro.build.version.codename";
static final String BUILD_TAGS = "ro.build.tags";
/** 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 int mCmdTimeout = 2 * 60 * 1000;
/** The time in ms to wait for a 'long' command to complete. */
private long mLongCmdTimeout = 25 * 60 * 1000;
private static final int FLAG_PRIMARY = 1; // From the UserInfo class
private IDevice mIDevice;
private IDeviceRecovery mRecovery = new WaitDeviceRecovery();
private final IDeviceStateMonitor mStateMonitor;
private TestDeviceState mState = TestDeviceState.ONLINE;
private final ReentrantLock mFastbootLock = new ReentrantLock();
private LogcatReceiver mLogcatReceiver;
private boolean mFastbootEnabled = true;
private TestDeviceOptions mOptions = new TestDeviceOptions();
private Process mEmulatorProcess;
private SizeLimitedOutputStream mEmulatorOutput;
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;
/**
* Interface for a generic device communication attempt.
*/
private abstract interface DeviceAction {
/**
* Execute the device operation.
*
* @return <code>true</code> if operation is performed successfully, <code>false</code>
* otherwise
* @throws Exception if operation terminated abnormally
*/
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException;
}
/**
* A {@link DeviceAction} for running a OS 'adb ....' command.
*/
private class AdbAction implements DeviceAction {
/** the output from the command */
String mOutput = null;
private String[] mCmd;
AdbAction(String[] cmd) {
mCmd = cmd;
}
@Override
public boolean run() throws TimeoutException, IOException {
CommandResult result = getRunUtil().runTimedCmd(getCommandTimeout(), mCmd);
// TODO: how to determine device not present with command failing for other reasons
if (result.getStatus() == CommandStatus.EXCEPTION) {
throw new IOException();
} else if (result.getStatus() == CommandStatus.TIMED_OUT) {
throw new TimeoutException();
} else if (result.getStatus() == CommandStatus.FAILED) {
// interpret as communication failure
throw new IOException();
}
mOutput = result.getStdout();
return true;
}
}
/**
* Creates a {@link TestDevice}.
*
* @param device the associated {@link IDevice}
* @param stateMonitor the {@link IDeviceStateMonitor} mechanism to use
* @param allocationMonitor the {@link IDeviceMonitor} to inform of allocation state changes.
* Can be null
*/
public AndroidNativeDevice(IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor) {
throwIfNull(device);
throwIfNull(stateMonitor);
mIDevice = device;
mStateMonitor = stateMonitor;
mAllocationMonitor = allocationMonitor;
}
/**
* Get the {@link RunUtil} instance to use.
* <p/>
* Exposed for unit testing.
*/
IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
/**
* {@inheritDoc}
*/
@Override
public void setOptions(TestDeviceOptions options) {
throwIfNull(options);
mOptions = options;
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
*/
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
*/
private 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 {
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}
*/
@Deprecated
@Override
public String getPropertySync(final String name) throws DeviceNotAvailableException {
return getProperty(name);
}
/**
* {@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);
}
/**
* {@see getProductType()}
*
* @param retryAttempts The number of times to try calling {@see recoverDevice()} if the
* device's product type cannot be found.
*/
private String internalGetProductType(int retryAttempts) throws DeviceNotAvailableException {
String productType = internalGetProperty("ro.hardware", "product", "Product type");
// Things will likely break if we don't have a valid product type. Try recovery (in case
// the device is only partially booted for some reason), and if that doesn't help, bail.
if (nullOrEmpty(productType)) {
if (retryAttempts > 0) {
recoverDevice();
productType = internalGetProductType(retryAttempts - 1);
}
if (nullOrEmpty(productType)) {
throw new DeviceNotAvailableException(String.format(
"Could not determine product type for device %s.", getSerialNumber()));
}
}
return productType;
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootProductType()
throws DeviceNotAvailableException, UnsupportedOperationException {
return getFastbootVariable("product");
}
/**
* {@inheritDoc}
*/
@Override
public String getProductVariant() throws DeviceNotAvailableException {
return internalGetProperty("ro.product.device", "variant", "Product variant");
}
/**
* {@inheritDoc}
*/
@Override
public String getFastbootProductVariant()
throws DeviceNotAvailableException, UnsupportedOperationException {
return getFastbootVariable("variant");
}
private String getFastbootVariable(String variableName)
throws DeviceNotAvailableException, UnsupportedOperationException {
CommandResult result = executeFastbootCommand("getvar", variableName);
if (result.getStatus() == CommandStatus.SUCCESS) {
Pattern fastbootProductPattern = Pattern.compile(variableName + ":\\s(.*)\\s");
// fastboot is weird, and may dump the output on stderr instead of stdout
String resultText = result.getStdout();
if (resultText == null || resultText.length() < 1) {
resultText = result.getStderr();
}
Matcher matcher = fastbootProductPattern.matcher(resultText);
if (matcher.find()) {
return matcher.group(1);
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String 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}
*/
@Deprecated
@Override
public void executeShellCommand(final String command, final IShellOutputReceiver receiver,
final int maxTimeToOutputShellResponse, int retryAttempts)
throws DeviceNotAvailableException {
executeShellCommand(command, receiver,
maxTimeToOutputShellResponse, TimeUnit.MILLISECONDS, retryAttempts);
}
/**
* {@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 String executeShellCommand(String command) throws DeviceNotAvailableException {
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
executeShellCommand(command, receiver);
String output = receiver.getOutput();
CLog.v("%s on %s returned %s", command, getSerialNumber(), output);
return output;
}
/**
* {@inheritDoc}
*/
@Override
public boolean runInstrumentationTests(final IRemoteAndroidTestRunner runner,
final Collection<ITestRunListener> listeners) throws DeviceNotAvailableException {
RunFailureListener failureListener = new RunFailureListener();
listeners.add(failureListener);
DeviceAction runTestsAction = new DeviceAction() {
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
runner.run(listeners);
return true;
}
};
boolean result = performDeviceAction(String.format("run %s instrumentation tests",
runner.getPackageName()), runTestsAction, 0);
if (failureListener.isRunFailure()) {
// run failed, might be system crash. Ensure device is up
if (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<ITestRunListener> 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,
ITestRunListener... listeners) throws DeviceNotAvailableException {
List<ITestRunListener> listenerList = new ArrayList<ITestRunListener>();
listenerList.addAll(Arrays.asList(listeners));
return runInstrumentationTests(runner, listenerList);
}
/**
* {@inheritDoc}
*/
@Override
public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
ITestRunListener... 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 {
//TODO: only keep API Level check once M is official
if (getApiLevel() > 22) {
return true;
}
String codeName = getProperty(BUILD_CODENAME_PROP).trim();
if (!"REL".equals(codeName)) {
// this is a development platform, check code name, if less than M, then not supported
if (codeName.charAt(0) < 'M') {
return false;
}
} else {
// released platform, none supports runtime permission yet
return false;
}
try {
long buildNumber = Long.parseLong(getBuildId());
// for platform commit 429270c3ed1da02914efb476be977dc3829d4c30
return buildNumber >= 1837705;
} catch (NumberFormatException nfe) {
// build id field is not a number, probably an eng build since we've already checked
// code name, assuming supported
return true;
}
}
/**
* helper method to throw exception if runtime permission isn't supported
* @throws DeviceNotAvailableException
*/
private void ensureRuntimePermissionSupported() throws DeviceNotAvailableException {
boolean runtimePermissionSupported = isRuntimePermissionSupported();
if (!runtimePermissionSupported) {
throw new UnsupportedOperationException(
"platform on device does not support runtime permission granting!");
}
}
/**
* Core implementation of package installation, with retries around
* {@link IDevice#installPackage(String, boolean, String...)}
* @param packageFile
* @param reinstall
* @param extraArgs
* @return the response from the installation
* @throws DeviceNotAvailableException
*/
private String internalInstallPackage(
final File packageFile, final boolean reinstall, final List<String> extraArgs)
throws DeviceNotAvailableException {
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction installAction = new DeviceAction() {
@Override
public boolean run() throws InstallException {
String result = getIDevice().installPackage(packageFile.getAbsolutePath(),
reinstall, extraArgs.toArray(new String[]{}));
response[0] = result;
return result == null;
}
};
performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
installAction, MAX_RETRY_ATTEMPTS);
return response[0];
}
/**
* {@inheritDoc}
*/
@Override
public String installPackage(final File packageFile, final boolean reinstall,
final String... extraArgs) throws DeviceNotAvailableException {
boolean runtimePermissionSupported = isRuntimePermissionSupported();
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
// grant all permissions by default if feature is supported
if (runtimePermissionSupported) {
args.add("-g");
}
return internalInstallPackage(packageFile, reinstall, args);
}
/**
* {@inheritDoc}
*/
@Override
public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
String... extraArgs) throws DeviceNotAvailableException {
ensureRuntimePermissionSupported();
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
if (grantPermissions) {
args.add("-g");
}
return internalInstallPackage(packageFile, reinstall, args);
}
/**
* {@inheritDoc}
*/
@Override
public String installPackageForUser(File packageFile, boolean reinstall, int userId,
String... extraArgs) throws DeviceNotAvailableException {
boolean runtimePermissionSupported = isRuntimePermissionSupported();
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
// grant all permissions by default if feature is supported
if (runtimePermissionSupported) {
args.add("-g");
}
args.add("--user");
args.add(Integer.toString(userId));
return internalInstallPackage(packageFile, reinstall, args);
}
/**
* {@inheritDoc}
*/
@Override
public String installPackageForUser(File packageFile, boolean reinstall,
boolean grantPermissions, int userId, String... extraArgs)
throws DeviceNotAvailableException {
ensureRuntimePermissionSupported();
List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
if (grantPermissions) {
args.add("-g");
}
args.add("--user");
args.add(Integer.toString(userId));
return internalInstallPackage(packageFile, reinstall, args);
}
public String installPackage(final File packageFile, final File certFile,
final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction installAction = new DeviceAction() {
@Override
public boolean run() throws InstallException, SyncException, IOException,
TimeoutException, AdbCommandRejectedException {
// TODO: create a getIDevice().installPackage(File, File...) method when the dist
// cert functionality is ready to be open sourced
String remotePackagePath = getIDevice().syncPackageToDevice(
packageFile.getAbsolutePath());
String remoteCertPath = getIDevice().syncPackageToDevice(
certFile.getAbsolutePath());
// trick installRemotePackage into issuing a 'pm install <apk> <cert>' command,
// by adding apk path to extraArgs, and using cert as the 'apk file'
String[] newExtraArgs = new String[extraArgs.length + 1];
System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
newExtraArgs[newExtraArgs.length - 1] = String.format("\"%s\"", remotePackagePath);
try {
response[0] = getIDevice().installRemotePackage(remoteCertPath, reinstall,
newExtraArgs);
} finally {
getIDevice().removeRemotePackage(remotePackagePath);
getIDevice().removeRemotePackage(remoteCertPath);
}
return true;
}
};
performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
installAction, MAX_RETRY_ATTEMPTS);
return response[0];
}
/**
* {@inheritDoc}
*/
@Override
public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
// use array to store response, so it can be returned to caller
final String[] response = new String[1];
DeviceAction uninstallAction = new DeviceAction() {
@Override
public boolean run() throws InstallException {
CLog.d("Uninstalling %s", packageName);
String result = getIDevice().uninstallPackage(packageName);
response[0] = result;
return result == null;
}
};
performDeviceAction(String.format("uninstall %s", packageName), uninstallAction,
MAX_RETRY_ATTEMPTS);
return response[0];
}
/**
* {@inheritDoc}
*/
@Override
public boolean pullFile(final String remoteFilePath, final File localFile)
throws DeviceNotAvailableException {
DeviceAction pullAction = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
SyncException {
SyncService syncService = null;
boolean status = false;
try {
syncService = getIDevice().getSyncService();
syncService.pullFile(interpolatePathVariables(remoteFilePath),
localFile.getAbsolutePath(), SyncService.getNullProgressMonitor());
status = true;
} catch (SyncException e) {
CLog.w("Failed to pull %s from %s to %s. Message %s", remoteFilePath,
getSerialNumber(), localFile.getAbsolutePath(), e.getMessage());
throw e;
} finally {
if (syncService != null) {
syncService.close();
}
}
return status;
}
};
return performDeviceAction(String.format("pull %s to %s", remoteFilePath,
localFile.getAbsolutePath()), pullAction, MAX_RETRY_ATTEMPTS);
}
/**
* {@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': %s", remoteFilePath, e);
} finally {
if (!success) {
FileUtil.deleteFile(localFile);
}
}
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 {
DeviceAction pushAction = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
SyncException {
SyncService syncService = null;
boolean status = false;
try {
syncService = getIDevice().getSyncService();
syncService.pushFile(localFile.getAbsolutePath(),
interpolatePathVariables(remoteFilePath),
SyncService.getNullProgressMonitor());
status = true;
} catch (SyncException e) {
CLog.w("Failed to push %s to %s on device %s. Message %s",
localFile.getAbsolutePath(), remoteFilePath, getSerialNumber(),
e.getMessage());
throw e;
} finally {
if (syncService != null) {
syncService.close();
}
}
return status;
}
};
return performDeviceAction(String.format("push %s to %s", localFile.getAbsolutePath(),
remoteFilePath), pushAction, MAX_RETRY_ATTEMPTS);
}
/**
* {@inheritDoc}
*/
@Override
public boolean pushString(final String contents, final String remoteFilePath)
throws DeviceNotAvailableException {
File tmpFile = null;
try {
tmpFile = FileUtil.createTempFile("temp", ".txt");
FileUtil.writeToFile(contents, tmpFile);
return pushFile(tmpFile, remoteFilePath);
} catch (IOException e) {
CLog.e(e);
return false;
} finally {
if (tmpFile != null) {
tmpFile.delete();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean doesFileExist(String destPath) throws DeviceNotAvailableException {
String lsGrep = executeShellCommand(String.format("ls \"%s\"", destPath));
return !lsGrep.contains("No such file or directory");
}
/**
* {@inheritDoc}
*/
@Override
public long getExternalStoreFreeSpace() throws DeviceNotAvailableException {
CLog.i("Checking free space for %s", getSerialNumber());
String externalStorePath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
String output = getDfOutput(externalStorePath);
// 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(externalStorePath, 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));
}
/**
* 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<MountPointInfo>(mountInfoLines.length);
for (String line : mountInfoLines) {
// We ignore the last two fields
// /dev/block/mtdblock4 /cache yaffs2 rw,nosuid,nodev,relatime 0 0
final String[] parts = line.split("\\s+", 5);
list.add(new MountPointInfo(parts[0], parts[1], parts[2], parts[3]));
}
return list;
}
/**
* {@inheritDoc}
*/
@Override
public MountPointInfo getMountPointInfo(String mountpoint) throws DeviceNotAvailableException {
// The overhead of parsing all of the lines should be minimal
List<MountPointInfo> mountpoints = getMountPointInfo();
for (MountPointInfo info : mountpoints) {
if (mountpoint.equals(info.mountpoint)) return info;
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public IFileEntry getFileEntry(String path) throws DeviceNotAvailableException {
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));
}
/**
* Retrieve the {@link FileListingService} for the {@link IDevice}, making multiple attempts
* and recovery operations if necessary.
* <p/>
* This is necessary because {@link IDevice#getFileListingService()} can return
* <code>null</code> if device is in fastboot. The symptom of this condition is that the
* current {@link #getIDevice()} is a {@link StubDevice}.
*
* @return the {@link FileListingService}
* @throws DeviceNotAvailableException if device communication is lost.
*/
private FileListingService getFileListingService() throws DeviceNotAvailableException {
final FileListingService[] service = new FileListingService[1];
DeviceAction serviceAction = new DeviceAction() {
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
service[0] = getIDevice().getFileListingService();
if (service[0] == null) {
// could not get file listing service - must be a stub device - enter recovery
throw new IOException("Could not get file listing service");
}
return true;
}
};
performDeviceAction("getFileListingService", serviceAction, MAX_RETRY_ATTEMPTS);
return service[0];
}
/**
* {@inheritDoc}
*/
@Override
public boolean pushDir(File localFileDir, String deviceFilePath)
throws DeviceNotAvailableException {
if (!localFileDir.isDirectory()) {
CLog.e("file %s is not a directory", localFileDir.getAbsolutePath());
return false;
}
File[] childFiles = localFileDir.listFiles();
if (childFiles == null) {
CLog.e("Could not read files in %s", localFileDir.getAbsolutePath());
return false;
}
for (File childFile : childFiles) {
String remotePath = String.format("%s/%s", deviceFilePath, childFile.getName());
if (childFile.isDirectory()) {
executeShellCommand(String.format("mkdir %s", remotePath));
if (!pushDir(childFile, remotePath)) {
return false;
}
} else if (childFile.isFile()) {
if (!pushFile(childFile, remotePath)) {
return false;
}
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean syncFiles(File localFileDir, String deviceFilePath)
throws DeviceNotAvailableException {
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<String>();
for (File localFile : localFiles) {
IFileEntry entry = remoteFileEntry.findChild(localFile.getName());
if (entry == null) {
CLog.d("Detected missing file path %s", localFile.getAbsolutePath());
filePathsToSync.add(localFile.getAbsolutePath());
} else if (localFile.isDirectory()) {
// This directory exists remotely. recursively sync it to sync only its newer files
// contents
if (!syncFiles(localFile, entry)) {
return false;
}
} else if (isNewer(localFile, entry)) {
CLog.d("Detected newer file %s", localFile.getAbsolutePath());
filePathsToSync.add(localFile.getAbsolutePath());
}
}
if (filePathsToSync.size() == 0) {
CLog.d("No files to sync");
return true;
}
final String files[] = filePathsToSync.toArray(new String[filePathsToSync.size()]);
DeviceAction syncAction = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
SyncException {
SyncService syncService = null;
boolean status = false;
try {
syncService = getIDevice().getSyncService();
syncService.push(files, remoteFileEntry.getFileEntry(),
SyncService.getNullProgressMonitor());
status = true;
} catch (SyncException e) {
CLog.w("Failed to sync files to %s on device %s. Message %s",
remoteFileEntry.getFullPath(), getSerialNumber(), e.getMessage());
throw e;
} finally {
if (syncService != null) {
syncService.close();
}
}
return status;
}
};
return performDeviceAction(String.format("sync files %s", remoteFileEntry.getFullPath()),
syncAction, MAX_RETRY_ATTEMPTS);
}
/**
* Queries the file listing service for a given directory
*
* @param remoteFileEntry
* @throws DeviceNotAvailableException
*/
FileEntry[] getFileChildren(final FileEntry remoteFileEntry)
throws DeviceNotAvailableException {
// time this operation because its known to hang
FileQueryAction action = new FileQueryAction(remoteFileEntry,
getIDevice().getFileListingService());
performDeviceAction("buildFileCache", action, MAX_RETRY_ATTEMPTS);
return action.mFileContents;
}
private class FileQueryAction implements DeviceAction {
FileEntry[] mFileContents = null;
private final FileEntry mRemoteFileEntry;
private final FileListingService mService;
FileQueryAction(FileEntry remoteFileEntry, FileListingService service) {
throwIfNull(remoteFileEntry);
throwIfNull(service);
mRemoteFileEntry = remoteFileEntry;
mService = service;
}
@Override
public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
ShellCommandUnresponsiveException {
mFileContents = mService.getChildrenSync(mRemoteFileEntry);
return true;
}
}
/**
* A {@link FilenameFilter} that rejects hidden (ie starts with ".") files.
*/
private static class NoHiddenFilesFilter implements FilenameFilter {
/**
* {@inheritDoc}
*/
@Override
public boolean accept(File dir, String name) {
return !name.startsWith(".");
}
}
/**
* Return <code>true</code> if local file is newer than remote file.
*/
private boolean isNewer(File localFile, IFileEntry entry) {
// remote times are in GMT timezone
final String entryTimeString = String.format("%s %s GMT", entry.getDate(), entry.getTime());
try {
// expected format of a FileEntry's date and time
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm zzz");
Date remoteDate = format.parse(entryTimeString);
// localFile.lastModified has granularity of ms, but remoteDate.getTime only has
// granularity of minutes. Shift remoteDate.getTime() backward by one minute so newly
// modified files get synced
return localFile.lastModified() > (remoteDate.getTime() - 60 * 1000);
} catch (ParseException e) {
CLog.e("Error converting remote time stamp %s for %s on device %s", entryTimeString,
entry.getFullPath(), getSerialNumber());
}
// sync file by default
return true;
}
/**
* {@inheritDoc}
*/
@Override
public String executeAdbCommand(String... cmdArgs) throws DeviceNotAvailableException {
final String[] fullCmd = buildAdbCommand(cmdArgs);
AdbAction adbAction = new AdbAction(fullCmd);
performDeviceAction(String.format("adb %s", cmdArgs[0]), adbAction, MAX_RETRY_ATTEMPTS);
return adbAction.mOutput;
}
/**
* {@inheritDoc}
*/
@Override
public CommandResult executeFastbootCommand(String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
return doFastbootCommand(getCommandTimeout(), cmdArgs);
}
/**
* {@inheritDoc}
*/
@Override
public CommandResult executeLongFastbootCommand(String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
return doFastbootCommand(getLongCommandTimeout(), cmdArgs);
}
/**
* @param cmdArgs
* @throws DeviceNotAvailableException
*/
private CommandResult doFastbootCommand(final long timeout, String... cmdArgs)
throws DeviceNotAvailableException, UnsupportedOperationException {
if (!mFastbootEnabled) {
throw new UnsupportedOperationException(String.format(
"Attempted to fastboot on device %s , but fastboot is not available. Aborting.",
getSerialNumber()));
}
final String[] fullCmd = buildFastbootCommand(cmdArgs);
for (int i = 0; i < MAX_RETRY_ATTEMPTS; i++) {
CommandResult result = new CommandResult(CommandStatus.EXCEPTION);
// block state changes while executing a fastboot command, since
// device will disappear from fastboot devices while command is being executed
mFastbootLock.lock();
try {
result = getRunUtil().runTimedCmd(timeout, fullCmd);
} finally {
mFastbootLock.unlock();
}
if (!isRecoveryNeeded(result)) {
return result;
}
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()));
}
/**
* {@inheritDoc}
*/
@Override
public boolean getUseFastbootErase() {
return mOptions.getUseFastbootErase();
}
/**
* {@inheritDoc}
*/
@Override
public void setUseFastbootErase(boolean useFastbootErase) {
mOptions.setUseFastbootErase(useFastbootErase);
}
/**
* {@inheritDoc}
*/
@Override
public CommandResult fastbootWipePartition(String partition)
throws DeviceNotAvailableException {
if (mOptions.getUseFastbootErase()) {
return executeLongFastbootCommand("erase", partition);
} else {
return executeLongFastbootCommand("format", partition);
}
}
/**
* Evaluate the given fastboot result to determine if recovery mode needs to be entered
*
* @param fastbootResult the {@link CommandResult} from a fastboot command
* @return <code>true</code> if recovery mode should be entered, <code>false</code> otherwise.
*/
private boolean isRecoveryNeeded(CommandResult fastbootResult) {
if (fastbootResult.getStatus().equals(CommandStatus.TIMED_OUT)) {
// fastboot commands always time out if devices is not present
return true;
} else {
// check for specific error messages in result that indicate bad device communication
// and recovery mode is needed
if (fastbootResult.getStderr() == null ||
fastbootResult.getStderr().contains("data transfer failure (Protocol error)") ||
fastbootResult.getStderr().contains("status read failed (No such device)")) {
CLog.w("Bad fastboot response from device %s. stderr: %s. Entering recovery",
getSerialNumber(), fastbootResult.getStderr());
return true;
}
}
return false;
}
/**
* Get the max time allowed in ms for commands.
*/
int getCommandTimeout() {
return mCmdTimeout;
}
/**
* Set the max time allowed in ms for commands.
*/
void setLongCommandTimeout(long timeout) {
mLongCmdTimeout = timeout;
}
/**
* Get the max time allowed in ms for commands.
*/
long getLongCommandTimeout() {
return mLongCmdTimeout;
}
/**
* Set the max time allowed in ms for commands.
*/
void setCommandTimeout(int timeout) {
mCmdTimeout = timeout;
}
/**
* Builds the OS command for the given adb command and args
*/
private String[] buildAdbCommand(String... commandArgs) {
return ArrayUtil.buildArray(new String[] {"adb", "-s", getSerialNumber()},
commandArgs);
}
/**
* Builds the OS command for the given fastboot command and args
*/
private String[] buildFastbootCommand(String... commandArgs) {
return ArrayUtil.buildArray(new String[] {"fastboot", "-s", getSerialNumber()},
commandArgs);
}
/**
* Performs an action on this device. Attempts to recover device and optionally retry command
* if action fails.
*
* @param actionDescription a short description of action to be performed. Used for logging
* purposes only.
* @param action the action to be performed
* @param retryAttempts the retry attempts to make for action if it fails but
* recovery succeeds
* @returns <code>true</code> if action was performed successfully
* @throws DeviceNotAvailableException if recovery attempt fails or max attempts done without
* success
*/
private boolean performDeviceAction(String actionDescription, final DeviceAction action,
int retryAttempts) throws DeviceNotAvailableException {
for (int i = 0; i < retryAttempts + 1; i++) {
try {
return action.run();
} catch (TimeoutException e) {
logDeviceActionException(actionDescription, e);
} catch (IOException e) {
logDeviceActionException(actionDescription, e);
} catch (InstallException e) {
logDeviceActionException(actionDescription, e);
} catch (SyncException e) {
logDeviceActionException(actionDescription, e);
// a SyncException is not necessarily a device communication problem
// do additional diagnosis
if (!e.getErrorCode().equals(SyncError.BUFFER_OVERRUN) &&
!e.getErrorCode().equals(SyncError.TRANSFER_PROTOCOL_ERROR)) {
// this is a logic problem, doesn't need recovery or to be retried
return false;
}
} catch (AdbCommandRejectedException e) {
logDeviceActionException(actionDescription, e);
} catch (ShellCommandUnresponsiveException e) {
CLog.w("Device %s stopped responding when attempting %s", getSerialNumber(),
actionDescription);
}
// TODO: currently treat all exceptions the same. In future consider different recovery
// mechanisms for time out's vs IOExceptions
recoverDevice();
}
if (retryAttempts > 0) {
throw new DeviceUnresponsiveException(String.format("Attempted %s multiple times "
+ "on device %s without communication success. Aborting.", actionDescription,
getSerialNumber()));
}
return false;
}
/**
* Log an entry for given exception
*
* @param actionDescription the action's description
* @param e the exception
*/
private void logDeviceActionException(String actionDescription, Exception e) {
CLog.w("%s (%s) when attempting %s on device %s", e.getClass().getSimpleName(),
getExceptionMessage(e), actionDescription, getSerialNumber());
}
/**
* Make a best effort attempt to retrieve a meaningful short descriptive message for given
* {@link Exception}
*
* @param e the {@link Exception}
* @return a short message
*/
private String getExceptionMessage(Exception e) {
StringBuilder msgBuilder = new StringBuilder();
if (e.getMessage() != null) {
msgBuilder.append(e.getMessage());
}
if (e.getCause() != null) {
msgBuilder.append(" cause: ");
msgBuilder.append(e.getCause().getClass().getSimpleName());
if (e.getCause().getMessage() != null) {
msgBuilder.append(" (");
msgBuilder.append(e.getCause().getMessage());
msgBuilder.append(")");
}
}
return msgBuilder.toString();
}
/**
* Attempts to recover device communication.
*
* @throws DeviceNotAvailableException if device is not longer available
*/
@Override
public void recoverDevice() throws DeviceNotAvailableException {
if (mRecoveryMode.equals(RecoveryMode.NONE)) {
CLog.i("Skipping recovery on %s", getSerialNumber());
getRunUtil().sleep(NONE_RECOVERY_MODE_DELAY);
return;
}
CLog.i("Attempting recovery on %s", getSerialNumber());
mRecovery.recoverDevice(mStateMonitor, mRecoveryMode.equals(RecoveryMode.ONLINE));
if (mRecoveryMode.equals(RecoveryMode.AVAILABLE)) {
// turn off recovery mode to prevent reentrant recovery
// TODO: look for a better way to handle this, such as doing postBootUp steps in
// recovery itself
mRecoveryMode = RecoveryMode.NONE;
// this might be a runtime reset - still need to run post boot setup steps
if (isEncryptionSupported() && isDeviceEncrypted()) {
unlockDevice();
}
postBootSetup();
mRecoveryMode = RecoveryMode.AVAILABLE;
} else if (mRecoveryMode.equals(RecoveryMode.ONLINE)) {
// turn off recovery mode to prevent reentrant recovery
// TODO: look for a better way to handle this, such as doing postBootUp steps in
// recovery itself
mRecoveryMode = RecoveryMode.NONE;
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
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
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 getLogcatDump() {
byte[] output = new byte[0];
try {
// use IDevice directly because we don't want callers to handle
// DeviceNotAvailableException for this method
CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
// add -d parameter to make this a non blocking call
getIDevice().executeShellCommand(LogcatReceiver.LOGCAT_CMD + " -d", receiver);
output = receiver.getOutput();
} catch (IOException e) {
CLog.w("Failed to get logcat dump from %s: ", getSerialNumber(), e.getMessage());
} catch (TimeoutException e) {
CLog.w("Failed to get logcat dump from %s: timeout", getSerialNumber());
} catch (AdbCommandRejectedException e) {
CLog.w("Failed to get logcat dump from %s: ", getSerialNumber(), e.getMessage());
} catch (ShellCommandUnresponsiveException e) {
CLog.w("Failed to get logcat dump from %s: ", getSerialNumber(), e.getMessage());
}
return new ByteArrayInputStreamSource(output);
}
/**
* {@inheritDoc}
*/
@Override
public void stopLogcat() {
if (mLogcatReceiver != null) {
mLogcatReceiver.stop();
mLogcatReceiver = null;
} else {
CLog.w("Attempting to stop logcat when not capturing for %s", getSerialNumber());
}
}
/**
* Factory method to create a {@link LogcatReceiver}.
* <p/>
* Exposed for unit testing.
*/
LogcatReceiver createLogcatReceiver() {
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() {
CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
try {
executeShellCommand(BUGREPORT_CMD, receiver, BUGREPORT_TIMEOUT, 0 /* don't retry */);
} catch (DeviceNotAvailableException e) {
// Log, but don't throw, so the caller can get the bugreport contents even if the device
// goes away
CLog.e("Device %s became unresponsive while retrieving bugreport", getSerialNumber());
}
return new ByteArrayInputStreamSource(receiver.getOutput());
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getScreenshot() throws DeviceNotAvailableException {
return getScreenshot("PNG");
}
/**
* {@inheritDoc}
*/
@Override
public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException {
if (!format.equalsIgnoreCase("PNG") && !format.equalsIgnoreCase("JPEG")){
CLog.e("Screenshot: Format %s is not supported, defaulting to PNG.", format);
format = "PNG";
}
ScreenshotAction action = new ScreenshotAction();
if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) {
byte[] imageData = compressRawImage(action.mRawScreenshot, format.toUpperCase());
if (imageData != null) {
return new ByteArrayInputStreamSource(imageData);
}
}
return null;
}
private class ScreenshotAction implements DeviceAction {
RawImage mRawScreenshot;
/**
* {@inheritDoc}
*/
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
mRawScreenshot = getIDevice().getScreenshot();
return mRawScreenshot != null;
}
}
private byte[] compressRawImage(RawImage rawImage, String format) {
BufferedImage image = null;
if ("JPEG".equalsIgnoreCase(format)) {
//JPEG does not support ARGB without a special encoder
image = new BufferedImage(rawImage.width, rawImage.height,
BufferedImage.TYPE_3BYTE_BGR);
}
else {
image = new BufferedImage(rawImage.width, rawImage.height,
BufferedImage.TYPE_INT_ARGB);
}
// borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java
int index = 0;
int IndexInc = rawImage.bpp >> 3;
for (int y = 0 ; y < rawImage.height ; y++) {
for (int x = 0 ; x < rawImage.width ; x++) {
int value = rawImage.getARGB(index);
index += IndexInc;
image.setRGB(x, y, value);
}
}
// Rescale to reduce size if needed
// Screenshot default format is 1080 x 1920, 8-bit/color RGBA
// By cutting in half we can easily keep good quality and smaller size
int shortEdge = Math.min(image.getHeight(), image.getWidth());
if (shortEdge > 720) {
Image resized = image.getScaledInstance(image.getWidth() / 2, image.getHeight() / 2,
Image.SCALE_SMOOTH);
image = new BufferedImage(image.getWidth() / 2, image.getHeight() / 2,
Image.SCALE_REPLICATE);
image.getGraphics().drawImage(resized, 0, 0, null);
}
// store compressed image in memory, and let callers write to persistent storage
// use initial buffer size of 128K
byte[] imageData = null;
ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024);
try {
if (ImageIO.write(image, format, imageOut)) {
imageData = imageOut.toByteArray();
} else {
CLog.e("Failed to compress screenshot to png");
}
} catch (IOException e) {
CLog.e("Failed to compress screenshot to png");
CLog.e(e);
}
StreamUtil.close(imageOut);
return imageData;
}
/**
* {@inheritDoc}
*/
@Override
public void clearLastConnectedWifiNetwork() {
mLastConnectedWifiSsid = null;
mLastConnectedWifiPsk = null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk)
throws DeviceNotAvailableException {
// Clears the last connected wifi network.
mLastConnectedWifiSsid = null;
mLastConnectedWifiPsk = null;
// Connects to wifi network. It retries up to {@link TestDeviceOptions@getWifiAttempts()}
// times and uses binary exponential back-offs when retrying.
Random rnd = new Random();
int backoffSlotCount = 2;
IWifiHelper wifi = createWifiHelper();
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());
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 (i < mOptions.getWifiAttempts()) {
int waitTime = rnd.nextInt(backoffSlotCount) * mOptions.getWifiRetryWaitTime();
backoffSlotCount *= 2;
CLog.i("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 {
if (!checkConnectivity()) {
return connectToWifiNetwork(wifiSsid, wifiPsk);
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isWifiEnabled() throws DeviceNotAvailableException {
try {
final IWifiHelper wifi = createWifiHelper();
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/>
* Exposed so unit tests can mock
*/
IWifiHelper createWifiHelper() throws DeviceNotAvailableException {
return new WifiHelper(this);
}
/**
* {@inheritDoc}
*/
@Override
public boolean clearErrorDialogs() throws DeviceNotAvailableException {
// attempt to clear error dialogs multiple times
for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) {
int numErrorDialogs = getErrorDialogCount();
if (numErrorDialogs == 0) {
return true;
}
doClearDialogs(numErrorDialogs);
}
if (getErrorDialogCount() > 0) {
// at this point, all attempts to clear error dialogs completely have failed
// it might be the case that the process keeps showing new dialogs immediately after
// clearing. There's really no workaround, but to dump an error
CLog.e("error dialogs still exist on %s.", getSerialNumber());
return false;
}
return true;
}
/**
* Detects the number of crash or ANR dialogs currently displayed.
* <p/>
* Parses output of 'dump activity processes'
*
* @return count of dialogs displayed
* @throws DeviceNotAvailableException
*/
private int getErrorDialogCount() throws DeviceNotAvailableException {
int errorDialogCount = 0;
Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*");
Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*");
String systemStatusOutput = executeShellCommand("dumpsys activity processes");
Matcher crashMatcher = crashPattern.matcher(systemStatusOutput);
while (crashMatcher.find()) {
errorDialogCount++;
}
Matcher anrMatcher = anrPattern.matcher(systemStatusOutput);
while (anrMatcher.find()) {
errorDialogCount++;
}
return errorDialogCount;
}
private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException {
CLog.i("Attempted to clear %d dialogs on %s", numDialogs, getSerialNumber());
for (int i=0; i < numDialogs; i++) {
// send DPAD_CENTER
executeShellCommand(DISMISS_DIALOG_CMD);
}
}
IDeviceStateMonitor getDeviceStateMonitor() {
return mStateMonitor;
}
/**
* {@inheritDoc}
*/
@Override
public void postBootSetup() throws DeviceNotAvailableException {
enableAdbRoot();
if (mOptions.isDisableKeyguard()) {
disableKeyguard();
}
for (String command : mOptions.getPostBootCommands()) {
executeShellCommand(command);
}
}
/**
* 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()));
}
}
/**
* Gets the adb shell command to disable the keyguard for this device.
* <p/>
* Exposed for unit testing.
*/
String getDisableKeyguardCmd() {
return mOptions.getDisableKeyguardCmd();
}
/**
* Attempts to disable the keyguard.
* <p>
* First wait for the input dispatch to become ready, this happens around the same time when the
* device reports BOOT_COMPLETE, apparently asynchronously, because current framework
* implementation has occasional race condition. Then command is sent to dismiss keyguard (works
* on non-secure ones only)
* @throws DeviceNotAvailableException
*/
void disableKeyguard() throws DeviceNotAvailableException {
long start = System.currentTimeMillis();
while (true) {
Boolean ready = isDeviceInputReady();
if (ready == null) {
// unsupported API level, bail
break;
}
if (ready) {
// input dispatch is ready, bail
break;
}
long timeSpent = System.currentTimeMillis() - start;
if (timeSpent > INPUT_DISPATCH_READY_TIMEOUT) {
CLog.w("Timeout after waiting %dms on enabling of input dispatch", timeSpent);
// break & proceed anyway
break;
} else {
getRunUtil().sleep(1000);
}
}
CLog.i("Attempting to disable keyguard on %s using %s", getSerialNumber(),
getDisableKeyguardCmd());
executeShellCommand(getDisableKeyguardCmd());
}
/**
* Tests the device to see if input dispatcher is ready
* @return <code>null</code> if not supported by platform, or the actual readiness state
* @throws DeviceNotAvailableException
*/
Boolean isDeviceInputReady() throws DeviceNotAvailableException {
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
executeShellCommand(TEST_INPUT_CMD, receiver);
String output = receiver.getOutput();
Matcher m = INPUT_DISPATCH_STATE_REGEX.matcher(output);
if (!m.find()) {
// output does not contain the line at all, implying unsupported API level, bail
return null;
}
return "1".equals(m.group(1));
}
/**
* {@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();
}
/**
* Exposed for unit testing.
*
* @throws DeviceNotAvailableException
*/
void doReboot() throws DeviceNotAvailableException, UnsupportedOperationException {
if (TestDeviceState.FASTBOOT == getDeviceState()) {
CLog.i("device %s in fastboot. Rebooting to userspace.", getSerialNumber());
executeFastbootCommand("reboot");
} else {
CLog.i("Rebooting device %s", getSerialNumber());
doAdbReboot(null);
waitForDeviceNotAvailable("reboot", DEFAULT_UNAVAILABLE_TIMEOUT);
}
}
/**
* Performs an reboot via framework power manager
*
* Must have root access, device must be API Level 18 or above
*
* @param into the mode to reboot into, currently supported: bootloader, recovery, leave it
* null for a plain reboot
* @return <code>true</code> if the device rebooted, <code>false</code> if not successful or
* unsupported
* @throws DeviceNotAvailableException
*/
private boolean doAdbFrameworkReboot(final String into) throws DeviceNotAvailableException {
// use framework reboot when:
// 1. device API level >= 18
// 2. has adb root
// 3. framework is running
if (!isEnableAdbRoot()) {
CLog.i("framework reboot is not supported; when enable root is disabled");
return false;
}
enableAdbRoot();
if (getApiLevel() >= 18 && isAdbRoot()) {
try {
// check framework running
String output = executeShellCommand("pm path android");
if (output == null || !output.contains("package:")) {
CLog.v("framework reboot: can't detect framework running");
return false;
}
String command = "svc power reboot";
if (into != null && !into.isEmpty()) {
command = String.format("%s %s", command, into);
}
executeShellCommand(command);
} catch (DeviceUnresponsiveException due) {
CLog.v("framework reboot: device unresponsive to shell command, using fallback");
return false;
}
return waitForDeviceNotAvailable(30 * 1000);
} else {
CLog.v("framework reboot: not supported");
return false;
}
}
/**
* Perform a adb reboot.
*
* @param into the bootloader name to reboot into, or <code>null</code> to just reboot the
* device.
* @throws DeviceNotAvailableException
*/
private void doAdbReboot(final String into) throws DeviceNotAvailableException {
// emulator doesn't support reboot, try just resetting framework and hoping for the best
if (getIDevice().isEmulator()) {
CLog.i("since emulator, performing shell stop & start instead of reboot");
executeShellCommand("stop");
executeShellCommand(String.format("setprop %s 0",
DeviceStateMonitor.BOOTCOMPLETE_PROP));
executeShellCommand("start");
return;
}
if (!doAdbFrameworkReboot(into)) {
DeviceAction rebootAction = new DeviceAction() {
@Override
public boolean run() throws TimeoutException, IOException,
AdbCommandRejectedException {
getIDevice().reboot(into);
return true;
}
};
performDeviceAction("reboot", rebootAction, MAX_RETRY_ATTEMPTS);
}
}
private void waitForDeviceNotAvailable(String operationDesc, long time) {
// TODO: a bit of a race condition here. Would be better to start a
// before the operation
if (!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());
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);
// wait for device to be back online
waitForDeviceOnline();
if (isAdbRoot()) {
return true;
}
CLog.w("'adb root' on %s unsuccessful on attempt %d of %d. Output: '%s'",
getSerialNumber(), i, attempts, output);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isAdbRoot() throws DeviceNotAvailableException {
String output = executeShellCommand("id");
return output.contains("uid=0(root)");
}
/**
* {@inheritDoc}
*/
@Override
public boolean encryptDevice(boolean inplace) throws DeviceNotAvailableException,
UnsupportedOperationException {
if (!isEncryptionSupported()) {
throw new UnsupportedOperationException(String.format("Can't encrypt device %s: "
+ "encryption not supported", getSerialNumber()));
}
if (isDeviceEncrypted()) {
CLog.d("Device %s is already encrypted, skipping", getSerialNumber());
return true;
}
enableAdbRoot();
String encryptMethod;
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().startsWith("500 0 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 = getPropertySync("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 && output.startsWith(ENCRYPTION_SUPPORTED_CODE) &&
output.contains(ENCRYPTION_SUPPORTED_USAGE));
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.
* <p/>
* Exposed for unit testing.
*/
IDeviceRecovery getRecovery() {
return mRecovery;
}
/**
* {@inheritDoc}
*/
@Override
public void setRecovery(IDeviceRecovery recovery) {
throwIfNull(recovery);
mRecovery = recovery;
}
/**
* {@inheritDoc}
*/
@Override
public void setRecoveryMode(RecoveryMode mode) {
throwIfNull(mRecoveryMode);
mRecoveryMode = mode;
}
/**
* {@inheritDoc}
*/
@Override
public RecoveryMode getRecoveryMode() {
return mRecoveryMode;
}
/**
* {@inheritDoc}
*/
@Override
public void setFastbootEnabled(boolean fastbootEnabled) {
mFastbootEnabled = fastbootEnabled;
}
/**
* {@inheritDoc}
*/
@Override
public void setDeviceState(final TestDeviceState deviceState) {
if (!deviceState.equals(getDeviceState())) {
// disable state changes while fastboot lock is held, because issuing fastboot command
// will disrupt state
if (getDeviceState().equals(TestDeviceState.FASTBOOT) && mFastbootLock.isLocked()) {
return;
}
mState = deviceState;
CLog.d("Device %s state is now %s", getSerialNumber(), deviceState);
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(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 {
return getInstalledPackageNames(new PkgFilter() {
@Override
public boolean accept(String pkgName, String apkPath) {
return true;
}
});
}
/**
* A {@link DeviceAction} for retrieving package system service info, and do retries on
* failures.
*/
private class DumpPkgAction implements DeviceAction {
Map<String, PackageInfo> mPkgInfoMap;
DumpPkgAction() {
}
@Override
public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, InstallException, SyncException {
DumpsysPackageReceiver receiver = new DumpsysPackageReceiver();
getIDevice().executeShellCommand("dumpsys package p", receiver);
mPkgInfoMap = receiver.getPackages();
if (mPkgInfoMap.size() == 0) {
// Package parsing can fail if package manager is currently down. throw exception
// to retry
CLog.w("no packages found from dumpsys package p.");
throw new IOException();
}
return true;
}
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException {
DumpPkgAction action = new DumpPkgAction();
performDeviceAction("dumpsys package p", action, MAX_RETRY_ATTEMPTS);
Set<String> pkgs = new HashSet<String>();
for (PackageInfo pkgInfo : action.mPkgInfoMap.values()) {
if (!pkgInfo.isSystemApp() || pkgInfo.isUpdatedSystemApp()) {
CLog.d("Found uninstallable package %s", pkgInfo.getPackageName());
pkgs.add(pkgInfo.getPackageName());
}
}
return pkgs;
}
/**
* {@inheritDoc}
*/
@Override
public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException {
DumpPkgAction action = new DumpPkgAction();
performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS);
return action.mPkgInfoMap.get(packageName);
}
private static interface PkgFilter {
boolean accept(String pkgName, String apkPath);
}
// TODO: convert this to use DumpPkgAction
private Set<String> getInstalledPackageNames(PkgFilter filter)
throws DeviceNotAvailableException {
Set<String> packages= new HashSet<String>();
String output = executeShellCommand(LIST_PACKAGES_CMD);
if (output != null) {
Matcher m = PACKAGE_REGEX.matcher(output);
while (m.find()) {
String packagePath = m.group(1);
String packageName = m.group(2);
if (filter.accept(packageName, packagePath)) {
packages.add(packageName);
}
}
}
return packages;
}
/**
* {@inheritDoc}
*/
@Override
public TestDeviceOptions getOptions() {
return mOptions;
}
/**
* {@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;
}
@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);
}
private long getDeviceTimeOffset(Date date) throws DeviceNotAvailableException {
String deviceTimeString = executeShellCommand("date +%s");
Long deviceTime = null;
long offset = 0;
try {
deviceTime = Long.valueOf(deviceTimeString.trim());
} catch (NumberFormatException nfe) {
CLog.i("Invalid device time: \"%s\", ignored.");
return 0;
}
if (date == null) {
date = new Date();
}
offset = date.getTime() - deviceTime * 1000;
CLog.d("Time offset = " + 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 boolean waitForBootComplete(long timeOut) throws DeviceNotAvailableException {
return mStateMonitor.waitForBootComplete(timeOut);
}
/**
* {@inheritDoc}
*/
@Override
public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
ArrayList<String[]> users = tokenizeListUsers();
if (users == null) {
return null;
}
ArrayList<Integer> userIds = new ArrayList<Integer>(users.size());
for (String[] user : users) {
userIds.add(Integer.parseInt(user[1]));
}
return userIds;
}
/**
* Tokenizes the output of 'pm list users'.
* The returned tokens for each user have the form: {"\tUserInfo", Integer.toString(id), name,
* Integer.toHexString(flag), "[running]"}; (the last one being optional)
* @return a list of arrays of strings, each element of the list representing the tokens
* for a user, or {@code null} if there was an error while tokenizing the adb command output.
*/
private ArrayList<String[]> tokenizeListUsers() throws DeviceNotAvailableException {
String command = "pm list users";
String commandOutput = executeShellCommand(command);
// Extract the id of all existing users.
String[] lines = commandOutput.split("\\r?\\n");
if (lines.length < 1) {
CLog.e("%s should contain at least one line", commandOutput);
return null;
}
if (!lines[0].equals("Users:")) {
CLog.e("%s in not a valid output for 'pm list users'", commandOutput);
return null;
}
ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1);
for (int i = 1; i < lines.length; i++) {
// Individual user is printed out like this:
// \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running]
String[] tokens = lines[i].split("\\{|\\}|:");
if (tokens.length != 4 && tokens.length != 5) {
CLog.e("%s doesn't contain 4 or 5 tokens", lines[i]);
return null;
}
users.add(tokens);
}
return users;
}
/**
* {@inheritDoc}
*/
@Override
public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
String command = "pm get-max-users";
String commandOutput = executeShellCommand(command);
try {
return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
} catch (NumberFormatException e) {
CLog.e("Failed to parse result: %s", commandOutput);
}
return 0;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isMultiUserSupported() throws DeviceNotAvailableException {
return getMaxNumberOfUsersSupported() > 1;
}
/**
* {@inheritDoc}
*/
@Override
public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException {
final String output = executeShellCommand(String.format("pm create-user %s", name));
if (output.startsWith("Success")) {
try {
return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
} catch (NumberFormatException e) {
CLog.e("Failed to parse result: %s", output);
}
} else {
CLog.e("Failed to create user: %s", output);
}
throw new IllegalStateException();
}
/**
* {@inheritDoc}
*/
@Override
public boolean removeUser(int userId) throws DeviceNotAvailableException {
final String output = executeShellCommand(String.format("pm remove-user %s", userId));
if (output.startsWith("Error")) {
CLog.e("Failed to remove user: %s", output);
return false;
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean startUser(int userId) throws DeviceNotAvailableException {
final String output = executeShellCommand(String.format("am start-user %s", userId));
if (output.startsWith("Error")) {
CLog.e("Failed to start user: %s", output);
return false;
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public void stopUser(int userId) throws DeviceNotAvailableException {
// No error or status code is returned.
executeShellCommand(String.format("am stop-user %s", userId));
}
/**
* {@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 {
ArrayList<String[]> users = tokenizeListUsers();
if (users == null) {
return null;
}
for (String[] user : users) {
int flag = Integer.parseInt(user[3], 16);
if ((flag & FLAG_PRIMARY) != 0) {
return Integer.parseInt(user[1]);
}
}
return null;
}
/**
* {@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 getDeviceClass() {
IDevice device = getIDevice();
if (device == null) {
CLog.w("No IDevice instance, cannot determine device class.");
return "";
}
return device.getClass().getSimpleName();
}
}