Merge "Remove a duplicate device state check"
diff --git a/src/com/android/tradefed/device/AndroidNativeDevice.java b/src/com/android/tradefed/device/AndroidNativeDevice.java
new file mode 100644
index 0000000..b91c903
--- /dev/null
+++ b/src/com/android/tradefed/device/AndroidNativeDevice.java
@@ -0,0 +1,3459 @@
+/*
+ * 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();
+    }
+}
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index 6248ecc..a5add86 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -109,13 +109,6 @@
     private DeviceRecoverer mDeviceRecoverer;
 
     /**
-     * Creator interface for {@link IManagedTestDevice}s
-     */
-    interface IManagedTestDeviceFactory {
-        IManagedTestDevice createDevice(IDevice stubDevice);
-    }
-
-    /**
      * The DeviceManager should be retrieved from the {@link GlobalConfiguration}
      */
     public DeviceManager() {
@@ -133,20 +126,8 @@
     @Override
     public void init(IDeviceSelection globalDeviceFilter,
             List<IDeviceMonitor> globalDeviceMonitors) {
-        init(globalDeviceFilter, globalDeviceMonitors, new IManagedTestDeviceFactory() {
-            @Override
-            public IManagedTestDevice createDevice(IDevice idevice) {
-                TestDevice testDevice = new TestDevice(idevice, new DeviceStateMonitor(
-                        DeviceManager.this, idevice, mFastbootEnabled), mDvcMon);
-                testDevice.setFastbootEnabled(mFastbootEnabled);
-                if (idevice instanceof FastbootDevice) {
-                    testDevice.setDeviceState(TestDeviceState.FASTBOOT);
-                } else if (idevice instanceof StubDevice) {
-                    testDevice.setDeviceState(TestDeviceState.ONLINE);
-                }
-                return testDevice;
-            }
-        });
+        init(globalDeviceFilter, globalDeviceMonitors,
+                new ManagedTestDeviceFactory(mFastbootEnabled, DeviceManager.this, mDvcMon));
     }
 
     /**
@@ -181,6 +162,7 @@
             startFastbootMonitor();
             // don't set fastboot enabled bit until mFastbootListeners has been initialized
             mFastbootEnabled = true;
+            deviceFactory.setFastbootEnabled(mFastbootEnabled);
             // TODO: consider only adding fastboot devices if explicit option is set, because
             // device property selection options won't work properly with a device in fastboot
             addFastbootDevices();
@@ -189,6 +171,7 @@
             mFastbootListeners = null;
             mFastbootMonitor = null;
             mFastbootEnabled = false;
+            deviceFactory.setFastbootEnabled(mFastbootEnabled);
         }
 
         // don't start adding devices until fastboot support has been established
diff --git a/src/com/android/tradefed/device/FileEntryWrapper.java b/src/com/android/tradefed/device/FileEntryWrapper.java
index 7a4db6c..ec97ef4 100644
--- a/src/com/android/tradefed/device/FileEntryWrapper.java
+++ b/src/com/android/tradefed/device/FileEntryWrapper.java
@@ -28,7 +28,7 @@
  */
 class FileEntryWrapper implements IFileEntry {
 
-    private final TestDevice mTestDevice;
+    private final AndroidNativeDevice mTestDevice;
     private final FileListingService.FileEntry mFileEntry;
     private Map<String, IFileEntry> mChildMap = null;
 
@@ -38,7 +38,7 @@
      * @param testDevice the {@link TestDevice} to use
      * @param entry the corresponding {@link FileEntry} to wrap
      */
-    FileEntryWrapper(TestDevice testDevice, FileEntry entry) {
+    FileEntryWrapper(AndroidNativeDevice testDevice, FileEntry entry) {
         mTestDevice = testDevice;
         mFileEntry = entry;
     }
diff --git a/src/com/android/tradefed/device/IManagedTestDeviceFactory.java b/src/com/android/tradefed/device/IManagedTestDeviceFactory.java
new file mode 100644
index 0000000..bcdad4b
--- /dev/null
+++ b/src/com/android/tradefed/device/IManagedTestDeviceFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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.IDevice;
+
+/**
+ * Creator interface for {@link IManagedTestDevice}s
+ */
+public interface IManagedTestDeviceFactory {
+
+    /**
+     * Create a {@link IManagedTestDevice} based on the {@link IDevice} passed to it.
+     *
+     * @param stubDevice that will define the type of device created
+     * @return a IManagedTestDevice created base on the IDevice
+     */
+    IManagedTestDevice createDevice(IDevice stubDevice);
+
+    /**
+     * Enable or not fastboot support for the device created.
+     * @param enable value set the support.
+     */
+    public void setFastbootEnabled(boolean enable);
+}
diff --git a/src/com/android/tradefed/device/ManagedDeviceList.java b/src/com/android/tradefed/device/ManagedDeviceList.java
index 70ebace..e4bcc66 100644
--- a/src/com/android/tradefed/device/ManagedDeviceList.java
+++ b/src/com/android/tradefed/device/ManagedDeviceList.java
@@ -17,7 +17,6 @@
 package com.android.tradefed.device;
 
 import com.android.ddmlib.IDevice;
-import com.android.tradefed.device.DeviceManager.IManagedTestDeviceFactory;
 import com.android.tradefed.device.IManagedTestDevice.DeviceEventResponse;
 import com.android.tradefed.util.ConditionPriorityBlockingQueue.IMatcher;
 
diff --git a/src/com/android/tradefed/device/ManagedTestDeviceFactory.java b/src/com/android/tradefed/device/ManagedTestDeviceFactory.java
new file mode 100644
index 0000000..39ef044
--- /dev/null
+++ b/src/com/android/tradefed/device/ManagedTestDeviceFactory.java
@@ -0,0 +1,64 @@
+/*
+ * 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.IDevice;
+import com.android.tradefed.device.DeviceManager.FastbootDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+/**
+ * Factory to create the different kind of devices that can be monitored by Tf
+ */
+public class ManagedTestDeviceFactory implements IManagedTestDeviceFactory {
+
+    private boolean mFastbootEnabled;
+    private IDeviceManager mDeviceManager;
+    private IDeviceMonitor mAllocationMonitor;
+
+    public ManagedTestDeviceFactory(boolean fastbootEnabled, IDeviceManager deviceManager,
+            IDeviceMonitor allocationMonitor) {
+        mFastbootEnabled = fastbootEnabled;
+        mDeviceManager = deviceManager;
+        mAllocationMonitor = allocationMonitor;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IManagedTestDevice createDevice(IDevice idevice) {
+        // TODO: Based on idevice chose what device to implement
+        TestDevice testDevice = new TestDevice(idevice,
+                new DeviceStateMonitor(mDeviceManager, idevice, mFastbootEnabled),
+                mAllocationMonitor);
+        testDevice.setFastbootEnabled(mFastbootEnabled);
+
+        if (idevice instanceof FastbootDevice) {
+            testDevice.setDeviceState(TestDeviceState.FASTBOOT);
+        } else if (idevice instanceof StubDevice) {
+            testDevice.setDeviceState(TestDeviceState.ONLINE);
+        }
+        return testDevice;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setFastbootEnabled(boolean enable) {
+        mFastbootEnabled = enable;
+    }
+}
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index 3703eef..3d9848c 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -13,3453 +13,22 @@
  * 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}
+ * Implementation of a {@link ITestDevice} for a full stack android device
  */
-class TestDevice implements IManagedTestDevice {
-
-    /** the default number of command retry attempts to perform */
-    static final int MAX_RETRY_ATTEMPTS = 2;
-    private static final String BUGREPORT_CMD = "bugreport";
-    /** 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:(.*)=(.*)");
-    private static final Pattern PING_REGEX = Pattern.compile(
-            "(?<send>\\d+) packets transmitted, (?<recv>\\d+) received, (?<loss>\\d+)% packet loss");
-    /** 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);
+public class TestDevice extends AndroidNativeDevice {
 
     /**
-     * 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.
+     * @param device
+     * @param stateMonitor
+     * @param allocationMonitor
      */
-    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 5 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
-     */
-    TestDevice(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 prop The name of the device property as returned by `adb shell getprop`
-     * @param fastbootVar The name of the equivalent fastboot variable to query. if {@code null},
-     * fastboot query will not be attempted
-     * @param description A simple description of the variable.  First letter should be capitalized.
-     * @return A string, possibly {@code null} or empty, containing the value of the given property
-     */
-    private String internalGetProperty(String 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
-     * @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);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    public String installPackage(final File packageFile, final File certFile,
-            final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
-        // use array to store response, so it can be returned to caller
-        final String[] response = new String[1];
-        DeviceAction installAction = new DeviceAction() {
-            @Override
-            public boolean run() throws InstallException, SyncException, IOException,
-            TimeoutException, AdbCommandRejectedException {
-                // TODO: create a getIDevice().installPackage(File, File...) method when the dist
-                // cert functionality is ready to be open sourced
-                String remotePackagePath = getIDevice().syncPackageToDevice(
-                        packageFile.getAbsolutePath());
-                String remoteCertPath = getIDevice().syncPackageToDevice(
-                        certFile.getAbsolutePath());
-                // trick installRemotePackage into issuing a 'pm install <apk> <cert>' command,
-                // by adding apk path to extraArgs, and using cert as the 'apk file'
-                String[] newExtraArgs = new String[extraArgs.length + 1];
-                System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
-                newExtraArgs[newExtraArgs.length - 1] = String.format("\"%s\"", remotePackagePath);
-                try {
-                    response[0] = getIDevice().installRemotePackage(remoteCertPath, reinstall,
-                            newExtraArgs);
-                } finally {
-                    getIDevice().removeRemotePackage(remotePackagePath);
-                    getIDevice().removeRemotePackage(remoteCertPath);
-                }
-                return true;
-            }
-        };
-        performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
-                installAction, MAX_RETRY_ATTEMPTS);
-        return response[0];
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
-        // use array to store response, so it can be returned to caller
-        final String[] response = new String[1];
-        DeviceAction uninstallAction = new DeviceAction() {
-            @Override
-            public boolean run() throws InstallException {
-                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 callback optional action to perform if action fails but recovery succeeds. If no post
-     *            recovery action needs to be taken pass in <code>null</code>
-     * @param retryAttempts the retry attempts to make for action if it fails but
-     *            recovery succeeds
-     * @returns <code>true</code> if action was performed successfully
-     * @throws DeviceNotAvailableException if recovery attempt fails or max attempts done without
-     *             success
-     */
-    private boolean performDeviceAction(String actionDescription, final DeviceAction action,
-            int retryAttempts) throws DeviceNotAvailableException {
-
-        for (int i = 0; i < retryAttempts + 1; i++) {
-            try {
-                return action.run();
-            } catch (TimeoutException e) {
-                logDeviceActionException(actionDescription, e);
-            } catch (IOException e) {
-                logDeviceActionException(actionDescription, e);
-            } catch (InstallException e) {
-                logDeviceActionException(actionDescription, e);
-            } catch (SyncException e) {
-                logDeviceActionException(actionDescription, e);
-                // a SyncException is not necessarily a device communication problem
-                // do additional diagnosis
-                if (!e.getErrorCode().equals(SyncError.BUFFER_OVERRUN) &&
-                        !e.getErrorCode().equals(SyncError.TRANSFER_PROTOCOL_ERROR)) {
-                    // this is a logic problem, doesn't need recovery or to be retried
-                    return false;
-                }
-            } catch (AdbCommandRejectedException e) {
-                logDeviceActionException(actionDescription, e);
-            } catch (ShellCommandUnresponsiveException e) {
-                CLog.w("Device %s stopped responding when attempting %s", getSerialNumber(),
-                        actionDescription);
-            }
-            // TODO: currently treat all exceptions the same. In future consider different recovery
-            // mechanisms for time out's vs IOExceptions
-            recoverDevice();
-        }
-        if (retryAttempts > 0) {
-            throw new DeviceUnresponsiveException(String.format("Attempted %s multiple times "
-                    + "on device %s without communication success. Aborting.", actionDescription,
-                    getSerialNumber()));
-        }
-        return false;
-    }
-
-    /**
-     * Log an entry for given exception
-     *
-     * @param actionDescription the action's description
-     * @param e the exception
-     */
-    private void logDeviceActionException(String actionDescription, Exception e) {
-        CLog.w("%s (%s) when attempting %s on device %s", e.getClass().getSimpleName(),
-                getExceptionMessage(e), actionDescription, getSerialNumber());
-    }
-
-    /**
-     * Make a best effort attempt to retrieve a meaningful short descriptive message for given
-     * {@link Exception}
-     *
-     * @param e the {@link Exception}
-     * @return a short message
-     */
-    private String getExceptionMessage(Exception e) {
-        StringBuilder msgBuilder = new StringBuilder();
-        if (e.getMessage() != null) {
-            msgBuilder.append(e.getMessage());
-        }
-        if (e.getCause() != null) {
-            msgBuilder.append(" cause: ");
-            msgBuilder.append(e.getCause().getClass().getSimpleName());
-            if (e.getCause().getMessage() != null) {
-                msgBuilder.append(" (");
-                msgBuilder.append(e.getCause().getMessage());
-                msgBuilder.append(")");
-            }
-        }
-        return msgBuilder.toString();
-    }
-
-    /**
-     * Attempts to recover device communication.
-     *
-     * @throws DeviceNotAvailableException if device is not longer available
-     */
-    @Override
-    public void recoverDevice() throws DeviceNotAvailableException {
-        if (mRecoveryMode.equals(RecoveryMode.NONE)) {
-            CLog.i("Skipping recovery on %s", getSerialNumber());
-            getRunUtil().sleep(NONE_RECOVERY_MODE_DELAY);
-            return;
-        }
-        CLog.i("Attempting recovery on %s", getSerialNumber());
-        mRecovery.recoverDevice(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();
+    public TestDevice(IDevice device, IDeviceStateMonitor stateMonitor,
+            IDeviceMonitor allocationMonitor) {
+        super(device, stateMonitor, allocationMonitor);
     }
 }
diff --git a/tests/src/com/android/tradefed/device/DeviceManagerTest.java b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
index 9115c2a..23bf724 100644
--- a/tests/src/com/android/tradefed/device/DeviceManagerTest.java
+++ b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
@@ -21,7 +21,6 @@
 import com.android.ddmlib.IDevice.DeviceState;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.config.IGlobalConfiguration;
-import com.android.tradefed.device.DeviceManager.IManagedTestDeviceFactory;
 import com.android.tradefed.device.IManagedTestDevice.DeviceEventResponse;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.CommandResult;
@@ -157,6 +156,11 @@
                 mMockTestDevice.setIDevice(idevice);
                 return mMockTestDevice;
             }
+
+            @Override
+            public void setFastbootEnabled(boolean enable) {
+                // ignore
+            }
         };
         mMockGlobalConfig = EasyMock.createNiceMock(IGlobalConfiguration.class);
 
diff --git a/tests/src/com/android/tradefed/device/ManagedDeviceListTest.java b/tests/src/com/android/tradefed/device/ManagedDeviceListTest.java
index e775956..a8eca4e 100644
--- a/tests/src/com/android/tradefed/device/ManagedDeviceListTest.java
+++ b/tests/src/com/android/tradefed/device/ManagedDeviceListTest.java
@@ -16,7 +16,6 @@
 package com.android.tradefed.device;
 
 import com.android.ddmlib.IDevice;
-import com.android.tradefed.device.DeviceManager.IManagedTestDeviceFactory;
 
 import junit.framework.TestCase;
 
@@ -38,6 +37,11 @@
                 return new TestDevice(stubDevice, EasyMock.createNiceMock(
                         IDeviceStateMonitor.class), null);
             }
+
+            @Override
+            public void setFastbootEnabled(boolean enable) {
+                // ignore
+            }
         });
     }