Compile against the devtools annotations to fix docs build. [DO NOT MERGE] am: 83533c5f87 am: 5e5b10e86e
am: 508d76f134  -s ours

* commit '508d76f134853cb82e4eb2dc7e86d657ed4a9b7e':
  Compile against the devtools annotations to fix docs build. [DO NOT MERGE]
diff --git a/.classpath b/.classpath
index cea4eac..3a2c618 100644
--- a/.classpath
+++ b/.classpath
@@ -4,10 +4,8 @@
 	<classpathentry kind="src" path="res"/>
 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/kxml2/kxml2-2.3.0.jar"/>
-	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/guavalib_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/guava/guava/src"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/jline-1.0_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/jline/src"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/junit_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/junit/src"/>
-	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
 	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/tf-remote-client"/>
 	<classpathentry kind="output" path="bin"/>
diff --git a/Android.mk b/Android.mk
index 87626a6..085eae3 100644
--- a/Android.mk
+++ b/Android.mk
@@ -29,7 +29,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES := junit kxml2-2.3.0 jline-1.0 tf-remote-client
 # emmalib is only a runtime dependency if generating code coverage reporters,
 # not a compile time dependency
-LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt emmalib tools-common-prebuilt devtools-annotations-prebuilt
+LOCAL_JAVA_LIBRARIES := emmalib tools-common-prebuilt
 
 include $(BUILD_HOST_JAVA_LIBRARY)
 
@@ -58,6 +58,8 @@
         -hdf android.whichdoc online \
         -hdf sac true \
         -hdf devices true \
+        -showAnnotation com.android.tradefed.config.OptionClass \
+        -showAnnotation com.android.tradefed.config.Option \
 
 include $(BUILD_DROIDDOC)
 
@@ -68,7 +70,7 @@
 # Note that this is incompatible with `make dist`.  If you want to make
 # the distribution, you must run `tapas` with the individual target names.
 .PHONY: tradefed-all
-tradefed-all: tradefed tradefed-tests tf-prod-tests tf-prod-metatests
+tradefed-all: tradefed tradefed-tests tf-prod-tests tf-prod-metatests tradefed_win script_help verify
 
 # ====================================
 include $(CLEAR_VARS)
@@ -76,20 +78,20 @@
 
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_PREBUILT_EXECUTABLES := tradefed.sh
+LOCAL_PREBUILT_EXECUTABLES := tradefed.sh tradefed_win.bat script_help.sh verify.sh
 include $(BUILD_HOST_PREBUILT)
 
 # Build all sub-directories
 include $(call all-makefiles-under,$(LOCAL_PATH))
 
 ########################################################
-# Zip up the built files and dist it as google-tradefed.zip
-ifneq (,$(filter tradefed, $(TARGET_BUILD_APPS)))
+# Zip up the built files and dist it as tradefed.zip
+ifneq (,$(filter tradefed tradefed-all, $(TARGET_BUILD_APPS)))
 
-tradefed_dist_host_jars := tradefed tradefed-tests ddmlib-prebuilt tf-prod-tests emmalib loganalysis loganalysis-tests tf-remote-client devtools-annotations-prebuilt
+tradefed_dist_host_jars := tradefed tradefed-tests tf-prod-tests emmalib loganalysis loganalysis-tests tf-remote-client
 tradefed_dist_host_jar_files := $(foreach m, $(tradefed_dist_host_jars), $(HOST_OUT_JAVA_LIBRARIES)/$(m).jar)
 
-tradefed_dist_host_exes := tradefed.sh
+tradefed_dist_host_exes := tradefed.sh tradefed_win.bat script_help.sh verify.sh
 tradefed_dist_host_exe_files := $(foreach m, $(tradefed_dist_host_exes), $(BUILD_OUT_EXECUTABLES)/$(m))
 
 tradefed_dist_test_apks := TradeFedUiTestApp TradeFedTestApp DeviceSetupUtil
diff --git a/prod-tests/Android.mk b/prod-tests/Android.mk
index 01b4f5a..5432a31 100644
--- a/prod-tests/Android.mk
+++ b/prod-tests/Android.mk
@@ -26,7 +26,7 @@
 LOCAL_MODULE := tf-prod-tests
 
 LOCAL_MODULE_TAGS := optional
-LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt tradefed loganalysis
+LOCAL_JAVA_LIBRARIES := tradefed loganalysis
 
 include $(BUILD_HOST_JAVA_LIBRARY)
 
diff --git a/prod-tests/res/config/template/local.xml b/prod-tests/res/config/template/local.xml
new file mode 100644
index 0000000..3ec5873
--- /dev/null
+++ b/prod-tests/res/config/template/local.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<!-- Common base configuration for local runs. -->
+<configuration description="Common base configuration for local runs">
+    <option name="bugreport-on-invocation-ended" value="true" />
+    <build_provider class="com.android.tradefed.build.BootstrapBuildProvider" />
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <option name="screen-always-on" value="on" />
+        <option name="screen-adaptive-brightness" value="off" />
+        <option name="screen-brightness" value="30" />
+    </target_preparer>
+
+    <template-include name="test" />
+
+    <logger class="com.android.tradefed.log.FileLogger">
+        <option name="log-level" value="VERBOSE" />
+        <option name="log-level-display" value="VERBOSE" />
+    </logger>
+    <log_saver class="com.android.tradefed.result.FileSystemLogSaver" />
+    <result_reporter class="com.android.tradefed.result.ConsoleResultReporter" />
+    <result_reporter class="com.android.tradefed.result.InvocationFailureEmailResultReporter" />
+    <result_reporter class="com.android.tradefed.result.DeviceUnavailEmailResultReporter" />
+    <template-include name="reporters" default="google/template/reporters/empty" />
+</configuration>
diff --git a/prod-tests/src/com/android/app/tests/AppLaunchTest.java b/prod-tests/src/com/android/app/tests/AppLaunchTest.java
index b6abd38..acf3816 100644
--- a/prod-tests/src/com/android/app/tests/AppLaunchTest.java
+++ b/prod-tests/src/com/android/app/tests/AppLaunchTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.app.tests;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.build.IAppBuildInfo;
 import com.android.tradefed.build.IBuildInfo;
@@ -120,7 +119,7 @@
         listener.testStarted(installTest);
         String result = getDevice().installPackage(apkFile, true);
         if (result != null) {
-            listener.testFailed(TestFailure.FAILURE, installTest, result);
+            listener.testFailed(installTest, result);
         }
         listener.testEnded(installTest, Collections.<String, String> emptyMap());
     }
diff --git a/prod-tests/src/com/android/bluetooth/tests/BluetoothStressTest.java b/prod-tests/src/com/android/bluetooth/tests/BluetoothStressTest.java
index f39b3a0..3cd1d81 100644
--- a/prod-tests/src/com/android/bluetooth/tests/BluetoothStressTest.java
+++ b/prod-tests/src/com/android/bluetooth/tests/BluetoothStressTest.java
@@ -597,7 +597,7 @@
                     newConf.append(line).append("\n");
                 }
             }
-            mTestDevice.executeAdbCommand("remount");
+            mTestDevice.remountSystemWritable();
             return mTestDevice.pushString(newConf.toString(), BTSNOOP_CONF_FILE);
         } catch (IOException e) {
             return false;
diff --git a/prod-tests/src/com/android/continuous/SmokeTestFailureReporter.java b/prod-tests/src/com/android/continuous/SmokeTestFailureReporter.java
index fad434c..e0e7785 100644
--- a/prod-tests/src/com/android/continuous/SmokeTestFailureReporter.java
+++ b/prod-tests/src/com/android/continuous/SmokeTestFailureReporter.java
@@ -17,12 +17,12 @@
 package com.android.continuous;
 
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.result.TestFailureEmailResultReporter;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestResult.TestStatus;
-import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.util.Email;
 import com.android.tradefed.util.IEmail;
 
@@ -98,14 +98,16 @@
 
     private String describeStatus(TestStatus status) {
         switch (status) {
-            case ERROR:
-                return "had an error";
             case FAILURE:
                 return "failed";
             case PASSED:
                 return "passed";
             case INCOMPLETE:
                 return "did not complete";
+            case ASSUMPTION_FAILURE:
+                return "assumption failed";
+            case IGNORED:
+                return "ignored";
         }
         return "had an unknown result";
     }
diff --git a/prod-tests/src/com/android/encryption/tests/EncryptionFunctionalityTest.java b/prod-tests/src/com/android/encryption/tests/EncryptionFunctionalityTest.java
index 2016982..9e38110 100644
--- a/prod-tests/src/com/android/encryption/tests/EncryptionFunctionalityTest.java
+++ b/prod-tests/src/com/android/encryption/tests/EncryptionFunctionalityTest.java
@@ -42,7 +42,7 @@
  */
 public class EncryptionFunctionalityTest implements IDeviceTest, IRemoteTest {
 
-    private static final int BOOT_TIMEOUT = 120 * 1000;
+    private static final int BOOT_TIMEOUT = 2 * 60 * 1000;
 
     ITestDevice mTestDevice = null;
 
@@ -93,7 +93,8 @@
                 mTestDevice.waitForDeviceAvailable();
                 stageEnd(false); // stage 6
                 mTestDevice.executeShellCommand("vdc cryptfs changepw default");
-                mTestDevice.reboot();
+                mTestDevice.nonBlockingReboot();
+                mTestDevice.waitForBootComplete(BOOT_TIMEOUT * 3);
                 stageEnd(false); // stage 7
             }
         } catch (DeviceNotAvailableException e) {
diff --git a/prod-tests/src/com/android/framework/tests/BandwidthMicroBenchMarkTest.java b/prod-tests/src/com/android/framework/tests/BandwidthMicroBenchMarkTest.java
index cad496e..458e4d8 100644
--- a/prod-tests/src/com/android/framework/tests/BandwidthMicroBenchMarkTest.java
+++ b/prod-tests/src/com/android/framework/tests/BandwidthMicroBenchMarkTest.java
@@ -18,6 +18,7 @@
 
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
 import com.android.framework.tests.BandwidthStats.CompareResult;
 import com.android.framework.tests.BandwidthStats.ComparisonRecord;
 import com.android.tradefed.config.Option;
@@ -29,7 +30,6 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.IRunUtil.IRunnableResult;
diff --git a/prod-tests/src/com/android/framework/tests/DataIdleTest.java b/prod-tests/src/com/android/framework/tests/DataIdleTest.java
index 12fed10..56d1c03 100644
--- a/prod-tests/src/com/android/framework/tests/DataIdleTest.java
+++ b/prod-tests/src/com/android/framework/tests/DataIdleTest.java
@@ -18,6 +18,7 @@
 
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -27,7 +28,6 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.RunUtil;
diff --git a/prod-tests/src/com/android/framework/tests/DownloadManagerHostTests.java b/prod-tests/src/com/android/framework/tests/DownloadManagerHostTests.java
index dff0370..fbea647 100644
--- a/prod-tests/src/com/android/framework/tests/DownloadManagerHostTests.java
+++ b/prod-tests/src/com/android/framework/tests/DownloadManagerHostTests.java
@@ -223,7 +223,7 @@
          * Take dumpsys wifi when test fails.
          */
         @Override
-        public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+        public void testFailed(TestIdentifier test, String trace) {
             try {
                 String output = mDevice.executeShellCommand("dumpsys wifi");
                 if (output == null) {
@@ -239,7 +239,7 @@
                 CLog.e("Error getting dumpsys wifi");
                 CLog.e(e);
             } finally {
-                super.testFailed(status, test, trace);
+                super.testFailed(test, trace);
             }
         }
     }
diff --git a/prod-tests/src/com/android/framework/tests/FrameworkPerfTest.java b/prod-tests/src/com/android/framework/tests/FrameworkPerfTest.java
index d0cb486..f80aea2 100644
--- a/prod-tests/src/com/android/framework/tests/FrameworkPerfTest.java
+++ b/prod-tests/src/com/android/framework/tests/FrameworkPerfTest.java
@@ -18,12 +18,12 @@
 
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.TestResult;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.IRunUtil;
diff --git a/prod-tests/src/com/android/framework/tests/FrameworkStressTest.java b/prod-tests/src/com/android/framework/tests/FrameworkStressTest.java
index d39b255..7df7657 100644
--- a/prod-tests/src/com/android/framework/tests/FrameworkStressTest.java
+++ b/prod-tests/src/com/android/framework/tests/FrameworkStressTest.java
@@ -18,6 +18,7 @@
 
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
 import com.android.loganalysis.item.BugreportItem;
 import com.android.loganalysis.item.LogcatItem;
 import com.android.loganalysis.parser.BugreportParser;
@@ -29,7 +30,6 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 
diff --git a/prod-tests/src/com/android/framework/tests/PackageManagerHostTestUtils.java b/prod-tests/src/com/android/framework/tests/PackageManagerHostTestUtils.java
index 37dbfeb..7bd29bb 100644
--- a/prod-tests/src/com/android/framework/tests/PackageManagerHostTestUtils.java
+++ b/prod-tests/src/com/android/framework/tests/PackageManagerHostTestUtils.java
@@ -473,24 +473,7 @@
      */
     public void determinePrivateAppPath(File apkFile, String pkgName)
             throws DeviceNotAvailableException {
-        String result = installFileForwardLocked(apkFile, true);
-        assertEquals(null, result);
-        waitForPackageManager();
-
-        // grep for package to make sure it is installed
-        assertTrue(doesPackageExist(pkgName));
-
-        // Determine path of secret path.
-        result = mDevice.executeShellCommand("pm path " + pkgName);
-        if (result.indexOf(PRE_JB_APP_PRIVATE_PATH) != -1) {
-            setAppPrivatePath(PRE_JB_APP_PRIVATE_PATH);
-        } else if (result.indexOf(JB_APP_PRIVATE_PATH) != -1) {
-            setAppPrivatePath(JB_APP_PRIVATE_PATH);
-        } else {
-            Assert.fail("Failed to locate private app path on device.");
-        }
-        CLog.d("Device private app path is: %s", getAppPrivatePath());
-        uninstallApp(pkgName);
+        setAppPrivatePath(JB_APP_PRIVATE_PATH);
     }
 
     /**
diff --git a/prod-tests/src/com/android/framework/tests/PackageManagerHostTests.java b/prod-tests/src/com/android/framework/tests/PackageManagerHostTests.java
index e5aa08f..552df68 100644
--- a/prod-tests/src/com/android/framework/tests/PackageManagerHostTests.java
+++ b/prod-tests/src/com/android/framework/tests/PackageManagerHostTests.java
@@ -938,7 +938,7 @@
                 return;
             }
             mPMHostUtils.installFile(getTestAppFilePath(SHARED_UID_APK_64), true);
-            assertEquals(ARMEABI_V7A, mPMHostUtils.getAbi(SHARED_UID_PKG_64));
+            assertEquals(ARM64_V8A, mPMHostUtils.getAbi(SHARED_UID_PKG_64));
         } finally {
             mPMHostUtils.uninstallApp(SHARED_UID_PKG_64);
         }
diff --git a/prod-tests/src/com/android/framework/tests/PackageManagerOTATestUtils.java b/prod-tests/src/com/android/framework/tests/PackageManagerOTATestUtils.java
index 07a0e3f..6cae21c 100644
--- a/prod-tests/src/com/android/framework/tests/PackageManagerOTATestUtils.java
+++ b/prod-tests/src/com/android/framework/tests/PackageManagerOTATestUtils.java
@@ -80,8 +80,7 @@
      */
     public void removeSystemApp(String systemApp, boolean reboot)
             throws DeviceNotAvailableException {
-        remountSystemRW();
-        mDevice.waitForDeviceAvailable();
+        mDevice.remountSystemWritable();
         String cmd = String.format("rm %s", systemApp);
         mDevice.executeShellCommand(cmd);
         if (reboot) {
@@ -163,8 +162,12 @@
             CLog.d("Failed to find node for xpath %s", xPathString);
             return false;
         }
-        CLog.d("Value of node %s: %s", xPathString, n.getNodeValue());
-        return n.getNodeValue().equalsIgnoreCase(value);
+        boolean result = n.getNodeValue().equalsIgnoreCase(value);
+        if (!result) {
+            CLog.v("Value of node %s: \"%s\", expected: \"%s\"",
+                    xPathString, n.getNodeValue(), value);
+        }
+        return result;
     }
 
     /**
@@ -245,15 +248,6 @@
     }
 
     /**
-     * Helper method to remount system partition.
-     *
-     * @throws DeviceNotAvailableException
-     */
-    public void remountSystemRW() throws DeviceNotAvailableException {
-        mDevice.executeAdbCommand("remount");
-    }
-
-    /**
      * Helper method to stop system shell.
      *
      * @throws DeviceNotAvailableException
@@ -294,7 +288,7 @@
      */
     public void pushSystemApp(final File localFile, final String deviceFilePath)
             throws DeviceNotAvailableException {
-        remountSystemRW();
+        mDevice.remountSystemWritable();
         stopSystem();
         mDevice.pushFile(localFile, deviceFilePath);
         startSystem();
diff --git a/prod-tests/src/com/android/framework/tests/PackageManagerOTATests.java b/prod-tests/src/com/android/framework/tests/PackageManagerOTATests.java
index 9204444..cb45803 100644
--- a/prod-tests/src/com/android/framework/tests/PackageManagerOTATests.java
+++ b/prod-tests/src/com/android/framework/tests/PackageManagerOTATests.java
@@ -363,13 +363,15 @@
     }
 
     /**
-     * Test when update has the same version.
+     * Test when updated system app has the same version. Package manager is expected to use
+     * the newly installed upgrade.
      * <p/>
      * Assumes adb is running as root in device under test.
      *
      * @throws DeviceNotAvailableException
      */
-    public void testSystemAppUpdatedSameVersion() throws DeviceNotAvailableException {
+    public void testSystemAppUpdatedSameVersion_PreferUpdatedApk()
+            throws DeviceNotAvailableException {
         mUtils.pushSystemApp(getTestAppFilePath(VERSION_2_APK), mSystemAppPath);
         mPackageXml = mUtils.pullPackagesXML();
         assertTrue("The package should be installed",
@@ -386,7 +388,7 @@
 
         mUtils.installFile(getTestAppFilePath(VERSION_2_APK), true);
         mPackageXml = mUtils.pullPackagesXML();
-        assertFalse("After system app upgrade, the path should be the upgraded app on /data",
+        assertFalse("After system app upgrade, the path should be the upgraded app in /data",
                 mUtils.expectEquals(mPackageXml, CODE_PATH_XPATH, mSystemAppPath));
         assertTrue("Package version should be 2",
                 mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
@@ -400,63 +402,10 @@
 
         mUtils.restartSystem();
         mPackageXml = mUtils.pullPackagesXML();
-        assertTrue("After reboot, the path should be the be installed",
+        assertFalse("After reboot, the path should be the upgraded app in /data",
                 mUtils.expectEquals(mPackageXml, CODE_PATH_XPATH, mSystemAppPath));
         assertTrue("Package version should be 2",
                 mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
-        assertFalse("Updated-package should NOT be present",
-                mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
-        assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
-        assertTrue("VIBRATE permission should be granted",
-                mUtils.packageHasPermission(PACKAGE_NAME, VIBRATE_PERMISSION));
-        assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
-                mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
-
-        mUtils.restartSystem();
-        mPackageXml = mUtils.pullPackagesXML();
-        assertTrue("After reboot, the path should be the be installed",
-                mUtils.expectEquals(mPackageXml, CODE_PATH_XPATH, mSystemAppPath));
-        assertTrue("Package version should be 2",
-                mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
-        assertFalse("Updated-package should NOT be present",
-                mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
-        assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
-        assertTrue("VIBRATE permission should be granted",
-                mUtils.packageHasPermission(PACKAGE_NAME, VIBRATE_PERMISSION));
-        assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
-                mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
-    }
-
-    /**
-     * Test when update has a different filename.
-     * <p/>
-     * Assumes adb is running as root in device under test.
-     *
-     * @throws DeviceNotAvailableException
-     */
-    public void testUpdatedSystemAppChangeFileName() throws DeviceNotAvailableException {
-        mUtils.pushSystemApp(getTestAppFilePath(VERSION_1_APK), mSystemAppPath);
-        mPackageXml = mUtils.pullPackagesXML();
-        assertNotNull("Failed to pull packages xml file from device", mPackageXml);
-        assertTrue("After system app push, the package should be installed",
-                mUtils.expectExists(mPackageXml, PACKAGE_XPATH));
-        assertTrue("Package version should be 1",
-                mUtils.expectEquals(mPackageXml, VERSION_XPATH, "1"));
-        assertFalse("Updated-package should not be present",
-                mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
-        assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
-        assertTrue("VIBRATE permission should be granted",
-                mUtils.packageHasPermission(PACKAGE_NAME, VIBRATE_PERMISSION));
-        assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
-                mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
-
-        mUtils.installFile(getTestAppFilePath(VERSION_2_APK), true);
-        mPackageXml = mUtils.pullPackagesXML();
-        assertTrue("After system app upgrade, the path should be the upgraded app on /data",
-                mUtils.expectStartsWith(mPackageXml, CODE_PATH_XPATH,
-                DATA_APP_DIRECTORY + PACKAGE_NAME));
-        assertTrue("Package version should be 2",
-                mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
         assertTrue("Updated-package should be present",
                 mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
         assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
@@ -465,21 +414,6 @@
         assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
                 mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
 
-        mUtils.removeSystemApp(mSystemAppPath, false);
-        mUtils.pushSystemApp(getTestAppFilePath(VERSION_2_APK), mDiffSystemAppPath);
-
-        mPackageXml = mUtils.pullPackagesXML();
-        assertTrue("After reboot, the system path should be correct",
-                mUtils.expectEquals(mPackageXml, CODE_PATH_XPATH, mDiffSystemAppPath));
-        assertTrue("Package version should be 2",
-                mUtils.expectEquals(mPackageXml, VERSION_XPATH, "2"));
-        assertFalse("Updated-package should not be present",
-                mUtils.expectExists(mPackageXml, UPDATE_PACKAGE_XPATH));
-        assertTrue("Package should have FLAG_SYSTEM", expectFlag(mPackageXml, FLAG_XPATH, 1));
-        assertTrue("VIBRATE permission should be granted",
-                mUtils.packageHasPermission(PACKAGE_NAME, VIBRATE_PERMISSION));
-        assertTrue("ACCESS_CACHE_FILESYSTEM permission should be granted",
-                mUtils.packageHasPermission(PACKAGE_NAME, CACHE_PERMISSION));
     }
 
     /**
diff --git a/prod-tests/src/com/android/graphics/tests/ImageProcessingTest.java b/prod-tests/src/com/android/graphics/tests/ImageProcessingTest.java
index cc3dc9f..9166c5d 100644
--- a/prod-tests/src/com/android/graphics/tests/ImageProcessingTest.java
+++ b/prod-tests/src/com/android/graphics/tests/ImageProcessingTest.java
@@ -18,6 +18,7 @@
 
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -26,7 +27,6 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.RunUtil;
diff --git a/prod-tests/src/com/android/graphics/tests/SkiaTest.java b/prod-tests/src/com/android/graphics/tests/SkiaTest.java
new file mode 100644
index 0000000..7d0dc18
--- /dev/null
+++ b/prod-tests/src/com/android/graphics/tests/SkiaTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2014 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.graphics.tests;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IFileEntry;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.util.RunUtil;
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import java.io.File;
+import java.util.Collections;
+
+/**
+ *  Test for running Skia native tests.
+ *
+ *  The test is not necessarily Skia specific, but it provides
+ *  functionality that allows native Skia tests to be run.
+ *
+ *  Includes options to specify the Skia test app to run (inside
+ *  nativetest directory), flags to pass to the test app, and a file
+ *  to retrieve off the device after the test completes. (Skia test
+ *  apps record their results to a json file, so retrieving this file
+ *  allows us to view the results so long as the app completed.)
+ */
+@OptionClass(alias = "skia_native_tests")
+public class SkiaTest implements IRemoteTest, IDeviceTest {
+    private ITestDevice mDevice;
+
+    static final String DEFAULT_NATIVETEST_PATH = "/data/nativetest";
+
+    @Option(name = "native-test-device-path",
+      description = "The path on the device where native tests are located.")
+    private String mNativeTestDevicePath = DEFAULT_NATIVETEST_PATH;
+
+    @Option(name = "skia-flags",
+        description = "Flags to pass to the skia program.")
+    private String mFlags = "";
+
+    @Option(name = "skia-app",
+        description = "Skia program to run.",
+        mandatory = true)
+    private String mSkiaApp = "";
+
+    @Option(name = "skia-json",
+        description = "Full path on device for json output file.")
+    private File mOutputFile = null;
+
+    @Option(name = "skia-pngs",
+        description = "Directory on device for holding png results for retrieval.")
+    private File mPngDir = null;
+
+    @Override
+    public void setDevice(ITestDevice device) {
+        mDevice = device;
+    }
+
+    @Override
+    public ITestDevice getDevice() {
+        return mDevice;
+    }
+
+    @Override
+    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+        if (mDevice == null) {
+            throw new IllegalArgumentException("Device has not been set");
+        }
+
+        listener.testRunStarted(mSkiaApp, 1);
+        long start = System.currentTimeMillis();
+
+        // Native Skia tests are in nativeTestDirectory/mSkiaApp/mSkiaApp.
+        String fullPath = mNativeTestDevicePath + "/"
+                + mSkiaApp + "/" + mSkiaApp;
+        IFileEntry app = mDevice.getFileEntry(fullPath);
+        TestIdentifier testId = new TestIdentifier(mSkiaApp, "testFileExists");
+        listener.testStarted(testId);
+        if (app == null) {
+            CLog.w("Could not find test %s in %s!", fullPath, mDevice.getSerialNumber());
+            listener.testFailed(testId, "Device does not have " + fullPath);
+            listener.testEnded(testId, null);
+        } else {
+            // The test for detecting the file has ended.
+            listener.testEnded(testId, null);
+            prepareDevice();
+            runTest(app);
+            retrieveFiles(mSkiaApp, listener);
+        }
+
+        listener.testRunEnded(System.currentTimeMillis() - start,
+                Collections.<String, String>emptyMap());
+    }
+
+    /**
+     *  Emulates running mkdirs on an ITestDevice.
+     *
+     *  Creates the directory named by dir *on device*, recursively creating missing parent
+     *  directories if necessary.
+     *
+     *  @param dir Directory to create.
+     */
+    private void mkdirs(File dir) throws DeviceNotAvailableException {
+        if (dir == null || mDevice.doesFileExist(dir.getPath())) {
+            return;
+        }
+
+        String dirName = dir.getPath();
+        CLog.v("creating folder '%s'", dirName);
+        mDevice.executeShellCommand("mkdir -p " + dirName);
+    }
+
+    /**
+     *  Do pre-test setup on the device.
+     *
+     *  Setup involves ensuring necessary directories exist and removing old
+     *  test result files.
+     */
+    private void prepareDevice() throws DeviceNotAvailableException {
+        if (mOutputFile != null) {
+            String path = mOutputFile.getPath();
+            if (mDevice.doesFileExist(path)) {
+                // Delete the file. We don't want to think this file from an
+                // earlier run represents this one.
+                CLog.v("Removing old file " + path);
+                mDevice.executeShellCommand("rm " + path);
+            } else {
+                // Ensure its containing folder exists.
+                mkdirs(mOutputFile.getParentFile());
+            }
+        }
+
+        if (mPngDir != null) {
+            String pngPath = mPngDir.getPath();
+            if (mDevice.doesFileExist(pngPath)) {
+                // Empty the old directory
+                mDevice.executeShellCommand("rm -rf " + pngPath + "/*");
+            } else {
+                mkdirs(mPngDir);
+            }
+        }
+    }
+
+    /**
+     *  Retrieve a file from the device and upload it to the listener.
+     *
+     *  Each file for uploading is considered its own test, so we can track
+     *  whether or not uploading succeeded.
+     *
+     *  @param remoteFile File on the device.
+     *  @param testIdClass String to be passed to TestIdentifier's constructor
+     *          as className.
+     *  @param testIdMethod String passed to TestIdentifier's constructor as
+     *          testName.
+     *  @param listener Listener for reporting test failure/success and
+     *          uploading files.
+     *  @param type LogDataType of the file being uploaded.
+     */
+    private void retrieveAndUploadFile(File remoteFile, String testIdClass, String testIdMethod,
+            ITestInvocationListener listener, LogDataType type) throws DeviceNotAvailableException {
+        String remotePath = remoteFile.getPath();
+        CLog.v("adb pull %s (using pullFile)", remotePath);
+        File localFile = mDevice.pullFile(remotePath);
+
+        TestIdentifier testId = new TestIdentifier(testIdClass, testIdMethod);
+        listener.testStarted(testId);
+        if (localFile == null) {
+            listener.testFailed(testId, "Failed to pull " + remotePath);
+        } else {
+            CLog.v("pulled result file to " + localFile.getPath());
+            FileInputStreamSource source = new FileInputStreamSource(localFile);
+            // Use the original name, for clarity.
+            listener.testLog(remoteFile.getName(), type, source);
+            source.cancel();
+            if (!localFile.delete()) {
+                CLog.w("Failed to delete temporary file %s", localFile.getPath());
+            }
+        }
+        listener.testEnded(testId, null);
+    }
+
+    /**
+     *  Retrieve files from the device.
+     *
+     *  Report to the listener whether retrieving the files succeeded.
+     *
+     *  @param appName Name of the app.
+     *  @param listener Listener for reporting results of file retrieval.
+     */
+    private void retrieveFiles(String appName,
+            ITestInvocationListener listener) throws DeviceNotAvailableException {
+        // FIXME: This could be achieved with DeviceFileReporter. Blocked on b/18408206.
+        if (mOutputFile != null) {
+            retrieveAndUploadFile(mOutputFile, appName, "outputJson", listener, LogDataType.TEXT);
+        }
+
+        if (mPngDir != null) {
+            String pngDir = mPngDir.getPath();
+            IFileEntry remotePngDir = mDevice.getFileEntry(pngDir);
+            for (IFileEntry pngFile : remotePngDir.getChildren(false)) {
+                if (pngFile.getName().endsWith("png")) {
+                    retrieveAndUploadFile(new File(pngFile.getFullPath()),
+                            "PngRetrieval", pngFile.getName(), listener, LogDataType.PNG);
+                }
+            }
+        }
+    }
+
+    /**
+     *  Run a test on a device.
+     *
+     *  @param app Test app to run.
+     */
+    private void runTest(IFileEntry app) throws DeviceNotAvailableException {
+        String fullPath = app.getFullEscapedPath();
+        // force file to be executable
+        mDevice.executeShellCommand(String.format("chmod 755 %s", fullPath));
+
+        // The device will not immediately capture logs in response to
+        // startLogcat. Instead, it delays 5 * 1000ms. See TestDevice.java
+        // mLogStartDelay. To ensure we see all the logs, sleep by the same
+        // amount.
+        mDevice.startLogcat();
+        RunUtil.getDefault().sleep(5 * 1000);
+
+        String cmd = fullPath + " " + mFlags;
+        CLog.v("Running '%s' on %s", cmd, mDevice.getSerialNumber());
+
+        mDevice.executeShellCommand("stop");
+        mDevice.executeShellCommand(cmd);
+        mDevice.executeShellCommand("start");
+    }
+}
diff --git a/prod-tests/src/com/android/graphics/tests/UiPerformanceTest.java b/prod-tests/src/com/android/graphics/tests/UiPerformanceTest.java
index 7dacb1d..b1016f2 100644
--- a/prod-tests/src/com/android/graphics/tests/UiPerformanceTest.java
+++ b/prod-tests/src/com/android/graphics/tests/UiPerformanceTest.java
@@ -131,7 +131,7 @@
 
             String rawFileList =
                     mTestDevice.executeShellCommand(String.format("ls \"%s\"", rawFileDir));
-            String[] rawFileString = rawFileList.split("\r\n");
+            String[] rawFileString = rawFileList.split("\r?\n");
             File resFile = null;
             InputStreamSource outputSource = null;
             for (int i = 0; i < rawFileString.length; i++) {
diff --git a/prod-tests/src/com/android/media/tests/AudioJitterTest.java b/prod-tests/src/com/android/media/tests/AudioJitterTest.java
index 5c12a86..7058cc7 100644
--- a/prod-tests/src/com/android/media/tests/AudioJitterTest.java
+++ b/prod-tests/src/com/android/media/tests/AudioJitterTest.java
@@ -17,7 +17,6 @@
 package com.android.media.tests;
 
 import com.android.ddmlib.CollectingOutputReceiver;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -43,7 +42,6 @@
 
     private ITestDevice mDevice;
 
-    /*   /home/android-test/testdata/media/sljitter */
     private static final String DEVICE_TEMPORARY_DIR_PATH = "/data/local/tmp/";
     private static final String JITTER_BINARY_FILENAME = "sljitter";
     private static final String JITTER_BINARY_DEVICE_PATH =
@@ -121,7 +119,7 @@
 
         if (errMsg != null) {
             CLog.e(errMsg);
-            listener.testFailed(TestFailure.FAILURE, testId, errMsg);
+            listener.testFailed(testId, errMsg);
             listener.testEnded(testId, metrics);
             listener.testRunFailed(errMsg);
         } else {
diff --git a/prod-tests/src/com/android/media/tests/CameraLatencyTest.java b/prod-tests/src/com/android/media/tests/CameraLatencyTest.java
index 64d055d..82375d4 100644
--- a/prod-tests/src/com/android/media/tests/CameraLatencyTest.java
+++ b/prod-tests/src/com/android/media/tests/CameraLatencyTest.java
@@ -167,9 +167,8 @@
 
         // Grab a bugreport if warranted
         if (auxListener.hasFailedTests()) {
-            CLog.i("Grabbing bugreport after test '%s' finished with %d failures and %d errors.",
-                    test.mTestName, auxListener.getNumFailedTests(),
-                    auxListener.getNumErrorTests());
+            CLog.i("Grabbing bugreport after test '%s' finished with %d failures.",
+                    test.mTestName, auxListener.getNumAllFailedTests());
             InputStreamSource bugreport = mTestDevice.getBugreport();
             listener.testLog(String.format("bugreport-%s.txt", test.mTestName),
                     LogDataType.BUGREPORT, bugreport);
diff --git a/prod-tests/src/com/android/media/tests/CameraPerformanceTest.java b/prod-tests/src/com/android/media/tests/CameraPerformanceTest.java
new file mode 100644
index 0000000..aee0316
--- /dev/null
+++ b/prod-tests/src/com/android/media/tests/CameraPerformanceTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2015 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.media.tests;
+
+import com.google.common.collect.ImmutableMap;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.StubTestInvocationListener;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.InstrumentationTest;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This test invocation runs android.hardware.camera2.cts.PerformanceTest -
+ * Camera2 API use case performance KPIs, such as camera open time, session creation time,
+ * shutter lag etc. The KPI data will be parsed and reported to dashboard.
+ */
+public class CameraPerformanceTest implements IDeviceTest, IRemoteTest {
+
+    private static final String LOG_TAG = CameraPerformanceTest.class.getSimpleName();
+    private static final String TEST_CLASS_NAME =
+            "android.hardware.camera2.cts.PerformanceTest";
+    private static final String TEST_PACKAGE_NAME = "com.android.cts.hardware";
+    private static final String TEST_RUNNER_NAME =
+            "android.support.test.runner.AndroidJUnitRunner";
+    private static final String RU_KEY = "camera_framework_performance";
+
+    private final int MAX_TEST_TIMEOUT = 10 * 60 * 1000; // 10 mins
+
+    @Option(name="method", shortName = 'm',
+            description="Used to specify a specific test method to run")
+    private String mMethodName = null;
+
+    private ITestDevice mDevice = null;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setDevice(ITestDevice device) {
+        mDevice = device;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ITestDevice getDevice() {
+        return mDevice;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+        CollectingListener collectingListener = new CollectingListener();
+        runTest(collectingListener);
+        Map<String, String> parsedMetrics = parseResult(collectingListener.mStdout);
+        postMetrics(listener, parsedMetrics);
+    }
+
+    private void runTest(ITestInvocationListener listener) throws DeviceNotAvailableException {
+        InstrumentationTest instr = new InstrumentationTest();
+        instr.setDevice(getDevice());
+        instr.setPackageName(TEST_PACKAGE_NAME);
+        instr.setRunnerName(TEST_RUNNER_NAME);
+        instr.setClassName(TEST_CLASS_NAME);
+        if (mMethodName != null) {
+            instr.setMethodName(mMethodName);
+        }
+        instr.setShellTimeout(MAX_TEST_TIMEOUT);
+        instr.run(listener);
+    }
+
+    /**
+     * Parse Camera Performance KPIs result from the stdout generated by each test run.
+     * Then put them all together to post the final report
+     *
+     * @return a {@link HashMap} that contains pairs of kpiName and kpiValue
+     */
+    private Map<String, String> parseResult(Map<String, String> metrics) {
+        Map<String, String> resultsAll = new HashMap<String, String>();
+        Camera2KpiParser parser = new Camera2KpiParser();
+        for (Map.Entry<String, String> metric : metrics.entrySet()) {
+            String testMethod = metric.getKey();
+            String stdout = metric.getValue();
+            CLog.d("test name %s", testMethod);
+            CLog.d("stdout %s", stdout);
+
+            // Get pairs of { KPI name, KPI value } from stdout that each test outputs.
+            // Assuming that a device has both the front and back cameras, parser will return
+            // 2 KPIs in HashMap. For an example of testCameraLaunch,
+            //   {
+            //     ("Camera 0 Camera launch time", "379.20"),
+            //     ("Camera 1 Camera launch time", "272.80"),
+            //   }
+            Map<String, String> testKpis = parser.parse(stdout, testMethod);
+            for (String k : testKpis.keySet()) {
+                if (resultsAll.containsKey(k)) {
+                    throw new RuntimeException(String.format("KPI name (%s) conflicts with " +
+                            "the existing names. ", k));
+                }
+            }
+
+            // Put each result together to post the final result
+            resultsAll.putAll(testKpis);
+        }
+        return resultsAll;
+    }
+
+    /**
+     * A listener to collect the stdout from each test run.
+     */
+    private class CollectingListener extends StubTestInvocationListener {
+        public Map<String, String> mStdout = new HashMap<String, String>();
+
+        @Override
+        public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
+            for (Map.Entry<String, String> metric : testMetrics.entrySet()) {
+                // capture only test name and stdout generated by test
+                mStdout.put(test.getTestName(), metric.getValue());
+            }
+        }
+    }
+
+    /**
+     * Data class of Camera Performance KPIs separated into summary and KPI items
+     */
+    private class Camera2KpiData {
+        public class KpiItem {
+            private String mTestId;     // "android.hardware.camera2.cts.PerformanceTest#testSingleCapture"
+            private String mCameraId;   // "0" or "1"
+            private String mKpiName;    // "Camera capture latency"
+            private String mType;       // "lower_better"
+            private String mUnit;       // "ms"
+            private String mKpiValue;   // "736.0 688.0 679.0 667.0 686.0"
+            private String mKey;        // primary key = cameraId + kpiName
+            private KpiItem(String testId, String cameraId, String kpiName, String type,
+                    String unit, String kpiValue) {
+                mTestId = testId;
+                mCameraId = cameraId;
+                mKpiName = kpiName;
+                mType = type;
+                mUnit = unit;
+                mKpiValue = kpiValue;
+                // Note that the key shouldn't contain ":" for side by side report.
+                mKey = String.format("Camera %s %s", cameraId, kpiName);
+            }
+            public String getTestId() { return mTestId; }
+            public String getCameraId() { return mCameraId; }
+            public String getKpiName() { return mKpiName; }
+            public String getType() { return mType; }
+            public String getUnit() { return mUnit; }
+            public String getKpiValue() { return mKpiValue; }
+            public String getKey() { return mKey; }
+        }
+
+        private KpiItem mSummary;
+        private Map<String, KpiItem> mKpis = new HashMap<String, KpiItem>();
+
+        public KpiItem createItem(String testId, String cameraId, String kpiName, String type,
+                String unit, String kpiValue) {
+            return new KpiItem(testId, cameraId, kpiName, type, unit, kpiValue);
+        }
+        public KpiItem getSummary() { return mSummary; }
+        public void setSummary(KpiItem summary) { mSummary = summary; }
+        public List<KpiItem> getKpisByKpiName(String kpiName) {
+            List<KpiItem> kpiItems = new ArrayList<KpiItem>();
+            for (KpiItem log : mKpis.values()) {
+                if (log.getKpiName().equals(kpiName)) {
+                    kpiItems.add(log);
+                }
+            }
+            return kpiItems;
+        }
+        public void addKpi(KpiItem kpiItem) {
+            mKpis.put(kpiItem.getKey(), kpiItem);
+        }
+    }
+
+    /**
+     * Parses the stdout generated by the underlying instrumentation test
+     * and returns it to test runner for later reporting.
+     *
+     * Format:
+     *   (summary message)| |(type)|(unit)|(value) ++++
+     *   (test id)|(message)|(type)|(unit)|(value)... +++
+     *   ...
+     *
+     * Example:
+     *   Camera launch average time for Camera 1| |lower_better|ms|586.6++++
+     *   android.hardware.camera2.cts.PerformanceTest#testCameraLaunch:171|Camera 0: Camera open time|lower_better|ms|74.0 100.0 70.0 67.0 82.0 +++
+     *   android.hardware.camera2.cts.PerformanceTest#testCameraLaunch:171|Camera 0: Camera configure stream time|lower_better|ms|9.0 5.0 5.0 8.0 5.0
+     *   ...
+     *
+     * See also com.android.cts.util.ReportLog for the format detail.
+     *
+     */
+    private class Camera2KpiParser {
+        private static final String LOG_SEPARATOR = "\\+\\+\\+";
+        private static final String SUMMARY_SEPARATOR = "\\+\\+\\+\\+";
+        private static final String LOG_ELEM_SEPARATOR = "|";
+        private final Pattern SUMMARY_REGEX = Pattern.compile(
+                "^(?<message>[^|]+)\\| \\|(?<type>[^|]+)\\|(?<unit>[^|]+)\\|(?<value>[0-9 .]+)");
+        private final Pattern KPI_REGEX = Pattern.compile(
+                "^(?<testId>[^|]+)\\|(?<message>[^|]+)\\|(?<type>[^|]+)\\|(?<unit>[^|]+)\\|(?<values>[0-9 .]+)");
+        // eg. "Camera 0: Camera capture latency"
+        private final Pattern KPI_KEY_REGEX = Pattern.compile(
+                "^Camera\\s+(?<cameraId>\\d+):\\s+(?<kpiName>.*)");
+
+        // HashMap that contains pairs of (testMethod), (the name of KPI to be reported)
+        // TODO(hyungtaekim) : Use MultiMap instead if more than one KPI need to be reported
+        private final ImmutableMap<String, String> REPORTING_KPIS =
+                new ImmutableMap.Builder<String, String>()
+                        .put("testCameraLaunch", "Camera launch time")
+                        .put("testSingleCapture", "Camera capture result latency")
+                        .build();
+
+        /**
+         * Parse Camera Performance KPIs result first, then leave the only KPIs that matter.
+         *
+         * @param input String to be parsed
+         * @param testMethod test method name used to leave the only metric that matters
+         * @return a {@link HashMap} that contains kpiName and kpiValue
+         */
+        public Map<String, String> parse(String input, String testMethod) {
+            return filter(parseToData(input), testMethod);
+        }
+
+        private Map<String, String> filter(Camera2KpiData data, String testMethod) {
+            Map<String, String> filtered = new HashMap<String, String>();
+            String kpiToReport = REPORTING_KPIS.get(testMethod);
+            // report the only selected items
+            List<Camera2KpiData.KpiItem> items = data.getKpisByKpiName(kpiToReport);
+            for (Camera2KpiData.KpiItem item : items) {
+                filtered.put(item.getKey(), item.getKpiValue());
+            }
+            return filtered;
+        }
+
+        private Camera2KpiData parseToData(String input) {
+            Camera2KpiData data = new Camera2KpiData();
+
+            // Split summary and KPIs from stdout passes as parameter.
+            String[] output = input.split(SUMMARY_SEPARATOR);
+            if (output.length != 2) {
+                throw new RuntimeException("Value not in correct format");
+            }
+            Matcher summaryMatcher = SUMMARY_REGEX.matcher(output[0].trim());
+
+            // Parse summary.
+            // Example: "Camera launch average time for Camera 1| |lower_better|ms|586.6++++"
+            if (summaryMatcher.matches()) {
+                data.setSummary(data.createItem(null,
+                        "-1",
+                        summaryMatcher.group("message"),
+                        summaryMatcher.group("type"),
+                        summaryMatcher.group("unit"),
+                        summaryMatcher.group("value")));
+            } else {
+                // Currently malformed summary won't block a test as it's not used for report.
+                CLog.w("Summary not in correct format");
+            }
+
+            // Parse KPIs.
+            // Example: "android.hardware.camera2.cts.PerformanceTest#testCameraLaunch:171|Camera 0: Camera open time|lower_better|ms|74.0 100.0 70.0 67.0 82.0 +++"
+            String[] kpis = output[1].split(LOG_SEPARATOR);
+            for (String kpi : kpis) {
+                Matcher kpiMatcher = KPI_REGEX.matcher(kpi.trim());
+                if (kpiMatcher.matches()) {
+                    String message = kpiMatcher.group("message");
+                    Matcher m = KPI_KEY_REGEX.matcher(message.trim());
+                    if (!m.matches()) {
+                        throw new RuntimeException("Value not in correct format");
+                    }
+                    String cameraId = m.group("cameraId");
+                    String kpiName = m.group("kpiName");
+                    // get average of kpi values
+                    String[] values = kpiMatcher.group("values").split("\\s+");
+                    double sum = 0;
+                    for (String value : values) {
+                        sum += Double.parseDouble(value);
+                    }
+                    String kpiValue = String.format("%.1f", sum / values.length);
+                    data.addKpi(data.createItem(kpiMatcher.group("testId"),
+                            cameraId,
+                            kpiName,
+                            kpiMatcher.group("type"),
+                            kpiMatcher.group("unit"),
+                            kpiValue));
+                } else {
+                    throw new RuntimeException("KPI not in correct format");
+                }
+            }
+            return data;
+        }
+    }
+
+    /**
+     * Report run metrics by creating an empty test run to stick them in.
+     *
+     * @param listener The {@link ITestInvocationListener} of test results
+     * @param metrics The {@link Map} that contains metrics for the given test
+     */
+    private void postMetrics(ITestInvocationListener listener, Map<String, String> metrics) {
+        listener.testRunStarted(RU_KEY, 1);
+        TestIdentifier testId = new TestIdentifier(RU_KEY, LOG_TAG);
+        listener.testStarted(testId);
+        listener.testEnded(testId, Collections.<String, String> emptyMap());
+        listener.testRunEnded(0, metrics);
+    }
+}
diff --git a/prod-tests/src/com/android/media/tests/CameraStressTest.java b/prod-tests/src/com/android/media/tests/CameraStressTest.java
index a7bf58a..c3504f5 100644
--- a/prod-tests/src/com/android/media/tests/CameraStressTest.java
+++ b/prod-tests/src/com/android/media/tests/CameraStressTest.java
@@ -203,8 +203,7 @@
         // Grab a bugreport if warranted
         if (auxListener.hasFailedTests()) {
             Log.e(LOG_TAG, String.format("Grabbing bugreport after test '%s' finished with " +
-                    "%d failures and %d errors.", test.mTestName, auxListener.getNumFailedTests(),
-                    auxListener.getNumErrorTests()));
+                    "%d failures.", test.mTestName, auxListener.getNumAllFailedTests()));
             InputStreamSource bugreport = mTestDevice.getBugreport();
             listener.testLog(String.format("bugreport-%s.txt", test.mTestName),
                     LogDataType.BUGREPORT, bugreport);
diff --git a/prod-tests/src/com/android/media/tests/MediaPlayerStressTest.java b/prod-tests/src/com/android/media/tests/MediaPlayerStressTest.java
index 07bef61..b1d911d 100644
--- a/prod-tests/src/com/android/media/tests/MediaPlayerStressTest.java
+++ b/prod-tests/src/com/android/media/tests/MediaPlayerStressTest.java
@@ -85,6 +85,8 @@
         mPatternMap.put("PlaybackCrash", "^Total Error: (\\d+)");
         mPatternMap.put("TrackLagging", "^Total Track Lagging: (\\d+)");
         mPatternMap.put("BadInterleave", "^Total Bad Interleaving: (\\d+)");
+        mPatternMap.put("FailedToCompleteWithNoError",
+                "^Total Failed To Complete With No Error: (\\d+)");
     }
 
     @Override
diff --git a/prod-tests/src/com/android/media/tests/VideoMultimeterRunner.java b/prod-tests/src/com/android/media/tests/VideoMultimeterRunner.java
new file mode 100644
index 0000000..af5a318
--- /dev/null
+++ b/prod-tests/src/com/android/media/tests/VideoMultimeterRunner.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2014 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.media.tests;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.util.CommandResult;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+
+/**
+ * A harness that test video playback with multiple devices and reports result.
+ */
+public class VideoMultimeterRunner extends VideoMultimeterTest
+    implements IDeviceTest, IRemoteTest {
+
+    @Option(name = "robot-util-path", description = "path for robot control util",
+            importance = Importance.ALWAYS, mandatory = true)
+    String mRobotUtilPath = "/tmp/robot_util.sh";
+
+    @Option(name = "device-map", description =
+            "Device serials map to location and audio input. May be repeated",
+            importance = Importance.ALWAYS)
+    Map<String, String> mDeviceMap = new HashMap<String, String>();
+
+    static final long ROBOT_TIMEOUT_MS = 60 * 1000;
+
+    static final Semaphore runToken = new Semaphore(1);
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void run(ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
+        long durationMs = 0;
+        TestIdentifier testId = new TestIdentifier(getClass()
+                .getCanonicalName(), RUN_KEY);
+
+        listener.testRunStarted(RUN_KEY, 0);
+        listener.testStarted(testId);
+
+        long testStartTime = System.currentTimeMillis();
+        Map<String, String> metrics = new HashMap<String, String>();
+
+        try {
+            CLog.v("Waiting to acquire run token");
+            runToken.acquire();
+
+            String deviceSerial = getDevice().getSerialNumber();
+
+            if (moveArm(deviceSerial) && setupTestEnv()) {
+                runMultimeterTest(listener, metrics);
+            } else {
+                listener.testFailed(testId, "Failed to set up environment");
+            }
+        } catch (InterruptedException e) {
+            CLog.d("Acquire run token interrupted");
+            listener.testFailed(testId, "Failed to acquire run token");
+        } finally {
+            runToken.release();
+            listener.testEnded(testId, metrics);
+            durationMs = System.currentTimeMillis() - testStartTime;
+            listener.testRunEnded(durationMs, metrics);
+        }
+    }
+
+    protected boolean moveArm(String deviceSerial) {
+        if (mDeviceMap.containsKey(deviceSerial)) {
+            CLog.v("Moving robot arm to device " + deviceSerial);
+            CommandResult cr = getRunUtil().runTimedCmd(
+                    ROBOT_TIMEOUT_MS, mRobotUtilPath, mDeviceMap.get(deviceSerial));
+            CLog.v(cr.getStdout());
+            return true;
+        } else {
+            CLog.e("Cannot find device in map, test failed");
+            return false;
+        }
+    }
+}
diff --git a/prod-tests/src/com/android/media/tests/VideoMultimeterTest.java b/prod-tests/src/com/android/media/tests/VideoMultimeterTest.java
index 2d01490..d8e5e80 100644
--- a/prod-tests/src/com/android/media/tests/VideoMultimeterTest.java
+++ b/prod-tests/src/com/android/media/tests/VideoMultimeterTest.java
@@ -23,12 +23,15 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.SnapshotInputStreamSource;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
 
+import java.io.ByteArrayInputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
@@ -42,45 +45,53 @@
  */
 public class VideoMultimeterTest implements IDeviceTest, IRemoteTest {
 
-    private static final String RUN_KEY = "video_multimeter";
+    static final String RUN_KEY = "video_multimeter";
 
     @Option(name = "multimeter-util-path", description = "path for multimeter control util",
             importance = Importance.ALWAYS)
-    private String mUtilPath = "/tmp/util.sh";
+    String mMeterUtilPath = "/tmp/util.sh";
 
-    private static final String START_VIDEO_PLAYER = "am start"
+    static final String START_VIDEO_PLAYER = "am start"
             + " -a android.intent.action.VIEW -t video/mp4 -d \"file://%s\""
             + " -n \"com.google.android.apps.plus/.phone.VideoViewActivity\"";
-    private static final String KILL_VIDEO_PLAYER = "am force-stop com.google.android.apps.plus";
-    private static final String ROTATE_LANDSCAPE = "content insert --uri content://settings/system"
+    static final String KILL_VIDEO_PLAYER = "am force-stop com.google.android.apps.plus";
+    static final String ROTATE_LANDSCAPE = "content insert --uri content://settings/system"
             + " --bind name:s:user_rotation --bind value:i:1";
 
-    private static final String VIDEO_DIR = "/sdcard/DCIM/Camera/";
+    static final String VIDEO_DIR = "/sdcard/DCIM/Camera/";
 
-    private static final String CALI_VIDEO_DEVICE_PATH = VIDEO_DIR + "video_cali.mp4";
+    static final String CALI_VIDEO_DEVICE_PATH = VIDEO_DIR + "video_cali.mp4";
 
-    private static final String TEST_VIDEO_1_DEVICE_PATH = VIDEO_DIR + "video.mp4";
-    private static final String TEST_VIDEO_1_PREFIX = "bbb_";
-    private static final long TEST_VIDEO_1_DURATION = 11 * 60; // in second
+    // FIXIT: move video path and info to options for flexibility
+    static final String TEST_VIDEO_1_DEVICE_PATH = VIDEO_DIR + "video.mp4";
+    static final String TEST_VIDEO_1_PREFIX = "24fps_";
+    static final float TEST_VIDEO_1_FPS = 24;
+    static final long TEST_VIDEO_1_DURATION = 11 * 60; // in second
 
-    private static final String TEST_VIDEO_2_DEVICE_PATH = VIDEO_DIR + "video2.mp4";
-    private static final String TEST_VIDEO_2_PREFIX = "60fps_";
-    private static final long TEST_VIDEO_2_DURATION = 5 * 60; // in second
+    static final String TEST_VIDEO_2_DEVICE_PATH = VIDEO_DIR + "video2.mp4";
+    static final String TEST_VIDEO_2_PREFIX = "60fps_";
+    static final float TEST_VIDEO_2_FPS = 60;
+    static final long TEST_VIDEO_2_DURATION = 5 * 60; // in second
 
-    private static final String CMD_GET_FRAMERATE_STATE = "GETF";
-    private static final String CMD_START_CALIBRATION = "STAC";
-    private static final String CMD_STOP_CALIBRATION = "STOC";
-    private static final String CMD_START_MEASUREMENT = "STAM";
-    private static final String CMD_STOP_MEASUREMENT = "STOM";
-    private static final String CMD_GET_NUM_FRAMES = "GETN";
-    private static final String CMD_GET_ALL_DATA = "GETD";
+    // Max number of trailing frames to trim
+    static final int TRAILING_FRAMES_MAX = 3;
+    // Min threshold for duration of trailing frames
+    static final long FRAME_DURATION_THRESHOLD_US = 500 * 1000; // 0.5s
 
-    private static final long DEVICE_SYNC_TIME_MS = 30 * 1000;
-    private static final long CALIBRATION_TIMEOUT_MS = 30 * 1000;
-    private static final long COMMAND_TIMEOUT_MS = 5 * 1000;
-    private static final long GETDATA_TIMEOUT_MS = 10 * 60 * 1000;
+    static final String CMD_GET_FRAMERATE_STATE = "GETF";
+    static final String CMD_START_CALIBRATION = "STAC";
+    static final String CMD_STOP_CALIBRATION = "STOC";
+    static final String CMD_START_MEASUREMENT = "STAM";
+    static final String CMD_STOP_MEASUREMENT = "STOM";
+    static final String CMD_GET_NUM_FRAMES = "GETN";
+    static final String CMD_GET_ALL_DATA = "GETD";
 
-    private ITestDevice mDevice;
+    static final long DEVICE_SYNC_TIME_MS = 30 * 1000;
+    static final long CALIBRATION_TIMEOUT_MS = 30 * 1000;
+    static final long COMMAND_TIMEOUT_MS = 5 * 1000;
+    static final long GETDATA_TIMEOUT_MS = 10 * 60 * 1000;
+
+    ITestDevice mDevice;
 
     /**
      * {@inheritDoc}
@@ -105,19 +116,19 @@
         }
     }
 
-    private boolean setupTestEnv() throws DeviceNotAvailableException {
+    protected boolean setupTestEnv() throws DeviceNotAvailableException {
         getRunUtil().sleep(DEVICE_SYNC_TIME_MS);
         CommandResult cr = getRunUtil().runTimedCmd(
-                COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_MEASUREMENT);
+                COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_MEASUREMENT);
 
         getDevice().setDate(new Date());
         CLog.i("syncing device time to host time");
         getRunUtil().sleep(3 * 1000);
 
         // start and stop to clear old data
-        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_START_MEASUREMENT);
+        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_START_MEASUREMENT);
         getRunUtil().sleep(3 * 1000);
-        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_MEASUREMENT);
+        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_MEASUREMENT);
         getRunUtil().sleep(3 * 1000);
         CLog.i("Stopping measurement: " + cr.getStdout());
         getDevice().unlockDevice();
@@ -128,7 +139,7 @@
         getRunUtil().sleep(3 * 1000);
         rotateScreen();
         getRunUtil().sleep(1 * 1000);
-        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_START_CALIBRATION);
+        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_START_CALIBRATION);
         CLog.i("Starting calibration: " + cr.getStdout());
 
         // check whether multimeter is calibrated
@@ -137,14 +148,15 @@
         while (!isCalibrated
                 && System.currentTimeMillis() - calibrationStartTime <= CALIBRATION_TIMEOUT_MS) {
             getRunUtil().sleep(1 * 1000);
-            cr = getRunUtil().runTimedCmd(2 * 1000, mUtilPath, CMD_GET_FRAMERATE_STATE);
+            cr = getRunUtil().runTimedCmd(2 * 1000, mMeterUtilPath, CMD_GET_FRAMERATE_STATE);
             if (cr.getStdout().contains("calib0")) {
                 isCalibrated = true;
             }
         }
         getDevice().executeShellCommand(KILL_VIDEO_PLAYER);
         if (!isCalibrated) {
-            cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_CALIBRATION);
+            cr = getRunUtil().runTimedCmd(
+                    COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_CALIBRATION);
             CLog.e("Calibration timed out.");
             return false;
         } else {
@@ -165,16 +177,17 @@
 
         rotateScreen();
         getRunUtil().sleep(1 * 1000);
-        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_START_MEASUREMENT);
+        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_START_MEASUREMENT);
         CLog.i("Starting measurement: " + cr.getStdout());
 
         // end measurement
         getRunUtil().sleep(durationSecond * 1000);
 
-        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_MEASUREMENT);
+        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_MEASUREMENT);
         CLog.i("Stopping measurement: " + cr.getStdout());
         if (cr == null || !cr.getStdout().contains("OK")) {
-            cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_STOP_MEASUREMENT);
+            cr = getRunUtil().runTimedCmd(
+                    COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_STOP_MEASUREMENT);
             CLog.i("Retry - Stopping measurement: " + cr.getStdout());
         }
 
@@ -182,23 +195,33 @@
         getDevice().clearErrorDialogs();
     }
 
-    private Map<String, String> getResult(Map<String, String> metrics,
-            String keyprefix, boolean lipsync) {
+    private Map<String, String> getResult(ITestInvocationListener listener,
+            Map<String, String> metrics, String keyprefix, float fps, boolean lipsync) {
         CommandResult cr;
 
         // get number of results
         getRunUtil().sleep(5 * 1000);
-        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mUtilPath, CMD_GET_NUM_FRAMES);
+        cr = getRunUtil().runTimedCmd(COMMAND_TIMEOUT_MS, mMeterUtilPath, CMD_GET_NUM_FRAMES);
         String frameNum = cr.getStdout();
         CLog.i("Number of results: " + frameNum);
 
-        // get all results
-        cr = getRunUtil().runTimedCmd(GETDATA_TIMEOUT_MS, mUtilPath, CMD_GET_ALL_DATA);
+        // get all results and write to output file
+        cr = getRunUtil().runTimedCmd(GETDATA_TIMEOUT_MS, mMeterUtilPath, CMD_GET_ALL_DATA);
         String allData = cr.getStdout();
-        CLog.i("Data: " + allData);
+        listener.testLog(keyprefix, LogDataType.TEXT, new SnapshotInputStreamSource(
+                new ByteArrayInputStream(allData.getBytes())));
 
         // parse results
-        return parseResult(metrics, frameNum, allData, keyprefix, lipsync);
+        return parseResult(metrics, frameNum, allData, keyprefix, fps, lipsync);
+    }
+
+    protected void runMultimeterTest(ITestInvocationListener listener,
+            Map<String,String> metrics) throws DeviceNotAvailableException {
+        doMeasurement(TEST_VIDEO_1_DEVICE_PATH, TEST_VIDEO_1_DURATION);
+        metrics = getResult(listener, metrics, TEST_VIDEO_1_PREFIX, TEST_VIDEO_1_FPS, true);
+
+        doMeasurement(TEST_VIDEO_2_DEVICE_PATH, TEST_VIDEO_2_DURATION);
+        metrics = getResult(listener, metrics, TEST_VIDEO_2_PREFIX, TEST_VIDEO_2_FPS, true);
     }
 
     /**
@@ -217,11 +240,7 @@
         Map<String, String> metrics = new HashMap<String, String>();
 
         if (setupTestEnv()) {
-            doMeasurement(TEST_VIDEO_1_DEVICE_PATH, TEST_VIDEO_1_DURATION);
-            metrics = getResult(metrics, TEST_VIDEO_1_PREFIX, true);
-
-            doMeasurement(TEST_VIDEO_2_DEVICE_PATH, TEST_VIDEO_2_DURATION);
-            metrics = getResult(metrics, TEST_VIDEO_2_PREFIX, true);
+            runMultimeterTest(listener, metrics);
         }
 
         long durationMs = System.currentTimeMillis() - testStartTime;
@@ -236,16 +255,22 @@
      * @return a {@link HashMap} that contains metrics keys and results
      */
     private Map<String, String> parseResult(Map<String, String> metrics,
-            String numFrames, String result, String keyprefix, boolean lipsync) {
-        CLog.i("== Video Multimeter Result '%s' ==", keyprefix);
+            String numFrames, String result, String keyprefix, float fps,
+            boolean lipsync) {
+        final int MISSING_FRAME_CEILING = 5; //5+ frames missing count the same
+        final double[] MISSING_FRAME_WEIGHT = {0.0, 1.0, 2.5, 5.0, 6.25, 8.0};
 
+        CLog.i("== Video Multimeter Result '%s' ==", keyprefix);
         Pattern p = Pattern.compile("OK\\s+(\\d+)$");
         Matcher m = p.matcher(numFrames.trim());
+        String frameCapturedStr = "0";
+        long frameCaptured = 0;
         if (m.matches()) {
-            String numFrame = m.group(1);
-            metrics.put(keyprefix + "frame_captured", numFrame);
-            CLog.i("Captured frames: " + numFrame);
-            if (Integer.parseInt(numFrame) == 0) {
+            frameCapturedStr = m.group(1);
+            metrics.put(keyprefix + "frame_captured", frameCapturedStr);
+            CLog.i("Captured frames: " + frameCapturedStr);
+            frameCaptured = Long.parseLong(frameCapturedStr);
+            if (frameCaptured == 0) {
                 // no frame captured
                 CLog.w("No frame captured for " + keyprefix);
                 return metrics;
@@ -255,49 +280,96 @@
             return metrics;
         }
 
-        // Get total captured frames from the last line of result
+        // Get total captured frames and calculate smoothness and freezing score
         // format: "OK (time); (frame duration); (marker color); (total dropped frames)"
+        p = Pattern.compile("OK\\s+\\d+;\\s*(-?\\d+);\\s*[a-z]+;\\s*(\\d+)");
         String[] lines = result.split(System.getProperty("line.separator"));
-        for (int i = lines.length - 1; i >= 0; i--) {
-            p = Pattern.compile("OK\\s+\\d+;\\s*\\d+;\\s*[a-z]+;\\s*(\\d+)");
+        String totalDropFrame = "-1";
+        String lastDropFrame = "0";
+        long frameCount = 0;
+        long consecutiveDropFrame = 0;
+        double freezingPenalty = 0.0;
+        long frameDuration = 0;
+        double offByOne = 0;
+        double offByMultiple = 0;
+        double expectedFrameDurationInUs = 1000000.0 / fps;
+        for (int i = 0; i < lines.length; i++) {
             m = p.matcher(lines[i].trim());
             if (m.matches()) {
-                String dropFrame = m.group(1);
-                metrics.put(keyprefix + "frame_drop", dropFrame);
-                CLog.i("Dropped frames: " + dropFrame);
-                break;
+                frameCount++;
+                frameDuration = Long.parseLong(m.group(1));
+                totalDropFrame = m.group(2);
+                // trim the last few data points if needed
+                if (frameCount >= frameCaptured - TRAILING_FRAMES_MAX - 1 &&
+                        frameDuration > FRAME_DURATION_THRESHOLD_US) {
+                    metrics.put(keyprefix + "frame_captured", String.valueOf(frameCount));
+                    break;
+                }
+                if (lastDropFrame.equals(totalDropFrame)) {
+                    if (consecutiveDropFrame > 0) {
+                      freezingPenalty += MISSING_FRAME_WEIGHT[(int) (Math.min(consecutiveDropFrame,
+                              MISSING_FRAME_CEILING))] * consecutiveDropFrame;
+                      consecutiveDropFrame = 0;
+                    }
+                } else {
+                    consecutiveDropFrame++;
+                }
+                lastDropFrame = totalDropFrame;
+
+                if (frameDuration < expectedFrameDurationInUs * 0.5) {
+                    offByOne++;
+                } else if (frameDuration > expectedFrameDurationInUs * 1.5) {
+                    if (frameDuration < expectedFrameDurationInUs * 2.5) {
+                        offByOne++;
+                    } else {
+                        offByMultiple++;
+                    }
+                }
             }
         }
-        if (!metrics.containsKey(keyprefix + "frame_drop")) {
+        if (totalDropFrame.equals("-1")) {
             // no matching result found
             CLog.w("No result found for " + keyprefix);
             return metrics;
+        } else {
+            metrics.put(keyprefix + "frame_drop", totalDropFrame);
+            CLog.i("Dropped frames: " + totalDropFrame);
         }
+        double smoothnessScore = 100.0 - (offByOne / frameCaptured) * 100.0 -
+                (offByMultiple / frameCaptured) * 300.0;
+        metrics.put(keyprefix + "smoothness", String.valueOf(smoothnessScore));
+        CLog.i("Off by one frame: " + offByOne);
+        CLog.i("Off by multiple frames: " + offByMultiple);
+        CLog.i("Smoothness score: " + smoothnessScore);
+
+        double freezingScore = 100.0 - 100.0 * freezingPenalty / frameCaptured;
+        metrics.put(keyprefix + "freezing", String.valueOf(freezingScore));
+        CLog.i("Freezing score: " + freezingScore);
 
         // parse lipsync results (the audio and video synchronization offset)
         // format: "OK (time); (frame duration); (marker color); (total dropped frames); (lipsync)"
+        p = Pattern.compile("OK\\s+\\d+;\\s*\\d+;\\s*[a-z]+;\\s*\\d+;\\s*(-?\\d+)");
         if (lipsync) {
             ArrayList<Integer> lipsyncVals = new ArrayList<Integer>();
             StringBuilder lipsyncValsStr = new StringBuilder("[");
             long lipsyncSum = 0;
             for (int i = 0; i < lines.length; i++) {
-                p = Pattern.compile("OK\\s+\\d+;\\s*\\d+;\\s*[a-z]+;\\s*\\d+;\\s*(-?\\d+)");
                 m = p.matcher(lines[i].trim());
                 if (m.matches()) {
                     int lipSyncVal = Integer.parseInt(m.group(1));
                     lipsyncVals.add(lipSyncVal);
                     lipsyncValsStr.append(lipSyncVal);
-                    lipsyncValsStr.append(" ,");
+                    lipsyncValsStr.append(", ");
                     lipsyncSum += lipSyncVal;
                 }
             }
             if (lipsyncVals.size() > 0) {
                 lipsyncValsStr.append("]");
+                CLog.i("Lipsync values: " + lipsyncValsStr);
                 Collections.sort(lipsyncVals);
                 int lipsyncCount = lipsyncVals.size();
                 int minLipsync = lipsyncVals.get(0);
                 int maxLipsync = lipsyncVals.get(lipsyncCount - 1);
-                CLog.i("Lipsync values: " + lipsyncVals.toString());
                 metrics.put(keyprefix + "lipsync_count", String.valueOf(lipsyncCount));
                 CLog.i("Lipsync Count: " + lipsyncCount);
                 metrics.put(keyprefix + "lipsync_min", String.valueOf(lipsyncVals.get(0)));
@@ -315,7 +387,7 @@
         return metrics;
     }
 
-    private IRunUtil getRunUtil() {
+    protected IRunUtil getRunUtil() {
         return RunUtil.getDefault();
     }
 }
diff --git a/prod-tests/src/com/android/monkey/MonkeyBase.java b/prod-tests/src/com/android/monkey/MonkeyBase.java
index a9fef9b..6a0b59a 100644
--- a/prod-tests/src/com/android/monkey/MonkeyBase.java
+++ b/prod-tests/src/com/android/monkey/MonkeyBase.java
@@ -80,6 +80,8 @@
     private static final String LAUNCH_APP_CMD = "am start -W -n '%s' " +
             "-a android.intent.action.MAIN -c android.intent.category.LAUNCHER -f 0x10200000";
 
+    private static final String NULL_UPTIME = "0.00";
+
     /**
      * Helper to run a monkey command with an absolute timeout.
      * <p>
@@ -204,6 +206,10 @@
     @Option(name = "screenshot", description = "Take a device screenshot on monkey completion")
     private boolean mScreenshot = false;
 
+    @Option(name = "ignore-security-exceptions",
+            description = "Ignore SecurityExceptions while injecting events")
+    private boolean mIgnoreSecurityExceptions = true;
+
     private ITestDevice mTestDevice = null;
     private MonkeyLogItem mMonkeyLog = null;
     private BugreportItem mBugreport = null;
@@ -281,7 +287,11 @@
 
         StringBuilder outputBuilder = new StringBuilder();
         CommandHelper commandHelper = new CommandHelper();
+
+        long start = System.currentTimeMillis();
         long duration = 0;
+        Date dateAfter = null;
+        String uptimeAfter = NULL_UPTIME;
 
         // Generate the monkey log prefix, which includes the device uptime
         outputBuilder.append(String.format("# %s - device uptime = %s: Monkey command used " +
@@ -289,31 +299,33 @@
 
         try {
             onMonkeyStart();
-            long start = System.currentTimeMillis();
             commandHelper.runCommand(mTestDevice, command, getMonkeyTimeoutMs());
-            duration = System.currentTimeMillis() - start;
         } finally {
-            onMonkeyFinish();
-            outputBuilder.append(commandHelper.getOutput());
-
-            // Generate the monkey log suffix, which includes the device uptime.
-            outputBuilder.append(String.format("\n# %s - device uptime = %s: Monkey command ran " +
-                    "for: %d:%02d (mm:ss)\n", new Date().toString(), getUptime(),
-                    duration / 1000 / 60, duration / 1000 % 60));
-
             // Wait for device to recover if it's not online.  If it hasn't recovered, ignore.
             try {
                 mTestDevice.waitForDeviceOnline(2 * 60 * 1000);
-            } catch (DeviceNotAvailableException e) {
-                CLog.w("Device %s not available after 2 minutes.", mTestDevice.getSerialNumber());
+                duration = System.currentTimeMillis() - start;
+                dateAfter = new Date();
+                uptimeAfter = getUptime();
+                onMonkeyFinish();
+                takeScreenshot(listener, "screenshot");
+
+                mBugreport = takeBugreport(listener, BUGREPORT_NAME);
+                // FIXME: Remove this once traces.txt is no longer needed.
+                takeTraces(listener);
+            } finally {
+                // @@@ DO NOT add anything that requires device interaction into this block     @@@
+                // @@@ logging that no longer requires device interaction MUST be in this block @@@
+                outputBuilder.append(commandHelper.getOutput());
+                if (dateAfter == null) {
+                    dateAfter = new Date();
+                }
+                // Generate the monkey log suffix, which includes the device uptime.
+                outputBuilder.append(String.format("\n# %s - device uptime = %s: Monkey command "
+                        + "ran for: %d:%02d (mm:ss)\n", dateAfter.toString(), uptimeAfter,
+                        duration / 1000 / 60, duration / 1000 % 60));
+                mMonkeyLog = createMonkeyLog(listener, MONKEY_LOG_NAME, outputBuilder.toString());
             }
-
-            takeScreenshot(listener, "screenshot");
-
-            mBugreport = takeBugreport(listener, BUGREPORT_NAME);
-            // FIXME: Remove this once traces.txt is no longer needed.
-            takeTraces(listener);
-            mMonkeyLog = createMonkeyLog(listener, MONKEY_LOG_NAME, outputBuilder.toString());
         }
 
         checkResults();
@@ -423,7 +435,9 @@
             cmdList.add(cat);
         }
 
-        cmdList.add("--ignore-security-exceptions");
+        if (mIgnoreSecurityExceptions) {
+            cmdList.add("--ignore-security-exceptions");
+        }
 
         if (mThrottle >= 1) {
             cmdList.add("--throttle");
@@ -472,8 +486,8 @@
     /**
      * Get a {@link String} containing the number seconds since the device was booted.
      * <p>
-     * {@code "0.00"} is returned if the device becomes unresponsive. Used in the monkey log prefix
-     * and suffix.
+     * {@code NULL_UPTIME} is returned if the device becomes unresponsive. Used in the monkey log
+     * prefix and suffix.
      * </p>
      */
     protected String getUptime() {
@@ -496,7 +510,7 @@
             CLog.e("Device %s became unresponsive while getting the uptime.",
                     mTestDevice.getSerialNumber());
         }
-        return "0.00";
+        return NULL_UPTIME;
     }
 
     /**
@@ -560,10 +574,6 @@
      * Check the results and return if valid or throw an assertion error if not valid.
      */
     private void checkResults() {
-        if (!isRetriable()) {
-            return;
-        }
-
         Assert.assertNotNull("Monkey log is null", mMonkeyLog);
         Assert.assertNotNull("Bugreport is null", mBugreport);
         Assert.assertNotNull("Bugreport is empty", mBugreport.getTime());
@@ -603,4 +613,4 @@
     protected long getMonkeyTimeoutMs() {
         return mMonkeyTimeout * 60 * 1000;
     }
-}
+}
\ No newline at end of file
diff --git a/prod-tests/src/com/android/monkey/MonkeyBrillopadForwarder.java b/prod-tests/src/com/android/monkey/MonkeyBrillopadForwarder.java
index a36f3e9..a2f1516 100644
--- a/prod-tests/src/com/android/monkey/MonkeyBrillopadForwarder.java
+++ b/prod-tests/src/com/android/monkey/MonkeyBrillopadForwarder.java
@@ -135,12 +135,12 @@
             if (!status.equals(MonkeyStatus.FINISHED)) {
                 String failure = String.format("%s.\n%s", status.getDescription(),
                         crashTrace.toString());
-                super.testFailed(TestFailure.FAILURE, monkeyTest, failure);
+                super.testFailed(monkeyTest, failure);
             }
         } catch (AssertionError e) {
-            super.testFailed(TestFailure.FAILURE, monkeyTest, Throwables.getStackTraceAsString(e));
+            super.testFailed(monkeyTest, Throwables.getStackTraceAsString(e));
         } catch (RuntimeException e) {
-            super.testFailed(TestFailure.ERROR, monkeyTest, Throwables.getStackTraceAsString(e));
+            super.testFailed(monkeyTest, Throwables.getStackTraceAsString(e));
         } finally {
             super.testEnded(monkeyTest, monkeyMetrics);
         }
diff --git a/prod-tests/src/com/android/monkey/MonkeyPackageDiff.java b/prod-tests/src/com/android/monkey/MonkeyPackageDiff.java
deleted file mode 100644
index 49ff4bf..0000000
--- a/prod-tests/src/com/android/monkey/MonkeyPackageDiff.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright (C) 2013 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.monkey;
-
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
-import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.Option.Importance;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.ByteArrayInputStreamSource;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.testtype.IDeviceTest;
-import com.android.tradefed.testtype.IRemoteTest;
-import com.google.common.base.Joiner;
-
-import junit.framework.Assert;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Compares the list of packages the monkey can run against a golden file and fails on diffs.
- */
-public class MonkeyPackageDiff implements IDeviceTest, IRemoteTest {
-    private static final String TEST_KEY = "MonkeyPackageDiff";
-    private static final String MONKEY_CMD = "monkey -v -v -v %s0";
-    private static final Pattern PACKAGE_PATTERN = Pattern.compile(
-            "^//\\s+\\+ [^\\(]+\\(from package ([^\\)]+)\\)$");
-
-    @Option(name = "category", description = "Monkey app category. May be repeated.")
-    private Collection<String> mCategories = new LinkedList<String>();
-
-    @Option(name = "golden-file", description = "The golden file containing the list of packages",
-            importance = Importance.ALWAYS, mandatory = true)
-    private File mGoldenFile = null;
-
-    @Option(name = "ignore-package", description = "Package name to ignore. May be repeated.")
-    private Collection<String> mIgnoreList = new HashSet<String>();
-
-    ITestDevice mDevice = null;
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
-        Assert.assertNotNull(getDevice());
-
-        Set<String> expectedPackages = null;
-        try {
-            expectedPackages = readGoldenFile();
-        } catch (IOException e) {
-            Assert.fail("Could not read golden file");
-        }
-
-        StringBuilder categories = new StringBuilder();
-        for (String category : mCategories) {
-            categories.append("-c ").append(category).append(" ");
-        }
-        String cmd = String.format(MONKEY_CMD, categories.toString());
-        String output = getDevice().executeShellCommand(cmd);
-
-        SortedSet<String> actualPackages = new TreeSet<String>();
-        for (String line : output.split("\n")) {
-            line = line.trim();
-            Matcher m = PACKAGE_PATTERN.matcher(line);
-            if (m.matches()) {
-                actualPackages.add(m.group(1));
-            }
-        }
-
-        StringBuilder packageList = new StringBuilder();
-        for (String pack : actualPackages) {
-            packageList.append(pack).append("\n");
-        }
-        listener.testLog("packages", LogDataType.TEXT,
-                new ByteArrayInputStreamSource(packageList.toString().getBytes()));
-
-        SortedSet<String> addedPackages = new TreeSet<String>();
-        for (String pack : actualPackages) {
-            if (!expectedPackages.contains(pack) && !mIgnoreList.contains(pack)) {
-                addedPackages.add(pack);
-            }
-        }
-        SortedSet<String> removedPackages = new TreeSet<String>();
-        for (String pack : expectedPackages) {
-            if (!actualPackages.contains(pack) && !mIgnoreList.contains(pack)) {
-                removedPackages.add(pack);
-            }
-        }
-
-        reportMetrics(listener, addedPackages, removedPackages);
-    }
-
-    /**
-     * Report the metrics and fail the tests if any packages are added or removed.
-     */
-    private void reportMetrics(ITestInvocationListener listener, Set<String> addedPackages,
-            Set<String> removedPackages) {
-        listener.testRunStarted(TEST_KEY, 0);
-        Map<String, String> metrics = new HashMap<String, String>();
-        Map<String, String> emptyMap = Collections.emptyMap();
-
-        TestIdentifier testId = new TestIdentifier(getClass().getCanonicalName(), "added");
-        listener.testStarted(testId);
-        metrics.put("added", Integer.toString(addedPackages.size()));
-        if (!addedPackages.isEmpty()) {
-            String message = String.format("Added packages: %s",
-                    Joiner.on(", ").join(addedPackages));
-            listener.testFailed(TestFailure.FAILURE, testId, message);
-        }
-        listener.testEnded(testId, emptyMap);
-
-        testId = new TestIdentifier(getClass().getCanonicalName(), "removed");
-        listener.testStarted(testId);
-        metrics.put("removed", Integer.toString(removedPackages.size()));
-        if (!removedPackages.isEmpty()) {
-            String message = String.format("Removed packages: %s",
-                    Joiner.on(", ").join(removedPackages));
-            listener.testFailed(TestFailure.FAILURE, testId, message);
-        }
-        listener.testEnded(testId, emptyMap);
-
-        CLog.d("About to report monkey package diff metrics: %s", metrics);
-        listener.testRunEnded(0, metrics);
-    }
-
-    /**
-     * Read the golden file and return a set of strings.
-     */
-    private Set<String> readGoldenFile() throws IOException {
-        Set<String> packages = new HashSet<String>();
-        BufferedReader reader = new BufferedReader(new FileReader(mGoldenFile));
-        String line;
-        try {
-            while ((line = reader.readLine()) != null) {
-                line = line.trim();
-                if (!"".equals(line)) {
-                    packages.add(line);
-                }
-            }
-        } finally {
-            reader.close();
-        }
-        return packages;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setDevice(ITestDevice device) {
-        mDevice = device;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public ITestDevice getDevice() {
-        return mDevice;
-    }
-
-}
diff --git a/prod-tests/src/com/android/performance/tests/AppLaunchMetricsTest.java b/prod-tests/src/com/android/performance/tests/AppLaunchMetricsTest.java
deleted file mode 100644
index fc2bbba..0000000
--- a/prod-tests/src/com/android/performance/tests/AppLaunchMetricsTest.java
+++ /dev/null
@@ -1,395 +0,0 @@
-/*
- * Copyright (C) 2011 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.performance.tests;
-
-import com.android.ddmlib.IDevice;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
-import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.SnapshotInputStreamSource;
-import com.android.tradefed.testtype.IDeviceTest;
-import com.android.tradefed.testtype.IRemoteTest;
-import com.android.tradefed.util.RunUtil;
-import com.android.tradefed.util.StreamUtil;
-
-import junit.framework.Assert;
-import junit.framework.TestCase;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Runs the app launch test.
- * <p>
- * Launches each app and records the amount of time it took to launch the app.
- * </p>
- */
-public class AppLaunchMetricsTest implements IDeviceTest, IRemoteTest {
-    private static final String APP_LAUNCH = "/data/framework/app_launch";
-    private static final String APP_LIST_FILE = "launch_list.txt";
-    private static final String APP_OUTPUT_FILE = "launch_perf_output.txt";
-
-    private static final String TEST_KEY = "ApplicationStartupTime";
-
-    private static final String LAUNCH_TIME_NAME = "app_launch_times";
-    private static final String BUGREPORT_NAME = "app_launch_bugreport";
-
-    /** The pattern to match the app-name argument */
-    private static final Pattern APP_NAME_PATTERN = Pattern.compile("(.+),(.+)");
-    /** The pattern of the output */
-    private static final Pattern APP_TIME_PATTERN = Pattern.compile("(.+)\\|(\\d+)");
-
-    private ITestDevice mTestDevice;
-    private String mAppListPath = null;
-    private String mAppOutputPath = null;
-
-    @Option(name = "app-name", description = "The name of the app in the launcher or an app, key "
-            + "pair.  E.G. \"Browser\" or \"Browser,android_browser\". May be repeated.")
-    private Collection<String> mAppNames = new ArrayList<String>();
-
-    /**
-     * Class that stores useful info about the app.
-     */
-    static class AppInfo {
-        private String mName = null;
-        private String mOutputKey = null;
-        private String mPostKey = null;
-        private Integer mTime = null;
-
-        public AppInfo(String name) {
-            mName = name;
-            mPostKey = mOutputKey = makeOutputKey(name);
-        }
-
-        public AppInfo(String name, String key) {
-            mName = name;
-            mOutputKey = makeOutputKey(name);
-            mPostKey = key;
-        }
-
-        public String getAppListEntry() {
-            return String.format("%s,%s\n", mName, mPostKey);
-        }
-
-        public String getName() {
-            return mName;
-        }
-
-        public String getPostKey() {
-            return mPostKey;
-        }
-
-        public String getOutputKey() {
-            return mOutputKey;
-        }
-
-        public Integer getTime() {
-            return mTime;
-        }
-
-        public void setTime(Integer time) {
-            mTime = time;
-        }
-
-        private String makeOutputKey(String name) {
-            return name.toLowerCase().replaceAll(" ", "");
-        }
-    }
-
-    private Map<String, AppInfo> mAppInfos = new HashMap<String, AppInfo>();
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
-        Assert.assertNotNull(mTestDevice);
-
-        mAppListPath = new File(mTestDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE),
-                APP_LIST_FILE).getAbsolutePath();
-        mAppOutputPath = new File(mTestDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE),
-                APP_OUTPUT_FILE).getAbsolutePath();
-
-        setupAppInfos();
-
-        // Setup the device
-        mTestDevice.executeShellCommand(String.format("rm %s %s", mAppListPath, mAppOutputPath));
-        mTestDevice.pushString(generateAppList(), mAppListPath);
-        mTestDevice.executeShellCommand(String.format("chmod 750 %s", APP_LAUNCH));
-
-        // Sleep 30 seconds to let device settle.
-        RunUtil.getDefault().sleep(30 * 1000);
-
-        // Run the test
-        String output = mTestDevice.executeShellCommand(APP_LAUNCH);
-
-        CLog.d("App launch output: %s", output);
-        logOutputFile(listener);
-    }
-
-    /**
-     * Sets up the {@link AppInfo} map based on the app-name args.
-     * <p>
-     * Generates from {@link appNames}, a collection of Strings formated as either an app name or as
-     * an app name, key pair with a comma separator.  The key for the map will be the lowercase name
-     * with spaces removed.
-     * </p>
-     */
-    private void setupAppInfos() {
-        for (String app : mAppNames) {
-            Matcher m = APP_NAME_PATTERN.matcher(app);
-            AppInfo info;
-            if (m.matches()) {
-                info = new AppInfo(m.group(1), m.group(2));
-            } else {
-                info = new AppInfo(app);
-            }
-            mAppInfos.put(info.getOutputKey(), info);
-        }
-    }
-
-    /**
-     * Generate the app list as a String.
-     *
-     * @return the app list to push to the device.
-     */
-    private String generateAppList() {
-        StringBuilder sb = new StringBuilder();
-        for (AppInfo info : mAppInfos.values()) {
-            sb.append(info.getAppListEntry());
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Parses and logs the output file.
-     *
-     * @param listener the {@link ITestInvocationListener}
-     * @throws DeviceNotAvailableException If the device is not available.
-     */
-    private void logOutputFile(ITestInvocationListener listener)
-            throws DeviceNotAvailableException {
-        File outputFile = null;
-        InputStreamSource outputSource = null;
-
-        try {
-            outputFile = mTestDevice.pullFile(mAppOutputPath);
-            if (outputFile != null) {
-                outputSource = new SnapshotInputStreamSource(new FileInputStream(outputFile));
-                listener.testLog(LAUNCH_TIME_NAME, LogDataType.TEXT, outputSource);
-                parseOutputFile(StreamUtil.getStringFromStream(new BufferedInputStream(
-                        new FileInputStream(outputFile))));
-            }
-        } catch(IOException e) {
-            CLog.e("Got IOException: %s", e);
-        } finally {
-            if (outputFile != null) {
-                outputFile.delete();
-            }
-            if (outputSource != null) {
-                outputSource.cancel();
-            }
-        }
-
-        if (shouldTakeBugreport()) {
-            InputStreamSource bugreport = mTestDevice.getBugreport();
-            try {
-                listener.testLog(BUGREPORT_NAME, LogDataType.TEXT, bugreport);
-            } finally {
-                bugreport.cancel();
-            }
-        }
-
-        reportMetrics(listener);
-    }
-
-    /**
-     * Parses the output file and populate the {@link AppInfo} objects with the launch times.
-     *
-     * @param contents The file contents.
-     * @throws IOException If an IOException is caused.
-     */
-    private void parseOutputFile(String contents) throws IOException {
-        for (String line : contents.split("\n")) {
-            Matcher m = APP_TIME_PATTERN.matcher(line);
-            if (m.matches()) {
-                AppInfo appInfo = mAppInfos.get(m.group(1).toLowerCase());
-                if (appInfo != null) {
-                    appInfo.setTime(Integer.parseInt(m.group(2)));
-                }
-            }
-        }
-    }
-
-    /**
-     * Report the metrics and attach it to the listener.
-     * <p>
-     * If any of the app times are {@code null}, that app is assumed to not have launched and will
-     * be marked as failed.
-     * </p>
-     * @param listener the {@link ITestInvocationListener}
-     */
-    private void reportMetrics(ITestInvocationListener listener) {
-        listener.testRunStarted(TEST_KEY, 0);
-        Map<String, String> metrics = new HashMap<String, String>();
-
-        for (AppInfo appInfo : mAppInfos.values()) {
-            TestIdentifier testId = new TestIdentifier(getClass().getCanonicalName(),
-                    appInfo.getPostKey());
-            listener.testStarted(testId);
-            if (appInfo.getTime() != null) {
-                metrics.put(appInfo.getPostKey(), Integer.toString(appInfo.getTime()));
-            } else {
-                listener.testFailed(TestFailure.FAILURE, testId, "No app launch time");
-            }
-            Map<String, String> empty = Collections.emptyMap();
-            listener.testEnded(testId, empty);
-        }
-        CLog.d("About to report app launch metrics: %s", metrics);
-        listener.testRunEnded(0, metrics);
-    }
-
-    /**
-     * If a bugreport should be taken after the run.
-     *
-     * @return true if any of the apps have a {@code null} launch time.
-     */
-    private boolean shouldTakeBugreport() {
-        for (AppInfo appInfo : mAppInfos.values()) {
-            if (appInfo.getTime() == null) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setDevice(ITestDevice device) {
-        mTestDevice = device;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public ITestDevice getDevice() {
-        return mTestDevice;
-    }
-
-    public static class MetaTest extends TestCase {
-        AppLaunchMetricsTest mTestInstance = null;
-
-        @Override
-        public void setUp() throws Exception {
-            mTestInstance = new AppLaunchMetricsTest();
-
-            mTestInstance.mAppNames.add("App 1");
-            mTestInstance.mAppNames.add("App 2,key2");
-        }
-
-        public void testAppInfo() throws Exception {
-            AppInfo info = new AppInfo("app_name");
-            assertEquals("app_name", info.getName());
-            assertEquals("app_name", info.getOutputKey());
-            assertEquals("app_name", info.getPostKey());
-
-            info = new AppInfo("AppName");
-            assertEquals("AppName", info.getName());
-            assertEquals("appname", info.getOutputKey());
-            assertEquals("appname", info.getPostKey());
-
-            info = new AppInfo("App Name");
-            assertEquals("App Name", info.getName());
-            assertEquals("appname", info.getOutputKey());
-            assertEquals("appname", info.getPostKey());
-
-            info = new AppInfo("App & Name");
-            assertEquals("App & Name", info.getName());
-            assertEquals("app&name", info.getOutputKey());
-            assertEquals("app&name", info.getPostKey());
-            assertEquals("App & Name,app&name\n", info.getAppListEntry());
-
-            info = new AppInfo("App Name", "key");
-            assertEquals("App Name", info.getName());
-            assertEquals("appname", info.getOutputKey());
-            assertEquals("key", info.getPostKey());
-
-            assertNull(info.getTime());
-            info.setTime(0);
-            assertEquals(new Integer(0), info.getTime());
-            assertEquals("App Name,key\n", info.getAppListEntry());
-        }
-
-        public void testSetupAppInfos() throws Exception {
-            mTestInstance.setupAppInfos();
-            assertEquals(2, mTestInstance.mAppInfos.size());
-            assertNotNull(mTestInstance.mAppInfos.get("app1"));
-            assertEquals("App 1", mTestInstance.mAppInfos.get("app1").getName());
-            assertEquals("app1", mTestInstance.mAppInfos.get("app1").getOutputKey());
-            assertEquals("app1", mTestInstance.mAppInfos.get("app1").getPostKey());
-            assertNotNull(mTestInstance.mAppInfos.get("app2"));
-            assertEquals("App 2", mTestInstance.mAppInfos.get("app2").getName());
-            assertEquals("app2", mTestInstance.mAppInfos.get("app2").getOutputKey());
-            assertEquals("key2", mTestInstance.mAppInfos.get("app2").getPostKey());
-        }
-
-        public void testGenerateAppList() throws Exception {
-            mTestInstance.setupAppInfos();
-            assertEquals(2, mTestInstance.mAppInfos.size());
-
-            assertTrue(mTestInstance.generateAppList().contains("App 1,app1\n"));
-            assertTrue(mTestInstance.generateAppList().contains("App 2,key2\n"));
-        }
-
-        public void testParseOutputFile_success() throws Exception {
-            mTestInstance.setupAppInfos();
-            assertEquals(2, mTestInstance.mAppInfos.size());
-
-            mTestInstance.parseOutputFile("app1|1234\napp2|5678\n");
-            assertFalse(mTestInstance.shouldTakeBugreport());
-            assertEquals(new Integer(1234), mTestInstance.mAppInfos.get("app1").getTime());
-            assertEquals(new Integer(5678), mTestInstance.mAppInfos.get("app2").getTime());
-        }
-
-        public void testParseOutputFile_fail() throws Exception {
-            mTestInstance.setupAppInfos();
-            assertEquals(2, mTestInstance.mAppInfos.size());
-
-            mTestInstance.parseOutputFile("app1|1234\n");
-            assertTrue(mTestInstance.shouldTakeBugreport());
-            assertEquals(new Integer(1234), mTestInstance.mAppInfos.get("app1").getTime());
-            assertNull(mTestInstance.mAppInfos.get("app2").getTime());
-        }
-    }
-}
diff --git a/prod-tests/src/com/android/performance/tests/FioBenchmarkTest.java b/prod-tests/src/com/android/performance/tests/FioBenchmarkTest.java
index 99d0f1b..994c4f1 100644
--- a/prod-tests/src/com/android/performance/tests/FioBenchmarkTest.java
+++ b/prod-tests/src/com/android/performance/tests/FioBenchmarkTest.java
@@ -377,6 +377,10 @@
             description="The number of worker jobs for the media server benchmark.")
     private int mMediaScannerWorkerJobCount = 4;
 
+    @Option(name="key-suffix",
+            description="The suffix to add to the reporting key in order to override the default")
+    private String mKeySuffix = null;
+
     /**
      * Sets up all the benchmarks.
      */
@@ -811,6 +815,7 @@
         mTestDevice.executeShellCommand(String.format("rm -r %s", mTmpDir));
         mTestDevice.executeShellCommand(String.format("rm -r %s", mFioDir));
         mTestDevice.executeShellCommand("start");
+        mTestDevice.waitForDeviceAvailable();
     }
 
     /**
@@ -859,7 +864,9 @@
 
         // Report metrics
         Map<String, String> metrics = new HashMap<String, String>();
-        listener.testRunStarted(test.mKey, 0);
+        String key = mKeySuffix == null ? test.mKey : test.mKey + mKeySuffix;
+
+        listener.testRunStarted(key, 0);
         for (PerfMetricInfo m : test.mPerfMetrics) {
             if (!output.mResults.containsKey(m.mJobName)) {
                 CLog.w("Job name %s was not found in the results", m.mJobName);
@@ -873,7 +880,8 @@
                 CLog.w("%s was not in results for the job %s", m.mFieldName, m.mJobName);
             }
         }
-        CLog.d("About to report metrics to %s: %s", test.mKey, metrics);
+
+        CLog.d("About to report metrics to %s: %s", key, metrics);
         listener.testRunEnded(0, metrics);
     }
 
diff --git a/prod-tests/src/com/android/performance/tests/GLBenchmarkTest.java b/prod-tests/src/com/android/performance/tests/GLBenchmarkTest.java
index 5d44f78..a2647e1 100644
--- a/prod-tests/src/com/android/performance/tests/GLBenchmarkTest.java
+++ b/prod-tests/src/com/android/performance/tests/GLBenchmarkTest.java
@@ -16,7 +16,6 @@
 
 package com.android.performance.tests;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
@@ -195,7 +194,7 @@
         }
         if (errMsg != null) {
             CLog.e(errMsg);
-            listener.testFailed(TestFailure.FAILURE, testId, errMsg);
+            listener.testFailed(testId, errMsg);
             listener.testEnded(testId, metrics);
             listener.testRunFailed(errMsg);
         } else {
diff --git a/prod-tests/src/com/android/performance/tests/GeekbenchTest.java b/prod-tests/src/com/android/performance/tests/GeekbenchTest.java
index c3d2a8e..a23da0e 100644
--- a/prod-tests/src/com/android/performance/tests/GeekbenchTest.java
+++ b/prod-tests/src/com/android/performance/tests/GeekbenchTest.java
@@ -17,7 +17,6 @@
 package com.android.performance.tests;
 
 import com.android.ddmlib.CollectingOutputReceiver;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -142,7 +141,7 @@
 
         if (errMsg != null) {
             CLog.e(errMsg);
-            listener.testFailed(TestFailure.FAILURE, testId, errMsg);
+            listener.testFailed(testId, errMsg);
             listener.testEnded(testId, metrics);
             listener.testRunFailed(errMsg);
         } else {
diff --git a/prod-tests/src/com/android/performance/tests/StartupMetricsTest.java b/prod-tests/src/com/android/performance/tests/StartupMetricsTest.java
index 8f89c7e..86cc149 100644
--- a/prod-tests/src/com/android/performance/tests/StartupMetricsTest.java
+++ b/prod-tests/src/com/android/performance/tests/StartupMetricsTest.java
@@ -47,7 +47,7 @@
     public static final String BUGREPORT_LOG_NAME = "bugreport_startup.txt";
 
     @Option(name="boot-time-ms", description="Timeout in ms to wait for device to boot.")
-    private long mBootTimeMs = 5 * 60 * 1000;
+    private long mBootTimeMs = 20 * 60 * 1000;
 
     @Option(name="boot-poll-time-ms", description="Delay in ms between polls for device to boot.")
     private long mBootPoolTimeMs = 500;
@@ -78,6 +78,10 @@
      */
     void executeRebootTest(ITestInvocationListener listener) throws DeviceNotAvailableException {
         Map<String, String> runMetrics = new HashMap<String, String>();
+        String upTimeString = mTestDevice.executeShellCommand("cat /proc/uptime");
+        upTimeString  = upTimeString.split(" ")[0];
+        assert(Double.parseDouble(upTimeString) > 0);
+        runMetrics.put("init-boot", upTimeString);
         mTestDevice.setRecoveryMode(RecoveryMode.NONE);
         CLog.d("Reboot test start.");
         mTestDevice.nonBlockingReboot();
diff --git a/prod-tests/src/com/android/performance/tests/VellamoBenchmark.java b/prod-tests/src/com/android/performance/tests/VellamoBenchmark.java
index 419925d..0da7637 100644
--- a/prod-tests/src/com/android/performance/tests/VellamoBenchmark.java
+++ b/prod-tests/src/com/android/performance/tests/VellamoBenchmark.java
@@ -14,7 +14,6 @@
 
 package com.android.performance.tests;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -140,7 +139,7 @@
         }
         if (errMsg != null) {
             CLog.e(errMsg);
-            listener.testFailed(TestFailure.FAILURE, testId, errMsg);
+            listener.testFailed(testId, errMsg);
         }
         long durationMs = System.currentTimeMillis() - testStartTime;
         metrics.put("total", Double.toString(sumScore));
diff --git a/prod-tests/src/com/android/sdk/EmulatorBootTest.java b/prod-tests/src/com/android/sdk/EmulatorBootTest.java
index 677c7dc..86db8f0 100644
--- a/prod-tests/src/com/android/sdk/EmulatorBootTest.java
+++ b/prod-tests/src/com/android/sdk/EmulatorBootTest.java
@@ -16,7 +16,7 @@
 
 package com.android.sdk;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
+import com.android.ddmlib.Log;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.sdk.tests.EmulatorGpsPreparer;
 import com.android.sdk.tests.EmulatorSmsPreparer;
@@ -26,6 +26,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.targetprep.BuildError;
 import com.android.tradefed.targetprep.SdkAvdPreparer;
@@ -102,17 +103,19 @@
             mAvdPreparer.setUp(mDevice, mBuildInfo);
             mSmsPreparer.setUp(mDevice, mBuildInfo);
             mGpsPreparer.setUp(mDevice, mBuildInfo);
+            
+            checkLauncherRunningOnEmulator(mDevice);
         }
         catch(BuildError b) {
-            listener.testFailed(TestFailure.ERROR, bootTest, StreamUtil.getStackTrace(b));
+            listener.testFailed(bootTest, StreamUtil.getStackTrace(b));
             // throw exception to prevent other tests from executing needlessly
             throw new DeviceUnresponsiveException("The emulator failed to boot", b);
         }
         catch(RuntimeException e) {
-            listener.testFailed(TestFailure.ERROR, bootTest, StreamUtil.getStackTrace(e));
+            listener.testFailed(bootTest, StreamUtil.getStackTrace(e));
             throw e;
         } catch (TargetSetupError e) {
-            listener.testFailed(TestFailure.ERROR, bootTest, StreamUtil.getStackTrace(e));
+            listener.testFailed(bootTest, StreamUtil.getStackTrace(e));
             throw new RuntimeException(e);
         }
         finally {
@@ -120,4 +123,27 @@
             listener.testRunEnded(0, new HashMap<String,String>());
         }
     }
+    
+    private void checkLauncherRunningOnEmulator(ITestDevice device) throws BuildError, DeviceNotAvailableException {
+        Integer apiLevel = device.getApiLevel();
+        String cmd = "ps";
+        if (apiLevel >= 21) {
+            cmd = "am stack list";
+        } else if (apiLevel == 19) {
+            cmd = "am stack boxes";
+        }
+        String cmdResult = device.executeShellCommand(cmd);
+        CLog.i("%s on device %s is %s", cmd, mDevice.getSerialNumber(), cmdResult);
+
+        String[] cmdResultLines = cmdResult.split("\n");
+        int i;
+        for(i = 0; i < cmdResultLines.length; ++i) {
+            if (cmdResultLines[i].contains("com.android.launcher")) {
+                break;
+            }
+        }
+        if(i == cmdResultLines.length) {
+            throw new BuildError("The emulator do not have launcher run");
+        }
+    }
 }
diff --git a/prod-tests/src/com/android/sdk/tests/SdkTestAppTest.java b/prod-tests/src/com/android/sdk/tests/SdkTestAppTest.java
index 6978174..6996bdd 100644
--- a/prod-tests/src/com/android/sdk/tests/SdkTestAppTest.java
+++ b/prod-tests/src/com/android/sdk/tests/SdkTestAppTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.sdk.tests;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.ISdkBuildInfo;
@@ -182,10 +181,10 @@
             runTestAppTest(target, testAppDir, isLibrary);
         } catch (AssertionError e) {
             CLog.w("%s failed. %s", testId, e);
-            listener.testFailed(TestFailure.FAILURE, testId, getThrowableTraceAsString(e));
+            listener.testFailed(testId, getThrowableTraceAsString(e));
         } catch (Throwable t) {
             CLog.w("%s failed. %s", testId, t);
-            listener.testFailed(TestFailure.ERROR, testId, getThrowableTraceAsString(t));
+            listener.testFailed(testId, getThrowableTraceAsString(t));
         }
         listener.testEnded(testId, Collections.EMPTY_MAP);
     }
diff --git a/prod-tests/src/com/android/security/tests/SELinuxDenialsTests.java b/prod-tests/src/com/android/security/tests/SELinuxDenialsTests.java
index 117157e..b14ad71 100644
--- a/prod-tests/src/com/android/security/tests/SELinuxDenialsTests.java
+++ b/prod-tests/src/com/android/security/tests/SELinuxDenialsTests.java
@@ -52,7 +52,7 @@
 public class SELinuxDenialsTests implements IRemoteTest, IDeviceTest {
 
     private ITestDevice mDevice;
-    private static final String ADB_SHELL_KERNEL_LOGS_CMD = "su -c dmesg";
+    private static final String ADB_SHELL_KERNEL_LOGS_CMD = "dmesg";
     private static final String DMESG_OUTPUT_FILE_NAME = "dmesg_output";
 
     @Option(name="selinux-domains-file",
diff --git a/prod-tests/src/com/android/sensor/tests/SingleSensorTests.java b/prod-tests/src/com/android/sensor/tests/SingleSensorTests.java
index c6389dc..aedf1bc 100644
--- a/prod-tests/src/com/android/sensor/tests/SingleSensorTests.java
+++ b/prod-tests/src/com/android/sensor/tests/SingleSensorTests.java
@@ -77,7 +77,7 @@
         String rawFileList = getDevice().executeShellCommand(
                 String.format("ls ${EXTERNAL_STORAGE}/%s*", mOutputPrefix));
         if (rawFileList != null && !rawFileList.contains("No such file or directory")) {
-            String[] filePaths = rawFileList.split("\r\n");
+            String[] filePaths = rawFileList.split("\r?\n");
             for (String filePath : filePaths) {
                 pullFile(listener, filePath);
             }
diff --git a/prod-tests/src/com/android/wireless/tests/ConnectivityManagerTest.java b/prod-tests/src/com/android/wireless/tests/ConnectivityManagerTest.java
index c2f4b0f..36ba4ed 100644
--- a/prod-tests/src/com/android/wireless/tests/ConnectivityManagerTest.java
+++ b/prod-tests/src/com/android/wireless/tests/ConnectivityManagerTest.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.BugreportCollector;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.testtype.IDeviceTest;
@@ -39,7 +40,6 @@
 public class ConnectivityManagerTest implements IRemoteTest, IDeviceTest {
     private ITestDevice mTestDevice = null;
 
-    private static long START_TIMER = 5 * 60 * 1000; //5 minutes
     // Define instrumentation test package and runner.
     private static final String TEST_PACKAGE_NAME =
         "com.android.connectivitymanagertest";
@@ -49,8 +49,6 @@
         String.format("%s.functional.ConnectivityManagerMobileTest", TEST_PACKAGE_NAME);
     private static final int TEST_TIMER = 60 * 60 * 1000;  // 1 hour
 
-    private RadioHelper mRadioHelper;
-
     @Option(name="ssid", description="The ssid used for wifi connection.")
     private String mSsid = null;
 
@@ -63,6 +61,9 @@
     @Option(name="wifi-only")
     private boolean mWifiOnly = false;
 
+    @Option(name="start-sleep", description="The amount of time to sleep before starting in secs.")
+    private int mStartSleepSecs = 5 * 60;
+
     @Override
     public void setDevice(ITestDevice testDevice) {
         mTestDevice = testDevice;
@@ -74,22 +75,22 @@
     }
 
     @Override
-    public void run(ITestInvocationListener standardListener)
+    public void run(ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         Assert.assertNotNull(mTestDevice);
         Assert.assertNotNull(mSsid);
-        mRadioHelper = new RadioHelper(mTestDevice);
-        RunUtil.getDefault().sleep(START_TIMER);
+
+        CLog.d("Sleeping for %d secs", mStartSleepSecs);
+        RunUtil.getDefault().sleep(mStartSleepSecs * 1000);
+
         if (!mWifiOnly) {
-            // capture a bugreport if activation or data setup failed
-            if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
-                mRadioHelper.getBugreport(standardListener);
-                return;
-            }
+            final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+            Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+            Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
         }
+
         // Add bugreport listener for bugreport after each test case fails
-        BugreportCollector bugListener = new
-            BugreportCollector(standardListener, mTestDevice);
+        BugreportCollector bugListener = new BugreportCollector(listener, mTestDevice);
         bugListener.addPredicate(BugreportCollector.AFTER_FAILED_TESTCASES);
         bugListener.setDescriptiveName("connectivity_manager_test");
         // Device may reboot during the test, to capture a bugreport after that,
diff --git a/prod-tests/src/com/android/wireless/tests/RadioHelper.java b/prod-tests/src/com/android/wireless/tests/RadioHelper.java
index 1928f5f..68f88c6 100644
--- a/prod-tests/src/com/android/wireless/tests/RadioHelper.java
+++ b/prod-tests/src/com/android/wireless/tests/RadioHelper.java
@@ -18,9 +18,6 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
 
@@ -52,14 +49,14 @@
      * Get phone type 0 - None, 1 - GSM, 2 - CDMA
      */
     private String getPhoneType() throws DeviceNotAvailableException {
-        return mDevice.getPropertySync("gsm.current.phone-type");
+        return mDevice.getProperty("gsm.current.phone-type");
     }
 
     /**
      * Get sim state
      */
     private String getSimState() throws DeviceNotAvailableException {
-        return mDevice.getPropertySync("gsm.sim.state");
+        return mDevice.getProperty("gsm.sim.state");
     }
 
     /**
@@ -155,8 +152,8 @@
 
     /**
      * Wait for device data setup
-     * @return true if data setup succeeded
-     * @return false if data setup failed
+     *
+     * @return true if data setup succeeded, false otherwise
      */
     public boolean waitForDataSetup() throws DeviceNotAvailableException {
         long startTime = System.currentTimeMillis();
@@ -168,13 +165,4 @@
         }
         return false;
     }
-
-    // capture a bugreport
-    public void getBugreport(ITestInvocationListener listener)
-            throws DeviceNotAvailableException {
-        CLog.d("Capture a bugreport");
-        InputStreamSource bugreport = mDevice.getBugreport();
-        listener.testLog("bugreport", LogDataType.BUGREPORT, bugreport);
-        bugreport.cancel();
-    }
 }
diff --git a/prod-tests/src/com/android/wireless/tests/SmsStressTest.java b/prod-tests/src/com/android/wireless/tests/SmsStressTest.java
index ba1da84..2171f64 100644
--- a/prod-tests/src/com/android/wireless/tests/SmsStressTest.java
+++ b/prod-tests/src/com/android/wireless/tests/SmsStressTest.java
@@ -15,10 +15,10 @@
  */
 package com.android.wireless.tests;
 
-import com.android.ddmlib.IDevice;
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -47,56 +47,51 @@
  * Run the Sms stress test. This test stresses sms message sending and receiving
  */
 public class SmsStressTest implements IRemoteTest, IDeviceTest {
-    private ITestDevice mTestDevice = null;
+    private static final String TEST_PACKAGE_NAME = "com.android.messagingtests";
+    private static final String TEST_RUNNER_NAME = "android.test.InstrumentationTestRunner";
 
-    // Define instrumentation test package and runner.
-    private static final String TEST_PACKAGE_NAME = "com.android.mms.tests";
-    private static final String TEST_RUNNER_NAME = "com.android.mms.SmsTestRunner";
-    private static final String TEST_CLASS_NAME = "com.android.mms.ui.SmsStressTest";
+    private static final String OUTPUT_PATH = "result.txt";
 
     private static final String ITEM_KEY = "single_thread";
     private static final String METRICS_NAME = "sms_stress";
-    private static final Pattern MESSAGE_PATTERN =
-            Pattern.compile("^send message (\\d+) out of (\\d+)");
+
+    private static final String OUTPUT_PASSED = "passed";
+    private static final String OUTPUT_FAILED = "failed";
+    private static final Pattern OUTPUT_PATTERN = Pattern.compile(
+            String.format("^(\\d+),(%s|%s)$", OUTPUT_PASSED, OUTPUT_FAILED));
+
     private static final String INSERT_COMMAND =
             "sqlite3 /data/data/com.android.providers.settings/databases/settings.db "
             + "\"INSERT INTO global (name, value) values (\'%s\',\'%s\');\"";
-    private String mOutputFile = "result.txt";
 
-    @Option(name="recipient",
-            description="The recipient of sms messages")
-    private String mRecipient = null;
+    private enum MessagingApp {
+        HANGOUTS ("com.android.messagingtests.stress.HangoutsStressTest"),
+        MESSAGING ("com.android.messagingtests.stress.MessagingStressTest"),
+        MESSENGER ("com.android.messagingtests.stress.MessengerStressTest");
 
-    @Option(name="messages",
-            description="The total number of messages to send")
-    private int mNumMessages = 100;
+        private final String mTestClass;
 
-    @Option(name="messagefile",
-            description="The file to load sending message")
-    private String mMessageFile = null;
+        MessagingApp(String testClass) {
+            mTestClass = testClass;
+        }
 
-    @Option(name="recipientfile",
-            description="The file to load recipients")
-    private String mRecipientFile = null;
-
-    @Option(name="receivetimer",
-            description="The timer before verifying messages receiption when sending sms"
-            + "to the test device itself (s)")
-    private int mReceiveTimer = 300;
-
-    @Option(name="sendinterval",
-            description="The time interval between two consecutive sms.")
-    private int mSendInterval = 10;
-
-    @Override
-    public void setDevice(ITestDevice testDevice) {
-        mTestDevice = testDevice;
+        public String getTestClass() {
+            return mTestClass;
+        }
     }
 
-    @Override
-    public ITestDevice getDevice() {
-        return mTestDevice;
-    }
+    private ITestDevice mTestDevice = null;
+
+    @Option(name="iterations", description="The total number of iterations to run",
+            importance=Importance.ALWAYS)
+    private int mIterations = 100;
+
+    @Option(name="phone-number", description="The phone number to use")
+    private String mPhoneNumber = null;
+
+    @Option(name="app", description="The default messaging app on the device",
+            importance=Importance.IF_UNSET, mandatory=true)
+    private MessagingApp mApp = null;
 
     /**
      * Configure device with special settings
@@ -113,91 +108,84 @@
     }
 
     /**
-     * Run sms stress test and parse test results
+     * Run messaging stress test and parse test results
      */
     @Override
-    public void run(ITestInvocationListener standardListener)
+    public void run(ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         Assert.assertNotNull(mTestDevice);
         setupDevice();
-        RadioHelper mRadioHelper = new RadioHelper(mTestDevice);
-        // Capture a bugreport if activation or data setup failed
-        if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
-            mRadioHelper.getBugreport(standardListener);
-            return;
-        }
+
+        final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+        Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+        Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
 
         IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(
                 TEST_PACKAGE_NAME, TEST_RUNNER_NAME, mTestDevice.getIDevice());
-        runner.setClassName(TEST_CLASS_NAME);
-        if (mRecipient != null) {
-            runner.addInstrumentationArg("recipient", mRecipient);
+        runner.setClassName(mApp.getTestClass());
+        runner.addInstrumentationArg("iterations", Integer.toString(mIterations));
+        if (mPhoneNumber != null) {
+            runner.addInstrumentationArg("phone_number", mPhoneNumber);
         }
-        if (mMessageFile != null) {
-            runner.addInstrumentationArg("messagefile", mMessageFile);
-        }
-        if (mRecipientFile != null) {
-            runner.addInstrumentationArg("messagefile", mMessageFile);
-        }
-        runner.addInstrumentationArg("messages", Integer.toString(mNumMessages));
-        runner.addInstrumentationArg(
-                "receivetimer", Integer.toString(mReceiveTimer));
-        runner.addInstrumentationArg(
-                "sendinterval", Integer.toString(mSendInterval));
+        mTestDevice.runInstrumentationTests(runner, listener);
 
-        mTestDevice.runInstrumentationTests(runner, standardListener);
-        logOutputFile(standardListener);
-        cleanOutputFiles();
+        InputStreamSource screenshot = mTestDevice.getScreenshot();
+        try {
+            listener.testLog(String.format("screenshot"),
+                    LogDataType.PNG, screenshot);
+        } finally {
+            StreamUtil.cancel(screenshot);
+        }
+
+        parseOutput(listener);
     }
 
     /**
      * Collect test results and report test results.
-     *
-     * @param listener
      */
-    private void logOutputFile(ITestInvocationListener listener)
-        throws DeviceNotAvailableException {
-        // Capture a bugreport right after the test
-        InputStreamSource bugreport = mTestDevice.getBugreport();
-        listener.testLog("bugreport", LogDataType.BUGREPORT, bugreport);
-        bugreport.cancel();
-
+    private void parseOutput(ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
+        File outputFile = null;
         InputStreamSource outputSource = null;
-        Map<String, String> runMetrics = new HashMap<String, String>();
-        File resFile = null;
-        BufferedReader br = null;
-        try {
-            resFile = mTestDevice.pullFileFromExternal(mOutputFile);
-            if (resFile == null) {
-              return;
-            }
-            // Save a copy of the output file
-            CLog.d("Sending %d byte file %s into the logosphere!",
-                    resFile.length(), resFile);
-            outputSource = new SnapshotInputStreamSource(new FileInputStream(resFile));
-            listener.testLog(mOutputFile, LogDataType.TEXT, outputSource);
+        BufferedReader outputReader = null;
+        Integer iterations = null;
 
-            // Parse the results file and post results to test listener
-            br = new BufferedReader(new FileReader(resFile));
-            String line = null;
-            while ((line = br.readLine()) != null) {
-                Matcher match = MESSAGE_PATTERN.matcher(line);
-                if (match.matches()) {
-                    String value = match.group(1);
-                    CLog.d("iteration: %s", value);
-                    runMetrics.put(ITEM_KEY, value);
+        try {
+            outputFile = mTestDevice.pullFileFromExternal(OUTPUT_PATH);
+            if (outputFile != null) {
+                CLog.d("Sending %d byte file %s into the logosphere!",
+                        outputFile.length(), outputFile);
+                outputSource = new SnapshotInputStreamSource(new FileInputStream(outputFile));
+                listener.testLog("sms_stress_output", LogDataType.TEXT, outputSource);
+
+                outputReader = new BufferedReader(new FileReader(outputFile));
+                String line = null;
+                while ((line = outputReader.readLine()) != null) {
+                    Matcher m = OUTPUT_PATTERN.matcher(line);
+                    if (m.matches()) {
+                        if (OUTPUT_PASSED.equals(m.group(2))) {
+                            iterations = Integer.parseInt(m.group(1));
+                        }
+                    } else {
+                        CLog.w("Line '%s' did not match output pattern", line);
+                    }
                 }
             }
+
+            Map<String, String> metrics = new HashMap<String, String>();
+            metrics.put(ITEM_KEY, Integer.toString(iterations == null ? 0 : iterations + 1));
+            reportMetrics(METRICS_NAME, listener, metrics);
         } catch (IOException e) {
-            CLog.e("IOException while reading from data stream: %s", e);
+            CLog.e("IOException parsing output file: %s", e);
+            Assert.fail("IOException parsing output file");
         } finally {
-            FileUtil.deleteFile(resFile);
+            FileUtil.deleteFile(outputFile);
             StreamUtil.cancel(outputSource);
-            StreamUtil.close(br);
+            StreamUtil.close(outputReader);
         }
-        reportMetrics(METRICS_NAME, listener, runMetrics);
     }
 
+
     /**
      * Report run metrics by creating an empty test run to stick them in
      */
@@ -210,11 +198,18 @@
     }
 
     /**
-     * Clean up output files from the last test run
+     * {@inheritDoc}
      */
-    private void cleanOutputFiles() throws DeviceNotAvailableException {
-        CLog.d("Remove output file: %s", mOutputFile);
-        String extStore = mTestDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
-        mTestDevice.executeShellCommand(String.format("rm %s/%s", extStore, mOutputFile));
+    @Override
+    public void setDevice(ITestDevice testDevice) {
+        mTestDevice = testDevice;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ITestDevice getDevice() {
+        return mTestDevice;
     }
 }
diff --git a/prod-tests/src/com/android/wireless/tests/TelephonyStabilityTest.java b/prod-tests/src/com/android/wireless/tests/TelephonyStabilityTest.java
index e1704eb..ee36b0a 100644
--- a/prod-tests/src/com/android/wireless/tests/TelephonyStabilityTest.java
+++ b/prod-tests/src/com/android/wireless/tests/TelephonyStabilityTest.java
@@ -106,7 +106,6 @@
     private long mScreenTimeoutMin = 30;
 
     private ITestDevice mTestDevice = null;
-    private RadioHelper mRadioHelper;
 
     /**
      * Run the telephony stability test  and collect results
@@ -117,11 +116,10 @@
         Assert.assertNotNull(mPhoneNumber);
 
         setScreenTimeout();
-        mRadioHelper = new RadioHelper(mTestDevice);
-        if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
-            mRadioHelper.getBugreport(listener);
-            return;
-        }
+
+        final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+        Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+        Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
 
         IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(TEST_PACKAGE_NAME,
                 TEST_RUNNER_NAME, mTestDevice.getIDevice());
@@ -165,6 +163,14 @@
                     bugreport.cancel();
                 }
             }
+
+            InputStreamSource screenshot = mTestDevice.getScreenshot();
+            try {
+                listener.testLog(String.format("screenshot_%04d", lastBugreportIteration),
+                        LogDataType.PNG, screenshot);
+            } finally {
+                StreamUtil.cancel(screenshot);
+            }
         }
         reportMetrics(listener, metrics);
     }
diff --git a/prod-tests/src/com/android/wireless/tests/TelephonyTest.java b/prod-tests/src/com/android/wireless/tests/TelephonyTest.java
index 8fae8e3..2ad9022 100644
--- a/prod-tests/src/com/android/wireless/tests/TelephonyTest.java
+++ b/prod-tests/src/com/android/wireless/tests/TelephonyTest.java
@@ -83,7 +83,6 @@
     private long mStartPauseDurationSec = 2;
 
     private ITestDevice mTestDevice = null;
-    private RadioHelper mRadioHelper;
 
     /**
      * Run the telephony outgoing call stress test
@@ -95,12 +94,9 @@
         Assert.assertNotNull(mTestDevice);
         Assert.assertNotNull(mPhoneNumber);
 
-        mRadioHelper = new RadioHelper(mTestDevice);
-        // wait for data connection
-        if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
-            mRadioHelper.getBugreport(listener);
-            return;
-        }
+        final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+        Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+        Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
 
         IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(TEST_PACKAGE_NAME,
                 TEST_RUNNER_NAME, mTestDevice.getIDevice());
@@ -126,11 +122,15 @@
         if (successfulIterations < mIterations) {
             mTestDevice.waitForDeviceOnline(30 * 1000);
             InputStreamSource bugreport = mTestDevice.getBugreport();
+            InputStreamSource screenshot = mTestDevice.getScreenshot();
             try {
                 listener.testLog(String.format("bugreport_%s", successfulIterations),
                         LogDataType.BUGREPORT, bugreport);
+                listener.testLog(String.format("screenshot_%s", successfulIterations),
+                        LogDataType.PNG, screenshot);
             } finally {
-                bugreport.cancel();
+                StreamUtil.cancel(bugreport);
+                StreamUtil.cancel(screenshot);
             }
         }
 
diff --git a/prod-tests/src/com/android/wireless/tests/VpnTest.java b/prod-tests/src/com/android/wireless/tests/VpnTest.java
index 0598118..66cfcc1 100644
--- a/prod-tests/src/com/android/wireless/tests/VpnTest.java
+++ b/prod-tests/src/com/android/wireless/tests/VpnTest.java
@@ -20,7 +20,6 @@
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.BugreportCollector;
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.ITestInvocationListener;
diff --git a/prod-tests/src/com/android/wireless/tests/WifiStressTest.java b/prod-tests/src/com/android/wireless/tests/WifiStressTest.java
index 000cdb6..01d1c5e 100644
--- a/prod-tests/src/com/android/wireless/tests/WifiStressTest.java
+++ b/prod-tests/src/com/android/wireless/tests/WifiStressTest.java
@@ -67,7 +67,6 @@
     private static final int RECONNECT_TEST_TIMER = 12 * 60 * 60 * 1000; // 12 hours
 
     private String mOutputFile = "WifiStressTestOutput.txt";
-    private RadioHelper mRadioHelper;
 
     /**
      * Stores the test cases that we should consider running.
@@ -96,45 +95,46 @@
             description="The number of iterations to run soft ap stress test")
     private String mApIteration = "100";
 
-    @Option(name="scan-iteration",
-            description="The number of iterations to run WiFi scanning test")
-    private String mScanIteration = "100";
+    @Option(name="idle-time",
+        description="The device idle time after screen off")
+    private String mIdleTime = "30"; // 30 seconds
 
     @Option(name="reconnect-iteration",
             description="The number of iterations to run WiFi reconnection stress test")
     private String mReconnectionIteration = "100";
 
-    @Option(name="reconnect-ssid",
-            description="The ssid for WiFi recoonection stress test")
-    private String mReconnectionSsid = "securenetdhcp";
-
     @Option(name="reconnect-password",
             description="The password for the above ssid in WiFi reconnection stress test")
     private String mReconnectionPassword = "androidwifi";
 
-    @Option(name="idle-time",
-            description="The device idle time after screen off")
-    private String mIdleTime = "30"; // 30 seconds
+    @Option(name="reconnect-ssid",
+        description="The ssid for WiFi recoonection stress test")
+    private String mReconnectionSsid = "securenetdhcp";
+
+    @Option(name="reconnection-test",
+        description="Option to run the wifi reconnection stress test")
+    private boolean mReconnectionTestFlag = true;
+
+    @Option(name="scan-iteration",
+        description="The number of iterations to run WiFi scanning test")
+    private String mScanIteration = "100";
 
     @Option(name="scan-test",
             description="Option to run the scan stress test")
     private boolean mScanTestFlag = true;
 
+    @Option(name="skip-set-device-screen-timeout",
+            description="Option to skip screen timeout configuration")
+    private boolean mSkipSetDeviceScreenTimeout = false;
+
     @Option(name="tether-test",
             description="Option to run the tethering stress test")
     private boolean mTetherTestFlag = true;
 
-    @Option(name="reconnection-test",
-            description="Option to run the wifi reconnection stress test")
-    private boolean mReconnectionTestFlag = true;
-
     @Option(name="wifi-only")
     private boolean mWifiOnly = false;
 
-    private void setupTests() throws DeviceNotAvailableException {
-        // get RadioHelper
-        mRadioHelper = new RadioHelper(mTestDevice);
-
+    private void setupTests() {
         if (mTestList != null) {
             return;
         }
@@ -185,7 +185,7 @@
      * Configure screen timeout property
      * @throws DeviceNotAvailableException
      */
-    private void configDevice() throws DeviceNotAvailableException {
+    private void setDeviceScreenTimeout() throws DeviceNotAvailableException {
         // Set device screen_off_timeout as svc power can be set to false in the Wi-Fi test
         String command = ("sqlite3 /data/data/com.android.providers.settings/databases/settings.db "
                 + "\"UPDATE system SET value=\'600000\' WHERE name=\'screen_off_timeout\';\"");
@@ -225,13 +225,15 @@
             throws DeviceNotAvailableException {
         Assert.assertNotNull(mTestDevice);
         setupTests();
-        configDevice();
+        if (!mSkipSetDeviceScreenTimeout) {
+            setDeviceScreenTimeout();
+        }
         RunUtil.getDefault().sleep(START_TIMER);
+
         if (!mWifiOnly) {
-            if (!mRadioHelper.radioActivation() || !mRadioHelper.waitForDataSetup()) {
-                mRadioHelper.getBugreport(standardListener);
-                return;
-            }
+            final RadioHelper radioHelper = new RadioHelper(mTestDevice);
+            Assert.assertTrue("Radio activation failed", radioHelper.radioActivation());
+            Assert.assertTrue("Data setup failed", radioHelper.waitForDataSetup());
         }
 
         IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(
diff --git a/prod-tests/tests/Android.mk b/prod-tests/tests/Android.mk
index 67d958f..f7f1e18 100644
--- a/prod-tests/tests/Android.mk
+++ b/prod-tests/tests/Android.mk
@@ -26,7 +26,7 @@
 LOCAL_MODULE := tf-prod-metatests
 LOCAL_MODULE_TAGS := optional
 LOCAL_STATIC_JAVA_LIBRARIES := easymock
-LOCAL_JAVA_LIBRARIES := tradefed tf-prod-tests ddmlib-prebuilt
+LOCAL_JAVA_LIBRARIES := tradefed tf-prod-tests
 
 include $(BUILD_HOST_JAVA_LIBRARY)
 
diff --git a/prod-tests/tests/src/com/android/continuous/SmokeTestFailureReporterTest.java b/prod-tests/tests/src/com/android/continuous/SmokeTestFailureReporterTest.java
index cbfa689..2d143f0 100644
--- a/prod-tests/tests/src/com/android/continuous/SmokeTestFailureReporterTest.java
+++ b/prod-tests/tests/src/com/android/continuous/SmokeTestFailureReporterTest.java
@@ -16,7 +16,6 @@
 
 package com.android.continuous;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.IBuildInfo;
@@ -68,7 +67,7 @@
         mReporter.invocationStarted(build);
         mReporter.testRunStarted("testrun", 1);
         mReporter.testStarted(testId);
-        mReporter.testFailed(TestFailure.FAILURE, testId, trace);
+        mReporter.testFailed(testId, trace);
         mReporter.testEnded(testId, emptyMap);
         mReporter.testRunEnded(2, emptyMap);
         mReporter.invocationEnded(1);
@@ -113,7 +112,7 @@
         mReporter.testEnded(testPass1, emptyMap);
 
         mReporter.testStarted(testFail);
-        mReporter.testFailed(TestFailure.FAILURE, testFail, trace);
+        mReporter.testFailed(testFail, trace);
         mReporter.testEnded(testFail, emptyMap);
 
         mReporter.testStarted(testPass2);
diff --git a/remote/.classpath b/remote/.classpath
index fc4759a..ac373ae 100644
--- a/remote/.classpath
+++ b/remote/.classpath
@@ -5,5 +5,7 @@
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/guavalib_intermediates/javalib.jar" sourcepath="/TRADEFED_ROOT/external/guava/guava/src"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
+	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/guava/guava-15.0.jar"/>
+	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/out/host/common/obj/JAVA_LIBRARIES/jsr305lib_intermediates/javalib.jar"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/remote/Android.mk b/remote/Android.mk
index 0793f33..0c1919c 100644
--- a/remote/Android.mk
+++ b/remote/Android.mk
@@ -25,8 +25,7 @@
 
 LOCAL_MODULE_TAGS := optional
 # only depend on ddmlib for the Log class
-LOCAL_JAVA_LIBRARIES := ddmlib-prebuilt
-LOCAL_STATIC_JAVA_LIBRARIES := json-prebuilt guavalib
+LOCAL_STATIC_JAVA_LIBRARIES := json-prebuilt jsr305lib guava-15.0-prebuilt ddmlib-prebuilt devtools-annotations-prebuilt
 
 include $(BUILD_HOST_JAVA_LIBRARY)
 
diff --git a/remote/src/com/android/tradefed/command/remote/DeviceDescriptor.java b/remote/src/com/android/tradefed/command/remote/DeviceDescriptor.java
index 6ef8d39..88a5905 100644
--- a/remote/src/com/android/tradefed/command/remote/DeviceDescriptor.java
+++ b/remote/src/com/android/tradefed/command/remote/DeviceDescriptor.java
@@ -76,4 +76,12 @@
     public String getBatteryLevel() {
         return mBatteryLevel;
     }
+
+    /**
+     * Provides a description with serials, product and build id
+     */
+    @Override
+    public String toString() {
+        return String.format("[%s %s:%s %s]", mSerial, mProduct, mProductVariant, mBuildId);
+    }
 }
diff --git a/res/apks/wifiutil/PREBUILT b/res/apks/wifiutil/PREBUILT
index 66fee64..6cf18fc 100644
--- a/res/apks/wifiutil/PREBUILT
+++ b/res/apks/wifiutil/PREBUILT
@@ -1,4 +1,4 @@
 This apk can be rebuilt from
         platform/tools/tradefederation
 
-By running `m WifiUtil` on revision db5f64dd8f9ecb25b703b6564faa90e977ed56d6
+By running `m WifiUtil` on revision 5c4cc0d542cfdfcf92ce53db7a4fb30ea89fb9c7 
diff --git a/res/apks/wifiutil/WifiUtil.apk b/res/apks/wifiutil/WifiUtil.apk
index a316b2d..2f0cb29 100644
--- a/res/apks/wifiutil/WifiUtil.apk
+++ b/res/apks/wifiutil/WifiUtil.apk
Binary files differ
diff --git a/script_help.sh b/script_help.sh
new file mode 100755
index 0000000..eddfe52
--- /dev/null
+++ b/script_help.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+# A library script to help other scripts run within an Android build environment, or while deployed
+# on a target host.  Intended to be used with the `source` bash built-in command.  Defines the
+# following environment variables:
+# JAVA_VERSION, RDBG_FLAG, TF_PATH, TRADEFED_OPTS
+#
+# It will react to the following environment variables, if they are set:
+# TF_DEBUG, TRADEFED_OPTS_FILE
+
+
+checkPath() {
+    if ! type -P "$1" &> /dev/null; then
+        echo "Unable to find $1 in path."
+        exit
+    fi;
+}
+
+checkFile() {
+    if [ ! -f "$1" ]; then
+        echo "Unable to locate $1"
+        exit
+    fi;
+}
+
+checkPath java
+
+# check java version
+java_version_string=$(java -version 2>&1)
+JAVA_VERSION=$(echo "$java_version_string" | grep '[ "]1\.7[\. "$$]')
+if [ "${JAVA_VERSION}" == "" ]; then
+    echo "Wrong java version. 1.7 is required."
+    exit
+fi
+
+# check debug flag and set up remote debugging
+if [ -n "${TF_DEBUG}" ]; then
+    if [ -z "${TF_DEBUG_PORT}" ]; then
+        TF_DEBUG_PORT=10088
+    fi
+    RDBG_FLAG="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=${TF_DEBUG_PORT}"
+fi
+
+# first try to find TF jars in same dir as this script
+CUR_DIR=$(dirname "$0")
+if [ -f "${CUR_DIR}/tradefed.jar" ]; then
+    TF_PATH="${CUR_DIR}/*"
+elif [ ! -z "${ANDROID_HOST_OUT}" ]; then
+    # in an Android build env, tradefed.jar should be in
+    # $ANDROID_HOST_OUT/tradefed/
+    if [ -f "${ANDROID_HOST_OUT}/tradefed/tradefed.jar" ]; then
+        # We intentionally pass the asterisk through without shell expansion
+        TF_PATH="${ANDROID_HOST_OUT}/tradefed/*"
+    fi
+fi
+
+if [ -z "${TF_PATH}" ]; then
+    echo "ERROR: Could not find tradefed jar files"
+    exit
+fi
+
+# set any host specific options
+# file format for file at $TRADEFED_OPTS_FILE is one line per host with the following format:
+# <hostname>=<options>
+# for example:
+# hostname.domain.com=-Djava.io.tmpdir=/location/on/disk -Danother=false ...
+# hostname2.domain.com=-Djava.io.tmpdir=/different/location -Danother=true ...
+if [ -e "${TRADEFED_OPTS_FILE}" ]; then
+    # pull the line for this host and take everything after the first =
+    export TRADEFED_OPTS=`grep "^$HOSTNAME=" "$TRADEFED_OPTS_FILE" | cut -d '=' -f 2-`
+fi
diff --git a/src/com/android/tradefed/build/BootstrapBuildProvider.java b/src/com/android/tradefed/build/BootstrapBuildProvider.java
index 3ff2ec8..4c16516 100644
--- a/src/com/android/tradefed/build/BootstrapBuildProvider.java
+++ b/src/com/android/tradefed/build/BootstrapBuildProvider.java
@@ -22,6 +22,8 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 
+import java.io.File;
+
 /**
  * A {@link IDeviceBuildProvider} that bootstraps build info from the test device
  *
@@ -54,11 +56,17 @@
     @Option(name="build-target", description="build target name to supply.")
     private String mBuildTargetName = "bootstrapped";
 
+    @Option(name="branch", description="build branch name to supply.")
+    private String mBranch = null;
+
     @Option(name="shell-available-timeout",
             description="Time to wait in seconds for device shell to become available. " +
             "Default to 300 seconds.")
     private long mShellAvailableTimeout = 5 * 60;
 
+    @Option(name="tests-dir", description="Path to top directory of expanded tests zip")
+    private File mTestsDir = null;
+
     @Override
     public IBuildInfo getBuild() throws BuildRetrievalError {
         throw new UnsupportedOperationException("Call getBuild(ITestDevice)");
@@ -78,19 +86,26 @@
     @Override
     public IBuildInfo getBuild(ITestDevice device) throws BuildRetrievalError,
             DeviceNotAvailableException {
-        IBuildInfo info = new BuildInfo(device.getBuildId(), mTestTag, mBuildTargetName);
+        String buildId = device.getBuildId();
+        IBuildInfo info = new DeviceBuildInfo(buildId, mTestTag, mBuildTargetName);
         if (!device.waitForDeviceShell(mShellAvailableTimeout * 1000)) {
             throw new DeviceNotAvailableException(
                     String.format("Shell did not become available in %d seconds",
                             mShellAvailableTimeout));
         }
-        info.setBuildBranch(String.format("%s-%s-%s-%s",
-                device.getProperty("ro.product.brand"),
-                device.getProperty("ro.product.name"),
-                device.getProductVariant(),
-                device.getProperty("ro.build.version.release")));
+        if (mBranch == null) {
+            mBranch = String.format("%s-%s-%s-%s",
+                    device.getProperty("ro.product.brand"),
+                    device.getProperty("ro.product.name"),
+                    device.getProductVariant(),
+                    device.getProperty("ro.build.version.release"));
+        }
+        info.setBuildBranch(mBranch);
         info.setBuildFlavor(device.getBuildFlavor());
         info.addBuildAttribute("build_alias", device.getBuildAlias());
+        if (mTestsDir != null && mTestsDir.isDirectory()) {
+            info.setFile("testsdir", mTestsDir, buildId);
+        }
         return info;
     }
 }
diff --git a/src/com/android/tradefed/build/FolderBuildInfo.java b/src/com/android/tradefed/build/FolderBuildInfo.java
index 287bb7b..b213e77 100644
--- a/src/com/android/tradefed/build/FolderBuildInfo.java
+++ b/src/com/android/tradefed/build/FolderBuildInfo.java
@@ -91,6 +91,8 @@
             // fall through
             CLog.w("hardlink of %s %s failed: " + e.toString(), orig.getAbsolutePath(),
                     dest.getAbsolutePath());
+            // Clean up after failure.
+            FileUtil.recursiveDelete(dest);
         }
         FileUtil.recursiveCopy(orig, dest);
     }
diff --git a/src/com/android/tradefed/command/CommandOptions.java b/src/com/android/tradefed/command/CommandOptions.java
index 16a746d..f207049 100644
--- a/src/com/android/tradefed/command/CommandOptions.java
+++ b/src/com/android/tradefed/command/CommandOptions.java
@@ -36,6 +36,9 @@
             importance = Importance.ALWAYS)
     private boolean mFullHelpMode = false;
 
+    @Option(name = "json-help", description = "display the full help in json format.")
+    private boolean mJsonHelpMode = false;
+
     @Option(name = "dry-run",
             description = "build but don't actually run the command.  Intended as a quick check " +
                     "to ensure that a command is runnable.",
@@ -49,8 +52,7 @@
     private boolean mNoisyDryRunMode = false;
 
     @Option(name = "min-loop-time", description =
-            "the minimum invocation time in ms when in loop mode.",
-            updateRule = OptionUpdateRule.LEAST)
+            "the minimum invocation time in ms when in loop mode.")
     private Long mMinLoopTime = 10L * 60L * 1000L;
 
     @Option(name = "max-random-loop-time", description =
@@ -98,6 +100,22 @@
     }
 
     /**
+     * Set the json help mode for the config.
+     * <p/>
+     * Exposed for testing.
+     */
+    void setJsonHelpMode(boolean jsonHelpMode) {
+        mJsonHelpMode = jsonHelpMode;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isJsonHelpMode() {
+        return mJsonHelpMode;
+    }
+
+    /**
      * Set the dry run mode for the config.
      * <p/>
      * Exposed for testing.
@@ -179,7 +197,7 @@
         try {
             OptionCopier.copyOptions(this, clone);
         } catch (ConfigurationException e) {
-            CLog.e("failed to clone command options", e);
+            CLog.e("failed to clone command options: %s", e.getMessage());
         }
         return clone;
     }
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 00ec78a..65c4b18 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -17,6 +17,7 @@
 package com.android.tradefed.command;
 
 import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.IDevice;
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.command.CommandFileParser.CommandLine;
@@ -40,16 +41,22 @@
 import com.android.tradefed.device.IDeviceManager;
 import com.android.tradefed.device.IDeviceMonitor;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.NoDeviceException;
 import com.android.tradefed.invoker.IRescheduler;
 import com.android.tradefed.invoker.ITestInvocation;
 import com.android.tradefed.invoker.TestInvocation;
 import com.android.tradefed.log.LogRegistry;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.StubTestInvocationListener;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.ResultForwarder;
 import com.android.tradefed.util.ArrayUtil;
+import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.QuotationAwareTokenizer;
+import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.TableFormatter;
 
+import org.json.JSONException;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -66,8 +73,10 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
 
 /**
  * A scheduler for running TradeFederation commands across all available devices.
@@ -125,6 +134,10 @@
     // FIXME: enable this to be enabled or disabled on a per-cmdfile basis
     private boolean mReloadCmdfiles = false;
 
+    @Option(name = "max-poll-time", description =
+            "ms between forced command scheduler execution time")
+    private long mPollTime = 30 * 1000; // 30 seconds
+
     private enum CommandState {
         WAITING_FOR_DEVICE("Wait_for_device"),
         EXECUTING("Executing"),
@@ -381,34 +394,40 @@
      * <p/>
      * Returns device to device manager and remote handover server if applicable.
      */
-    private class FreeDeviceHandler extends StubTestInvocationListener implements
+    private class FreeDeviceHandler extends ResultForwarder implements
             IScheduledInvocationListener {
 
         private final IDeviceManager mDeviceManager;
 
-        FreeDeviceHandler(IDeviceManager deviceManager) {
+        FreeDeviceHandler(IDeviceManager deviceManager,
+                IScheduledInvocationListener... listeners) {
+            super(listeners);
             mDeviceManager = deviceManager;
         }
 
         @Override
         public void invocationComplete(ITestDevice device, FreeDeviceState deviceState) {
+            for (ITestInvocationListener listener : getListeners()) {
+                ((IScheduledInvocationListener) listener).invocationComplete(device, deviceState);
+            }
+
             mDeviceManager.freeDevice(device, deviceState);
             remoteFreeDevice(device);
         }
     }
 
     private class InvocationThread extends Thread {
-        private final IScheduledInvocationListener mListener;
+        private final IScheduledInvocationListener[] mListeners;
         private final ITestDevice mDevice;
         private final ExecutableCommand mCmd;
         private final ITestInvocation mInvocation;
         private long mStartTime = -1;
 
-        public InvocationThread(String name, IScheduledInvocationListener listener,
-                ITestDevice device, ExecutableCommand command) {
+        public InvocationThread(String name, ITestDevice device, ExecutableCommand command,
+                IScheduledInvocationListener... listeners) {
             // create a thread group so LoggerRegistry can identify this as an invocationThread
             super(new ThreadGroup(name), name);
-            mListener = listener;
+            mListeners = listeners;
             mDevice = device;
             mCmd = command;
             mInvocation = createRunInstance();
@@ -424,10 +443,11 @@
             mStartTime = System.currentTimeMillis();
             ITestInvocation instance = getInvocation();
             IConfiguration config = mCmd.getConfiguration();
+
             try {
                 mCmd.commandStarted();
                 instance.invoke(mDevice, config, new Rescheduler(mCmd.getCommandTracker()),
-                        mListener);
+                        mListeners);
             } catch (DeviceUnresponsiveException e) {
                 CLog.w("Device %s is unresponsive. Reason: %s", mDevice.getSerialNumber(),
                         e.getMessage());
@@ -450,7 +470,9 @@
                 // when freed
                 removeInvocationThread(this);
                 mCmd.commandFinished(elapsedTime);
-                mListener.invocationComplete(mDevice, deviceState);
+                for (final IScheduledInvocationListener listener : mListeners) {
+                    listener.invocationComplete(mDevice, deviceState);
+                }
             }
         }
 
@@ -461,6 +483,50 @@
         ITestDevice getDevice() {
             return mDevice;
         }
+
+        /**
+         * Stops a running invocation. {@link CommandScheduler#shutdownHard()} will stop
+         * all running invocations.
+         */
+        public void stopInvocation(String message) {
+            if (mDevice != null && mDevice.getIDevice().isOnline()) {
+                // Kill all running processes on device.
+                try {
+                    mDevice.executeShellCommand("am kill-all");
+                } catch (DeviceNotAvailableException e) {
+                    CLog.w("failed to kill process on device %s: %s", mDevice.getSerialNumber(), e);
+                }
+            }
+            RunUtil.getDefault().interrupt(this, message);
+            super.interrupt();
+        }
+
+        /**
+         * Checks whether the device battery level is above the required value to keep running the
+         * invocation.
+         */
+        public void checkDeviceBatteryLevel() {
+            final Integer cutoffBattery = mCmd.getConfiguration().getDeviceOptions()
+                    .getCutoffBattery();
+            if (mDevice != null && cutoffBattery != null) {
+                final IDevice device = mDevice.getIDevice();
+                int batteryLevel = -1;
+                try {
+                    batteryLevel = device.getBattery(0, TimeUnit.MILLISECONDS).get();
+                } catch (InterruptedException | ExecutionException e) {
+                    // fall through
+                }
+                CLog.d("device %s: battey level=%d%%", device.getSerialNumber(), batteryLevel);
+                // This logic is based on the assumption that batterLevel will be 0 or -1 if TF
+                // fails to fetch a valid battery level or the device is not using a battery.
+                if (0 < batteryLevel && batteryLevel < cutoffBattery) {
+                    CLog.i("Stopping %s: battery too low (%d%% < %d%%)",
+                            getName(), batteryLevel, cutoffBattery);
+                    stopInvocation(String.format(
+                            "battery too low (%d%% < %d%%)", batteryLevel, cutoffBattery));
+                }
+            }
+        }
     }
 
     /**
@@ -590,7 +656,8 @@
 
             while (!isShutdown()) {
                 // wait until processing is required again
-                mCommandProcessWait.waitAndReset();
+                mCommandProcessWait.waitAndReset(mPollTime);
+                checkInvocations();
                 processReadyCommands(manager);
             }
             mCommandTimer.shutdown();
@@ -617,7 +684,18 @@
         }
     }
 
-    private void processReadyCommands(IDeviceManager manager) {
+    void checkInvocations() {
+        CLog.d("Checking invocations...");
+        final List<InvocationThread> copy;
+        synchronized(this) {
+            copy = new ArrayList<InvocationThread>(mInvocationThreadMap.values());
+        }
+        for (InvocationThread thread : copy) {
+            thread.checkDeviceBatteryLevel();
+        }
+    }
+
+    protected void processReadyCommands(IDeviceManager manager) {
         Map<ExecutableCommand, ITestDevice> scheduledCommandMap = new HashMap<>();
         // minimize length of synchronized block by just matching commands with device first,
         // then scheduling invocations/adding looping commands back to queue
@@ -642,8 +720,8 @@
         for (Map.Entry<ExecutableCommand, ITestDevice> cmdDeviceEntry : scheduledCommandMap
                 .entrySet()) {
             ExecutableCommand cmd = cmdDeviceEntry.getKey();
-            startInvocation(new FreeDeviceHandler(getDeviceManager()), cmdDeviceEntry.getValue(),
-                    cmd);
+            startInvocation(cmdDeviceEntry.getValue(), cmd,
+                    new FreeDeviceHandler(getDeviceManager()));
             if (cmd.isLoopMode()) {
                 addNewExecCommandToQueue(cmd.getCommandTracker());
             }
@@ -710,6 +788,13 @@
             getConfigFactory().printHelpForConfig(args, true, System.out);
         } else if (config.getCommandOptions().isFullHelpMode()) {
             getConfigFactory().printHelpForConfig(args, false, System.out);
+        } else if (config.getCommandOptions().isJsonHelpMode()) {
+            try {
+                // Convert the JSON usage to a string (with 4 space indentation) and print to stdout
+                System.out.println(config.getJsonCommandUsage().toString(4));
+            } catch (JSONException e) {
+                CLog.logAndDisplay(LogLevel.ERROR, "Failed to get json command usage: %s", e);
+            }
         } else if (config.getCommandOptions().isDryRunMode()) {
             config.validateOptions();
             String cmdLine = QuotationAwareTokenizer.combineTokens(args);
@@ -744,8 +829,9 @@
         File cmdFile = new File(cmdFilePath);
         if (mReloadCmdfiles && getCommandFileWatcher().isFileWatched(cmdFile)) {
             CLog.logAndDisplay(LogLevel.INFO,
-                    "cmd file %s is already running and being watched for changes", cmdFilePath);
-            return;
+                    "cmd file %s is already running and being watched for changes. Reloading",
+                    cmdFilePath);
+            removeCommandsFromFile(cmdFile);
         }
         internalAddCommandFile(cmdFile, extraArgs);
     }
@@ -827,7 +913,7 @@
             String commandFilePath) {
         mCurrentCommandId++;
         CLog.d("Creating command tracker id %d for command args: '%s'", mCurrentCommandId,
-                ArrayUtil.join(" ", args));
+                ArrayUtil.join(" ", (Object[])args));
         return new CommandTracker(mCurrentCommandId, args, commandFilePath);
     }
 
@@ -905,6 +991,34 @@
      * {@inheritDoc}
      */
     @Override
+    public void execCommand(IScheduledInvocationListener listener, String[] args)
+            throws ConfigurationException, NoDeviceException {
+        assertStarted();
+        IDeviceManager manager = getDeviceManager();
+        CommandTracker cmdTracker = createCommandTracker(args, null);
+        IConfiguration config = getConfigFactory().createConfigurationFromArgs(
+                cmdTracker.getArgs());
+        config.validateOptions();
+
+        ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
+        ITestDevice device;
+
+        synchronized(this) {
+            device = manager.allocateDevice(config.getDeviceRequirements());
+            if (device == null) {
+                throw new NoDeviceException("no device is available for command: " + args);
+            }
+            CLog.i("Executing '%s' on '%s'", cmdTracker.getArgs()[0], device.getSerialNumber());
+            mExecutingCommands.add(execCmd);
+        }
+
+        startInvocation(device, execCmd, listener, new FreeDeviceHandler(manager));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public void execCommand(IScheduledInvocationListener listener, ITestDevice device, String[] args)
             throws ConfigurationException {
         assertStarted();
@@ -914,19 +1028,24 @@
         config.validateOptions();
         CLog.i("Executing '%s' on '%s'", cmdTracker.getArgs()[0], device.getSerialNumber());
         ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
-        startInvocation(listener, device, execCmd);
+
+        synchronized(this) {
+            mExecutingCommands.add(execCmd);
+        }
+
+        startInvocation(device, execCmd, listener);
     }
 
     /**
      * Spawns off thread to run invocation for given device.
      *
-     * @param callback the {@link IInvocationCompleteHandler} to invoke when complete
      * @param device the {@link ITestDevice}
      * @param cmd the {@link ExecutableCommand} to execute
+     * @param listeners the {@link IScheduledInvocationLister}s to invoke when complete
      * @return the thread that will run the invocation
      */
-    private void startInvocation(IScheduledInvocationListener listener, ITestDevice device,
-            ExecutableCommand cmd) {
+    private void startInvocation(ITestDevice device, ExecutableCommand cmd,
+            IScheduledInvocationListener... listeners) {
         if (hasInvocationThread(device)) {
             throw new IllegalStateException(
                     String.format("Attempting invocation on device %s when one is already running",
@@ -934,8 +1053,8 @@
         }
         CLog.d("starting invocation for command id %d", cmd.getCommandTracker().getId());
         final String invocationName = String.format("Invocation-%s", device.getSerialNumber());
-        InvocationThread invocationThread = new InvocationThread(invocationName, listener, device,
-                cmd);
+        InvocationThread invocationThread = new InvocationThread(invocationName, device, cmd,
+                listeners);
         invocationThread.start();
         addInvocationThread(invocationThread);
     }
@@ -958,11 +1077,11 @@
         mInvocationThreadMap.put(invThread.getDevice(), invThread);
     }
 
-    private synchronized boolean isShutdown() {
+    protected synchronized boolean isShutdown() {
         return mCommandTimer.isShutdown() || (mShutdownOnEmpty && getAllCommandsSize() == 0);
     }
 
-    private synchronized boolean isShuttingDown() {
+    protected synchronized boolean isShuttingDown() {
         return mCommandTimer.isShutdown() || mShutdownOnEmpty;
     }
 
@@ -1170,7 +1289,11 @@
     @Override
     public synchronized void shutdownHard() {
         shutdown();
-        CLog.logAndDisplay(LogLevel.WARN, "Force killing adb connection");
+
+        CLog.logAndDisplay(LogLevel.WARN, "Stopping invocation threads...");
+        for (InvocationThread thread : mInvocationThreadMap.values()) {
+            thread.stopInvocation("TF is shutting down");
+        }
         getDeviceManager().terminateHard();
     }
 
@@ -1235,22 +1358,89 @@
      * {@inheritDoc}
      */
     @Override
-    public boolean stopInvocation(ITestInvocation invocation) throws UnsupportedOperationException {
-        throw new UnsupportedOperationException();
+    public synchronized boolean stopInvocation(ITestInvocation invocation) {
+        for (InvocationThread thread : mInvocationThreadMap.values()) {
+            if (thread.getInvocation() == invocation) {
+                thread.interrupt();
+                return true;
+            }
+        }
+        return false;
     }
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public void displayCommandsInfo(PrintWriter printWriter) {
+    public void displayCommandsInfo(PrintWriter printWriter, String regex) {
         assertStarted();
+        Pattern regexPattern = null;
+        if (regex != null) {
+            regexPattern = Pattern.compile(regex);
+        }
+
         List<CommandTracker> cmds = getCommandTrackers();
         Collections.sort(cmds, new CommandTrackerIdComparator());
         for (CommandTracker cmd : cmds) {
-            String cmdDesc = String.format("Command %d: [%s] %s", cmd.getId(),
-                    getTimeString(cmd.getTotalExecTime()), getArgString(cmd.getArgs()));
-            printWriter.println(cmdDesc);
+            String argString = getArgString(cmd.getArgs());
+            if (regexPattern == null || regexPattern.matcher(argString).find()) {
+                String cmdDesc = String.format("Command %d: [%s] %s", cmd.getId(),
+                        getTimeString(cmd.getTotalExecTime()), argString);
+                printWriter.println(cmdDesc);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dumpCommandsXml(PrintWriter printWriter, String regex) {
+        assertStarted();
+        Pattern regexPattern = null;
+        if (regex != null) {
+            regexPattern = Pattern.compile(regex);
+        }
+
+        List<ExecutableCommandState> cmdCopy = getAllCommands();
+        for (ExecutableCommandState cmd : cmdCopy) {
+            String[] args = cmd.cmd.getCommandTracker().getArgs();
+            String argString = getArgString(args);
+            if (regexPattern == null || regexPattern.matcher(argString).find()) {
+                // Use the config name prefixed by config__ for the file path
+                String xmlPrefix = "config__" + args[0].replace("/", "__") + "__";
+
+                // If the command line contains --template:map test config, use that config for the
+                // file path.  This is because in the template system, many tests will have same
+                // base config and the distinguishing feature is the test included.
+                boolean templateIncludeFound = false;
+                boolean testFound = false;
+                for (String arg : args) {
+                    if ("--template:map".equals(arg)) {
+                        templateIncludeFound = true;
+                    } else if (templateIncludeFound && "test".equals(arg)) {
+                        testFound = true;
+                    } else {
+                        if (templateIncludeFound && testFound) {
+                            xmlPrefix = "config__" + arg.replace("/", "__") + "__";
+                        }
+                        templateIncludeFound = false;
+                        testFound = false;
+                    }
+                }
+
+                try {
+                    File xmlFile = FileUtil.createTempFile(xmlPrefix, ".xml");
+                    PrintWriter writer = new PrintWriter(xmlFile);
+                    cmd.cmd.getConfiguration().dumpXml(writer);
+                    printWriter.println(String.format("Saved command dump to %s",
+                            xmlFile.getAbsolutePath()));
+                } catch (IOException e) {
+                    // Log exception and continue
+                    CLog.e("Could not dump config xml");
+                    CLog.e(e);
+                }
+            }
         }
     }
 
@@ -1361,6 +1551,9 @@
          * @return true if event received before time elapsed, false otherwise
          */
         public synchronized boolean waitForEvent(long maxWaitTime) {
+            if (maxWaitTime == 0) {
+                return waitForEvent();
+            }
             long startTime = System.currentTimeMillis();
             long remainingTime = maxWaitTime;
             while (!mEventReceived && remainingTime > 0) {
@@ -1397,11 +1590,11 @@
         }
 
         /**
-         * Wait indefinitely for event to be received, and reset state back to 'no event received'
+         * Wait for given ms for event to be received, and reset state back to 'no event received'
          * upon completion.
          */
-        public synchronized void waitAndReset() {
-            waitForEvent();
+        public synchronized void waitAndReset(long maxWaitTime) {
+            waitForEvent(maxWaitTime);
             reset();
         }
 
diff --git a/src/com/android/tradefed/command/Console.java b/src/com/android/tradefed/command/Console.java
index cf723a8..4f7c5de 100644
--- a/src/com/android/tradefed/command/Console.java
+++ b/src/com/android/tradefed/command/Console.java
@@ -45,6 +45,7 @@
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.regex.Pattern;
 
 /**
@@ -73,6 +74,7 @@
     protected static final String VERSION_PATTERN = "version";
     protected static final String REMOVE_PATTERN = "remove";
     protected static final String DEBUG_PATTERN = "debug";
+    protected static final String LIST_COMMANDS_PATTERN = "c(?:ommands)?";
 
     protected static final String LINE_SEPARATOR = System.getProperty("line.separator");
 
@@ -401,30 +403,39 @@
 
         commandHelp.put(LIST_PATTERN, String.format(
                 "%s help:" + LINE_SEPARATOR +
-                "\ti[nvocations]  List all invocation threads" + LINE_SEPARATOR +
-                "\td[evices]      List all detected or known devices" + LINE_SEPARATOR +
-                "\tc[ommands]     List all commands currently waiting to be executed" +
+                "\ti[nvocations]         List all invocation threads" + LINE_SEPARATOR +
+                "\td[evices]             List all detected or known devices" + LINE_SEPARATOR +
+                "\tc[ommands]            List all commands currently waiting to be executed" +
                 LINE_SEPARATOR +
-                "\tconfigs        List all known configurations" +
-                LINE_SEPARATOR, LIST_PATTERN));
+                "\tc[ommands] [pattern]  List all commands matching the pattern and currently " +
+                "waiting to be executed" + LINE_SEPARATOR +
+                "\tconfigs               List all known configurations" + LINE_SEPARATOR,
+                LIST_PATTERN));
 
         commandHelp.put(DUMP_PATTERN, String.format(
                 "%s help:" + LINE_SEPARATOR +
-                "\ts[tack]            Dump the stack traces of all threads" + LINE_SEPARATOR +
-                "\tl[ogs]             Dump the logs of all invocations to files" + LINE_SEPARATOR +
-                "\tc[onfig] <config>  Dump the content of the specified config" + LINE_SEPARATOR +
-                "\tcommandQueue       Dump the contents of the commmand execution queue" +
-                LINE_SEPARATOR,
+                "\ts[tack]             Dump the stack traces of all threads" + LINE_SEPARATOR +
+                "\tl[ogs]              Dump the logs of all invocations to files" + LINE_SEPARATOR +
+                "\tc[onfig] <config>   Dump the content of the specified config" + LINE_SEPARATOR +
+                "\tcommandQueue        Dump the contents of the commmand execution queue" +
+                LINE_SEPARATOR +
+                "\tcommands            Dump all the config XML for the commands waiting to be " +
+                "executed" + LINE_SEPARATOR +
+                "\tcommands [pattern]  Dump all the config XML for the commands matching the " +
+                "pattern and waiting to be executed" + LINE_SEPARATOR +
+                "\te[nv]               Dump the environment variables available to test harness " +
+                "process" + LINE_SEPARATOR,
                 DUMP_PATTERN));
 
         commandHelp.put(RUN_PATTERN, String.format(
                 "%s help:" + LINE_SEPARATOR +
-                "\tcommand <config>  [options]       Run the specified command" + LINE_SEPARATOR +
-                "\t<config> [options]                Shortcut for the above: run specified command" +
-                    LINE_SEPARATOR +
-                "\tcmdfile <cmdfile.txt>             Run the specified commandfile" + LINE_SEPARATOR +
+                "\tcommand <config> [options]        Run the specified command" + LINE_SEPARATOR +
+                "\t<config> [options]                Shortcut for the above: run specified " +
+                "command" + LINE_SEPARATOR +
+                "\tcmdfile <cmdfile.txt>             Run the specified commandfile" +
+                LINE_SEPARATOR +
                 "\tcommandAndExit <config> [options] Run the specified command, and run " +
-                "'exit -c' immediately afterward" + LINE_SEPARATOR,
+                "'exit -c' immediately afterward" + LINE_SEPARATOR +
                 "\tcmdfileAndExit <cmdfile.txt>      Run the specified commandfile, and run " +
                 "'exit -c' immediately afterward" + LINE_SEPARATOR,
                 RUN_PATTERN));
@@ -437,8 +448,8 @@
 
         commandHelp.put(REMOVE_PATTERN, String.format(
                 "%s help:" + LINE_SEPARATOR +
-                "\tremove allCommands  Remove all commands currently waiting to be executed"
-                        + LINE_SEPARATOR,
+                "\tremove allCommands  Remove all commands currently waiting to be executed" +
+                LINE_SEPARATOR,
                 REMOVE_PATTERN));
 
         commandHelp.put(DEBUG_PATTERN, String.format(
@@ -469,9 +480,18 @@
         trie.put(new Runnable() {
                     @Override
                     public void run() {
-                        mScheduler.displayCommandsInfo(new PrintWriter(System.out, true));
+                        mScheduler.displayCommandsInfo(new PrintWriter(System.out, true), null);
                     }
-                }, LIST_PATTERN, "c(?:ommands)?");
+                }, LIST_PATTERN, LIST_COMMANDS_PATTERN);
+        ArgRunnable<CaptureList> listCmdRun = new ArgRunnable<CaptureList>() {
+            @Override
+            public void run(CaptureList args) {
+                // Skip 2 tokens to get past listPattern and "commands"
+                String pattern = args.get(2).get(0);
+                mScheduler.displayCommandsInfo(new PrintWriter(System.out, true), pattern);
+            }
+        };
+        trie.put(listCmdRun, LIST_PATTERN, LIST_COMMANDS_PATTERN, "(.*)");
         trie.put(new Runnable() {
             @Override
             public void run() {
@@ -510,6 +530,29 @@
             }
         }, DUMP_PATTERN, "commandQueue");
 
+        trie.put(new Runnable() {
+            @Override
+            public void run() {
+                mScheduler.dumpCommandsXml(new PrintWriter(System.out, true), null);
+            }
+        }, DUMP_PATTERN, LIST_COMMANDS_PATTERN);
+        ArgRunnable<CaptureList> dumpCmdRun = new ArgRunnable<CaptureList>() {
+            @Override
+            public void run(CaptureList args) {
+                // Skip 2 tokens to get past listPattern and "commands"
+                String pattern = args.get(2).get(0);
+                mScheduler.dumpCommandsXml(new PrintWriter(System.out, true), pattern);
+            }
+        };
+        trie.put(dumpCmdRun, DUMP_PATTERN, LIST_COMMANDS_PATTERN, "(.*)");
+
+        trie.put(new Runnable() {
+            @Override
+            public void run() {
+                dumpEnv();
+            }
+        }, DUMP_PATTERN, "e(?:nv)?");
+
         // Run commands
         ArgRunnable<CaptureList> runRunCommand = new ArgRunnable<CaptureList>() {
             @Override
@@ -873,6 +916,17 @@
     }
 
     /**
+     * Dumps the environment variables to console, sorted by variable names
+     */
+    private void dumpEnv() {
+        // use TreeMap to sort variables by name
+        Map<String, String> env = new TreeMap<>(System.getenv());
+        for (Map.Entry<String, String> entry : env.entrySet()) {
+            printLine(String.format("\t%s=%s", entry.getKey(), entry.getValue()));
+        }
+    }
+
+    /**
      * Sets the console starting arguments.
      *
      * @param mainArgs the arguments
diff --git a/src/com/android/tradefed/command/ICommandOptions.java b/src/com/android/tradefed/command/ICommandOptions.java
index 38618dc..a4a5b56 100644
--- a/src/com/android/tradefed/command/ICommandOptions.java
+++ b/src/com/android/tradefed/command/ICommandOptions.java
@@ -32,6 +32,11 @@
     public boolean isFullHelpMode();
 
     /**
+     * Returns <code>true</code> if full json help mode has been requested
+     */
+    public boolean isJsonHelpMode();
+
+    /**
      * Return <code>true</code> if we should <emph>skip</emph> adding this command to the queue.
      */
     public boolean isDryRunMode();
diff --git a/src/com/android/tradefed/command/ICommandScheduler.java b/src/com/android/tradefed/command/ICommandScheduler.java
index b8aa37c..beaf4ee 100644
--- a/src/com/android/tradefed/command/ICommandScheduler.java
+++ b/src/com/android/tradefed/command/ICommandScheduler.java
@@ -20,6 +20,7 @@
 import com.android.tradefed.config.IConfigurationFactory;
 import com.android.tradefed.device.FreeDeviceState;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.NoDeviceException;
 import com.android.tradefed.invoker.ITestInvocation;
 import com.android.tradefed.result.ITestInvocationListener;
 
@@ -89,6 +90,18 @@
     public boolean addCommand(String[] args, long totalExecTime) throws ConfigurationException;
 
     /**
+     * Directly allocates a device and executes a command without adding it to the command queue.
+     *
+     * @param listener the {@link IScheduledInvocationListener} to be informed
+     * @param args the command arguments
+     *
+     * @throws ConfigurationException if command was invalid
+     * @throws NoDeviceException if there is no device to use
+     */
+    public void execCommand(IScheduledInvocationListener listener, String[] args)
+            throws ConfigurationException, NoDeviceException;
+
+    /**
      * Directly execute command on already allocated device.
      *
      * @param listener the {@link IScheduledInvocationListener} to be informed
@@ -198,8 +211,20 @@
      * Output a list of current commands.
      *
      * @param printWriter the {@link PrintWriter} to output to.
+     * @param regex the regular expression to which commands should be matched in order to be
+     * printed.  If null, then all commands will be printed.
      */
-    public void displayCommandsInfo(PrintWriter printWriter);
+    public void displayCommandsInfo(PrintWriter printWriter, String regex);
+
+    /**
+     * Dump the expanded xml file for the command with all
+     * {@link com.android.tradefed.config.Option} values specified for all current commands.
+     *
+     * @param printWriter the {@link PrintWriter} to output the status to.
+     * @param regex the regular expression to which commands should be matched in order for the
+     * xml file to be dumped.  If null, then all commands will be dumped.
+     */
+    public void dumpCommandsXml(PrintWriter printWriter, String regex);
 
     /**
      * Output detailed debug info on state of command execution queue.
diff --git a/src/com/android/tradefed/command/Verify.java b/src/com/android/tradefed/command/Verify.java
new file mode 100644
index 0000000..a87861c
--- /dev/null
+++ b/src/com/android/tradefed/command/Verify.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2015 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.command;
+
+import com.android.tradefed.config.ArgsOptionParser;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Alternate Trade Federation entrypoint to validate command files
+ */
+public class Verify {
+
+    private static final int EXIT_STATUS_OKAY = 0x0;
+    private static final int EXIT_STATUS_FAILED = 0x1;
+
+    @Option(name = "cmdfile", description = "command file to verify")
+    private List<File> mCmdFiles = new ArrayList<>();
+
+    @Option(name = "show-commands", description = "Whether to print all generated commands")
+    private boolean mShowCommands = false;
+
+    @Option(name = "quiet", shortName = 'q', description = "Whether to silence all output. " +
+            "Overrides all other output-related settings.")
+    private boolean mQuiet = false;
+
+    @Option(name = "help", shortName = 'h', description = "Print help")
+    private boolean mHelp = false;
+
+    /**
+     * Returns whether the "--help"/"-h" option was passed to the instance
+     */
+    public boolean isHelpMode() {
+        return mHelp;
+    }
+
+    /**
+     * Program main entrypoint
+     */
+    public static void main(final String[] mainArgs) throws InterruptedException,
+            ConfigurationException {
+        try {
+            Verify verify = new Verify();
+            ArgsOptionParser optionSetter = new ArgsOptionParser(verify);
+            optionSetter.parse(mainArgs);
+            if (verify.isHelpMode()) {
+                // Print help, then exit
+                System.err.println(ArgsOptionParser.getOptionHelp(false, verify));
+                System.exit(EXIT_STATUS_OKAY);
+            }
+
+            if (verify.run()) {
+                // true == everything's good
+                System.exit(EXIT_STATUS_OKAY);
+            } else {
+                // false == whoopsie!
+                System.exit(EXIT_STATUS_FAILED);
+            }
+
+        } finally {
+            System.err.flush();
+            System.out.flush();
+        }
+    }
+
+    /**
+     * Start validating all specified cmdfiles
+     */
+    public boolean run() {
+        boolean anyFailures = false;
+
+        for (File cmdFile : mCmdFiles) {
+            try {
+                // if verify returns false, then we set anyFailures to true
+                anyFailures |= !runVerify(cmdFile);
+
+            } catch (Throwable t) {
+                if (!mQuiet) {
+                    System.err.format("Caught exception while parsing \"%s\"\n", cmdFile);
+                    System.err.println(t);
+                }
+                anyFailures = true;
+            }
+        }
+
+        return !anyFailures;
+    }
+
+    /**
+     * Validate the specified cmdfile
+     */
+    public boolean runVerify(File cmdFile) {
+        final CommandFileParser parser = new CommandFileParser();
+        try {
+            List<CommandFileParser.CommandLine> commands = parser.parseFile(cmdFile);
+            if (!mQuiet) {
+                System.out.format("Successfully parsed %d commands from cmdfile %s\n",
+                        commands.size(), cmdFile);
+
+                if (mShowCommands) {
+                    int i = 1;
+                    int digits = (int) Math.ceil(Math.log10(commands.size()));
+                    // Create a format string that will leave enough space for an index prefix
+                    // without mucking up alignment
+                    String format = String.format("%%%dd: %%s\n", digits);
+                    for (CommandFileParser.CommandLine cmd : commands) {
+                        System.out.format(format, i++, cmd);
+                    }
+                }
+                System.out.println();
+            }
+        } catch (ConfigurationException | IOException e) {
+            if (!mQuiet) {
+                System.err.format("Failed to parse %s:\n", cmdFile);
+                System.err.println(e);
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/src/com/android/tradefed/config/ArgsOptionParser.java b/src/com/android/tradefed/config/ArgsOptionParser.java
index dc0d581..12d1efb 100644
--- a/src/com/android/tradefed/config/ArgsOptionParser.java
+++ b/src/com/android/tradefed/config/ArgsOptionParser.java
@@ -175,7 +175,71 @@
      * @throws ConfigurationException if error occurred parsing the arguments.
      */
     public List<String> parse(List<String> args) throws ConfigurationException {
-        return parseOptions(args.listIterator());
+        final List<String> leftovers = new ArrayList<String>();
+        final ListIterator<String> argsIter = args.listIterator();
+
+        // Scan 'args'.
+        while (argsIter.hasNext() && parseArg(argsIter.next(), argsIter, leftovers)) {
+            // This loop has no body.  All of the work happens by side-effect in parseArg(...)
+        }
+
+        // Package up the leftovers.
+        while (argsIter.hasNext()) {
+            leftovers.add(argsIter.next());
+        }
+        return leftovers;
+    }
+
+    /**
+     * A best-effort version of {@link #parse(String... args)}.  If a ConfigurationException is
+     * thrown, that exception is captured internally, and the remaining arguments (including the
+     * argument which caused the exception to be thrown) are returned.  This method does not throw.
+     *
+     * @return a {@link List} of the left over arguments
+     */
+    public List<String> parseBestEffort(String... args) {
+        return parseBestEffort(Arrays.asList(args));
+    }
+
+    /**
+     * Alternate {@link #parseBestEffort(String... args)} method that takes a {@link List} of
+     * arguments
+     *
+     * @return a {@link List} of the left over arguments
+     */
+    public List<String> parseBestEffort(List<String> args) {
+        final List<String> leftovers = new ArrayList<String>(args.size());
+        final ListIterator<String> argsIter = args.listIterator();
+        int lastProcessedIdx = -1;
+
+        /* (not javadoc)
+         * Scan 'args'.  Note that each call to parseArg(...) will advance argsIter a number
+         * of places that depends on the arity of the particular Option being processed.  For a
+         * boolean Option, it will not advance.  For a standard unary Option, it will advance
+         * 1 place.  For a Map Option, it will advance two places (1 for key, 1 for value).
+         *
+         * For this reason, we grab the index from the iterator itself, rather than trying to
+         * increment it ourselves.
+         */
+        while (argsIter.hasNext()) {
+            lastProcessedIdx = argsIter.nextIndex();
+            final String arg = argsIter.next();
+            try {
+                // All of the work happens within parseArg(...) by side-effect
+                if (!parseArg(arg, argsIter, leftovers)) break;
+            } catch (ConfigurationException e) {
+                // Something failed.  Add all of the not-fully-processed and not-yet-processed args
+                // to leftovers and return
+                leftovers.addAll(args.subList(lastProcessedIdx, args.size()));
+                return leftovers;
+            }
+        }
+
+        // Package up the leftovers.
+        while (argsIter.hasNext()) {
+            leftovers.add(argsIter.next());
+        }
+        return leftovers;
     }
 
     /**
@@ -191,33 +255,32 @@
         }
     }
 
-    private List<String> parseOptions(ListIterator<String> args) throws ConfigurationException {
-        final List<String> leftovers = new ArrayList<String>();
+    /**
+     * Attempts to parse the specified argument.
+     *
+     * @return {@code true} if iteration should continue, or {@code false} if iteration should stop
+     */
+    private boolean parseArg(String arg, ListIterator<String> args, List<String> leftovers)
+            throws ConfigurationException {
+        if (arg.equals(OPTION_NAME_PREFIX)) {
+            // "--" marks the end of options and the beginning of positional arguments.
+            return false;
 
-        // Scan 'args'.
-        while (args.hasNext()) {
-            final String arg = args.next();
-            if (arg.equals(OPTION_NAME_PREFIX)) {
-                // "--" marks the end of options and the beginning of positional arguments.
-                break;
-            } else if (arg.startsWith(OPTION_NAME_PREFIX)) {
-                // A long option.
-                parseLongOption(arg, args);
-            } else if (arg.startsWith(SHORT_NAME_PREFIX)) {
-                // A short option.
-                parseGroupedShortOptions(arg, args);
-            } else {
-                // The first non-option marks the end of options.
-                leftovers.add(arg);
-                break;
-            }
-        }
+        } else if (arg.startsWith(OPTION_NAME_PREFIX)) {
+            // A long option.
+            parseLongOption(arg, args);
+            return true;
 
-        // Package up the leftovers.
-        while (args.hasNext()) {
-            leftovers.add(args.next());
+        } else if (arg.startsWith(SHORT_NAME_PREFIX)) {
+            // A short option.
+            parseGroupedShortOptions(arg, args);
+            return true;
+
+        } else {
+            // The first non-option marks the end of options.
+            leftovers.add(arg);
+            return false;
         }
-        return leftovers;
     }
 
     private void parseLongOption(String arg, ListIterator<String> args)
diff --git a/src/com/android/tradefed/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index 166e90a..85587fb 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -34,14 +34,25 @@
 import com.android.tradefed.targetprep.StubTargetPreparer;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.StubTest;
+import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.QuotationAwareTokenizer;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.kxml2.io.KXmlSerializer;
+
+import java.io.IOException;
 import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * A concrete {@link IConfiguration} implementation that stores the loaded config objects in a map
@@ -60,12 +71,25 @@
     public static final String DEVICE_REQUIREMENTS_TYPE_NAME = "device_requirements";
     public static final String DEVICE_OPTIONS_TYPE_NAME = "device_options";
 
+    // additional element names used for emitting the configuration XML.
+    private static final String CONFIGURATION_NAME = "configuration";
+    private static final String OPTION_NAME = "option";
+    private static final String CLASS_NAME = "class";
+    private static final String NAME_NAME = "name";
+    private static final String KEY_NAME = "key";
+    private static final String VALUE_NAME = "value";
+
     private static Map<String, ObjTypeInfo> sObjTypeMap = null;
 
     /** Mapping of config object type name to config objects. */
     private Map<String, List<Object>> mConfigMap;
+    /** Cached {@link OptionSetter} that must be kept in-sync with {@code mConfigMap} */
+    private OptionSetter mCachedOptionSetter = null;
     private final String mName;
     private final String mDescription;
+    // original command line used to create this given configuration.
+    private String[] mCommandLine;
+
 
     /**
      * Container struct for built-in config object type
@@ -145,6 +169,28 @@
         return mDescription;
     }
 
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setCommandLine(String[] arrayArgs) {
+        mCommandLine = arrayArgs;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getCommandLine() {
+        //FIXME: obfuscated passwords from command line.
+        if (mCommandLine != null && mCommandLine.length != 0) {
+            return QuotationAwareTokenizer.combineTokens(mCommandLine);
+        }
+        // If no args were available return null.
+        return null;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -271,13 +317,28 @@
     }
 
     /**
+     * Returns a cached OptionSetter which is appropriate for setting options on all objects which
+     * will be returned by {@link getAllConfigurationObjects()}.  Note that, for cache coherency,
+     * the cache variable {@code mCachedOptionSetter} <emph>must</emph> be set to {@code null}
+     * anytime that {@code mConfigMap} is modified.
+     * <p />
+     * To improve thread-safety, all modifications of {@code mConfigMap} should be done atomically
+     * with an invalidation of {@code mCachedOptionSetter}, from the perspective of this method.
+     */
+    private synchronized OptionSetter getOptionSetter() throws ConfigurationException {
+        if (mCachedOptionSetter == null) {
+            mCachedOptionSetter = new OptionSetter(getAllConfigurationObjects());
+        }
+        return mCachedOptionSetter;
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
     public void injectOptionValue(String optionName, String optionValue)
             throws ConfigurationException {
-        OptionSetter optionSetter = new OptionSetter(getAllConfigurationObjects());
-        optionSetter.setOptionValue(optionName, optionValue);
+        getOptionSetter().setOptionValue(optionName, optionValue);
     }
 
     /**
@@ -286,8 +347,7 @@
     @Override
     public void injectOptionValue(String optionName, String optionKey, String optionValue)
             throws ConfigurationException {
-        OptionSetter optionSetter = new OptionSetter(getAllConfigurationObjects());
-        optionSetter.setOptionMapValue(optionName, optionKey, optionValue);
+        getOptionSetter().setOptionMapValue(optionName, optionKey, optionValue);
     }
 
     /**
@@ -402,11 +462,12 @@
      * {@inheritDoc}
      */
     @Override
-    public void setConfigurationObject(String typeName, Object configObject)
+    public synchronized void setConfigurationObject(String typeName, Object configObject)
             throws ConfigurationException {
         if (configObject == null) {
             throw new IllegalArgumentException("configObject cannot be null");
         }
+        mCachedOptionSetter = null;  // Keep this in sync with mConfigMap
         mConfigMap.remove(typeName);
         addObject(typeName, configObject);
     }
@@ -415,11 +476,12 @@
      * {@inheritDoc}
      */
     @Override
-    public void setConfigurationObjectList(String typeName, List<?> configList)
+    public synchronized void setConfigurationObjectList(String typeName, List<?> configList)
             throws ConfigurationException {
         if (configList == null) {
             throw new IllegalArgumentException("configList cannot be null");
         }
+        mCachedOptionSetter = null;  // Keep this in sync with mConfigMap
         mConfigMap.remove(typeName);
         for (Object configObject : configList) {
             addObject(typeName, configObject);
@@ -433,7 +495,9 @@
      * @param configObject the configuration object
      * @throws ConfigurationException if object was not the correct type
      */
-    private void addObject(String typeName, Object configObject) throws ConfigurationException {
+    private synchronized void addObject(String typeName, Object configObject) throws ConfigurationException {
+        mCachedOptionSetter = null;  // Keep this in sync with mConfigMap
+
         List<Object> objList = mConfigMap.get(typeName);
         if (objList == null) {
             objList = new ArrayList<Object>(1);
@@ -499,13 +563,10 @@
      * {@inheritDoc}
      */
     @Override
-    public void setOptionsFromCommandLineArgs(List<String> listArgs) throws ConfigurationException {
+    public List<String> setOptionsFromCommandLineArgs(List<String> listArgs)
+            throws ConfigurationException {
         ArgsOptionParser parser = new ArgsOptionParser(getAllConfigurationObjects());
-        List<String> unprocessedArgs = parser.parse(listArgs);
-        if (unprocessedArgs.size() > 0) {
-            throw new ConfigurationException(String.format(
-                    "Invalid arguments provided. Unprocessed arguments: %s", unprocessedArgs));
-        }
+        return parser.parse(listArgs);
     }
 
     /**
@@ -546,6 +607,66 @@
     }
 
     /**
+     * {@inheritDoc}
+     */
+    @Override
+    public JSONArray getJsonCommandUsage() throws JSONException {
+        JSONArray ret = new JSONArray();
+        for (Map.Entry<String, List<Object>> configObjectsEntry : mConfigMap.entrySet()) {
+            for (Object optionObject : configObjectsEntry.getValue()) {
+
+                // Build a JSON representation of the current class
+                JSONObject jsonClass = new JSONObject();
+                jsonClass.put("name", configObjectsEntry.getKey());
+                if (optionObject.getClass().isAnnotationPresent(OptionClass.class)) {
+                    OptionClass optionClass =
+                            optionObject.getClass().getAnnotation(OptionClass.class);
+                    jsonClass.put("alias", optionClass.alias());
+                }
+                jsonClass.put("class", optionObject.getClass().getName());
+
+                // For each of the @Option annotated fields
+                Collection<Field> optionFields =
+                        OptionSetter.getOptionFieldsForClass(optionObject.getClass());
+                JSONArray jsonFields = new JSONArray();
+                for (Field field : optionFields) {
+                    Option option = field.getAnnotation(Option.class);
+
+                    // Build a JSON representation of the current field
+                    JSONObject jsonField = new JSONObject();
+
+                    jsonField.put("name", option.name());
+                    if (option.shortName() != Option.NO_SHORT_NAME) {
+                        jsonField.put("shortName", option.shortName());
+                    }
+                    jsonField.put("description", option.description());
+                    jsonField.put("importance", option.importance());
+                    jsonField.put("mandatory", option.mandatory());
+                    jsonField.put("isTimeVal", option.isTimeVal());
+                    jsonField.put("javaClass", field.getType().getName());
+                    try {
+                        field.setAccessible(true);
+                        Object value = field.get(optionObject);
+                        jsonField.put("defaultValue", value == null ? "null" : value.toString());
+                    } catch (IllegalAccessException e) {
+                        // Shouldn't happen
+                        throw new RuntimeException(e);
+                    }
+
+                    // Add the JSON field representation to the JSON class representation
+                    jsonFields.put(jsonField);
+                }
+                jsonClass.put("fields", jsonFields);
+
+                // Add the JSON class representation to the list
+                ret.put(jsonClass);
+            }
+        }
+
+        return ret;
+    }
+
+    /**
      * Prints out the available config options for given configuration object.
      *
      * @param importantOnly print only the important options
@@ -567,4 +688,96 @@
     public void validateOptions() throws ConfigurationException {
         new ArgsOptionParser(getAllConfigurationObjects()).validateMandatoryOptions();
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dumpXml(PrintWriter output) throws IOException {
+        KXmlSerializer serializer = new KXmlSerializer();
+        serializer.setOutput(output);
+        serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+        serializer.startDocument("UTF-8", null);
+        serializer.startTag(null, CONFIGURATION_NAME);
+
+        dumpClassToXml(serializer, BUILD_PROVIDER_TYPE_NAME, getBuildProvider());
+        for (ITargetPreparer preparer : getTargetPreparers()) {
+            dumpClassToXml(serializer, TARGET_PREPARER_TYPE_NAME, preparer);
+        }
+        for (IRemoteTest test : getTests()) {
+            dumpClassToXml(serializer, TEST_TYPE_NAME, test);
+        }
+        dumpClassToXml(serializer, DEVICE_RECOVERY_TYPE_NAME, getDeviceRecovery());
+        dumpClassToXml(serializer, LOGGER_TYPE_NAME, getLogOutput());
+        dumpClassToXml(serializer, LOG_SAVER_TYPE_NAME, getLogSaver());
+        for (ITestInvocationListener listener : getTestInvocationListeners()) {
+            dumpClassToXml(serializer, RESULT_REPORTER_TYPE_NAME, listener);
+        }
+        dumpClassToXml(serializer, CMD_OPTIONS_TYPE_NAME, getCommandOptions());
+        dumpClassToXml(serializer, DEVICE_REQUIREMENTS_TYPE_NAME, getDeviceRequirements());
+        dumpClassToXml(serializer, DEVICE_OPTIONS_TYPE_NAME, getDeviceOptions());
+
+        serializer.endTag(null, CONFIGURATION_NAME);
+        serializer.endDocument();
+    }
+
+    /**
+     * Add a class to the command XML dump.
+     */
+    private void dumpClassToXml(KXmlSerializer serializer, String classTypeName, Object obj)
+            throws IOException {
+        serializer.startTag(null, classTypeName);
+        serializer.attribute(null, CLASS_NAME, obj.getClass().getName());
+        dumpOptionsToXml(serializer, obj);
+        serializer.endTag(null, classTypeName);
+    }
+
+    /**
+     * Add all the options of class to the command XML dump.
+     */
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private void dumpOptionsToXml(KXmlSerializer serializer, Object obj) throws IOException {
+        for (Field field : OptionSetter.getOptionFieldsForClass(obj.getClass())) {
+            Option option = field.getAnnotation(Option.class);
+            Object fieldVal = OptionSetter.getFieldValue(field, obj);
+            if (fieldVal == null) {
+                continue;
+            } else if (fieldVal instanceof Collection) {
+                for (Object entry : (Collection) fieldVal) {
+                    dumpOptionToXml(serializer, option.name(), null, entry.toString());
+                }
+            } else if (fieldVal instanceof Map) {
+                Map map = (Map) fieldVal;
+                for (Object entryObj : map.entrySet()) {
+                    Map.Entry entry = (Entry) entryObj;
+                    dumpOptionToXml(serializer, option.name(), entry.getKey().toString(),
+                            entry.getValue().toString());
+                }
+            } else if (fieldVal instanceof MultiMap) {
+                MultiMap multimap = (MultiMap) fieldVal;
+                for (Object keyObj : multimap.keySet()) {
+                    for (Object valueObj : multimap.get(keyObj)) {
+                        dumpOptionToXml(serializer, option.name(), keyObj.toString(),
+                                valueObj.toString());
+                    }
+                }
+            } else {
+                dumpOptionToXml(serializer, option.name(), null, fieldVal.toString());
+            }
+        }
+    }
+
+    /**
+     * Add a single option to the command XML dump.
+     */
+    private void dumpOptionToXml(KXmlSerializer serializer, String name, String key, String value)
+            throws IOException {
+        serializer.startTag(null, OPTION_NAME);
+        serializer.attribute(null, NAME_NAME, name);
+        if (key != null) {
+            serializer.attribute(null, KEY_NAME, key);
+        }
+        serializer.attribute(null, VALUE_NAME, value);
+        serializer.endTag(null, OPTION_NAME);
+    }
 }
diff --git a/src/com/android/tradefed/config/ConfigurationFactory.java b/src/com/android/tradefed/config/ConfigurationFactory.java
index 12532c8..811ee82 100644
--- a/src/com/android/tradefed/config/ConfigurationFactory.java
+++ b/src/com/android/tradefed/config/ConfigurationFactory.java
@@ -23,6 +23,7 @@
 
 import java.io.BufferedInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -31,6 +32,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.List;
@@ -48,8 +50,69 @@
     private static IConfigurationFactory sInstance = null;
     private static final String CONFIG_SUFFIX = ".xml";
     private static final String CONFIG_PREFIX = "config/";
+    private static final String CONFIG_SPLIT = "|";
 
-    private Map<String, ConfigurationDef> mConfigDefMap;
+    private Map<ConfigId, ConfigurationDef> mConfigDefMap;
+
+    /**
+     * A simple struct-like class that stores a configuration's name alongside the arguments for
+     * any {@code <template-include>} tags it may contain.  Because the actual bits stored by the
+     * configuration may vary with template arguments, they must be considered as essential a part
+     * of the configuration's identity as the filename.
+     */
+    static class ConfigId {
+        public String name = null;
+        public Map<String, String> templateMap = new HashMap<>();
+
+        /**
+         * No-op constructor
+         */
+        public ConfigId() {}
+
+        /**
+         * Convenience constructor.  Equivalent to calling two-arg constructor with {@code null}
+         * {@code templateMap}.
+         */
+        public ConfigId(String name) {
+            this(name, null);
+        }
+
+        /**
+         * Two-arg convenience constructor.  {@code templateMap} may be null.
+         */
+        public ConfigId(String name, Map<String, String> templateMap) {
+            this.name = name;
+            if (templateMap != null) {
+                this.templateMap.putAll(templateMap);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int hashCode() {
+            return 2 * ((name == null) ? 0 : name.hashCode()) + 3 * templateMap.hashCode();
+        }
+
+        private boolean matches(Object a, Object b) {
+            if (a == null && b == null) return true;
+            if (a == null || b == null) return false;
+            return a.equals(b);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean equals(Object other) {
+            if (other == null) return false;
+            if (!(other instanceof ConfigId)) return false;
+
+            final ConfigId otherConf = (ConfigId) other;
+            return matches(name, otherConf.name) && matches(templateMap, otherConf.templateMap);
+        }
+    }
 
     /**
      * A {@link IClassPathFilter} for configuration XML files.
@@ -62,8 +125,9 @@
         @Override
         public boolean accept(String pathName) {
             // only accept entries that match the pattern, and that we don't already know about
+            final ConfigId pathId = new ConfigId(pathName);
             return pathName.startsWith(CONFIG_PREFIX) && pathName.endsWith(CONFIG_SUFFIX) &&
-                    !mConfigDefMap.containsKey(pathName);
+                    !mConfigDefMap.containsKey(pathId);
         }
 
         /**
@@ -111,27 +175,91 @@
          * {@inheritDoc}
          */
         @Override
-        public ConfigurationDef getConfigurationDef(String name) throws ConfigurationException {
-            // first attempt to load cached config def
-            ConfigurationDef def = mConfigDefMap.get(name);
+        public ConfigurationDef getConfigurationDef(String name, Map<String, String> templateMap)
+                throws ConfigurationException {
+            // FIXME: Currently this does not support on the fly reload of configs.
+            // We need to clear the cache.
+            String configName = name;
+            if (!isBundledConfig(name)) {
+                configName = getAbsolutePath(null, name);
+            }
+
+            final ConfigId configId = new ConfigId(name, templateMap);
+            ConfigurationDef def = mConfigDefMap.get(configId);
+
             if (def == null) {
-                // not found - load from file
-                def = new ConfigurationDef(name);
-                loadConfiguration(name, def);
-                mConfigDefMap.put(name, def);
+                def = new ConfigurationDef(configName);
+                loadConfiguration(configName, def, templateMap);
+
+                mConfigDefMap.put(configId, def);
             }
             return def;
         }
 
-        @Override
-        public void loadIncludedConfiguration(ConfigurationDef parent, String name)
-                throws ConfigurationException {
-            if (mIncludedConfigs.contains(name)) {
-                throw new ConfigurationException(String.format(
-                        "Circular configuration include: config '%s' is already included", name));
+        /**
+         * Returns true if it is a config file found inside the classpath.
+         */
+        private boolean isBundledConfig(String name) {
+            InputStream configStream = getClass().getResourceAsStream(
+                    String.format("/%s%s%s", getConfigPrefix(), name, CONFIG_SUFFIX));
+            return configStream != null;
+        }
+
+        /**
+         * Get the absolute path of a local config file.
+         * @param root parent path of config file
+         * @param name config file
+         * @return absolute path for local config file.
+         * @throws ConfigurationException
+         */
+        private String getAbsolutePath(String root, String name) throws ConfigurationException {
+            File file = new File(name);
+            if (!file.isAbsolute()) {
+                if (root == null) {
+                    // if root directory was not specified, get the current working directory.
+                    root = System.getProperty("user.dir");
+                }
+                file = new File(root, name);
             }
-            mIncludedConfigs.add(name);
-            loadConfiguration(name, parent);
+            try {
+                return file.getCanonicalPath();
+            } catch (IOException e) {
+                throw new ConfigurationException(String.format(
+                        "Failure when trying to determine local file canonical path %s", e));
+            }
+        }
+
+        @Override
+        /**
+         * Configs that are bundled inside the tradefed.jar can only include other configs also
+         * bundled inside tradefed.jar. However, local (external) configs can include both local
+         * (external) and bundled configs.
+         */
+        public void loadIncludedConfiguration(ConfigurationDef def, String parentName, String name)
+                throws ConfigurationException {
+            String config_name = name;
+            if (!isBundledConfig(name)) {
+                try {
+                    // Ensure bundled configs are not including local configs.
+                    if (isBundledConfig(parentName)) {
+                        throw new ConfigurationException(String.format("Invalid include; bundled " +
+                    "config '%s' is trying to include local config '%s'.", parentName, name));
+                    }
+                    // Local configs' include should be relative to their parent's path.
+                    String parentRoot = new File(parentName).getParentFile().getCanonicalPath();
+                    config_name = getAbsolutePath(parentRoot, name);
+                } catch  (IOException e) {
+                    throw new ConfigurationException(String.format(
+                            "Failure when trying to determine local file canonical path %s", e));
+                }
+            }
+            if (mIncludedConfigs.contains(config_name)) {
+                throw new ConfigurationException(String.format(
+                        "Circular configuration include: config '%s' is already included",
+                        config_name));
+            }
+            mIncludedConfigs.add(config_name);
+            loadConfiguration(config_name, def, null);
         }
 
         /**
@@ -139,15 +267,18 @@
          *
          * @param name the name of a built-in configuration to load or a file
          *            path to configuration xml to load
-         * @return the loaded {@link ConfigurationDef}
+         * @param def the loaded {@link ConfigurationDef}
+         * @param templateMap map from template-include names to their respective concrete
+         *                    configuration files
          * @throws ConfigurationException if a configuration with given
          *             name/file path cannot be loaded or parsed
          */
-        void loadConfiguration(String name, ConfigurationDef def) throws ConfigurationException {
+        void loadConfiguration(String name, ConfigurationDef def, Map<String, String> templateMap)
+                throws ConfigurationException {
             Log.i(LOG_TAG, String.format("Loading configuration '%s'", name));
             BufferedInputStream bufStream = getConfigStream(name);
             ConfigurationXmlParser parser = new ConfigurationXmlParser(this);
-            parser.parse(def, name, bufStream);
+            parser.parse(def, name, bufStream, templateMap);
         }
 
         /**
@@ -160,7 +291,7 @@
     }
 
     ConfigurationFactory() {
-        mConfigDefMap = new Hashtable<String, ConfigurationDef>();
+        mConfigDefMap = new Hashtable<ConfigId, ConfigurationDef>();
     }
 
     /**
@@ -181,9 +312,9 @@
      * @return {@link ConfigurationDef}
      * @throws ConfigurationException if an error occurred loading the config
      */
-    private ConfigurationDef getConfigurationDef(String name, boolean isGlobal)
-            throws ConfigurationException {
-        return new ConfigLoader(isGlobal).getConfigurationDef(name);
+    private ConfigurationDef getConfigurationDef(String name, boolean isGlobal,
+            Map<String, String> templateMap) throws ConfigurationException {
+        return new ConfigLoader(isGlobal).getConfigurationDef(name, templateMap);
     }
 
     /**
@@ -192,9 +323,29 @@
     @Override
     public IConfiguration createConfigurationFromArgs(String[] arrayArgs)
             throws ConfigurationException {
+        return createConfigurationFromArgs(arrayArgs, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IConfiguration createConfigurationFromArgs(String[] arrayArgs,
+            List<String> unconsumedArgs) throws ConfigurationException {
         List<String> listArgs = new ArrayList<String>(arrayArgs.length);
         IConfiguration config = internalCreateConfigurationFromArgs(arrayArgs, listArgs);
-        config.setOptionsFromCommandLineArgs(listArgs);
+        config.setCommandLine(arrayArgs);
+        final List<String> tmpUnconsumedArgs = config.setOptionsFromCommandLineArgs(listArgs);
+
+        if (unconsumedArgs == null && tmpUnconsumedArgs.size() > 0) {
+            // (unconsumedArgs == null) is taken as a signal that the caller expects all args to
+            // be processed.
+            throw new ConfigurationException(String.format(
+                    "Invalid arguments provided. Unprocessed arguments: %s", tmpUnconsumedArgs));
+        } else if (unconsumedArgs != null) {
+            // Return the unprocessed args
+            unconsumedArgs.addAll(tmpUnconsumedArgs);
+        }
 
         return config;
     }
@@ -205,9 +356,9 @@
      * Note will not populate configuration with values from options
      *
      * @param arrayArgs the full list of command line arguments, including the config name
-     * @param optionArgsRef an empty list, that will be populated with the remaining option
-     *                      arguments
-     * @return
+     * @param optionArgsRef an empty list, that will be populated with the option arguments left
+     *                      to be interpreted
+     * @return An {@link IConfiguration} object representing the configuration that was loaded
      * @throws ConfigurationException
      */
     private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs,
@@ -215,10 +366,17 @@
         if (arrayArgs.length == 0) {
             throw new ConfigurationException("Configuration to run was not specified");
         }
-        optionArgsRef.addAll(Arrays.asList(arrayArgs));
+        final List<String> listArgs = new ArrayList<>(Arrays.asList(arrayArgs));
         // first arg is config name
-        final String configName = optionArgsRef.remove(0);
-        ConfigurationDef configDef = getConfigurationDef(configName, false);
+        final String configName = listArgs.remove(0);
+
+        // Steal ConfigurationXmlParser arguments from the command line
+        final ConfigurationXmlParserSettings parserSettings = new ConfigurationXmlParserSettings();
+        final ArgsOptionParser templateArgParser = new ArgsOptionParser(parserSettings);
+        optionArgsRef.addAll(templateArgParser.parseBestEffort(listArgs));
+
+        ConfigurationDef configDef = getConfigurationDef(configName, false,
+                parserSettings.templateMap);
         return configDef.createConfiguration();
     }
 
@@ -256,7 +414,7 @@
         optionArgsRef.addAll(Arrays.asList(arrayArgs));
         // first arg is config name
         final String configName = optionArgsRef.remove(0);
-        ConfigurationDef configDef = getConfigurationDef(configName, false);
+        ConfigurationDef configDef = getConfigurationDef(configName, false, null);
         return configDef.createGlobalConfiguration();
     }
 
@@ -302,9 +460,10 @@
         ClassPathScanner cpScanner = new ClassPathScanner();
         Set<String> configNames = cpScanner.getClassPathEntries(new ConfigClasspathFilter());
         for (String configName : configNames) {
+            final ConfigId configId = new ConfigId(configName);
             try {
-                ConfigurationDef configDef = getConfigurationDef(configName, false);
-                mConfigDefMap.put(configName, configDef);
+                ConfigurationDef configDef = getConfigurationDef(configName, false, null);
+                mConfigDefMap.put(configId, configDef);
             } catch (ConfigurationException e) {
                 ps.printf("Failed to load %s: %s", configName, e.getMessage());
                 ps.println();
@@ -392,22 +551,22 @@
      * @throws ConfigurationException if one or more configs failed to load
      */
     void loadAndPrintAllConfigs() throws ConfigurationException {
-       loadAllConfigs(false);
-       boolean failed = false;
-       ByteArrayOutputStream baos = new ByteArrayOutputStream();
-       PrintStream ps = new PrintStream(baos);
-       for (ConfigurationDef def : mConfigDefMap.values()) {
-           try {
-               def.createConfiguration().printCommandUsage(false,
-                       new PrintStream(StreamUtil.nullOutputStream()));
-           } catch (ConfigurationException e) {
-               ps.printf("Failed to print %s: %s", def.getName(), e.getMessage());
-               ps.println();
-               failed = true;
-           }
-       }
-       if (failed) {
-           throw new ConfigurationException(baos.toString());
-       }
+        loadAllConfigs(false);
+        boolean failed = false;
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        PrintStream ps = new PrintStream(baos);
+        for (ConfigurationDef def : mConfigDefMap.values()) {
+            try {
+                def.createConfiguration().printCommandUsage(false,
+                        new PrintStream(StreamUtil.nullOutputStream()));
+            } catch (ConfigurationException e) {
+                ps.printf("Failed to print %s: %s", def.getName(), e.getMessage());
+                ps.println();
+                failed = true;
+            }
+        }
+        if (failed) {
+            throw new ConfigurationException(baos.toString());
+        }
     }
 }
diff --git a/src/com/android/tradefed/config/ConfigurationXmlParser.java b/src/com/android/tradefed/config/ConfigurationXmlParser.java
index 1c9ed57..5cf10ce 100644
--- a/src/com/android/tradefed/config/ConfigurationXmlParser.java
+++ b/src/com/android/tradefed/config/ConfigurationXmlParser.java
@@ -23,6 +23,8 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Collections;
+import java.util.Map;
 
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParser;
@@ -37,21 +39,58 @@
     /**
      * SAX callback object. Handles parsing data from the xml tags.
      */
-    private static class ConfigHandler extends DefaultHandler {
+    static class ConfigHandler extends DefaultHandler {
 
         private static final String OBJECT_TAG = "object";
         private static final String OPTION_TAG = "option";
         private static final String INCLUDE_TAG = "include";
+        private static final String TEMPLATE_INCLUDE_TAG = "template-include";
         private static final String CONFIG_TAG = "configuration";
 
-        private ConfigurationDef mConfigDef;
-        private String mCurrentConfigObject;
+        /**
+         * A simple class to encapsulate a failure to resolve a &lt;template-include&gt;.  This
+         * allows the error to be easily detected programmatically.
+         */
+        @SuppressWarnings("serial")
+        private class TemplateResolutionError extends ConfigurationException {
+            TemplateResolutionError(String templateName) {
+                super(String.format(
+                        "Failed to parse config xml '%s'. Reason: " +
+                        "Couldn't resolve template-include named " +
+                        "'%s': No 'default' attribute and no matching manual resolution. " +
+                        "Try using argument --template:map %s (config path)",
+                        mConfigDef.getName(), templateName, templateName));
+            }
+        }
+
+        /** Note that this simply hasn't been implemented; it is not intentionally forbidden. */
+        static final String INNER_TEMPLATE_INCLUDE_ERROR =
+                "Configurations which contain a <template-include> tag, not having a 'default' " +
+                "attribute, may not be the target of any <include> or <template-include> tag. " +
+                "However, configuration '%s' attempted to include configuration '%s', which " +
+                "contains a <template-include> tag without a 'default' attribute.";
+
+        // Settings
         private final IConfigDefLoader mConfigDefLoader;
+        private final ConfigurationDef mConfigDef;
+        private final Map<String, String> mTemplateMap;
+        private final String mName;
+
+        // State-holding members
+        private String mCurrentConfigObject;
         private Boolean isLocalConfig = null;
 
-        ConfigHandler(ConfigurationDef def, IConfigDefLoader loader) {
+        ConfigHandler(ConfigurationDef def, String name, IConfigDefLoader loader,
+                Map<String, String> templateMap) {
+            mName = name;
             mConfigDef = def;
             mConfigDefLoader = loader;
+
+            if (templateMap == null) {
+                mTemplateMap = Collections.<String, String>emptyMap();
+            } else {
+                mTemplateMap = templateMap;
+            }
         }
 
         @Override
@@ -113,8 +152,44 @@
                     throwException("Missing 'name' attribute for include");
                 }
                 try {
-                    mConfigDefLoader.loadIncludedConfiguration(mConfigDef, includeName);
+                    mConfigDefLoader.loadIncludedConfiguration(mConfigDef, mName, includeName);
                 } catch (ConfigurationException e) {
+                    if (e instanceof TemplateResolutionError) {
+                        // The actual cause of this error is that recursive <template-include>
+                        // invocations aren't currently supported.  So replace that exception
+                        // with something more useful.
+                        throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
+                                mConfigDef.getName(), includeName));
+                    }
+
+                    throw new SAXException(e);
+                }
+
+            } else if (TEMPLATE_INCLUDE_TAG.equals(localName)) {
+                final String templateName = attributes.getValue("name");
+                if (templateName == null) {
+                    throwException("Missing 'name' attribute for template-include");
+                }
+
+                String includeName = mTemplateMap.get(templateName);
+                if (includeName == null) {
+                    includeName = attributes.getValue("default");
+                }
+                if (includeName == null) {
+                    throw new SAXException(new TemplateResolutionError(templateName));
+                }
+
+                try {
+                    mConfigDefLoader.loadIncludedConfiguration(mConfigDef, mName, includeName);
+                } catch (ConfigurationException e) {
+                    if (e instanceof TemplateResolutionError) {
+                        // The actual cause of this error is that recursive <template-include>
+                        // invocations aren't currently supported.  So replace that exception
+                        // with something more useful.
+                        throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
+                                mConfigDef.getName(), includeName));
+                    }
+
                     throw new SAXException(e);
                 }
 
@@ -165,13 +240,14 @@
      * @param xmlInput the configuration xml to parse
      * @throws ConfigurationException if input could not be parsed or had invalid format
      */
-    void parse(ConfigurationDef configDef, String name, InputStream xmlInput)
-            throws ConfigurationException {
+    void parse(ConfigurationDef configDef, String name, InputStream xmlInput,
+            Map<String, String> templateMap) throws ConfigurationException {
         try {
             SAXParserFactory parserFactory = SAXParserFactory.newInstance();
             parserFactory.setNamespaceAware(true);
             SAXParser parser = parserFactory.newSAXParser();
-            ConfigHandler configHandler = new ConfigHandler(configDef, mConfigDefLoader);
+            ConfigHandler configHandler = new ConfigHandler(configDef, name, mConfigDefLoader,
+                    templateMap);
             parser.parse(new InputSource(xmlInput), configHandler);
         } catch (ParserConfigurationException e) {
             throwConfigException(name, e);
diff --git a/src/com/android/tradefed/config/ConfigurationXmlParserSettings.java b/src/com/android/tradefed/config/ConfigurationXmlParserSettings.java
new file mode 100644
index 0000000..71729a6
--- /dev/null
+++ b/src/com/android/tradefed/config/ConfigurationXmlParserSettings.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 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.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A simple class to accept settings for the ConfigurationXmlParser
+ * <p />
+ * To pass settings to this class, the alias is mandatory.  So something like
+ * {@code --template:map name filename.xml} will work, but this would NOT work:
+ * {@code --map name filename.xml}.
+ */
+@OptionClass(alias = "template", global_namespace = false)
+class ConfigurationXmlParserSettings {
+    @Option(name = "map", description = "Map the <template-include> tag with the specified " +
+            "name to the specified actual configuration file.  Configuration file " +
+            "resolution will happen as with a standard <include> tag.")
+    public Map<String, String> templateMap = new HashMap<>();
+}
diff --git a/src/com/android/tradefed/config/GlobalConfiguration.java b/src/com/android/tradefed/config/GlobalConfiguration.java
index 64378fc..a50366c 100644
--- a/src/com/android/tradefed/config/GlobalConfiguration.java
+++ b/src/com/android/tradefed/config/GlobalConfiguration.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.device.IDeviceManager;
 import com.android.tradefed.device.IDeviceMonitor;
 import com.android.tradefed.device.IDeviceSelection;
+import com.android.tradefed.device.IMultiDeviceRecovery;
 import com.android.tradefed.log.ITerribleFailureHandler;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.MultiMap;
@@ -48,6 +49,7 @@
     public static final String HOST_OPTIONS_TYPE_NAME = "host_options";
     public static final String DEVICE_REQUIREMENTS_TYPE_NAME = "device_requirements";
     public static final String SCHEDULER_TYPE_NAME = "command_scheduler";
+    public static final String MULTI_DEVICE_RECOVERY_TYPE_NAME = "multi_device_recovery";
 
     private static Map<String, ObjTypeInfo> sObjTypeMap = null;
     private static IGlobalConfiguration sInstance = null;
@@ -191,6 +193,8 @@
                     new ObjTypeInfo(ITerribleFailureHandler.class, false));
             sObjTypeMap.put(SCHEDULER_TYPE_NAME,
                     new ObjTypeInfo(ICommandScheduler.class, false));
+            sObjTypeMap.put(MULTI_DEVICE_RECOVERY_TYPE_NAME,
+                    new ObjTypeInfo(IMultiDeviceRecovery.class, true));
 
         }
         return sObjTypeMap;
@@ -234,6 +238,7 @@
      * {@inheritDoc}
      */
     @Override
+    @SuppressWarnings("unchecked")
     public List<IDeviceMonitor> getDeviceMonitors() {
         return (List<IDeviceMonitor>) getConfigurationObjectList(DEVICE_MONITOR_TYPE_NAME);
     }
@@ -273,6 +278,16 @@
     /**
      * {@inheritDoc}
      */
+    @Override
+    @SuppressWarnings("unchecked")
+    public List<IMultiDeviceRecovery> getMultiDeviceRecoveryHandlers() {
+        return (List<IMultiDeviceRecovery>)getConfigurationObjectList(
+                MULTI_DEVICE_RECOVERY_TYPE_NAME);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
 //    @Override
     public List<?> getConfigurationObjectList(String typeName) {
         return mConfigMap.get(typeName);
diff --git a/src/com/android/tradefed/config/IConfigDefLoader.java b/src/com/android/tradefed/config/IConfigDefLoader.java
index a4cb980..b8e03b0 100644
--- a/src/com/android/tradefed/config/IConfigDefLoader.java
+++ b/src/com/android/tradefed/config/IConfigDefLoader.java
@@ -16,6 +16,8 @@
 
 package com.android.tradefed.config;
 
+import java.util.Map;
+
 /**
  * Interface for retrieving a ConfigurationDef.
  */
@@ -25,21 +27,24 @@
      * Retrieve the {@link ConfigurationDef} for the given name
      *
      * @param name
+     * @param templateMap map of template-include names to configuration filenames
      * @return {@link ConfigurationDef}
      * @throws ConfigurationException if an error occurred loading the config
      */
-    ConfigurationDef getConfigurationDef(String name) throws ConfigurationException;
+    ConfigurationDef getConfigurationDef(String name, Map<String, String> templateMap)
+            throws ConfigurationException;
 
     boolean isGlobalConfig();
 
     /**
      * Load a config's data into the given {@link ConfigurationDef}
      *
-     * @param parent the {@link ConfigurationDef} to load the data into
+     * @param def the {@link ConfigurationDef} to load the data into
+     * @param parentName the name of the parent config
      * @param name the name of config to include
      * @return {@link ConfigurationDef}
      * @throws ConfigurationException if an error occurred loading the config
      */
-    void loadIncludedConfiguration(ConfigurationDef parent, String name)
+    void loadIncludedConfiguration(ConfigurationDef def, String parentName, String name)
             throws ConfigurationException;
 }
diff --git a/src/com/android/tradefed/config/IConfiguration.java b/src/com/android/tradefed/config/IConfiguration.java
index 6e5a5b7..54cb674 100644
--- a/src/com/android/tradefed/config/IConfiguration.java
+++ b/src/com/android/tradefed/config/IConfiguration.java
@@ -27,7 +27,12 @@
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.testtype.IRemoteTest;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.IOException;
 import java.io.PrintStream;
+import java.io.PrintWriter;
 import java.util.List;
 
 /**
@@ -273,8 +278,10 @@
      * @see {@link ArgsOptionParser} for expected format
      *
      * @param listArgs the command line arguments
+     * @return the unconsumed arguments
      */
-    public void setOptionsFromCommandLineArgs(List<String> listArgs) throws ConfigurationException;
+    public List<String> setOptionsFromCommandLineArgs(List<String> listArgs)
+            throws ConfigurationException;
 
     /**
      * Outputs a command line usage help text for this configuration to given printStream.
@@ -287,6 +294,13 @@
             throws ConfigurationException;
 
     /**
+     * Returns a JSON representation of this configuration.
+     *
+     * @throws JSONException
+     */
+    public JSONArray getJsonCommandUsage() throws JSONException;
+
+    /**
      * Validate option values.
      * <p/>
      * Currently this will just validate that all mandatory options have been set
@@ -294,4 +308,29 @@
      * @throws ConfigurationException if config is not valid
      */
     public void validateOptions() throws ConfigurationException;
+
+    /**
+     * Sets the command line used to create this {@link IConfiguration}.
+     * This stores the whole command line, including the configuration name,
+     * unlike setOptionsFromCommandLineArgs.
+     *
+     * @param arrayArgs the command line
+     */
+    public void setCommandLine(String[] arrayArgs);
+
+    /**
+     * Gets the the command line used to create this {@link IConfiguration}.
+     *
+     * @return the command line used to create this {@link IConfiguration}.
+     */
+    public String getCommandLine();
+
+    /**
+     * Gets the expanded XML file for the config with all options shown for this
+     * {@link IConfiguration} as a {@link String}.
+     *
+     * @param output the writer to print the xml to.
+     * @throws IOException
+     */
+    public void dumpXml(PrintWriter output) throws IOException;
 }
diff --git a/src/com/android/tradefed/config/IConfigurationFactory.java b/src/com/android/tradefed/config/IConfigurationFactory.java
index e4b0bae..bb015f2 100644
--- a/src/com/android/tradefed/config/IConfigurationFactory.java
+++ b/src/com/android/tradefed/config/IConfigurationFactory.java
@@ -25,17 +25,32 @@
 public interface IConfigurationFactory {
 
     /**
+     * A convenience method which calls {@link createConfigurationFromArgs(String[], List<String>)}
+     * with a {@code null} second argument.  Thus, it will throw {@link ConfigurationException} if
+     * any unconsumed arguments remain.
+     *
+     * @see createConfigurationFromArgs(String[] List<String>)
+     */
+    public IConfiguration createConfigurationFromArgs(String[] args) throws ConfigurationException;
+
+    /**
      * Create the {@link IConfiguration} from command line arguments.
      * <p/>
      * Expected format is "CONFIG [options]", where CONFIG is the built-in configuration name or
      * a file path to a configuration xml file.
      *
      * @param args the command line arguments
+     * @param unconsumedArgs a List which will be populated with the arguments that were not
+     *                       consumed by the Objects associated with the specified config. If this
+     *                       is {@code null}, then the implementation will throw
+     *                       {@link ConfigurationException} if any unprocessed args remain.
+     *
      * @return the loaded {@link IConfiguration}. The delegate object {@link Option} fields have
      *         been populated with values in args.
      * @throws {@link ConfigurationException} if configuration could not be loaded
      */
-    public IConfiguration createConfigurationFromArgs(String[] args) throws ConfigurationException;
+    public IConfiguration createConfigurationFromArgs(String[] args, List<String> unconsumedArgs)
+            throws ConfigurationException;
 
     /**
      * Create a {@link IGlobalConfiguration} from command line arguments.
@@ -88,5 +103,4 @@
      * @param out the {@link PrintStream} to dump output to
      */
     public void dumpConfig(String configName, PrintStream out);
-
 }
diff --git a/src/com/android/tradefed/config/IGlobalConfiguration.java b/src/com/android/tradefed/config/IGlobalConfiguration.java
index 808565a..0ac5028 100644
--- a/src/com/android/tradefed/config/IGlobalConfiguration.java
+++ b/src/com/android/tradefed/config/IGlobalConfiguration.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.device.IDeviceManager;
 import com.android.tradefed.device.IDeviceMonitor;
 import com.android.tradefed.device.IDeviceSelection;
+import com.android.tradefed.device.IMultiDeviceRecovery;
 import com.android.tradefed.log.ITerribleFailureHandler;
 
 import java.util.List;
@@ -153,6 +154,13 @@
     public ICommandScheduler getCommandScheduler();
 
     /**
+     * Gets the list of {@link IMultiDeviceRecovery} to use from the configuration.
+     *
+     * @return the list of {@link IMultiDeviceRecovery}, or <code>null</code> if not set.
+     */
+    public List<IMultiDeviceRecovery> getMultiDeviceRecoveryHandlers();
+
+    /**
      * Set the {@link IDeviceManager}, replacing any existing values. This sets the manager
      * for the test devices
      *
diff --git a/src/com/android/tradefed/config/Option.java b/src/com/android/tradefed/config/Option.java
index c27eeb7..7bc8a5a 100644
--- a/src/com/android/tradefed/config/Option.java
+++ b/src/com/android/tradefed/config/Option.java
@@ -82,7 +82,26 @@
      *   <li>The field is an empty {@link java.util.Collection}.</li>
      * </ul>
      */
-     boolean mandatory() default false;
+    boolean mandatory() default false;
+
+    /**
+     * Whether the option represents a time value.
+     * <p />
+     * If this is a time value, time-specific suffixes will be parsed.  The field <emph>MUST</emph>
+     * be a {@code long} or {@code Long} for this flag to be valid.  A
+     * {@code ConfigurationException} will be thrown otherwise.
+     * <p />
+     * The default unit is millis.  The configuration framework will accept {@code s} for seconds
+     * (1000 millis), {@code m} for minutes (60 seconds), {@code h} for hours (60 minutes),
+     * or {@code d} for days (24 hours).
+     * <p />
+     * Units may be mixed and matched, so long as each unit appears at most once, and so long as
+     * all units which do appear are listed in decreasing order of scale.  So, for instance,
+     * {@code h} may only appear before {@code m}, and may only appear after {@code d}.  As a
+     * specific example, "1d2h3m4s5ms" would be a valid time value, as would "4" or "4ms".  All
+     * embedded whitespace is discarded.
+     */
+    boolean isTimeVal() default false;
 
     /**
      * Controls the behavior when an option is specified multiple times.  Note that this rule is
diff --git a/src/com/android/tradefed/config/OptionSetter.java b/src/com/android/tradefed/config/OptionSetter.java
index e420a24..4cf4d0f 100644
--- a/src/com/android/tradefed/config/OptionSetter.java
+++ b/src/com/android/tradefed/config/OptionSetter.java
@@ -18,6 +18,8 @@
 
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.ArrayUtil;
+import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.TimeVal;
 import com.google.common.base.Objects;
 
 import java.io.File;
@@ -82,6 +84,7 @@
 
         handlers.put(String.class, new StringHandler());
         handlers.put(File.class, new FileHandler());
+        handlers.put(TimeVal.class, new TimeValHandler());
     }
 
     private static Handler getHandler(Type type) throws ConfigurationException {
@@ -96,7 +99,8 @@
                             "cannot handle nested parameterized type " + type);
                 }
                 return getHandler(actualType);
-            } else if (Map.class.isAssignableFrom(rawClass)) {
+            } else if (Map.class.isAssignableFrom(rawClass) ||
+                    MultiMap.class.isAssignableFrom(rawClass)) {
                 // handle Map
                 Type keyType = parameterizedType.getActualTypeArguments()[0];
                 Type valueType = parameterizedType.getActualTypeArguments()[1];
@@ -111,8 +115,8 @@
                 return new MapHandler(getHandler(keyType), getHandler(valueType));
             } else {
                 throw new ConfigurationException(String.format(
-                        "can't handle parameterized type %s; only Collection and Map are supported",
-                        type));
+                        "can't handle parameterized type %s; only Collection, Map, and MultiMap "
+                        + "are supported", type));
             }
         }
         if (type instanceof Class) {
@@ -134,6 +138,13 @@
                 throw new ConfigurationException(String.format(
                         "Cannot handle non-parameterized map %s.  Use a generic Map to specify "
                         + "desired element types.", type));
+            } else if (MultiMap.class.isAssignableFrom(cType)) {
+                // could handle by just having a default of treating
+                // contents as String but consciously decided this
+                // should be an error
+                throw new ConfigurationException(String.format(
+                        "Cannot handle non-parameterized multimap %s.  Use a generic MultiMap to "
+                        + "specify desired element types.", type));
             }
             return handlers.get(cType);
         }
@@ -141,6 +152,50 @@
                 type));
     }
 
+    /**
+     * Does some magic to distinguish TimeVal long field from normal long fields, then calls
+     * {@see #getHandler(Type)} in the appropriate manner.
+     */
+    private Handler getHandlerOrTimeVal(Field field, Object optionSource)
+            throws ConfigurationException {
+        // Do some magic to distinguish TimeVal long fields from normal long fields
+        final Option option = field.getAnnotation(Option.class);
+        if (option == null) {
+            // Shouldn't happen, but better to check.
+            throw new ConfigurationException(String.format(
+                    "internal error: @Option annotation for field %s in class %s was " +
+                    "unexpectedly null",
+                    field.getName(), optionSource.getClass().getName()));
+        }
+
+        final Type type = field.getGenericType();
+        if (option.isTimeVal()) {
+            // We've got a field that marks itself as a time value.  First off, verify that it's
+            // a compatible type
+            if (type instanceof Class) {
+                final Class<?> cType = (Class<?>) type;
+                if (long.class.equals(cType) || Long.class.equals(cType)) {
+                    // Parse time value and return a Long
+                    return new TimeValLongHandler();
+
+                } else if (TimeVal.class.equals(cType)) {
+                    // Parse time value and return a TimeVal object
+                    return new TimeValHandler();
+                }
+            }
+
+            throw new ConfigurationException(String.format("Only fields of type long, " +
+                    "Long, or TimeVal may be declared as isTimeVal.  Field %s has " +
+                    "incompatible type %s.", field.getName(), field.getGenericType()));
+
+        } else {
+            // Note that fields declared as TimeVal (or Generic types with TimeVal parameters) will
+            // follow this branch, but will still work as expected.
+            return getHandler(type);
+        }
+    }
+
+
     private final Collection<Object> mOptionSources;
     private final Map<String, OptionFieldsForName> mOptionMap;
 
@@ -243,13 +298,13 @@
      * @throws ConfigurationException if Option cannot be found or valueText is wrong type
      */
     public void setOptionValue(String optionName, String valueText) throws ConfigurationException {
-        OptionFieldsForName optionFields = fieldsForArg(optionName);
+        final OptionFieldsForName optionFields = fieldsForArg(optionName);
         for (Map.Entry<Object, Field> fieldEntry : optionFields) {
 
-            Object optionSource = fieldEntry.getKey();
-            Field field = fieldEntry.getValue();
-            Handler handler = getHandler(field.getGenericType());
-            Object value = handler.translate(valueText);
+            final Object optionSource = fieldEntry.getKey();
+            final Field field = fieldEntry.getValue();
+            final Handler handler = getHandlerOrTimeVal(field, optionSource);
+            final Object value = handler.translate(valueText);
             if (value == null) {
                 final String type = field.getType().getSimpleName();
                 throw new ConfigurationException(
@@ -303,6 +358,22 @@
                             "for option '%s') in class '%s'",
                             field.getName(), optionName, optionSource.getClass().getName()));
                 }
+            } else if (MultiMap.class.isAssignableFrom(field.getType())) {
+                MultiMap multimap = (MultiMap)field.get(optionSource);
+                if (multimap == null) {
+                    throw new ConfigurationException(String.format(
+                            "internal error: no storage allocated for field '%s' (used for " +
+                            "option '%s') in class '%s'",
+                            field.getName(), optionName, optionSource.getClass().getName()));
+                }
+                if (value instanceof MultiMap) {
+                    multimap.putAll((MultiMap)value);
+                } else {
+                    throw new ConfigurationException(String.format(
+                            "internal error: value provided for field '%s' is not a multimap " +
+                            "(used for option '%s') in class '%s'",
+                            field.getName(), optionName, optionSource.getClass().getName()));
+                }
             } else {
                 final Option option = field.getAnnotation(Option.class);
                 if (option == null) {
@@ -378,18 +449,28 @@
             }
             try {
                 field.setAccessible(true);
-                if (!Map.class.isAssignableFrom(field.getType())) {
+                if (Map.class.isAssignableFrom(field.getType())) {
+                    Map map = (Map)field.get(optionSource);
+                    if (map == null) {
+                        throw new ConfigurationException(String.format(
+                                "internal error: no storage allocated for field '%s' (used for " +
+                                "option '%s') in class '%s'",
+                                field.getName(), optionName, optionSource.getClass().getName()));
+                    }
+                    map.put(pair.mKey, pair.mValue);
+                } else if (MultiMap.class.isAssignableFrom(field.getType())) {
+                    MultiMap multimap = (MultiMap)field.get(optionSource);
+                    if (multimap == null) {
+                        throw new ConfigurationException(String.format(
+                                "internal error: no storage allocated for field '%s' (used for " +
+                                "option '%s') in class '%s'",
+                                field.getName(), optionName, optionSource.getClass().getName()));
+                    }
+                    multimap.put(pair.mKey, pair.mValue);
+                } else {
                     throw new ConfigurationException(String.format(
                             "internal error: not a map field!"));
                 }
-                Map map = (Map)field.get(optionSource);
-                if (map == null) {
-                    throw new ConfigurationException(String.format(
-                            "internal error: no storage allocated for field '%s' (used for " +
-                            "option '%s') in class '%s'",
-                            field.getName(), optionName, optionSource.getClass().getName()));
-                }
-                map.put(pair.mKey, pair.mValue);
             } catch (IllegalAccessException e) {
                 throw new ConfigurationException(String.format(
                         "internal error when setting option '%s'", optionName), e);
@@ -553,6 +634,11 @@
                     if (m.isEmpty()) {
                         unsetOptions.add(realOptName);
                     }
+                } else if (value instanceof MultiMap) {
+                    MultiMap m = (MultiMap) value;
+                    if (m.isEmpty()) {
+                        unsetOptions.add(realOptName);
+                    }
                 }
             }
         }
@@ -614,6 +700,11 @@
             if (map.isEmpty()) {
                 return null;
             }
+        } else if (fieldValue instanceof MultiMap) {
+            MultiMap multimap = (MultiMap)fieldValue;
+            if (multimap.isEmpty()) {
+                return null;
+            }
         }
         return fieldValue.toString();
     }
@@ -807,6 +898,36 @@
         }
     }
 
+    private static class TimeValLongHandler extends Handler {
+        /**
+         * We parse the string as a time value, and return a {@code long}
+         */
+        @Override
+        Object translate(String valueText) {
+            try {
+                return TimeVal.fromString(valueText);
+
+            } catch (NumberFormatException ex) {
+                return null;
+            }
+        }
+    }
+
+    private static class TimeValHandler extends Handler {
+        /**
+         * We parse the string as a time value, and return a {@code TimeVal}
+         */
+        @Override
+        Object translate(String valueText) {
+            try {
+                return new TimeVal(valueText);
+
+            } catch (NumberFormatException ex) {
+                return null;
+            }
+        }
+    }
+
     private static class FloatHandler extends Handler {
         @Override
         Object translate(String valueText) {
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index 35c7e32..acd7255 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -34,6 +34,7 @@
 import com.android.tradefed.util.CommandStatus;
 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 com.android.tradefed.util.TableFormatter;
 import com.google.common.annotations.VisibleForTesting;
@@ -49,8 +50,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 /**
  * {@inheritDoc}
@@ -61,13 +60,21 @@
 
     /** max wait time in ms for fastboot devices command to complete */
     private static final long FASTBOOT_CMD_TIMEOUT = 1 * 60 * 1000;
-    /**  time to wait in ms between fastboot devices requests */
+    /** time to wait in ms between fastboot devices requests */
     private static final long FASTBOOT_POLL_WAIT_TIME = 5 * 1000;
-    /** time to wait for device adb shell responsive connection before declaring it unavailable
-     * for testing */
+    /**
+     * time to wait for device adb shell responsive connection before declaring it unavailable for
+     * testing
+     */
     private static final int CHECK_WAIT_DEVICE_AVAIL_MS = 30 * 1000;
 
-    /** a {@link DeviceSelectionOptions} that matches any device.  Visible for testing. */
+    /* the max size of the emulator output in bytes */
+    private static final long MAX_EMULATOR_OUTPUT = 20 * 1024 * 1024;
+
+    /* the emulator output log name */
+    private static final String EMULATOR_OUTPUT = "emulator_log";
+
+    /** a {@link DeviceSelectionOptions} that matches any device. Visible for testing. */
     static final IDeviceSelection ANY_DEVICE_OPTIONS = new DeviceSelectionOptions();
     private static final String NULL_DEVICE_SERIAL_PREFIX = "null-device";
     private static final String EMULATOR_SERIAL_PREFIX = "emulator";
@@ -85,15 +92,22 @@
     private FastbootMonitor mFastbootMonitor;
     private boolean mIsTerminated = false;
     private IDeviceSelection mGlobalDeviceFilter;
-    @Option(name="max-emulators",
+
+    @Option(name = "max-emulators",
             description = "the maximum number of emulators that can be allocated at one time")
     private int mNumEmulatorSupported = 1;
-    @Option(name="max-null-devices",
+    @Option(name = "max-null-devices",
             description = "the maximum number of no device runs that can be allocated at one time.")
     private int mNumNullDevicesSupported = 1;
 
     private boolean mSynchronousMode = false;
 
+    @Option(name = "device-recovery-interval",
+            description = "the interval in ms between attempts to recover unavailable devices.")
+    private long mDeviceRecoveryInterval = 10 * 60 * 1000;
+
+    private DeviceRecoverer mDeviceRecoverer;
+
     /**
      * Creator interface for {@link IManagedTestDevice}s
      */
@@ -109,7 +123,7 @@
 
     @Override
     public void init() {
-        init(null,null);
+        init(null, null);
     }
 
     /**
@@ -160,7 +174,8 @@
         }
         mManagedDeviceList = new ManagedDeviceList(deviceFactory);
 
-        if (isFastbootAvailable()) {
+        final FastbootHelper fastboot = new FastbootHelper(getRunUtil());
+        if (fastboot.isFastbootAvailable()) {
             mFastbootListeners = Collections.synchronizedSet(new HashSet<IFastbootListener>());
             mFastbootMonitor = new FastbootMonitor();
             startFastbootMonitor();
@@ -178,7 +193,7 @@
 
         // don't start adding devices until fastboot support has been established
         // TODO: Temporarily increase default timeout as workaround for syncFiles timeouts
-        DdmPreferences.setTimeOut(30*1000);
+        DdmPreferences.setTimeOut(30 * 1000);
         mAdbBridge = createAdbBridge();
         mManagedDeviceListener = new ManagedDeviceListener();
         // It's important to add the listener before initializing the ADB bridge to avoid a race
@@ -199,6 +214,10 @@
         mAdbBridge.init(false /* client support */, "adb");
         addEmulators();
         addNullDevices();
+
+        List<IMultiDeviceRecovery> recoverers = getGlobalConfig().getMultiDeviceRecoveryHandlers();
+        mDeviceRecoverer = new DeviceRecoverer(recoverers);
+        startDeviceRecoverer();
     }
 
     /**
@@ -219,23 +238,6 @@
     }
 
     /**
-     * Determine if fastboot is available for use.
-     */
-    private boolean isFastbootAvailable() {
-        CommandResult fastbootResult = getRunUtil().runTimedCmdSilently(5000, "fastboot", "help");
-        if (fastbootResult.getStatus() == CommandStatus.SUCCESS) {
-            return true;
-        }
-        if (fastbootResult.getStderr() != null &&
-            fastbootResult.getStderr().indexOf("usage: fastboot") >= 0) {
-            CLog.logAndDisplay(LogLevel.WARN,
-                              "You are running an older version of fastboot, please update it.");
-            return true;
-        }
-        return false;
-    }
-
-    /**
      * Start fastboot monitoring.
      * <p/>
      * Exposed for unit testing.
@@ -245,6 +247,15 @@
     }
 
     /**
+     * Start device recovery.
+     * <p/>
+     * Exposed for unit testing.
+     */
+    void startDeviceRecoverer() {
+        mDeviceRecoverer.start();
+    }
+
+    /**
      * Get the {@link IGlobalConfiguration} instance to use.
      * <p />
      * Exposed for unit testing.
@@ -263,7 +274,17 @@
     }
 
     /**
+     * Create a {@link RunUtil} instance to use.
+     * <p/>
+     * Exposed for unit testing.
+     */
+    IRunUtil createRunUtil() {
+        return new RunUtil();
+    }
+
+    /**
      * Asynchronously checks if device is available, and adds to queue
+     *
      * @param device
      */
     private void checkAndAddAvailableDevice(final IManagedTestDevice testDevice) {
@@ -280,7 +301,7 @@
             public void run() {
                 CLog.d("checking new device %s responsiveness", testDevice.getSerialNumber());
                 if (testDevice.getMonitor().waitForDeviceShell(CHECK_WAIT_DEVICE_AVAIL_MS)) {
-                    DeviceEventResponse r =  mManagedDeviceList.handleDeviceEvent(testDevice,
+                    DeviceEventResponse r = mManagedDeviceList.handleDeviceEvent(testDevice,
                             DeviceEvent.AVAILABLE_CHECK_PASSED);
                     if (r.stateChanged && r.allocationState == DeviceAllocationState.Available) {
                         CLog.logAndDisplay(LogLevel.INFO, "Detected new device %s",
@@ -299,7 +320,7 @@
                 }
             }
         };
-        if (mSynchronousMode ) {
+        if (mSynchronousMode) {
             checkRunnable.run();
         } else {
             Thread checkThread = new Thread(checkRunnable, threadName);
@@ -341,9 +362,10 @@
     }
 
     private void addFastbootDevices() {
-        Set<String> serials = getDevicesOnFastboot();
+        final FastbootHelper fastboot = new FastbootHelper(getRunUtil());
+        Set<String> serials = fastboot.getDevices();
         if (serials != null) {
-            for (String serial: serials) {
+            for (String serial : serials) {
                 FastbootDevice d = new FastbootDevice(serial);
                 if (mGlobalDeviceFilter != null && mGlobalDeviceFilter.matches(d)) {
                     addAvailableDevice(d);
@@ -424,6 +446,8 @@
         if (ideviceToReturn.isEmulator() && managedDevice.getEmulatorProcess() != null) {
             try {
                 killEmulator(device);
+                // stop emulator output log
+                device.stopEmulatorOutput();
                 // emulator killed - return a stub device
                 // TODO: this is a bit of a hack. Consider having DeviceManager inject a StubDevice
                 // when deviceDisconnected event is received
@@ -484,12 +508,15 @@
 
         try {
             CLog.i("launching emulator with %s", fullArgs.toString());
-            Process p = runUtil.runCmdInBackground(fullArgs);
+            SizeLimitedOutputStream emulatorOutput = new SizeLimitedOutputStream(
+                    MAX_EMULATOR_OUTPUT, EMULATOR_OUTPUT, ".txt");
+            Process p = runUtil.runCmdInBackground(fullArgs, emulatorOutput);
             // sleep a small amount to wait for process to start successfully
             getRunUtil().sleep(500);
             assertEmulatorProcessAlive(p);
-            IManagedTestDevice managedDevice = (IManagedTestDevice)device;
-            managedDevice.setEmulatorProcess(p);
+            TestDevice testDevice = (TestDevice) device;
+            testDevice.setEmulatorProcess(p);
+            testDevice.setEmulatorOutputStream(emulatorOutput);
         } catch (IOException e) {
             // TODO: is this the most appropriate exception to throw?
             throw new DeviceNotAvailableException("Failed to start emulator process", e);
@@ -536,13 +563,13 @@
         if (console != null) {
             console.kill();
             // check and wait for device to become not avail
-            device.waitForDeviceNotAvailable(5*1000);
+            device.waitForDeviceNotAvailable(5 * 1000);
             // lets ensure process is killed too - fall through
         } else {
             CLog.w("Could not get emulator console for %s", device.getSerialNumber());
         }
         // lets try killing the process
-        Process emulatorProcess = ((IManagedTestDevice)device).getEmulatorProcess();
+        Process emulatorProcess = ((IManagedTestDevice) device).getEmulatorProcess();
         if (emulatorProcess != null) {
             emulatorProcess.destroy();
             if (isProcessRunning(emulatorProcess)) {
@@ -551,7 +578,7 @@
                 forceKillProcess(emulatorProcess, device.getSerialNumber());
             }
         }
-        if (!device.waitForDeviceNotAvailable(20*1000)) {
+        if (!device.waitForDeviceNotAvailable(20 * 1000)) {
             throw new DeviceNotAvailableException(String.format("Failed to kill emulator %s",
                     device.getSerialNumber()));
         }
@@ -572,7 +599,7 @@
                 f.setAccessible(true);
                 Integer pid = (Integer)f.get(emulatorProcess);
                 if (pid != null) {
-                    RunUtil.getDefault().runTimedCmd(5*1000, "kill", "-9", pid.toString());
+                    RunUtil.getDefault().runTimedCmd(5 * 1000, "kill", "-9", pid.toString());
                 }
             } catch (NoSuchFieldException e) {
                 CLog.d("got NoSuchFieldException when attempting to read process pid");
@@ -613,7 +640,7 @@
         CLog.i("Reconnecting device %s to adb over tcpip", usbDevice.getSerialNumber());
         ITestDevice tcpDevice = null;
         if (usbDevice instanceof IManagedTestDevice) {
-            IManagedTestDevice managedUsbDevice = (IManagedTestDevice)usbDevice;
+            IManagedTestDevice managedUsbDevice = (IManagedTestDevice) usbDevice;
             String ipAndPort = managedUsbDevice.switchToAdbTcp();
             if (ipAndPort != null) {
                 CLog.d("Device %s was switched to adb tcp on %s", usbDevice.getSerialNumber(),
@@ -655,7 +682,7 @@
             }
             CLog.w("Failed to connect to device on %s, attempt %d of 3. Response: %s.",
                     ipAndPort, i, adbConnectResult);
-            getRunUtil().sleep(5*1000);
+            getRunUtil().sleep(5 * 1000);
         }
         return false;
     }
@@ -682,13 +709,16 @@
     @Override
     public synchronized void terminate() {
         checkInit();
-        if (!mIsTerminated ) {
+        if (!mIsTerminated) {
             mIsTerminated = true;
+            if (mDeviceRecoverer != null) {
+                mDeviceRecoverer.terminate();
+            }
             mAdbBridge.removeDeviceChangeListener(mManagedDeviceListener);
             mAdbBridge.terminate();
-            if (mFastbootMonitor != null) {
-                mFastbootMonitor.terminate();
-            }
+            // We are not terminating mFastbootMonitor here since it is a daemon thread.
+            // Early terminating it can cause other threads to be blocked if they check
+            // fastboot state of a device.
         }
     }
 
@@ -819,7 +849,7 @@
                     desc.getProductVariant(),
                     desc.getBuildId(),
                     desc.getBatteryLevel())
-            );
+                    );
         }
     }
 
@@ -832,7 +862,6 @@
         return o == null ? "unknown" : o.toString();
     }
 
-
     /**
      * A class to listen for and act on device presence updates from ddmlib
      */
@@ -925,12 +954,16 @@
         }
     }
 
+    /**
+     * A class to monitor and update fastboot state of devices.
+     */
     private class FastbootMonitor extends Thread {
 
         private boolean mQuit = false;
 
         FastbootMonitor() {
             super("FastbootMonitor");
+            setDaemon(true);
         }
 
         public void terminate() {
@@ -940,11 +973,12 @@
 
         @Override
         public void run() {
+            final FastbootHelper fastboot = new FastbootHelper(getRunUtil());
             while (!mQuit) {
                 // only poll fastboot devices if there are listeners, as polling it
                 // indiscriminately can cause fastboot commands to hang
                 if (!mFastbootListeners.isEmpty()) {
-                    Set<String> serials = getDevicesOnFastboot();
+                    Set<String> serials = fastboot.getDevices();
                     if (serials != null) {
                         mManagedDeviceList.updateFastbootStates(serials);
 
@@ -962,28 +996,37 @@
         }
     }
 
-    private Set<String> getDevicesOnFastboot() {
-        CommandResult fastbootResult = getRunUtil().runTimedCmd(FASTBOOT_CMD_TIMEOUT,
-                "fastboot", "devices");
-        if (fastbootResult.getStatus().equals(CommandStatus.SUCCESS)) {
-            CLog.v("fastboot devices returned\n %s",
-                    fastbootResult.getStdout());
-            return parseDevicesOnFastboot(fastbootResult.getStdout());
-        } else {
-            CLog.w("'fastboot devices' failed. Result: %s, stderr: %s", fastbootResult.getStatus(),
-                    fastbootResult.getStderr());
-        }
-        return null;
-    }
+    /**
+     * A class for a thread which performs periodic device recovery operations.
+     */
+    private class DeviceRecoverer extends Thread {
 
-    static Set<String> parseDevicesOnFastboot(String fastbootOutput) {
-        Set<String> serials = new HashSet<String>();
-        Pattern fastbootPattern = Pattern.compile("([\\w\\d]+)\\s+fastboot\\s*");
-        Matcher fastbootMatcher = fastbootPattern.matcher(fastbootOutput);
-        while (fastbootMatcher.find()) {
-            serials.add(fastbootMatcher.group(1));
+        private boolean mQuit = false;
+        private List<IMultiDeviceRecovery> mMultiDeviceRecoverers;
+
+        public DeviceRecoverer(List<IMultiDeviceRecovery> multiDeviceRecoverers) {
+            super("DeviceRecoverer");
+            mMultiDeviceRecoverers = multiDeviceRecoverers;
         }
-        return serials;
+
+        @Override
+        public void run() {
+            while (!mQuit) {
+                getRunUtil().sleep(mDeviceRecoveryInterval);
+                if (mMultiDeviceRecoverers != null && !mMultiDeviceRecoverers.isEmpty()) {
+                    for (IMultiDeviceRecovery m : mMultiDeviceRecoverers) {
+                        // Always fetch a list of devices prior to running the recovery.
+                        List<DeviceDescriptor> devices = listAllDevices();
+                        m.recoverDevices(devices);
+                    }
+                }
+            }
+        }
+
+        public void terminate() {
+            mQuit = true;
+            interrupt();
+        }
     }
 
     @VisibleForTesting
diff --git a/src/com/android/tradefed/device/DeviceSelectionOptions.java b/src/com/android/tradefed/device/DeviceSelectionOptions.java
index 6476295..16f70ce 100644
--- a/src/com/android/tradefed/device/DeviceSelectionOptions.java
+++ b/src/com/android/tradefed/device/DeviceSelectionOptions.java
@@ -82,7 +82,7 @@
             "--max-battery is specified, skip devices that have an unknown battery level.  Note " +
             "that this may leave restart-looping devices in limbo indefinitely without manual " +
             "intervention.")
-    private boolean mRequireBatteryCheck = false;
+    private boolean mRequireBatteryCheck = true;
 
     @Option(name = "min-sdk-level", description = "Only run this test on devices that support " +
             "this Android SDK/API level")
@@ -436,8 +436,8 @@
         try {
             // use default 5 minutes freshness
             Future<Integer> batteryFuture = device.getBattery();
-            // don't block on battery level, get cached value
-            return batteryFuture.get(1, TimeUnit.MILLISECONDS);
+            // get cached value or wait up to 500ms for battery level query
+            return batteryFuture.get(500, TimeUnit.MILLISECONDS);
         } catch (InterruptedException | ExecutionException |
                 java.util.concurrent.TimeoutException e) {
             CLog.w("Failed to query battery level for %s: %s", device.getSerialNumber(),
diff --git a/src/com/android/tradefed/device/DeviceStateMonitor.java b/src/com/android/tradefed/device/DeviceStateMonitor.java
index 7a28fe7..6f31a50 100644
--- a/src/com/android/tradefed/device/DeviceStateMonitor.java
+++ b/src/com/android/tradefed/device/DeviceStateMonitor.java
@@ -46,6 +46,7 @@
 
     /** the time in ms to wait between 'poll for responsiveness' attempts */
     private static final long CHECK_POLL_TIME = 3 * 1000;
+    private static final long MAX_CHECK_POLL_TIME = 30 * 1000;
     /** the maximum operation time in ms for a 'poll for responsiveness' command */
     private static final int MAX_OP_TIME = 10 * 1000;
 
@@ -160,6 +161,7 @@
         CLog.i("Waiting %d ms for device %s shell to be responsive", waitTime,
                 getSerialNumber());
         long startTime = System.currentTimeMillis();
+        int counter = 1;
         while (System.currentTimeMillis() - startTime < waitTime) {
             final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
             final String cmd = "ls /system/bin/adb";
@@ -178,7 +180,8 @@
             } catch (ShellCommandUnresponsiveException e) {
                 CLog.i("%s failed: %s", cmd, e.getMessage());
             }
-            getRunUtil().sleep(CHECK_POLL_TIME);
+            getRunUtil().sleep(Math.min(CHECK_POLL_TIME * counter, MAX_CHECK_POLL_TIME));
+            counter++;
         }
         CLog.w("Device %s shell is unresponsive", getSerialNumber());
         return false;
@@ -234,6 +237,7 @@
     @Override
     public boolean waitForBootComplete(final long waitTime) {
         CLog.i("Waiting %d ms for device %s boot complete", waitTime, getSerialNumber());
+        int counter = 1;
         long startTime = System.currentTimeMillis();
         final String cmd = "getprop " + BOOTCOMPLETE_PROP;
         while ((System.currentTimeMillis() - startTime) < waitTime) {
@@ -247,7 +251,8 @@
             } catch (ExecutionException e) {
                 CLog.i("%s on device %s failed: %s", cmd, getSerialNumber(), e.getMessage());
             }
-            getRunUtil().sleep(CHECK_POLL_TIME);
+            getRunUtil().sleep(Math.min(CHECK_POLL_TIME * counter, MAX_CHECK_POLL_TIME));
+            counter++;
         }
         CLog.w("Device %s did not boot after %d ms", getSerialNumber(), waitTime);
         return false;
@@ -264,6 +269,7 @@
         CLog.i("Waiting %d ms for device %s package manager",
                 waitTime, getSerialNumber());
         long startTime = System.currentTimeMillis();
+        int counter = 1;
         while (System.currentTimeMillis() - startTime < waitTime) {
             final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
             final String cmd = "pm path android";
@@ -287,7 +293,8 @@
                 Log.i(LOG_TAG, String.format("%s on device %s failed: %s", cmd, getSerialNumber(),
                         e.getMessage()));
             }
-            getRunUtil().sleep(CHECK_POLL_TIME);
+            getRunUtil().sleep(Math.min(CHECK_POLL_TIME * counter, MAX_CHECK_POLL_TIME));
+            counter++;
         }
         Log.w(LOG_TAG, String.format("Device %s package manager is unresponsive",
                 getSerialNumber()));
@@ -305,6 +312,7 @@
         Log.i(LOG_TAG, String.format("Waiting %d ms for device %s external store", waitTime,
                 getSerialNumber()));
         long startTime = System.currentTimeMillis();
+        int counter = 1;
         while (System.currentTimeMillis() - startTime < waitTime) {
             final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
             final CollectingOutputReceiver bitBucket = new CollectingOutputReceiver();
@@ -354,7 +362,8 @@
                 Log.w(LOG_TAG, String.format("Failed to get external store mount point for %s",
                         getSerialNumber()));
             }
-            getRunUtil().sleep(CHECK_POLL_TIME);
+            getRunUtil().sleep(Math.min(CHECK_POLL_TIME * counter, MAX_CHECK_POLL_TIME));
+            counter++;
         }
         Log.w(LOG_TAG, String.format("Device %s external storage is not mounted after %d ms",
                 getSerialNumber(), waitTime));
diff --git a/src/com/android/tradefed/device/FastbootHelper.java b/src/com/android/tradefed/device/FastbootHelper.java
new file mode 100644
index 0000000..d5098e7
--- /dev/null
+++ b/src/com/android/tradefed/device/FastbootHelper.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2014 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.Log.LogLevel;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.IRunUtil;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A helper class for fastboot operations.
+ */
+public class FastbootHelper {
+
+    /** max wait time in ms for fastboot devices command to complete */
+    private static final long FASTBOOT_CMD_TIMEOUT = 1 * 60 * 1000;
+
+    private IRunUtil mRunUtil;
+
+    /**
+     * Constructor.
+     *
+     * @param runUtil a {@link IRunUtil}.
+     */
+    public FastbootHelper(final IRunUtil runUtil) {
+        if (runUtil == null) {
+            throw new IllegalArgumentException("runUtil cannot be null");
+        }
+        mRunUtil = runUtil;
+    }
+
+    /**
+     * Determine if fastboot is available for use.
+     */
+    public boolean isFastbootAvailable() {
+        // Run "fastboot help" to check the existence and the version of fastboot
+        // (Old versions do not support "help" command).
+        CommandResult fastbootResult = mRunUtil.runTimedCmdSilently(5000, "fastboot", "help");
+        if (fastbootResult.getStatus() == CommandStatus.SUCCESS) {
+            return true;
+        }
+        if (fastbootResult.getStderr() != null &&
+            fastbootResult.getStderr().indexOf("usage: fastboot") >= 0) {
+            CLog.logAndDisplay(LogLevel.WARN,
+                    "You are running an older version of fastboot, please update it.");
+            return true;
+        }
+        CLog.d("fastboot not available. stdout: %s, stderr: %s",
+                fastbootResult.getStdout(), fastbootResult.getStderr());
+        return false;
+    }
+
+
+    /**
+     * Returns a set of device serials in fastboot mode.
+     *
+     * @return a set of device serials.
+     */
+    public Set<String> getDevices() {
+        CommandResult fastbootResult = mRunUtil.runTimedCmd(FASTBOOT_CMD_TIMEOUT,
+                "fastboot", "devices");
+        if (fastbootResult.getStatus().equals(CommandStatus.SUCCESS)) {
+            CLog.v("fastboot devices returned\n %s",
+                    fastbootResult.getStdout());
+            return parseDevices(fastbootResult.getStdout());
+        } else {
+            CLog.w("'fastboot devices' failed. Result: %s, stderr: %s", fastbootResult.getStatus(),
+                    fastbootResult.getStderr());
+        }
+        return null;
+    }
+
+    /**
+     * Parses the output of "fastboot devices" command.
+     * Exposed for unit testing.
+     *
+     * @param fastbootOutput the output of fastboot command.
+     * @return a set of device serials.
+     */
+    Set<String> parseDevices(String fastbootOutput) {
+        Set<String> serials = new HashSet<String>();
+        Pattern fastbootPattern = Pattern.compile("([\\w\\d]+)\\s+fastboot\\s*");
+        Matcher fastbootMatcher = fastbootPattern.matcher(fastbootOutput);
+        while (fastbootMatcher.find()) {
+            serials.add(fastbootMatcher.group(1));
+        }
+        return serials;
+    }
+
+    /**
+     * Executes a fastboot command on a device and return the output.
+     *
+     * @param serial a device serial.
+     * @param command a fastboot command to run.
+     * @return the output of the fastboot command. null if the command failed.
+     */
+    public String executeCommand(String serial, String command) {
+        final CommandResult fastbootResult = mRunUtil.runTimedCmd(FASTBOOT_CMD_TIMEOUT,
+                "fastboot", "-s", serial, command);
+        if (fastbootResult.getStatus() != CommandStatus.SUCCESS) {
+            CLog.w("'fastboot -s %s %s' failed. Result: %s, stderr: %s", serial, command,
+                    fastbootResult.getStatus(), fastbootResult.getStderr());
+            return null;
+        }
+        return fastbootResult.getStdout();
+    }
+}
diff --git a/src/com/android/tradefed/device/IMultiDeviceRecovery.java b/src/com/android/tradefed/device/IMultiDeviceRecovery.java
new file mode 100644
index 0000000..351dd19
--- /dev/null
+++ b/src/com/android/tradefed/device/IMultiDeviceRecovery.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 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.tradefed.command.remote.DeviceDescriptor;
+import com.android.tradefed.config.GlobalConfiguration;
+
+import java.util.List;
+
+/**
+ * Interface for recovering multiple offline devices. There are some device recovery methods which
+ * can affect multiple devices (ex) restarting adb, resetting usb, ...). We can implement those
+ * recovery methods through this interface. Once the implementation is configured through
+ * {@link GlobalConfiguration}, {@link #recoverDevices(List<DeviceDescriptor>)} will be called
+ * periodically from {@link DeviceManager}.
+ */
+public interface IMultiDeviceRecovery {
+
+    /**
+     * Recovers offline devices on host.
+     *
+     * @param managedDevices a list of {@link DeviceDescriptor}s.
+     */
+    void recoverDevices(List<DeviceDescriptor> managedDevices);
+
+}
diff --git a/src/com/android/tradefed/device/ITestDevice.java b/src/com/android/tradefed/device/ITestDevice.java
index d659895..9624954 100644
--- a/src/com/android/tradefed/device/ITestDevice.java
+++ b/src/com/android/tradefed/device/ITestDevice.java
@@ -27,6 +27,7 @@
 
 import java.io.File;
 import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
@@ -409,7 +410,7 @@
      * <p/>
      * If connection with device is lost before test run completes, and recovery succeeds, all
      * listeners will be informed of testRunFailed and "false" will be returned. The test command
-     * will not be rerun.. It is left to callers to retry if necessary.
+     * will not be rerun. It is left to callers to retry if necessary.
      * <p/>
      * If connection with device is lost before test run completes, and recovery fails, all
      * listeners will be informed of testRunFailed and DeviceNotAvailableException will be thrown.
@@ -426,8 +427,8 @@
 
     /**
      * Convenience method for performing
-     * {@link #runInstrumentationTests(IRemoteAndroidTestRunner, Collection)} with one or listeners
-     * passed as parameters.
+     * {@link #runInstrumentationTests(IRemoteAndroidTestRunner, Collection)} with one or more
+     * listeners passed as parameters.
      *
      * @param runner the {@link IRemoteAndroidTestRunner} which runs the tests
      * @param listeners the test result listener(s)
@@ -440,6 +441,22 @@
             ITestRunListener... listeners) throws DeviceNotAvailableException;
 
     /**
+     * Same as {@link ITestDevice#runInstrumentationTests(IRemoteAndroidTestRunner, Collection)}
+     * but runs the test for the given user.
+     */
+
+    public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
+            Collection<ITestRunListener> listeners) throws DeviceNotAvailableException;
+
+    /**
+     * Same as
+     * {@link ITestDevice#runInstrumentationTests(IRemoteAndroidTestRunner, ITestRunListener...)}
+     * but runs the test for a given user.
+     */
+    public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
+            ITestRunListener... listeners) throws DeviceNotAvailableException;
+
+    /**
      * Install an Android package on device.
      *
      * @param packageFile the apk file to install
@@ -454,6 +471,59 @@
             throws DeviceNotAvailableException;
 
     /**
+     * Install an Android package on device.
+     * <p>Note: Only use cases that requires explicit control of granting runtime permission at
+     * install time should call this function.
+     * @param packageFile the apk file to install
+     * @param reinstall <code>true</code> if a reinstall should be performed
+     * @param grantPermissions if all runtime permissions should be granted at install time
+     * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
+     *            available options.
+     * @return a {@link String} with an error code, or <code>null</code> if success.
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *             recovered.
+     * @throws UnsupportedOperationException if runtime permission is not supported by the platform
+     *         on device.
+     */
+    public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
+            String... extraArgs) throws DeviceNotAvailableException;
+
+    /**
+     * Install an Android package on device for a given user.
+     *
+     * @param packageFile the apk file to install
+     * @param reinstall <code>true</code> if a reinstall should be performed
+     * @param userId the integer user id to install for.
+     * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
+     *            available options.
+     * @return a {@link String} with an error code, or <code>null</code> if success.
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *             recovered.
+     */
+    public String installPackageForUser(File packageFile, boolean reinstall, int userId,
+            String... extraArgs) throws DeviceNotAvailableException;
+
+    /**
+     * Install an Android package on device for a given user.
+     * <p>Note: Only use cases that requires explicit control of granting runtime permission at
+     * install time should call this function.
+     * @param packageFile the apk file to install
+     * @param reinstall <code>true</code> if a reinstall should be performed
+     * @param grantPermissions if all runtime permissions should be granted at install time
+     * @param userId the integer user id to install for.
+     * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
+     *            available options.
+     * @return a {@link String} with an error code, or <code>null</code> if success.
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *             recovered.
+     * @throws UnsupportedOperationException if runtime permission is not supported by the platform
+     *         on device.
+     */
+    public String installPackageForUser(File packageFile, boolean reinstall,
+            boolean grantPermissions, int userId, String... extraArgs)
+                    throws DeviceNotAvailableException;
+
+    /**
      * Uninstall an Android package from device.
      *
      * @param packageName the Android package to uninstall
@@ -704,11 +774,18 @@
     public InputStreamSource getScreenshot() throws DeviceNotAvailableException;
 
     /**
+     * Clears the last connected wifi network. This should be called when starting a new invocation
+     * to avoid connecting to the wifi network used in the previous test after device reboots.
+     */
+    public void clearLastConnectedWifiNetwork();
+
+    /**
      * Connects to a wifi network.
      * <p/>
      * Turns on wifi and blocks until a successful connection is made to the specified wifi network.
      * Once a connection is made, the instance will try to restore the connection after every reboot
-     * until {@link ITestDevice#disconnectFromWifi()} is called.
+     * until {@link ITestDevice#disconnectFromWifi()} or
+     * {@link ITestDevice#clearLastConnectedWifiNetwork()} is called.
      *
      * @param wifiSsid the wifi ssid to connect to
      * @param wifiPsk PSK passphrase or null if unencrypted
@@ -1107,4 +1184,79 @@
      * @throws DeviceNotAvailableException
      */
     public boolean waitForBootComplete(long timeOut) throws DeviceNotAvailableException;
+
+    /**
+     * Determines if multi user is supported.
+     *
+     * @return true if multi user is supported, false otherwise
+     * @throws DeviceNotAvailableException
+     */
+    public boolean isMultiUserSupported() throws DeviceNotAvailableException;
+
+    /**
+     * Create a user with a given name.
+     *
+     * @param name of the user to create on the device
+     * @return the integer for the user id created
+     * @throws DeviceNotAvailableException
+     */
+    public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException;
+
+    /**
+     * Remove a given user from the device.
+     *
+     * @param userId of the user to remove
+     * @return true if we were succesful in removing the user, false otherwise.
+     * @throws DeviceNotAvailableException
+     */
+    public boolean removeUser(int userId) throws DeviceNotAvailableException;
+
+    /**
+     * Gets the list of users on the device. Defaults to null.
+     *
+     * @return the list of user ids or null if there was an error.
+     * @throws DeviceNotAvailableException
+     */
+    ArrayList<Integer> listUsers() throws DeviceNotAvailableException;
+
+    /**
+     * Get the maximum number of supported users. Defaults to 0.
+     *
+     * @return an integer indicating the number of supported users
+     * @throws DeviceNotAvailableException
+     */
+    public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException;
+
+    /**
+     * Starts a given user in the background if it is currently stopped. If the user is already
+     * running in the background, this method is a NOOP.
+     * @param userId of the user to start in the background
+     * @return true if the user was succesfully started in the background.
+     * @throws DeviceNotAvailableException
+     */
+    public boolean startUser(int userId) throws DeviceNotAvailableException;
+
+    /**
+     * Stops a given user. If the user is already stopped, this method is a NOOP.
+     * @param userId of the user to stop.
+     * @throws DeviceNotAvailableException
+     */
+    public void stopUser(int userId) throws DeviceNotAvailableException;
+
+    /**
+     * Get the stream of emulator stdout and stderr
+     * @return emulator output
+     */
+    public InputStreamSource getEmulatorOutput();
+
+    /**
+     * Close and delete the emulator output.
+     */
+    public void stopEmulatorOutput();
+
+    /**
+     * Make the system partition on the device writable. May reboot the device.
+     * @throws DeviceNotAvailableException
+     */
+    public void remountSystemWritable() throws DeviceNotAvailableException;
 }
diff --git a/src/com/android/tradefed/device/NoDeviceException.java b/src/com/android/tradefed/device/NoDeviceException.java
new file mode 100644
index 0000000..4e81b20
--- /dev/null
+++ b/src/com/android/tradefed/device/NoDeviceException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 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;
+
+/**
+ * Thrown when there's no device to execute a given command.
+ */
+@SuppressWarnings("serial")
+public class NoDeviceException extends Exception {
+    /**
+     * Creates a {@link NoDeviceException}.
+     */
+    public NoDeviceException() {
+        super();
+    }
+
+    /**
+     * Creates a {@link NoDeviceException}.
+     *
+     * @param msg a descriptive message.
+     */
+    public NoDeviceException(String msg) {
+        super(msg);
+    }
+
+    /**
+     * Creates a {@link NoDeviceException}.
+     *
+     * @param msg a descriptive message.
+     * @param cause the root {@link Throwable} that caused the device to become unavailable.
+     */
+    public NoDeviceException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}
diff --git a/src/com/android/tradefed/device/StubDevice.java b/src/com/android/tradefed/device/StubDevice.java
index 6dc783b..09148a9 100644
--- a/src/com/android/tradefed/device/StubDevice.java
+++ b/src/com/android/tradefed/device/StubDevice.java
@@ -31,6 +31,8 @@
 import com.google.common.util.concurrent.SettableFuture;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
@@ -134,6 +136,7 @@
      * {@inheritDoc}
      */
     @Override
+    @Deprecated
     public Map<String, String> getProperties() {
         return null;
     }
@@ -150,6 +153,7 @@
      * {@inheritDoc}
      */
     @Override
+    @Deprecated
     public int getPropertyCount() {
         return 0;
     }
@@ -163,6 +167,16 @@
         throw new IOException("stub");
     }
 
+    /* (not javadoc)
+     * The parent method has no javadoc, so it's invalid for us to attempt to inherit
+     */
+    @Override
+    public RawImage getScreenshot(long timeout, TimeUnit unit)
+        throws TimeoutException, AdbCommandRejectedException, IOException {
+
+        throw new IOException("stub");
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -209,6 +223,15 @@
      * {@inheritDoc}
      */
     @Override
+    public void installPackages(List<String> apkFilePaths, int timeOutInMs, boolean reinstall,
+            String... extraArgs) throws InstallException {
+        throw new InstallException(new IOException("stub"));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public String installRemotePackage(String remoteFilePath, boolean reinstall,
             String... extraArgs) throws InstallException {
         throw new InstallException(new IOException("stub"));
@@ -329,6 +352,7 @@
      * {@inheritDoc}
      */
     @Override
+    @Deprecated
     public String getPropertySync(String name) throws TimeoutException,
             AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
         return null;
@@ -346,6 +370,7 @@
      * {@inheritDoc}
      */
     @Override
+    @Deprecated
     public String getPropertyCacheOrSync(String name) throws TimeoutException,
             AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
         return null;
@@ -355,6 +380,7 @@
      * {@inheritDoc}
      */
     @Override
+    @Deprecated
     public Integer getBatteryLevel() throws TimeoutException, AdbCommandRejectedException,
             IOException, ShellCommandUnresponsiveException {
         return null;
@@ -364,6 +390,7 @@
      * {@inheritDoc}
      */
     @Override
+    @Deprecated
     public Integer getBatteryLevel(long freshnessMs) throws TimeoutException,
             AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException {
         return null;
@@ -423,6 +450,7 @@
     public void startScreenRecorder(String remoteFilePath, ScreenRecorderOptions options,
             IShellOutputReceiver receiver) throws TimeoutException, AdbCommandRejectedException,
             IOException, ShellCommandUnresponsiveException {
+        // no-op
     }
 
     /* (non-Javadoc)
@@ -460,4 +488,36 @@
     public Future<Integer> getBattery(long freshnessTime, TimeUnit timeUnit) {
         return getBattery();
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public List<String> getAbis() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getDensity() {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLanguage() {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getRegion() {
+        return null;
+    }
 }
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index e075343..cd2f834 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -31,10 +31,12 @@
 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;
@@ -42,6 +44,7 @@
 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.BufferedImage;
@@ -58,6 +61,7 @@
 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;
@@ -80,6 +84,8 @@
     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])");
@@ -95,7 +101,7 @@
     /** 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 int ENCRYPTION_WIPE_TIMEOUT_MIN = 5;
+    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". */
@@ -115,14 +121,23 @@
     /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
     static final String DISMISS_DIALOG_CMD = "input keyevent 23";
 
-    private static final String BUILD_ID_PROP = "ro.build.version.incremental";
+    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";
 
     /** 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. */
@@ -134,11 +149,11 @@
     private TestDeviceState mState = TestDeviceState.ONLINE;
     private final ReentrantLock mFastbootLock = new ReentrantLock();
     private LogcatReceiver mLogcatReceiver;
-    private IFileEntry mRootFile = null;
     private boolean mFastbootEnabled = true;
 
     private TestDeviceOptions mOptions = new TestDeviceOptions();
     private Process mEmulatorProcess;
+    private SizeLimitedOutputStream mEmulatorOutput;
 
     private RecoveryMode mRecoveryMode = RecoveryMode.AVAILABLE;
 
@@ -148,8 +163,8 @@
     private DeviceAllocationState mAllocationState = DeviceAllocationState.Unknown;
     private IDeviceMonitor mAllocationMonitor = null;
 
-    private String mWifiSsid = null;
-    private String mWifiPsk = null;
+    private String mLastConnectedWifiSsid = null;
+    private String mLastConnectedWifiPsk = null;
     private boolean mNetworkMonitorEnabled = false;
 
     /**
@@ -485,6 +500,10 @@
      */
     @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) {
@@ -584,6 +603,56 @@
         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;
 
@@ -612,15 +681,68 @@
      * {@inheritDoc}
      */
     @Override
-    public String installPackage(final File packageFile, final boolean reinstall,
-            final String... extraArgs) throws DeviceNotAvailableException {
+    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;
+    }
+
+    /**
+     * Check whether platform on device supports runtime permission granting
+     * @return
+     * @throws DeviceNotAvailableException
+     */
+    boolean isRuntimePermissionSupported() throws DeviceNotAvailableException {
+        //TODO: change to API Level check once M is official
+        String codeName = getProperty(BUILD_CODENAME_PROP).trim();
+        if (!"MNC".equals(codeName)) {
+            // not MNC, probably REL or LMP or older
+            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
+            // for MNC 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);
+                        reinstall, extraArgs.toArray(new String[]{}));
                 response[0] = result;
                 return result == null;
             }
@@ -633,6 +755,69 @@
     /**
      * {@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
@@ -842,7 +1027,13 @@
         CLog.i("Checking free space for %s", getSerialNumber());
         String externalStorePath = getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
         String output = getDfOutput(externalStorePath);
-        Long available = parseFreeSpaceFromAvailable(output);
+        // Try coreutils/toybox style output first.
+        Long available = parseFreeSpaceFromModernOutput(externalStorePath, output);
+        if (available != null) {
+            return available;
+        }
+        // Then the two legacy toolbox formats.
+        available = parseFreeSpaceFromAvailable(output);
         if (available != null) {
             return available;
         }
@@ -875,7 +1066,8 @@
     }
 
     /**
-     * Parses a partitions available space from the legacy output of a 'df' command.
+     * Parses a partition's available space from the legacy output of a 'df' command, used
+     * pre-gingerbread.
      * <p/>
      * Assumes output format of:
      * <br>/
@@ -900,7 +1092,8 @@
     }
 
     /**
-     * Parses a partitions available space from the 'table-formatted' output of a 'df' command.
+     * 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/>
@@ -937,6 +1130,35 @@
     }
 
     /**
+     * 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 externalStorePath, String dfOutput) {
+        final Pattern pattern = Pattern.compile(String.format(
+                //Fs 1K-blks Used    Available Use%      Mounted on
+                "\\s+\\d+\\s+\\d+\\s+(\\d+)\\s+\\d+%%\\s+%s", externalStorePath));
+        Matcher matcher = pattern.matcher(dfOutput);
+        if (matcher.find()) {
+            try {
+                return Long.parseLong(matcher.group(1));
+            } catch (NumberFormatException e) {
+                // fall through
+            }
+        }
+        return null;
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
@@ -950,7 +1172,7 @@
     @Override
     public List<MountPointInfo> getMountPointInfo() throws DeviceNotAvailableException {
         final String mountInfo = executeShellCommand("cat /proc/mounts");
-        final String[] mountInfoLines = mountInfo.split("\r\n");
+        final String[] mountInfoLines = mountInfo.split("\r?\n");
         List<MountPointInfo> list = new ArrayList<MountPointInfo>(mountInfoLines.length);
 
         for (String line : mountInfoLines) {
@@ -983,11 +1205,9 @@
     public IFileEntry getFileEntry(String path) throws DeviceNotAvailableException {
         path = interpolatePathVariables(path);
         String[] pathComponents = path.split(FileListingService.FILE_SEPARATOR);
-        if (mRootFile == null) {
-            FileListingService service = getFileListingService();
-            mRootFile = new FileEntryWrapper(this, service.getRoot());
-        }
-        return FileEntryWrapper.getDescendant(mRootFile, Arrays.asList(pathComponents));
+        FileListingService service = getFileListingService();
+        IFileEntry rootFile = new FileEntryWrapper(this, service.getRoot());
+        return FileEntryWrapper.getDescendant(rootFile, Arrays.asList(pathComponents));
     }
 
     /**
@@ -1070,7 +1290,7 @@
         deviceFilePath = String.format("%s/%s", interpolatePathVariables(deviceFilePath),
                 localFileDir.getName());
         if (!doesFileExist(deviceFilePath)) {
-            executeShellCommand(String.format("mkdir %s", deviceFilePath));
+            executeShellCommand(String.format("mkdir -p %s", deviceFilePath));
         }
         IFileEntry remoteFileEntry = getFileEntry(deviceFilePath);
         if (remoteFileEntry == null) {
@@ -1697,13 +1917,26 @@
      * {@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.
-        mWifiSsid = null;
-        mWifiPsk = null;
+        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());
@@ -1714,8 +1947,8 @@
                 CLog.i("Successfully connected to wifi network %s(%s) on %s",
                         wifiSsid, wifiInfo.get("bssid"), getSerialNumber());
 
-                mWifiSsid = wifiSsid;
-                mWifiPsk = wifiPsk;
+                mLastConnectedWifiSsid = wifiSsid;
+                mLastConnectedWifiPsk = wifiPsk;
 
                 return true;
             } else {
@@ -1723,17 +1956,35 @@
                         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;
     }
 
     /**
-     * Check that device has network connectivity.
+     * {@inheritDoc}
      */
     @Override
     public boolean checkConnectivity() throws DeviceNotAvailableException {
-        final IWifiHelper wifi = createWifiHelper();
-        return wifi.checkConnectivity(mOptions.getConnCheckUrl());
+        final int pingLoss = getPingLoss();
+        return (0 <= pingLoss && pingLoss < 100);
+    }
+
+    int getPingLoss() throws DeviceNotAvailableException {
+        final String output = executeShellCommand(
+                "ping -c 1 -w 5 -s 1024 " + mOptions.getPingIpOrHost());
+        final Matcher stat = PING_REGEX.matcher(output);
+        if (stat.find()) {
+            return Integer.parseInt(stat.group("loss"));
+        }
+        // Return -1 if we failed to parse output.
+        return -1;
     }
 
     /**
@@ -1803,8 +2054,8 @@
     public boolean disconnectFromWifi() throws DeviceNotAvailableException {
         CLog.i("Disconnecting from wifi on %s", getSerialNumber());
         // Clears the last connected wifi network.
-        mWifiSsid = null;
-        mWifiPsk = null;
+        mLastConnectedWifiSsid = null;
+        mLastConnectedWifiPsk = null;
 
         IWifiHelper wifi = createWifiHelper();
         return wifi.disconnectFromNetwork();
@@ -1944,14 +2195,20 @@
         if (mOptions.isDisableKeyguard()) {
             disableKeyguard();
         }
-        if (mWifiSsid != null) {
-            // mWifiSsid is set to null if connection fails
-            final String wifiSsid = mWifiSsid;
-            if (!connectToWifiNetworkIfNeeded(mWifiSsid, mWifiPsk)) {
-                throw new NetworkNotAvailableException(
-                        String.format("Failed to connect to wifi network %s on %s after reboot",
-                                wifiSsid, getSerialNumber()));
-            }
+        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()) {
@@ -1960,6 +2217,28 @@
         }
     }
 
+    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()));
+        }
+    }
+
     // TODO: consider exposing this method
     private void postOnlineSetup() throws DeviceNotAvailableException  {
         if (isEnableAdbRoot()) {
@@ -2074,6 +2353,7 @@
 
         if (mStateMonitor.waitForDeviceAvailable(mOptions.getRebootTimeout()) != null) {
             postBootSetup();
+            postBootWifiSetup();
             return;
         } else {
             recoverDevice();
@@ -2154,6 +2434,11 @@
         // 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
@@ -2275,7 +2560,7 @@
         enableAdbRoot();
 
         String encryptMethod;
-        int timeout;
+        long timeout;
         if (inplace) {
             encryptMethod = "inplace";
             timeout = ENCRYPTION_INPLACE_TIMEOUT_MIN;
@@ -2327,8 +2612,8 @@
         if (!mOptions.getUseFastbootErase()) {
             rebootIntoBootloader();
             fastbootWipePartition("userdata");
-            reboot();
-
+            rebootUntilOnline();
+            waitForDeviceAvailable(ENCRYPTION_WIPE_TIMEOUT_MIN * 60 * 1000);
             return true;
         }
 
@@ -2337,7 +2622,7 @@
         String output = executeShellCommand("vdc volume list");
         String[] splitOutput;
         if (output != null) {
-            splitOutput = output.split("\r\n");
+            splitOutput = output.split("\r?\n");
             for (String line : splitOutput) {
                 if (line.startsWith("110 ") && line.contains("sdcard /mnt/sdcard") &&
                         !line.endsWith("0")) {
@@ -2371,7 +2656,7 @@
                 setRecoveryMode(cachedRecoveryMode);
                 return false;
             }
-            splitOutput = output.split("\r\n");
+            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);
@@ -2664,6 +2949,46 @@
     }
 
     /**
+     * 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
@@ -2862,4 +3187,135 @@
     public boolean waitForBootComplete(long timeOut) throws DeviceNotAvailableException {
         return mStateMonitor.waitForBootComplete(timeOut);
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ArrayList<Integer> listUsers() 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<Integer> users = new ArrayList<Integer>();
+        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(Integer.parseInt(tokens[1]));
+        }
+        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");
+        long verityVersion = 0;
+        if (verity != null && !verity.isEmpty()) {
+            try {
+                verityVersion = Long.parseLong(verity);
+            } catch (NumberFormatException nfe) {
+                // ignore but assign an arbitrary number since it's non-empty
+                CLog.d("unrecognized property value partition.system.verified=%s", verity);
+                verityVersion = 1;
+            }
+        }
+        if (verityVersion > 0) {
+            executeAdbCommand("disable-verity");
+            reboot();
+        }
+        executeAdbCommand("remount");
+        waitForDeviceAvailable();
+    }
 }
diff --git a/src/com/android/tradefed/device/TestDeviceOptions.java b/src/com/android/tradefed/device/TestDeviceOptions.java
index a657e32..b7b43a4 100644
--- a/src/com/android/tradefed/device/TestDeviceOptions.java
+++ b/src/com/android/tradefed/device/TestDeviceOptions.java
@@ -17,6 +17,9 @@
 
 import com.android.tradefed.config.Option;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Container for {@link ITestDevice} {@link Option}s
  */
@@ -82,6 +85,19 @@
             description = "default number of attempts to connect to wifi network.")
     private int mWifiAttempts = 5;
 
+    @Option(name = "wifi-retry-wait-time",
+            description = "the base wait time in ms between wifi connect retries. "
+            + "The actual wait time would be a multiple of this value.")
+    private int mWifiRetryWaitTime = 60 * 1000;
+
+    @Option(name = "post-boot-command",
+            description = "shell command to run after reboots during invocation")
+    private List<String> mPostBootCommands = new ArrayList<String>();
+
+    @Option(name = "cutoff-battery", description =
+            "the minimum battery level required to continue the invocation. Scale: 0-100")
+    private Integer mCutoffBattery = null;
+
     /**
      * Check whether adb root should be enabled on boot for this device
      */
@@ -269,4 +285,24 @@
         mWifiAttempts = wifiAttempts;
     }
 
+    /**
+     * @return the base wait time between wifi connect retries.
+     */
+    public int getWifiRetryWaitTime() {
+        return mWifiRetryWaitTime;
+    }
+
+    /**
+     * @return a list of shell commands to run after reboots.
+     */
+    public List<String> getPostBootCommands() {
+        return mPostBootCommands;
+    }
+
+    /**
+     * @return the minimum battery level to continue the invocation.
+     */
+    public Integer getCutoffBattery() {
+        return mCutoffBattery;
+    }
 }
diff --git a/src/com/android/tradefed/device/WaitDeviceRecovery.java b/src/com/android/tradefed/device/WaitDeviceRecovery.java
index 823b2aa..7a97952 100644
--- a/src/com/android/tradefed/device/WaitDeviceRecovery.java
+++ b/src/com/android/tradefed/device/WaitDeviceRecovery.java
@@ -21,6 +21,8 @@
 import com.android.ddmlib.TimeoutException;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
 
@@ -58,6 +60,10 @@
             description="maximum time in ms to wait for device shell to be responsive.")
     protected long mShellWaitTime = 30 * 1000;
 
+    @Option(name="fastboot-wait-time",
+            description="maximum time in ms to wait for a fastboot command result.")
+    protected long mFastbootWaitTime = 30 * 1000;
+
     @Option(name = "min-battery-after-recovery",
             description = "require a min battery level after successful recovery, " +
                           "default to 0 for ignoring.")
@@ -106,7 +112,7 @@
                     "Found device %s in fastboot but expected online. Rebooting...",
                     monitor.getSerialNumber()));
             // TODO: retry if failed
-            getRunUtil().runTimedCmd(20*1000, "fastboot", "-s", monitor.getSerialNumber(),
+            getRunUtil().runTimedCmd(mFastbootWaitTime, "fastboot", "-s", monitor.getSerialNumber(),
                     "reboot");
         }
 
@@ -115,7 +121,7 @@
         if (device == null) {
             handleDeviceNotAvailable(monitor, recoverUntilOnline);
             // function returning implies that recovery is successful, check battery level here
-            checkMinBatteryLevel(device);
+            checkMinBatteryLevel(getDeviceAfterRecovery(monitor));
             return;
         }
         // occasionally device is erroneously reported as online - double check that we can shell
@@ -123,7 +129,7 @@
         if (!monitor.waitForDeviceShell(mShellWaitTime)) {
             // treat this as a not available device
             handleDeviceNotAvailable(monitor, recoverUntilOnline);
-            checkMinBatteryLevel(device);
+            checkMinBatteryLevel(getDeviceAfterRecovery(monitor));
             return;
         }
 
@@ -135,7 +141,17 @@
         }
         // do a final check here when all previous if blocks are skipped or the last
         // handleDeviceUnresponsive was successful
-        checkMinBatteryLevel(device);
+        checkMinBatteryLevel(getDeviceAfterRecovery(monitor));
+    }
+
+    private IDevice getDeviceAfterRecovery(IDeviceStateMonitor monitor)
+            throws DeviceNotAvailableException {
+        IDevice device = monitor.waitForDeviceOnline();
+        if (device == null) {
+            throw new DeviceNotAvailableException(
+                    "Device still not online after successful recovery");
+        }
+        return device;
     }
 
     /**
@@ -262,7 +278,7 @@
         CLog.i("Found device %s in fastboot but potentially unresponsive.",
                 monitor.getSerialNumber());
         // TODO: retry reboot
-        getRunUtil().runTimedCmd(20*1000, "fastboot", "-s", monitor.getSerialNumber(),
+        getRunUtil().runTimedCmd(mFastbootWaitTime, "fastboot", "-s", monitor.getSerialNumber(),
                 "reboot-bootloader");
         // wait for device to reboot
         monitor.waitForDeviceNotAvailable(20*1000);
@@ -270,6 +286,13 @@
             throw new DeviceNotAvailableException(String.format(
                     "Device %s not in bootloader after reboot", monitor.getSerialNumber()));
         }
+        // running a meaningless command just to see whether the device is responsive.
+        CommandResult result = getRunUtil().runTimedCmd(mFastbootWaitTime, "fastboot", "-s",
+                monitor.getSerialNumber(), "getvar", "product");
+        if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
+            throw new DeviceNotAvailableException(String.format(
+                    "Device %s is in fastboot but unresponsive", monitor.getSerialNumber()));
+        }
     }
 
     /**
diff --git a/src/com/android/tradefed/device/WifiHelper.java b/src/com/android/tradefed/device/WifiHelper.java
index 2a9081c..2fb6fed 100644
--- a/src/com/android/tradefed/device/WifiHelper.java
+++ b/src/com/android/tradefed/device/WifiHelper.java
@@ -50,7 +50,7 @@
     static final String CHECK_PACKAGE_CMD =
             String.format("dumpsys package %s", INSTRUMENTATION_PKG);
     static final Pattern PACKAGE_VERSION_PAT = Pattern.compile("versionCode=(\\d*)");
-    static final int PACKAGE_VERSION_CODE = 20;
+    static final int PACKAGE_VERSION_CODE = 21;
 
     private static final String WIFIUTIL_APK_NAME = "WifiUtil.apk";
 
@@ -352,9 +352,9 @@
         if (result != null) {
             try {
                 final JSONObject json = new JSONObject(result);
-                final Iterator keys = json.keys();
+                final Iterator<String> keys = json.keys();
                 while (keys.hasNext()) {
-                    final String key = (String) keys.next();
+                    final String key = keys.next();
                     info.put(key, json.getString(key));
                 }
             } catch(final JSONException e) {
diff --git a/src/com/android/tradefed/invoker/ShardListener.java b/src/com/android/tradefed/invoker/ShardListener.java
index 273ef34..acc149f 100644
--- a/src/com/android/tradefed/invoker/ShardListener.java
+++ b/src/com/android/tradefed/invoker/ShardListener.java
@@ -16,14 +16,14 @@
 package com.android.tradefed.invoker;
 
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestResult.TestStatus;
-import com.android.tradefed.result.TestRunResult;
 
 import java.util.Map;
 
@@ -104,12 +104,18 @@
     private void forwardTestResults(Map<TestIdentifier, TestResult> testResults) {
         for (Map.Entry<TestIdentifier, TestResult> testEntry : testResults.entrySet()) {
             mMasterListener.testStarted(testEntry.getKey());
-            if (testEntry.getValue().getStatus().equals(TestStatus.ERROR)) {
-                mMasterListener.testFailed(TestFailure.ERROR, testEntry.getKey(),
-                        testEntry.getValue().getStackTrace());
-            } else if (testEntry.getValue().getStatus().equals(TestStatus.FAILURE)) {
-                mMasterListener.testFailed(TestFailure.FAILURE, testEntry.getKey(),
-                        testEntry.getValue().getStackTrace());
+            switch (testEntry.getValue().getStatus()) {
+                case FAILURE:
+                    mMasterListener.testFailed(testEntry.getKey(),
+                            testEntry.getValue().getStackTrace());
+                    break;
+                case ASSUMPTION_FAILURE:
+                    mMasterListener.testAssumptionFailure(testEntry.getKey(),
+                            testEntry.getValue().getStackTrace());
+                    break;
+                case IGNORED:
+                    mMasterListener.testIgnored(testEntry.getKey());
+                    break;
             }
             if (!testEntry.getValue().getStatus().equals(TestStatus.INCOMPLETE)) {
                 mMasterListener.testEnded(testEntry.getKey(), testEntry.getValue().getMetrics());
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 02d9740..4b32f36 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -49,6 +49,9 @@
 import com.android.tradefed.testtype.IResumableTest;
 import com.android.tradefed.testtype.IRetriableTest;
 import com.android.tradefed.testtype.IShardableTest;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunInterruptedException;
+import com.android.tradefed.util.RunUtil;
 
 import junit.framework.Test;
 
@@ -74,9 +77,11 @@
 
     static final String TRADEFED_LOG_NAME = "host_log";
     static final String DEVICE_LOG_NAME = "device_logcat";
+    static final String EMULATOR_LOG_NAME = "emulator_log";
     static final String BUILD_ERROR_BUGREPORT_NAME = "build_error_bugreport";
     static final String DEVICE_UNRESPONSIVE_BUGREPORT_NAME = "device_unresponsive_bugreport";
     static final String INVOCATION_ENDED_BUGREPORT_NAME = "invocation_ended_bugreport";
+    static final String TARGET_SETUP_ERROR_BUGREPORT_NAME = "target_setup_error_bugreport";
     static final String BATT_TAG = "[battery level]";
 
     private String mStatus = "(not invoked)";
@@ -197,15 +202,21 @@
         ITestInvocationListener listener = new LogSaverResultForwarder(config.getLogSaver(),
                 allListeners);
 
+        IBuildInfo info = null;
+
         try {
             mStatus = "fetching build";
             config.getLogOutput().init();
             getLogRegistry().registerLogger(config.getLogOutput());
+            device.clearLastConnectedWifiNetwork();
             device.setOptions(config.getDeviceOptions());
             if (config.getDeviceOptions().isLogcatCaptureEnabled()) {
                 device.startLogcat();
             }
-            IBuildInfo info = null;
+            String cmdLineArgs = config.getCommandLine();
+            if (cmdLineArgs != null) {
+                CLog.i("Invocation was started with cmd: %s", cmdLineArgs);
+            }
             if (config.getBuildProvider() instanceof IDeviceBuildProvider) {
                 info = ((IDeviceBuildProvider)config.getBuildProvider()).getBuild(device);
             } else {
@@ -428,32 +439,38 @@
             IRescheduler rescheduler, ITestInvocationListener listener) throws Throwable {
 
         boolean resumed = false;
+        String bugreportName = null;
         long startTime = System.currentTimeMillis();
         long elapsedTime = -1;
+        Throwable exception = null;
+        Exception tearDownException = null;
 
         info.setDeviceSerial(device.getSerialNumber());
         startInvocation(config, device, info, listener);
         try {
             logDeviceBatteryLevel(device, "initial");
-
             prepareAndRun(config, device, info, listener);
         } catch (BuildError e) {
+            exception = e;
             CLog.w("Build %s failed on device %s. Reason: %s", info.getBuildId(),
                     device.getSerialNumber(), e.toString());
-            takeBugreport(device, listener, BUILD_ERROR_BUGREPORT_NAME);
+            bugreportName = BUILD_ERROR_BUGREPORT_NAME;
             reportFailure(e, listener, config, info, rescheduler);
         } catch (TargetSetupError e) {
+            exception = e;
             CLog.e("Caught exception while running invocation");
             CLog.e(e);
+            bugreportName = TARGET_SETUP_ERROR_BUGREPORT_NAME;
             reportFailure(e, listener, config, info, rescheduler);
         } catch (DeviceNotAvailableException e) {
+            exception = e;
             // log a warning here so its captured before reportLogs is called
             CLog.w("Invocation did not complete due to device %s becoming not available. " +
                     "Reason: %s", device.getSerialNumber(), e.getMessage());
             if ((e instanceof DeviceUnresponsiveException)
                     && TestDeviceState.ONLINE.equals(device.getDeviceState())) {
                 // under certain cases it might still be possible to grab a bugreport
-                takeBugreport(device, listener, DEVICE_UNRESPONSIVE_BUGREPORT_NAME);
+                bugreportName = DEVICE_UNRESPONSIVE_BUGREPORT_NAME;
             }
             resumed = resume(config, info, rescheduler, System.currentTimeMillis() - startTime);
             if (!resumed) {
@@ -462,16 +479,45 @@
                 CLog.i("Rescheduled failed invocation for resume");
             }
             throw e;
-        } catch (RuntimeException e) {
-            // log a warning here so its captured before reportLogs is called
-            CLog.e("Unexpected exception when running invocation: %s", e.toString());
+        } catch (RunInterruptedException e) {
+            CLog.w("Invocation interrupted");
+            reportFailure(e, listener, config, info, rescheduler);
+        } catch (AssertionError e) {
+            exception = e;
+            CLog.e("Caught AssertionError while running invocation: %s", e.toString());
             CLog.e(e);
             reportFailure(e, listener, config, info, rescheduler);
-            throw e;
-        } catch (AssertionError e) {
-            CLog.w("Caught AssertionError while running invocation: ", e.toString());
-            reportFailure(e, listener, config, info, rescheduler);
+        } catch (Throwable t) {
+            exception = t;
+            // log a warning here so its captured before reportLogs is called
+            CLog.e("Unexpected exception when running invocation: %s", t.toString());
+            CLog.e(t);
+            reportFailure(t, listener, config, info, rescheduler);
+            throw t;
         } finally {
+            getRunUtil().allowInterrupt(false);
+            if (config.getCommandOptions().takeBugreportOnInvocationEnded()) {
+                if (bugreportName != null) {
+                    CLog.i("Bugreport to be taken for failure instead of invocation ended.");
+                } else {
+                    bugreportName = INVOCATION_ENDED_BUGREPORT_NAME;
+                }
+            }
+            if (bugreportName != null) {
+                takeBugreport(device, listener, bugreportName);
+            }
+            mStatus = "tearing down";
+            try {
+                doTeardown(config, device, info, exception);
+            } catch (DeviceNotAvailableException|RuntimeException e) {
+                tearDownException = e;
+                if (exception == null) {
+                    // only log & report when the exception is new during tear down
+                    CLog.e("Exception when tearing down invocation: %s", tearDownException.toString());
+                    CLog.e(tearDownException);
+                    reportFailure(tearDownException, listener, config, info, rescheduler);
+                }
+            }
             mStatus = "done running tests";
             try {
                 reportLogs(device, listener, config.getLogOutput());
@@ -483,6 +529,12 @@
                 config.getBuildProvider().cleanUp(info);
             }
         }
+        if (tearDownException != null) {
+            // this means a DNAE or RTE has happened during teardown, need to throw
+            // if there was a preceding RTE or DNAE stored in 'exception', it would have already
+            // been thrown before exiting the previous try...catch...finally block
+            throw tearDownException;
+        }
     }
 
     /**
@@ -490,37 +542,12 @@
      */
     private void prepareAndRun(IConfiguration config, ITestDevice device, IBuildInfo info,
             ITestInvocationListener listener) throws Throwable {
-        // use the JUnit3 logic for handling exceptions when running tests
-        Throwable exception = null;
-
-        try {
-            logDeviceBatteryLevel(device, "initial -> setup");
-            doSetup(config, device, info);
-            logDeviceBatteryLevel(device, "setup -> test");
-            runTests(device, config, listener);
-            logDeviceBatteryLevel(device, "after test");
-        } catch (Throwable running) {
-            exception = running;
-        } finally {
-            try {
-                if (config.getCommandOptions().takeBugreportOnInvocationEnded()) {
-                    takeBugreport(device, listener, INVOCATION_ENDED_BUGREPORT_NAME);
-                }
-            } catch (Throwable bugreport) {
-                exception = bugreport;
-            } finally {
-                try {
-                    doTeardown(config, device, info, exception);
-                } catch (Throwable tearingDown) {
-                    if (exception == null) {
-                        exception = tearingDown;
-                    }
-                }
-            }
-        }
-        if (exception != null) {
-            throw exception;
-        }
+        getRunUtil().allowInterrupt(true);
+        logDeviceBatteryLevel(device, "initial -> setup");
+        doSetup(config, device, info);
+        logDeviceBatteryLevel(device, "setup -> test");
+        runTests(device, config, listener);
+        logDeviceBatteryLevel(device, "after test");
     }
 
     private void doSetup(IConfiguration config, ITestDevice device, IBuildInfo info)
@@ -532,7 +559,8 @@
 
     private void doTeardown(IConfiguration config, ITestDevice device, IBuildInfo info,
             Throwable exception) throws DeviceNotAvailableException {
-
+        // Clear wifi settings, to prevent wifi errors from interfering with teardown process.
+        device.clearLastConnectedWifiNetwork();
         List<ITargetPreparer> preparers = config.getTargetPreparers();
         ListIterator<ITargetPreparer> itr = preparers.listIterator(preparers.size());
         while (itr.hasPrevious()) {
@@ -624,14 +652,21 @@
             ILeveledLogOutput logger) {
         InputStreamSource logcatSource = null;
         InputStreamSource globalLogSource = logger.getLog();
+        InputStreamSource emulatorOutput = null;
         if (device != null) {
             logcatSource = device.getLogcat();
             device.stopLogcat();
+            if (device.getIDevice() != null && device.getIDevice().isEmulator()) {
+                emulatorOutput = device.getEmulatorOutput();
+            }
         }
 
         if (logcatSource != null) {
             listener.testLog(DEVICE_LOG_NAME, LogDataType.LOGCAT, logcatSource);
         }
+        if (emulatorOutput != null) {
+            listener.testLog(EMULATOR_LOG_NAME, LogDataType.TEXT, emulatorOutput);
+        }
         listener.testLog(TRADEFED_LOG_NAME, LogDataType.TEXT, globalLogSource);
 
 
@@ -639,6 +674,9 @@
         if (logcatSource != null) {
             logcatSource.cancel();
         }
+        if (emulatorOutput != null) {
+            emulatorOutput.cancel();
+        }
         globalLogSource.cancel();
 
         // once tradefed log is reported, all further log calls for this invocation can get lost
@@ -671,6 +709,15 @@
     }
 
     /**
+     * Utility method to fetch the default {@link IRunUtil} singleton
+     * <p />
+     * Exposed for unit testing.
+     */
+    IRunUtil getRunUtil() {
+        return RunUtil.getDefault();
+    }
+
+    /**
      * Runs the test.
      *
      * @param device the {@link ITestDevice} to run tests on
diff --git a/src/com/android/tradefed/log/TerribleFailureEmailHandler.java b/src/com/android/tradefed/log/TerribleFailureEmailHandler.java
index 80432e7..c89bc94 100644
--- a/src/com/android/tradefed/log/TerribleFailureEmailHandler.java
+++ b/src/com/android/tradefed/log/TerribleFailureEmailHandler.java
@@ -55,7 +55,14 @@
             description = "The prefix to be added to the beginning of the email subject.")
     private String mSubjectPrefix = DEFAULT_SUBJECT_PREFIX;
 
+    @Option(name = "min-email-interval",
+            description = "The minimum interval between emails in ms. " +
+                    "If a new WTF happens within this interval from the previous one, " +
+                    "it will be ignored.")
+    private long mMinEmailInterval = 5 * 60 * 1000;
+
     private IEmail mMailer;
+    private long mLastEmailSentTime = 0;
 
     /**
      * Create a {@link TerribleFailureEmailHandler}
@@ -95,6 +102,15 @@
     }
 
     /**
+     * Sets the minimum email interval.
+     *
+     * @param interval
+     */
+    public void setMinEmailInterval(long interval) {
+        mMinEmailInterval = interval;
+    }
+
+    /**
      * Gets the local host name of the machine.
      *
      * @return the name of the host machine, or "unknown host" if unknown
@@ -109,6 +125,13 @@
     }
 
     /**
+     * Gets the current time in milliseconds.
+     */
+    protected long getCurrentTimeMillis() {
+        return System.currentTimeMillis();
+    }
+
+    /**
      * A method to generate the subject for email reports.
      * The subject will be formatted as:
      *     "<subject-prefix> on <local-host-name>"
@@ -184,6 +207,14 @@
             return false;
         }
 
+        final long now = getCurrentTimeMillis();
+        if (0 < mMinEmailInterval && now - mLastEmailSentTime < mMinEmailInterval) {
+            // TODO: consider queuing up skipped failures and send it later.
+            CLog.w("Skipped to send %s email: email interval %dms < %dms", DEFAULT_SUBJECT_PREFIX,
+                    now - mLastEmailSentTime, mMinEmailInterval);
+            return false;
+        }
+
         Message msg = generateEmailMessage(description, cause);
 
         try {
@@ -197,6 +228,8 @@
             CLog.e(e);
             return false;
         }
+
+        mLastEmailSentTime = now;
         return true;
     }
 
diff --git a/src/com/android/tradefed/result/BugreportCollector.java b/src/com/android/tradefed/result/BugreportCollector.java
index 153d393..15d3247 100644
--- a/src/com/android/tradefed/result/BugreportCollector.java
+++ b/src/com/android/tradefed/result/BugreportCollector.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.result;
 
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -337,7 +338,7 @@
                         break;
 
                     case FAILED_TESTCASE:
-                        if (curResult.getNumFailedTests() + curResult.getNumErrorTests() == 1) {
+                        if (curResult.getNumAllFailedTests() == 1) {
                             applicableFreqs.add(Freq.FIRST);
                         }
                         break;
@@ -443,9 +444,20 @@
      * {@inheritDoc}
      */
     @Override
-    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
-        mListener.testFailed(status, test, trace);
-        mCollector.testFailed(status, test, trace);
+    public void testFailed(TestIdentifier test, String trace) {
+        mListener.testFailed(test, trace);
+        mCollector.testFailed(test, trace);
+        check(Relation.AFTER, Noun.FAILED_TESTCASE, test);
+        reset();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testAssumptionFailure(TestIdentifier test, String trace) {
+        mListener.testAssumptionFailure(test, trace);
+        mCollector.testAssumptionFailure(test, trace);
         check(Relation.AFTER, Noun.FAILED_TESTCASE, test);
         reset();
     }
@@ -548,5 +560,10 @@
     public TestSummary getSummary() {
         return mListener.getSummary();
     }
+
+    @Override
+    public void testIgnored(TestIdentifier test) {
+        // ignore
+    }
 }
 
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index f67a272..5572613 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -16,9 +16,10 @@
 package com.android.tradefed.result;
 
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
-import com.android.tradefed.result.TestResult.TestStatus;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -40,6 +41,12 @@
         Collections.synchronizedMap(new LinkedHashMap<String, TestRunResult>());
     private TestRunResult mCurrentResults =  new TestRunResult();
 
+    /** represents sums of tests in each TestStatus state for all runs.
+     * Indexed by TestStatus.ordinal() */
+    private int[] mStatusCounts = new int[TestStatus.values().length];
+    /** tracks if mStatusCounts is accurate, or if it needs to be recalculated */
+    private boolean mIsCountDirty = true;
+
     @Option(name = "aggregate-metrics", description =
         "attempt to add test metrics values for test runs with the same name." )
     private boolean mIsAggregateMetrics = false;
@@ -87,11 +94,13 @@
             mCurrentResults = mRunResultsMap.get(name);
         } else {
             // new run
-            mCurrentResults = new TestRunResult(name);
+            mCurrentResults = new TestRunResult();
+            mCurrentResults.setAggregateMetrics(mIsAggregateMetrics);
+
             mRunResultsMap.put(name, mCurrentResults);
         }
-        mCurrentResults.setRunComplete(false);
-        mCurrentResults.setRunFailureError(null);
+        mCurrentResults.testRunStarted(name, numTests);
+        mIsCountDirty = true;
     }
 
     /**
@@ -99,7 +108,8 @@
      */
     @Override
     public void testStarted(TestIdentifier test) {
-        mCurrentResults.reportTestStarted(test);
+        mIsCountDirty = true;
+        mCurrentResults.testStarted(test);
     }
 
     /**
@@ -107,19 +117,30 @@
      */
     @Override
     public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
-        mCurrentResults.reportTestEnded(test, testMetrics);
+        mIsCountDirty = true;
+        mCurrentResults.testEnded(test, testMetrics);
     }
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public void testFailed(TestFailure testFailure, TestIdentifier test, String trace) {
-        if (testFailure.equals(TestFailure.ERROR)) {
-            mCurrentResults.reportTestFailure(test, TestStatus.ERROR, trace);
-        } else {
-            mCurrentResults.reportTestFailure(test, TestStatus.FAILURE, trace);
-        }
+    public void testFailed(TestIdentifier test, String trace) {
+        mIsCountDirty = true;
+        mCurrentResults.testFailed(test, trace);
+    }
+
+    @Override
+    public void testAssumptionFailure(TestIdentifier test, String trace) {
+        mIsCountDirty = true;
+        mCurrentResults.testAssumptionFailure(test, trace);
+
+    }
+
+    @Override
+    public void testIgnored(TestIdentifier test) {
+        mIsCountDirty = true;
+        mCurrentResults.testIgnored(test);
     }
 
     /**
@@ -127,9 +148,8 @@
      */
     @Override
     public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
-        mCurrentResults.setRunComplete(true);
-        mCurrentResults.addMetrics(runMetrics, mIsAggregateMetrics);
-        mCurrentResults.addElapsedTime(elapsedTime);
+        mIsCountDirty = true;
+        mCurrentResults.testRunEnded(elapsedTime, runMetrics);
     }
 
     /**
@@ -137,7 +157,8 @@
      */
     @Override
     public void testRunFailed(String errorMessage) {
-        mCurrentResults.setRunFailureError(errorMessage);
+        mIsCountDirty = true;
+        mCurrentResults.testRunFailed(errorMessage);
     }
 
     /**
@@ -145,8 +166,8 @@
      */
     @Override
     public void testRunStopped(long elapsedTime) {
-        mCurrentResults.setRunComplete(true);
-        mCurrentResults.addElapsedTime(elapsedTime);
+        mIsCountDirty = true;
+        mCurrentResults.testRunStopped(elapsedTime);
     }
 
     /**
@@ -173,58 +194,35 @@
      * Gets the total number of complete tests for all runs.
      */
     public int getNumTotalTests() {
-        return getNumFailedTests() + getNumErrorTests() + getNumPassedTests();
-    }
-
-    /**
-     * Gets the total number of failed tests for all runs.
-     */
-    public int getNumFailedTests() {
-        int numFailedTests = 0;
-        for (TestRunResult result : mRunResultsMap.values()) {
-            numFailedTests += result.getNumFailedTests();
+        int total = 0;
+        // force test count
+        getNumTestsInState(TestStatus.PASSED);
+        for (TestStatus s : TestStatus.values()) {
+            total += mStatusCounts[s.ordinal()];
         }
-        return numFailedTests;
+        return total;
     }
 
     /**
-     * Gets the total number of error tests for all runs.
+     * Gets the number of tests in given state for this run.
      */
-    public int getNumErrorTests() {
-        int numErrorTests = 0;
-        for (TestRunResult result : mRunResultsMap.values()) {
-            numErrorTests += result.getNumErrorTests();
+    public int getNumTestsInState(TestStatus status) {
+        if (mIsCountDirty) {
+            for (TestRunResult result : mRunResultsMap.values()) {
+                for (TestStatus s : TestStatus.values()) {
+                    mStatusCounts[s.ordinal()] += result.getNumTestsInState(s);
+                }
+            }
+            mIsCountDirty = false;
         }
-        return numErrorTests;
+        return mStatusCounts[status.ordinal()];
     }
 
     /**
-     * Gets the total number of passed tests for all runs.
-     */
-    public int getNumPassedTests() {
-        int numPassedTests = 0;
-        for (TestRunResult result : mRunResultsMap.values()) {
-            numPassedTests += result.getNumPassedTests();
-        }
-        return numPassedTests;
-    }
-
-    /**
-     * Gets the total number of incomplete tests for all runs.
-     */
-    public int getNumIncompleteTests() {
-        int numIncompleteTests = 0;
-        for (TestRunResult result : mRunResultsMap.values()) {
-            numIncompleteTests += result.getNumIncompleteTests();
-        }
-        return numIncompleteTests;
-    }
-
-    /**
-     * @return true if invocation had any failed or error tests.
+     * @return true if invocation had any failed or assumption failed tests.
      */
     public boolean hasFailedTests() {
-        return getNumErrorTests() > 0 || getNumFailedTests() > 0;
+        return getNumAllFailedTests() > 0;
     }
 
     /**
@@ -259,4 +257,13 @@
     public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
         // ignore
     }
+
+    /**
+     * Return total number of tests in a failure state (failed, assumption failure)
+     * @return
+     */
+    public int getNumAllFailedTests() {
+        return getNumTestsInState(TestStatus.FAILURE) +
+                getNumTestsInState(TestStatus.ASSUMPTION_FAILURE);
+    }
 }
diff --git a/src/com/android/tradefed/result/ConsoleResultReporter.java b/src/com/android/tradefed/result/ConsoleResultReporter.java
new file mode 100644
index 0000000..f1174ea
--- /dev/null
+++ b/src/com/android/tradefed/result/ConsoleResultReporter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2015 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.result;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Result reporter to print the test results to the console.
+ * <p>
+ * Prints each test run, each test case, and test metrics, test logs, and test file locations.
+ * <p>
+ */
+public class ConsoleResultReporter extends CollectingTestListener implements ILogSaverListener {
+    private static final String LOG_TAG = ConsoleResultReporter.class.getSimpleName();
+
+    private List<LogFile> mLogFiles = new LinkedList<>();
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void invocationEnded(long elapsedTime) {
+        Log.logAndDisplay(LogLevel.INFO, LOG_TAG, getInvocationSummary());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream,
+            LogFile logFile) {
+        mLogFiles.add(logFile);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setLogSaver(ILogSaver logSaver) {
+        // Ignore. This class doesn't save any additional files.
+    }
+
+    /**
+     * Get the invocation summary as a string.
+     */
+    String getInvocationSummary() {
+        if (getRunResults().isEmpty() && mLogFiles.isEmpty()) {
+            return "No test results\n";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (TestRunResult testRunResult : getRunResults()) {
+            sb.append(getTestRunSummary(testRunResult));
+        }
+        if (!mLogFiles.isEmpty()) {
+            sb.append("Log Files:\n");
+            for (LogFile logFile : mLogFiles) {
+                final String url = logFile.getUrl();
+                sb.append(String.format("  %s\n", url != null ? url : logFile.getPath()));
+            }
+        }
+        return "Test results:\n" + sb.toString().trim() + "\n";
+    }
+
+    /**
+     * Get the test run summary as a string including run metrics.
+     */
+    String getTestRunSummary(TestRunResult testRunResult) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(String.format("%s:", testRunResult.getName()));
+        if (testRunResult.getNumTests() > 0) {
+            sb.append(String.format(" %d Test%s, %d Passed, %d Failed, %d Ignored",
+                    testRunResult.getNumCompleteTests(),
+                    testRunResult.getNumCompleteTests() == 1 ? "" : "s", // Pluralize Test
+                    testRunResult.getNumTestsInState(TestStatus.PASSED),
+                    testRunResult.getNumAllFailedTests(),
+                    testRunResult.getNumTestsInState(TestStatus.IGNORED)));
+        } else if (testRunResult.getRunMetrics().size() == 0) {
+            sb.append(" No results");
+        }
+        sb.append("\n");
+        Map<TestIdentifier, TestResult> testResults = testRunResult.getTestResults();
+        for (Map.Entry<TestIdentifier, TestResult> entry : testResults.entrySet()) {
+            sb.append(getTestSummary(entry.getKey(), entry.getValue()));
+        }
+        Map<String, String> metrics = testRunResult.getRunMetrics();
+        if (metrics != null && !metrics.isEmpty()) {
+            List<String> metricKeys = new ArrayList<String>(metrics.keySet());
+            Collections.sort(metricKeys);
+            for (String metricKey : metricKeys) {
+                sb.append(String.format("  %s: %s\n", metricKey, metrics.get(metricKey)));
+            }
+        }
+        sb.append("\n");
+        return sb.toString();
+    }
+
+    /**
+     * Get the test summary as string including test metrics.
+     */
+    String getTestSummary(TestIdentifier testId, TestResult testResult) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(String.format("  %s: %s\n", testId.toString(), testResult.getStatus()));
+        Map<String, String> metrics = testResult.getMetrics();
+        if (metrics != null && !metrics.isEmpty()) {
+            List<String> metricKeys = new ArrayList<String>(metrics.keySet());
+            Collections.sort(metricKeys);
+            for (String metricKey : metricKeys) {
+                sb.append(String.format("    %s: %s\n", metricKey, metrics.get(metricKey)));
+            }
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/src/com/android/tradefed/result/DeviceFileReporter.java b/src/com/android/tradefed/result/DeviceFileReporter.java
index cec6594..f3017cd 100644
--- a/src/com/android/tradefed/result/DeviceFileReporter.java
+++ b/src/com/android/tradefed/result/DeviceFileReporter.java
@@ -26,10 +26,13 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A utility class that checks the device for files and sends them to
@@ -40,8 +43,30 @@
     private final ITestInvocationListener mListener;
     private final ITestDevice mDevice;
 
+    /** Whether to ignore files that have already been captured by a prior Pattern */
+    private boolean mSkipRepeatFiles = true;
+    /** The files which have already been reported */
+    private Set<String> mReportedFiles = new HashSet<String>();
+
+    /** Whether to attempt to infer data types for patterns with {@code UNKNOWN} data type */
+    private boolean mInferDataTypes = true;
+
     private LogDataType mDefaultFileType = LogDataType.UNKNOWN;
 
+    private static final Map<String, LogDataType> DATA_TYPE_REVERSE_MAP = new HashMap<>();
+
+    static {
+        // Make it easy to map backward from file extension to LogDataType
+        for (LogDataType type : LogDataType.values()) {
+            // Extracted extension will contain a leading dot
+            final String ext = "." + type.getFileExt();
+            if (DATA_TYPE_REVERSE_MAP.containsKey(ext)) {
+                continue;
+            }
+
+            DATA_TYPE_REVERSE_MAP.put(ext, type);
+        }
+    }
     /**
      * Initialize a new DeviceFileReporter with the provided {@link ITestDevice}
      */
@@ -101,6 +126,30 @@
     }
 
     /**
+     * Whether or not to skip files which have already been reported.  This is only relevant when
+     * multiple patterns are being used, and two or more of those patterns match the same file.
+     * <p />
+     * Note that this <emph>must only</emph> be called prior to calling {@see #run()}.  Doing
+     * otherwise will cause undefined behavior.
+     */
+    public void setSkipRepeatFiles(boolean skip) {
+        mSkipRepeatFiles = skip;
+    }
+
+    /**
+     * Whether to <emph>attempt to</emph> infer the data types of {@code UNKNOWN} files by checking
+     * the file extensions against a list.
+     * <p />
+     * Note that, when enabled, these inferences will only be made for patterns with file type
+     * {@code UNKNOWN} (which includes patterns added without a specific type, and without the)
+     * default type having been set manually).  If the inference fails, the data type will remain
+     * as {@code UNKNOWN}.
+     */
+    public void setInferUnknownDataTypes(boolean infer) {
+        mInferDataTypes = infer;
+    }
+
+    /**
      * Actually search the filesystem for the specified patterns and send them to
      * {@link ITestInvocationListener#testLog} if found
      */
@@ -109,10 +158,16 @@
         for (Map.Entry<String, LogDataType> pat : mFilePatterns.entrySet()) {
             final String searchCmd = String.format("ls '%s'", pat.getKey());
             final String fileList = mDevice.executeShellCommand(searchCmd);
-            for (String filename : fileList.split("\r\n")) {
+
+            for (String filename : fileList.split("\r?\n")) {
                 if (filename.isEmpty() || filename.endsWith(": No such file or directory")) {
                     continue;
                 }
+                if (mSkipRepeatFiles && mReportedFiles.contains(filename)) {
+                    CLog.v("Skipping already-reported file %s", filename);
+                    continue;
+                }
+
                 File file = null;
                 InputStreamSource iss = null;
                 try {
@@ -121,8 +176,10 @@
                     file = mDevice.pullFile(filename);
                     CLog.v("Local file %s has size %d", file, file.length());
                     iss = createIssForFile(file);
-                    mListener.testLog(filename, pat.getValue(), iss);
+                    final LogDataType type = getDataType(filename, pat.getValue());
+                    mListener.testLog(filename, type, iss);
                     filenames.add(filename);
+                    mReportedFiles.add(filename);
                 } catch (IOException e) {
                     CLog.w("Failed to log file %s: %s", filename, e.getMessage());
                 } finally {
@@ -138,6 +195,32 @@
     }
 
     /**
+     * Returns the data type to use for a given file.  Will attempt to infer the data type from the
+     * file's extension IFF inferences are enabled, and the current data type is {@code UNKNOWN}.
+     */
+    LogDataType getDataType(String filename, LogDataType defaultType) {
+        if (!mInferDataTypes) return defaultType;
+        if (!LogDataType.UNKNOWN.equals(defaultType)) return defaultType;
+
+        CLog.d("Running type inference for file %s with default type %s", filename, defaultType);
+        String ext = FileUtil.getExtension(filename);
+        CLog.v("Found raw extension \"%s\"", ext);
+
+        // Normalize the extension
+        if (ext == null) return defaultType;
+        ext = ext.toLowerCase();
+
+        if (DATA_TYPE_REVERSE_MAP.containsKey(ext)) {
+            final LogDataType newType = DATA_TYPE_REVERSE_MAP.get(ext);
+            CLog.d("Inferred data type %s", newType);
+            return newType;
+        } else {
+            CLog.v("Failed to find a reverse map for extension \"%s\"", ext);
+            return defaultType;
+        }
+    }
+
+    /**
      * Create an {@link InputStreamSource} for a file
      * <p />
      * Exposed for unit testing
@@ -147,4 +230,3 @@
         return new SnapshotInputStreamSource(bufStr);
     }
 }
-
diff --git a/src/com/android/tradefed/result/EmailResultReporter.java b/src/com/android/tradefed/result/EmailResultReporter.java
index b1ac059..a371915 100644
--- a/src/com/android/tradefed/result/EmailResultReporter.java
+++ b/src/com/android/tradefed/result/EmailResultReporter.java
@@ -15,6 +15,8 @@
  */
 package com.android.tradefed.result;
 
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
@@ -216,8 +218,8 @@
             bodyBuilder.append(StreamUtil.getStackTrace(mInvocationThrowable));
             bodyBuilder.append("\n");
         }
-        bodyBuilder.append(String.format("Test results:  %d passed, %d failed, %d error\n\n",
-                getNumPassedTests(), getNumFailedTests(), getNumErrorTests()));
+        bodyBuilder.append(String.format("Test results:  %d passed, %d failed\n\n",
+                getNumTestsInState(TestStatus.PASSED), getNumAllFailedTests()));
         for (TestRunResult result : getRunResults()) {
             if (!result.getRunMetrics().isEmpty()) {
                 bodyBuilder.append(String.format("'%s' test run metrics: %s\n", result.getName(),
diff --git a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
index 2c05669..02ae4fc 100644
--- a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
+++ b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
@@ -24,6 +24,8 @@
 import junit.framework.TestListener;
 import junit.framework.TestResult;
 
+import org.junit.internal.AssumptionViolatedException;
+
 import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.util.Map;
@@ -58,16 +60,17 @@
      * {@inheritDoc}
      */
     @Override
-    public void testFailed(TestFailure status, TestIdentifier testId, String trace) {
+    public void testFailed(TestIdentifier testId, String trace) {
         Test test = new TestIdentifierResult(testId);
+        // TODO: is it accurate to represent the trace as AssertionFailedError?
+        mJUnitListener.addFailure(test, new AssertionFailedError(trace));
+    }
 
-        if (TestFailure.ERROR.equals(status)) {
-            Throwable throwable = new RemoteException(trace);
-            mJUnitListener.addError(test, throwable);
-        } else {
-            // TODO: is it accurate to represent the trace as AssertionFailedError?
-            mJUnitListener.addFailure(test, new AssertionFailedError(trace));
-        }
+    @Override
+    public void testAssumptionFailure(TestIdentifier testId, String trace) {
+        Test test = new TestIdentifierResult(testId);
+        AssumptionViolatedException throwable = new AssumptionViolatedException(trace);
+        mJUnitListener.addError(test, throwable);
     }
 
     /**
@@ -258,4 +261,9 @@
     public void testLog(String dataName, LogDataType logData, InputStreamSource dataStream) {
         // ignore
     }
+
+    @Override
+    public void testIgnored(TestIdentifier test) {
+        // ignore
+    }
 }
diff --git a/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java b/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java
index 653453c..e5e35a8 100644
--- a/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java
+++ b/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java
@@ -16,7 +16,6 @@
 
 package com.android.tradefed.result;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 
 import junit.framework.AssertionFailedError;
@@ -56,7 +55,7 @@
     @Override
     public void addError(Test test, Throwable t) {
         for (ITestInvocationListener listener : mInvocationListeners) {
-            listener.testFailed(TestFailure.ERROR, getTestId(test), getStackTrace(t));
+            listener.testFailed(getTestId(test), getStackTrace(t));
         }
     }
 
@@ -66,7 +65,7 @@
     @Override
     public void addFailure(Test test, AssertionFailedError t) {
         for (ITestInvocationListener listener : mInvocationListeners) {
-            listener.testFailed(TestFailure.FAILURE, getTestId(test), getStackTrace(t));
+            listener.testFailed(getTestId(test), getStackTrace(t));
         }
     }
 
diff --git a/src/com/android/tradefed/result/LogDataType.java b/src/com/android/tradefed/result/LogDataType.java
index 797cd60..d6185c1 100644
--- a/src/com/android/tradefed/result/LogDataType.java
+++ b/src/com/android/tradefed/result/LogDataType.java
@@ -22,6 +22,7 @@
 
     TEXT("txt", false, true),
     XML("xml", false, true),
+    HTML("html", true, true),
     PNG("png", true, false),
     ZIP("zip", true, false),
     GZIP("gz", true, false),
diff --git a/src/com/android/tradefed/result/LogFilesReporter.java b/src/com/android/tradefed/result/LogFilesReporter.java
new file mode 100644
index 0000000..38c5995
--- /dev/null
+++ b/src/com/android/tradefed/result/LogFilesReporter.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2014 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.result;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IFileEntry;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Helper test component that pulls files located on a device and adds them to the test logs.
+ * The component provides {@link IRemoteTest} and {@link IDeviceTest} services.
+ * <p>
+ * The path to files on the device is specified with upload-dir or upload-pattern options.
+ * The files will be removed if clean-upload-pattern option is provided.
+ */
+public class LogFilesReporter implements IRemoteTest, IDeviceTest {
+
+    @Option(name = "upload-pattern",
+            description = "A path pattern of files on the device to be added to the test logs.")
+    private String mUploadPattern = null;
+
+    // TODO(mdzyuba): upload-pattern does not work at the moment. Remove upload-dir once resolved.
+    @Option(name = "upload-dir",
+            description = "A folder on the device to be added to the test logs.")
+    private String mUploadDir = null;
+
+    @Option(name = "clean-upload-pattern",
+            description = "Clean files specified in \"upload-pattern\" after the test is done.")
+    private boolean mRemoveFilesSpecifiedByUploadPattern = true;
+
+    // A device instance.
+    private ITestDevice mDevice;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setDevice(ITestDevice device) {
+        mDevice = device;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ITestDevice getDevice() {
+        return mDevice;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+        if (mUploadPattern != null) {
+            uploadFilesOnDeviceToLogs(mUploadPattern, listener);
+            if (mRemoveFilesSpecifiedByUploadPattern) {
+                cleanFilesOnDevice(mUploadPattern);
+            }
+        }
+        if (mUploadDir != null) {
+            uploadFolderOnDeviceToLogs(mUploadDir, listener);
+            if (mRemoveFilesSpecifiedByUploadPattern) {
+                cleanFilesOnDevice(mUploadDir + "/*");
+            }
+        }
+    }
+
+    /**
+     * Uploads files from a device to test logs.
+     *
+     * @param filesPattern a path pattern to files on device to be added to the logs.
+     * @param listener a listener for test results from the test invocation.
+     * @throws DeviceNotAvailableException in case device is unavailable.
+     */
+    protected void uploadFilesOnDeviceToLogs(String filesPattern, ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
+        final DeviceFileReporter reporter = new DeviceFileReporter(getDevice(), listener);
+        reporter.addPatterns(filesPattern);
+        reporter.run();
+    }
+
+    /**
+     * Uploads files from a device to test logs.
+     *
+     * @param dir a full path to a folder on device containing files to be added to the logs.
+     * @param listener a listener for test results from the test invocation.
+     * @throws DeviceNotAvailableException in case device is unavailable.
+     */
+    protected void uploadFolderOnDeviceToLogs(String dir, ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
+        final DeviceFileReporter reporter = new DeviceFileReporter(getDevice(), listener);
+        Map<String, LogDataType> uploadFilePatterns = new LinkedHashMap<String, LogDataType>();
+        IFileEntry outputDir = getDevice().getFileEntry(dir);
+        if (outputDir != null) {
+            for (IFileEntry file : outputDir.getChildren(false)) {
+                LogDataType logDataType = LogDataType.UNKNOWN;
+                if (file.getName().endsWith(LogDataType.PNG.getFileExt())) {
+                    logDataType = LogDataType.PNG;
+                } else if (file.getName().endsWith(LogDataType.XML.getFileExt())) {
+                    logDataType = LogDataType.XML;
+                }
+                CLog.v(String.format("Adding file %s", file.getFullPath()));
+                uploadFilePatterns.put(file.getFullPath(), logDataType);
+            }
+            reporter.addPatterns(uploadFilePatterns);
+            reporter.run();
+        } else {
+            CLog.w("Directory not found on device: %s", dir);
+        }
+    }
+
+    /**
+     * Cleans the files on the device.
+     *
+     * @param pattern a path pattern to files to be removed.
+     * @throws DeviceNotAvailableException in case device is unavailable.
+     */
+    protected void cleanFilesOnDevice(String pattern) throws DeviceNotAvailableException {
+        String folder = pattern.substring(0, pattern.lastIndexOf('/'));
+        if (doesDirectoryExistOnDevice(folder)) {
+            getDevice().executeShellCommand(String.format("rm %s", pattern));
+        }
+    }
+
+    /**
+     * Checks to see if a directory exists on a device.
+     *
+     * @param folder a full path to a directory on a device to be verified.
+     * @return true if a directory exists, false otherwise.
+     * @throws DeviceNotAvailableException in case device is unavailable.
+     */
+    protected boolean doesDirectoryExistOnDevice(String folder) throws DeviceNotAvailableException {
+        IFileEntry outputDir = getDevice().getFileEntry(folder);
+        if (outputDir == null) {
+            CLog.w("Directory not found on device: %s", folder);
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/tradefed/result/NameMangleListener.java b/src/com/android/tradefed/result/NameMangleListener.java
index d3f4978..5598d0f 100644
--- a/src/com/android/tradefed/result/NameMangleListener.java
+++ b/src/com/android/tradefed/result/NameMangleListener.java
@@ -19,6 +19,8 @@
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.build.IBuildInfo;
 
+import junit.framework.TestFailure;
+
 import java.util.Map;
 
 /**
@@ -91,9 +93,27 @@
      * {@inheritDoc}
      */
     @Override
-    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+    public void testFailed(TestIdentifier test, String trace) {
         final TestIdentifier mangledTestId = mangleTestId(test);
-        mListener.testFailed(status, mangledTestId, trace);
+        mListener.testFailed(mangledTestId, trace);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testAssumptionFailure(TestIdentifier test, String trace) {
+        final TestIdentifier mangledTestId = mangleTestId(test);
+        mListener.testAssumptionFailure(mangledTestId, trace);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testIgnored(TestIdentifier test) {
+        final TestIdentifier mangledTestId = mangleTestId(test);
+        mListener.testIgnored(mangledTestId);
     }
 
     /**
diff --git a/src/com/android/tradefed/result/ResultForwarder.java b/src/com/android/tradefed/result/ResultForwarder.java
index 1ffbeb5..44e878c 100644
--- a/src/com/android/tradefed/result/ResultForwarder.java
+++ b/src/com/android/tradefed/result/ResultForwarder.java
@@ -184,9 +184,9 @@
      * {@inheritDoc}
      */
     @Override
-    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+    public void testFailed(TestIdentifier test, String trace) {
         for (ITestInvocationListener listener : mListeners) {
-            listener.testFailed(status, test, trace);
+            listener.testFailed(test, trace);
         }
     }
 
@@ -199,4 +199,18 @@
             listener.testEnded(test, testMetrics);
         }
     }
+
+    @Override
+    public void testAssumptionFailure(TestIdentifier test, String trace) {
+        for (ITestInvocationListener listener : mListeners) {
+            listener.testAssumptionFailure(test, trace);
+        }
+    }
+
+    @Override
+    public void testIgnored(TestIdentifier test) {
+        for (ITestInvocationListener listener : mListeners) {
+            listener.testIgnored(test);
+        }
+    }
 }
diff --git a/src/com/android/tradefed/result/StubTestRunListener.java b/src/com/android/tradefed/result/StubTestRunListener.java
index d63f9c9..831c6a4 100644
--- a/src/com/android/tradefed/result/StubTestRunListener.java
+++ b/src/com/android/tradefed/result/StubTestRunListener.java
@@ -37,7 +37,23 @@
      * {@inheritDoc}
      */
     @Override
-    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+    public void testFailed(TestIdentifier test, String trace) {
+        // ignore
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testAssumptionFailure(TestIdentifier test, String trace) {
+        // ignore
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testIgnored(TestIdentifier test) {
         // ignore
     }
 
diff --git a/src/com/android/tradefed/result/TestResult.java b/src/com/android/tradefed/result/TestResult.java
deleted file mode 100644
index ab84d1e..0000000
--- a/src/com/android/tradefed/result/TestResult.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.result;
-
-import com.android.ddmlib.testrunner.TestIdentifier;
-import com.google.common.base.Objects;
-
-import java.util.Map;
-
-/**
- * Container for a result of a single test.
- */
-public class TestResult {
-
-    public enum TestStatus {
-        /** Test error */
-        ERROR,
-        /** Test failed. */
-        FAILURE,
-        /** Test passed */
-        PASSED,
-        /** Test started but not ended */
-        INCOMPLETE
-    }
-
-    private TestStatus mStatus;
-    private String mStackTrace;
-    private Map<String, String> mMetrics;
-    // the start and end time of the test, measured via {@link System#currentTimeMillis()}
-    private long mStartTime = 0;
-    private long mEndTime = 0;
-
-    public TestResult() {
-        mStatus = TestStatus.INCOMPLETE;
-        mStartTime = System.currentTimeMillis();
-    }
-
-    /**
-     * Get the {@link TestStatus} result of the test.
-     */
-    public TestStatus getStatus() {
-        return mStatus;
-    }
-
-    /**
-     * Get the associated {@link String} stack trace. Should be <code>null</code> if
-     * {@link #getStatus()} is {@link TestStatus#PASSED}.
-     */
-    public String getStackTrace() {
-        return mStackTrace;
-    }
-
-    /**
-     * Get the associated test metrics.
-     */
-    public Map<String, String> getMetrics() {
-        return mMetrics;
-    }
-
-    /**
-     * Set the test metrics, overriding any previous values.
-     */
-    public void setMetrics(Map<String, String> metrics) {
-        mMetrics = metrics;
-    }
-
-    /**
-     * Return the {@link System#currentTimeMillis()} time that the
-     * {@link ITestInvocationListener#testStarted(TestIdentifier)} event was received.
-     */
-    public long getStartTime() {
-        return mStartTime;
-    }
-
-    /**
-     * Return the {@link System#currentTimeMillis()} time that the
-     * {@link ITestInvocationListener#testEnded(TestIdentifier, Map)} event was received.
-     */
-    public long getEndTime() {
-        return mEndTime;
-    }
-
-    /**
-     * Set the {@link TestStatus}.
-     */
-    public TestResult setStatus(TestStatus status) {
-       mStatus = status;
-       return this;
-    }
-
-    /**
-     * Set the stack trace.
-     */
-    public void setStackTrace(String trace) {
-        mStackTrace = trace;
-    }
-
-    /**
-     * Sets the end time
-     */
-    public void setEndTime(long currentTimeMillis) {
-        mEndTime = currentTimeMillis;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(mMetrics, mStackTrace, mStatus);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        TestResult other = (TestResult) obj;
-        return Objects.equal(mMetrics, other.mMetrics) &&
-                Objects.equal(mStackTrace, other.mStackTrace) &&
-                Objects.equal(mStatus, other.mStatus);
-    }
-
-}
diff --git a/src/com/android/tradefed/result/TestRunResult.java b/src/com/android/tradefed/result/TestRunResult.java
deleted file mode 100644
index 20ac5ee..0000000
--- a/src/com/android/tradefed/result/TestRunResult.java
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.result;
-
-import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.TestResult.TestStatus;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Holds results from a single test run
- */
-public class TestRunResult {
-    private final String mTestRunName;
-    // Uses a synchronized map to make thread safe.
-    // Uses a LinkedHashmap to have predictable iteration order
-    private Map<TestIdentifier, TestResult> mTestResults =
-        Collections.synchronizedMap(new LinkedHashMap<TestIdentifier, TestResult>());
-    private Map<String, String> mRunMetrics = new HashMap<String, String>();
-    private boolean mIsRunComplete = false;
-    private long mElapsedTime = 0;
-    private int mNumFailedTests = 0;
-    private int mNumErrorTests = 0;
-    private int mNumPassedTests = 0;
-    private int mNumInCompleteTests = 0;
-    private String mRunFailureError = null;
-
-    /**
-     * Create a {@link TestRunResult}.
-     *
-     * @param runName
-     */
-    public TestRunResult(String runName) {
-        mTestRunName = runName;
-    }
-
-    /**
-     * Create an empty{@link TestRunResult}.
-     */
-    public TestRunResult() {
-        this("not started");
-    }
-
-    /**
-     * @return the test run name
-     */
-    public String getName() {
-        return mTestRunName;
-    }
-
-    /**
-     * Gets a map of the test results.
-     * @return
-     */
-    public Map<TestIdentifier, TestResult> getTestResults() {
-        return mTestResults;
-    }
-
-    /**
-     * Adds test run metrics.
-     * <p/>
-     * @param runMetrics the run metrics
-     * @param aggregateMetrics if <code>true</code>, attempt to add given metrics values to any
-     * currently stored values. If <code>false</code>, replace any currently stored metrics with
-     * the same key.
-     */
-    public void addMetrics(Map<String, String> runMetrics, boolean aggregateMetrics) {
-        if (aggregateMetrics) {
-            for (Map.Entry<String, String> entry : runMetrics.entrySet()) {
-                String existingValue = mRunMetrics.get(entry.getKey());
-                String combinedValue = combineValues(existingValue, entry.getValue());
-                mRunMetrics.put(entry.getKey(), combinedValue);
-            }
-        } else {
-            mRunMetrics.putAll(runMetrics);
-        }
-    }
-
-    /**
-     * Combine old and new metrics value
-     *
-     * @param existingValue
-     * @param value
-     * @return
-     */
-    private String combineValues(String existingValue, String newValue) {
-        if (existingValue != null) {
-            try {
-                Long existingLong = Long.parseLong(existingValue);
-                Long newLong = Long.parseLong(newValue);
-                return Long.toString(existingLong + newLong);
-            } catch (NumberFormatException e) {
-                // not a long, skip to next
-            }
-            try {
-               Double existingDouble = Double.parseDouble(existingValue);
-               Double newDouble = Double.parseDouble(newValue);
-               return Double.toString(existingDouble + newDouble);
-            } catch (NumberFormatException e) {
-                // not a double either, fall through
-            }
-        }
-        // default to overriding existingValue
-        return newValue;
-    }
-
-    /**
-     * @return a {@link Map} of the test test run metrics.
-     */
-    public Map<String, String> getRunMetrics() {
-        return mRunMetrics;
-    }
-
-    /**
-     * Gets the set of completed tests.
-     */
-    public Set<TestIdentifier> getCompletedTests() {
-        Set<TestIdentifier> completedTests = new LinkedHashSet<TestIdentifier>();
-        for (Map.Entry<TestIdentifier, TestResult> testEntry : getTestResults().entrySet()) {
-            if (!testEntry.getValue().getStatus().equals(TestStatus.INCOMPLETE)) {
-                completedTests.add(testEntry.getKey());
-            }
-        }
-        return completedTests;
-    }
-
-    /**
-     * @return <code>true</code> if test run failed.
-     */
-    public boolean isRunFailure() {
-        return mRunFailureError != null;
-    }
-
-    /**
-     * @return <code>true</code> if test run finished.
-     */
-    public boolean isRunComplete() {
-        return mIsRunComplete;
-    }
-
-    void setRunComplete(boolean runComplete) {
-        mIsRunComplete = runComplete;
-    }
-
-    void addElapsedTime(long elapsedTime) {
-        mElapsedTime+= elapsedTime;
-    }
-
-    void setRunFailureError(String errorMessage) {
-        mRunFailureError  = errorMessage;
-    }
-
-    /**
-     * Gets the number of passed tests for this run.
-     */
-    public int getNumPassedTests() {
-        return mNumPassedTests;
-    }
-
-    /**
-     * Gets the number of tests in this run.
-     */
-    public int getNumTests() {
-        return mTestResults.size();
-    }
-
-    /**
-     * Gets the number of complete tests in this run ie with status != incomplete.
-     */
-    public int getNumCompleteTests() {
-        return getNumTests() - getNumIncompleteTests();
-    }
-
-    /**
-     * Gets the number of failed tests in this run.
-     */
-    public int getNumFailedTests() {
-        return mNumFailedTests;
-    }
-
-    /**
-     * Gets the number of error tests in this run.
-     */
-    public int getNumErrorTests() {
-        return mNumErrorTests;
-    }
-
-    /**
-     * Gets the number of incomplete tests in this run.
-     */
-    public int getNumIncompleteTests() {
-        return mNumInCompleteTests;
-    }
-
-    /**
-     * @return <code>true</code> if test run had any failed or error tests.
-     */
-    public boolean hasFailedTests() {
-        return getNumErrorTests() > 0 || getNumFailedTests() > 0;
-    }
-
-    /**
-     * @return
-     */
-    public long getElapsedTime() {
-        return mElapsedTime;
-    }
-
-    /**
-     * Return the run failure error message, <code>null</code> if run did not fail.
-     */
-    public String getRunFailureMessage() {
-        return mRunFailureError;
-    }
-
-    /**
-     * Report the start of a test.
-     * @param test
-     */
-    void reportTestStarted(TestIdentifier test) {
-        TestResult result = mTestResults.get(test);
-
-        if (result != null) {
-            CLog.d("Replacing result for %s", test);
-            switch (result.getStatus()) {
-                case ERROR:
-                    mNumErrorTests--;
-                    break;
-                case FAILURE:
-                    mNumFailedTests--;
-                    break;
-                case PASSED:
-                    mNumPassedTests--;
-                    break;
-            }
-        } else {
-            mNumInCompleteTests++;
-        }
-        mTestResults.put(test, new TestResult());
-    }
-
-    /**
-     * Report a test failure.
-     *
-     * @param test
-     * @param status
-     * @param trace
-     */
-    void reportTestFailure(TestIdentifier test, TestStatus status, String trace) {
-        TestResult result = mTestResults.get(test);
-        if (result == null) {
-            CLog.d("Received test failure for %s without testStarted", test);
-            result = new TestResult();
-            mTestResults.put(test, result);
-        } else if (result.getStatus().equals(TestStatus.PASSED)) {
-            // this should never happen...
-            CLog.d("Replacing passed result for %s", test);
-            mNumPassedTests--;
-        }
-
-        result.setStackTrace(trace);
-        switch (status) {
-            case ERROR:
-                mNumErrorTests++;
-                result.setStatus(TestStatus.ERROR);
-                break;
-            case FAILURE:
-                result.setStatus(TestStatus.FAILURE);
-                mNumFailedTests++;
-                break;
-        }
-    }
-
-    /**
-     * Report the end of the test
-     *
-     * @param test
-     * @param testMetrics
-     * @return <code>true</code> if test was recorded as passed, false otherwise
-     */
-    boolean reportTestEnded(TestIdentifier test, Map<String, String> testMetrics) {
-        TestResult result = mTestResults.get(test);
-        if (result == null) {
-            CLog.d("Received test ended for %s without testStarted", test);
-            result = new TestResult();
-            mTestResults.put(test, result);
-        } else {
-            mNumInCompleteTests--;
-        }
-
-        result.setEndTime(System.currentTimeMillis());
-        result.setMetrics(testMetrics);
-        if (result.getStatus().equals(TestStatus.INCOMPLETE)) {
-            result.setStatus(TestStatus.PASSED);
-            mNumPassedTests++;
-            return true;
-        }
-        return false;
-    }
-}
diff --git a/src/com/android/tradefed/result/TextResultReporter.java b/src/com/android/tradefed/result/TextResultReporter.java
index be84c17..ee8c3ee 100644
--- a/src/com/android/tradefed/result/TextResultReporter.java
+++ b/src/com/android/tradefed/result/TextResultReporter.java
@@ -44,9 +44,15 @@
      * {@inheritDoc}
      */
     @Override
-    public void testFailed(TestFailure status, TestIdentifier testId, String trace) {
+    public void testFailed(TestIdentifier testId, String trace) {
         ResultPrinter printer = (ResultPrinter)getJUnitListener();
-        printer.getWriter().format("\nTest %s: %s \n stack: %s ", status, testId, trace);
+        printer.getWriter().format("\nTest %s: failed \n stack: %s ", testId, trace);
+    }
+
+    @Override
+    public void testAssumptionFailure(TestIdentifier testId, String trace) {
+        ResultPrinter printer = (ResultPrinter)getJUnitListener();
+        printer.getWriter().format("\nTest %s: assumption failed \n stack: %s ", testId, trace);
     }
 
     /**
diff --git a/src/com/android/tradefed/result/XmlResultReporter.java b/src/com/android/tradefed/result/XmlResultReporter.java
index d6fae59..d48359b 100644
--- a/src/com/android/tradefed/result/XmlResultReporter.java
+++ b/src/com/android/tradefed/result/XmlResultReporter.java
@@ -19,10 +19,12 @@
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.TestResult.TestStatus;
 import com.android.tradefed.util.StreamUtil;
 
 import org.kxml2.io.KXmlSerializer;
@@ -98,9 +100,9 @@
     }
 
     @Override
-    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
-        super.testFailed(status, test, trace);
-        CLog.d("%s %s: %s", test, status, trace);
+    public void testFailed(TestIdentifier test, String trace) {
+        super.testFailed(test, trace);
+        CLog.d("%s : %s", test, trace);
     }
 
     /**
@@ -128,8 +130,7 @@
                     inputStream);
 
             String msg = String.format("XML test result file generated at %s. Total tests %d, " +
-                    "Failed %d, Error %d", log.getPath(), getNumTotalTests(), getNumFailedTests(),
-                    getNumErrorTests());
+                    "Failed %d", log.getPath(), getNumTotalTests(), getNumAllFailedTests());
             Log.logAndDisplay(LogLevel.INFO, LOG_TAG, msg);
         } catch (IOException e) {
             Log.e(LOG_TAG, "Failed to generate report data");
@@ -164,8 +165,9 @@
         serializer.startTag(ns, TESTSUITE);
         serializer.attribute(ns, ATTR_NAME, mBuildInfo.getTestTag());
         serializer.attribute(ns, ATTR_TESTS, Integer.toString(getNumTotalTests()));
-        serializer.attribute(ns, ATTR_FAILURES, Integer.toString(getNumFailedTests()));
-        serializer.attribute(ns, ATTR_ERRORS, Integer.toString(getNumErrorTests()));
+        serializer.attribute(ns, ATTR_FAILURES,
+                Integer.toString(getNumTestsInState(TestStatus.FAILURE)));
+        serializer.attribute(ns, ATTR_ERRORS, "0");
         serializer.attribute(ns, ATTR_TIME, Long.toString(elapsedTime));
         serializer.attribute(ns, TIMESTAMP, timestamp);
         serializer.attribute(ns, HOSTNAME, "localhost");
diff --git a/src/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java b/src/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
new file mode 100644
index 0000000..922255f
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2015 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.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.util.AaptParser;
+import com.android.tradefed.util.AbiFormatter;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A {@link ITargetPreparer} that installs all apps from a
+ * {@link IDeviceBuildInfo#getTestsDir()} folder onto device. For individual
+ * test app install please look at {@link TestAppInstallSetup}
+ */
+@OptionClass(alias = "all-tests-installer")
+public class AllTestAppsInstallSetup implements ITargetCleaner, IAbiReceiver {
+    @Option(name = AbiFormatter.FORCE_ABI_STRING,
+            description = AbiFormatter.FORCE_ABI_DESCRIPTION,
+            importance = Importance.IF_UNSET)
+    private String mForceAbi = null;
+
+    @Option(name = "install-arg",
+            description = "Additional arguments to be passed to install command, "
+                    + "including leading dash, e.g. \"-d\"")
+    private Collection<String> mInstallArgs = new ArrayList<>();
+
+    @Option(name = "cleanup-apks",
+            description = "Whether apks installed should be uninstalled after test. Note that the "
+                    + "preparer does not verify if the apks are successfully removed.")
+    private boolean mCleanup = false;
+
+    private IAbi mAbi = null;
+
+    private List<String> mPackagesInstalled = new ArrayList<>();
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+            DeviceNotAvailableException {
+        if (!(buildInfo instanceof IDeviceBuildInfo)) {
+            throw new TargetSetupError("Invalid buildInfo, expecting an IDeviceBuildInfo");
+        }
+        // Locate test dir where the test zip file was unzip to.
+        File testsDir = ((IDeviceBuildInfo) buildInfo).getTestsDir();
+        if (testsDir == null || !testsDir.exists()) {
+            throw new TargetSetupError("Failed to find a valid test zip directory.");
+        }
+        resolveAbi(device);
+        installApksRecursively(testsDir, device);
+    }
+
+    /**
+     * Install all apks found in a given directory.
+     *
+     * @param directory {@link File} directory to install from.
+     * @param device {@link ITestDevice} to install all apks to.
+     * @throws TargetSetupError
+     * @throws DeviceNotAvailableException
+     */
+    void installApksRecursively(File directory, ITestDevice device)
+            throws TargetSetupError, DeviceNotAvailableException {
+        if (directory == null || !directory.isDirectory()) {
+            throw new TargetSetupError("Invalid test zip directory!");
+        }
+        CLog.d("Installing all apks found in dir %s ...", directory.getAbsolutePath());
+        File[] files = directory.listFiles();
+        for (File f : files) {
+            if (f.isDirectory()) {
+                installApksRecursively(f, device);
+            }
+            if (FileUtil.getExtension(f.getAbsolutePath()).toLowerCase().equals(".apk")) {
+                installApk(f, device);
+            } else {
+                CLog.d("Skipping %s because it is not an apk", f.getAbsolutePath());
+            }
+        }
+    }
+
+    /**
+     * Installs a single app to the device.
+     *
+     * @param appFile {@link File} of the apk to install.
+     * @param device {@link ITestDevice} to install the apk to.
+     * @throws TargetSetupError
+     * @throws DeviceNotAvailableException
+     */
+    void installApk(File appFile, ITestDevice device) throws TargetSetupError,
+            DeviceNotAvailableException {
+
+        CLog.d("Installing apk from %s ...", appFile.getAbsolutePath());
+        String result = device.installPackage(appFile, true,
+                mInstallArgs.toArray(new String[] {}));
+        if (result != null) {
+            throw new TargetSetupError(
+                    String.format("Failed to install %s on %s. Reason: '%s'", appFile,
+                            device.getSerialNumber(), result));
+        }
+        if (mCleanup) {
+            AaptParser parser = AaptParser.parse(appFile);
+            if (parser == null) {
+                throw new TargetSetupError("apk installed but AaptParser failed");
+            }
+            mPackagesInstalled.add(parser.getPackageName());
+        }
+    }
+
+    /**
+     * Determines the abi arguments when installing the apk, if needed.
+     *
+     * @param device {@link ITestDevice}
+     * @throws DeviceNotAvailableException
+     */
+    void resolveAbi(ITestDevice device) throws DeviceNotAvailableException {
+        if (mAbi != null && mForceAbi != null) {
+            throw new IllegalStateException("cannot specify both abi flags");
+        }
+        String abiName = null;
+        if (mAbi != null) {
+            abiName = mAbi.getName();
+        } else if (mForceAbi != null) {
+            abiName = AbiFormatter.getDefaultAbi(device, mForceAbi);
+        }
+        if (abiName != null) {
+            mInstallArgs.add(String.format("--abi %s", abiName));
+        }
+    }
+
+    @Override
+    public void setAbi(IAbi abi) {
+        mAbi = abi;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+            throws DeviceNotAvailableException {
+        if (mCleanup && !(e instanceof DeviceNotAvailableException)) {
+            for (String packageName : mPackagesInstalled) {
+                String msg = device.uninstallPackage(packageName);
+                if (msg != null) {
+                    CLog.w(String.format("error uninstalling package '%s': %s",
+                            packageName, msg));
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/AltDirBehavior.java b/src/com/android/tradefed/targetprep/AltDirBehavior.java
new file mode 100644
index 0000000..f5e5cad
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/AltDirBehavior.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.targetprep;
+
+/**
+ * An enum to define alternative directory behaviors for various test artifact installers/pushers
+ * <p>
+ * @see {@link TestAppInstallSetup}, {@link TestFilePushSetup}
+ */
+public enum AltDirBehavior {
+    /**
+     * The alternative directories should be used as a fallback to look up the build artifacts. That
+     * is, if build artifacts cannot be found at the regularly configured location.
+     */
+    FALLBACK,
+
+    /**
+     * The alternative directories should be used as an override to look up the build artifacts.
+     * That is, alternative directories should be searched first for build artifacts.
+     */
+    OVERRIDE,
+}
diff --git a/src/com/android/tradefed/targetprep/AppSetup.java b/src/com/android/tradefed/targetprep/AppSetup.java
index 0f1fec8..089d83d 100644
--- a/src/com/android/tradefed/targetprep/AppSetup.java
+++ b/src/com/android/tradefed/targetprep/AppSetup.java
@@ -28,6 +28,7 @@
 import java.io.File;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -60,6 +61,10 @@
             "optional flag(s) to provide when installing apks.")
     private ArrayList<String> mInstallFlags = new ArrayList<>();
 
+    @Option(name = "post-install-cmd", description =
+            "optional post-install adb shell commands; can be repeated.")
+    private List<String> mPostInstallCmds = new ArrayList<>();
+
     /** contains package names of installed apps. Used for uninstall */
     private Set<String> mInstalledPkgs = new HashSet<String>();
 
@@ -102,6 +107,14 @@
             }
         }
 
+       if (mPostInstallCmds != null && !mPostInstallCmds.isEmpty()){
+           for (String cmd : mPostInstallCmds) {
+               // If the command had any output, the executeShellCommand method will log it at the
+               // VERBOSE level; so no need to do any logging from here.
+               CLog.d("About to run setup command on device %s: %s", device.getSerialNumber(), cmd);
+               device.executeShellCommand(cmd);
+           }
+       }
     }
 
     private void addPackageNameToUninstall(File apkFile) throws TargetSetupError {
diff --git a/src/com/android/tradefed/targetprep/BuildInfoRecorder.java b/src/com/android/tradefed/targetprep/BuildInfoRecorder.java
new file mode 100644
index 0000000..a567408
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/BuildInfoRecorder.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 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.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * An {@link ITargetPreparer} that writes build info meta data into a specified file.
+ * <p>
+ * The file is written in simple key-value pair format; each line of the file has:<br>
+ * <code>key=value</code><br>
+ * where <code>key</code> is a meta data field from {@link IBuildInfo}
+ * <p>
+ * Currently, only build id is written.
+ */
+@OptionClass(alias = "build-info-recorder")
+public class BuildInfoRecorder implements ITargetPreparer {
+
+    @Option(name = "build-info-file", description = "when specified, build info will be written "
+            + "into the file specified. Any existing file will be overwritten.")
+    private File mBuildInfoFile = null;
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+            BuildError, DeviceNotAvailableException {
+        if (mBuildInfoFile != null) {
+            try {
+                String alias = buildInfo.getBuildAttributes().get("build_alias");
+                if (alias == null) {
+                    alias = buildInfo.getBuildId();
+                }
+                FileUtil.writeToFile(String.format("%s=%s\n%s=%s\n",
+                        "build_id", buildInfo.getBuildId(),
+                        "build_alias", alias),
+                        mBuildInfoFile);
+            } catch (IOException ioe) {
+                CLog.e("Exception while writing build info into %s",
+                        mBuildInfoFile.getAbsolutePath());
+                CLog.e(ioe);
+            }
+        }
+    }
+
+}
diff --git a/src/com/android/tradefed/targetprep/DeviceCleaner.java b/src/com/android/tradefed/targetprep/DeviceCleaner.java
index a7c2151..e2d1c18 100644
--- a/src/com/android/tradefed/targetprep/DeviceCleaner.java
+++ b/src/com/android/tradefed/targetprep/DeviceCleaner.java
@@ -20,6 +20,7 @@
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDeviceState;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -77,7 +78,16 @@
     @Override
     public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
             throws DeviceNotAvailableException {
-        clean(device);
+        if (e instanceof DeviceFailedToBootError) {
+            CLog.w("boot failure: attempting to stop runtime instead of cleanup");
+            try {
+                device.executeShellCommand("stop");
+            } catch (DeviceUnresponsiveException due) {
+                CLog.w("boot failure: ignored DeviceUnresponsiveException during forced cleanup");
+            }
+        } else {
+            clean(device);
+        }
     }
 
     /**
diff --git a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
index b9b4ce8..c2ca9e2 100644
--- a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
@@ -59,6 +59,14 @@
         "specify if system should always be flashed even if already running desired build.")
     private boolean mForceSystemFlash = false;
 
+    /*
+     * A temporary workaround for special builds. Should be removed after changes from build team.
+     * Bug: 18078421
+     */
+    @Option(name = "skip-post-flash-flavor-check", description =
+            "specify if system flavor should not be checked after flash")
+    private boolean mSkipPostFlashFlavorCheck = false;
+
     @Option(name = "wipe-skip-list", description =
         "list of /data subdirectories to NOT wipe when doing UserDataFlashOption.TESTS_ZIP")
     private Collection<String> mDataWipeSkipList = new ArrayList<String>();
@@ -192,42 +200,48 @@
         if (!(buildInfo instanceof IDeviceBuildInfo)) {
             throw new IllegalArgumentException("Provided buildInfo is not a IDeviceBuildInfo");
         }
-        IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo)buildInfo;
-        device.setRecoveryMode(RecoveryMode.ONLINE);
-        IDeviceFlasher flasher = createFlasher(device);
-        // only surround fastboot related operations with flashing permit restriction
+        // don't allow interruptions during flashing operations.
+        getRunUtil().allowInterrupt(false);
         try {
-            takeFlashingPermit();
+            IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo)buildInfo;
+            device.setRecoveryMode(RecoveryMode.ONLINE);
+            IDeviceFlasher flasher = createFlasher(device);
+            // only surround fastboot related operations with flashing permit restriction
+            try {
+                takeFlashingPermit();
 
-            flasher.overrideDeviceOptions(device);
-            flasher.setUserDataFlashOption(mUserDataFlashOption);
-            flasher.setForceSystemFlash(mForceSystemFlash);
-            flasher.setDataWipeSkipList(mDataWipeSkipList);
-            preEncryptDevice(device, flasher);
-            flasher.flash(device, deviceBuild);
+                flasher.overrideDeviceOptions(device);
+                flasher.setUserDataFlashOption(mUserDataFlashOption);
+                flasher.setForceSystemFlash(mForceSystemFlash);
+                flasher.setDataWipeSkipList(mDataWipeSkipList);
+                preEncryptDevice(device, flasher);
+                flasher.flash(device, deviceBuild);
+            } finally {
+                returnFlashingPermit();
+            }
+            device.waitForDeviceOnline();
+            // device may lose date setting if wiped, update with host side date in case anything on
+            // device side malfunction with an invalid date
+            if (device.enableAdbRoot()) {
+                device.setDate(null);
+            }
+            checkBuild(device, deviceBuild);
+            postEncryptDevice(device, flasher);
+            // only want logcat captured for current build, delete any accumulated log data
+            device.clearLogcat();
+            try {
+                device.setRecoveryMode(RecoveryMode.AVAILABLE);
+                device.waitForDeviceAvailable(mDeviceBootTime);
+            } catch (DeviceUnresponsiveException e) {
+                // assume this is a build problem
+                throw new DeviceFailedToBootError(String.format(
+                        "Device %s did not become available after flashing %s",
+                        device.getSerialNumber(), deviceBuild.getDeviceBuildId()));
+            }
+            device.postBootSetup();
         } finally {
-            returnFlashingPermit();
+            getRunUtil().allowInterrupt(true);
         }
-        device.waitForDeviceOnline();
-        // device may lose date setting if wiped, update with host side date in case anything on
-        // device side malfunction with an invalid date
-        if (device.enableAdbRoot()) {
-            device.setDate(null);
-        }
-        checkBuild(device, deviceBuild);
-        postEncryptDevice(device, flasher);
-        // only want logcat captured for current build, delete any accumulated log data
-        device.clearLogcat();
-        try {
-            device.setRecoveryMode(RecoveryMode.AVAILABLE);
-            device.waitForDeviceAvailable(mDeviceBootTime);
-        } catch (DeviceUnresponsiveException e) {
-            // assume this is a build problem
-            throw new DeviceFailedToBootError(String.format(
-                    "Device %s did not become available after flashing %s",
-                    device.getSerialNumber(), deviceBuild.getDeviceBuildId()));
-        }
-        device.postBootSetup();
     }
 
     /**
@@ -236,8 +250,13 @@
      */
     private void checkBuild(ITestDevice device, IDeviceBuildInfo deviceBuild)
             throws DeviceNotAvailableException {
-        checkBuildAttribute(deviceBuild.getBuildId(), device.getBuildId());
-        checkBuildAttribute(deviceBuild.getBuildFlavor(), device.getBuildFlavor());
+        // Need to use deviceBuild.getDeviceBuildId instead of getBuildId because the build info
+        // could be an AppBuildInfo and return app build id. Need to be more explicit that we
+        // check for the device build here.
+        checkBuildAttribute(deviceBuild.getDeviceBuildId(), device.getBuildId());
+        if (!mSkipPostFlashFlavorCheck) {
+            checkBuildAttribute(deviceBuild.getBuildFlavor(), device.getBuildFlavor());
+        }
         // TODO: check bootloader and baseband versions too
     }
 
diff --git a/src/com/android/tradefed/targetprep/DeviceSetup.java b/src/com/android/tradefed/targetprep/DeviceSetup.java
index 1de8e04..424e64c 100644
--- a/src/com/android/tradefed/targetprep/DeviceSetup.java
+++ b/src/com/android/tradefed/targetprep/DeviceSetup.java
@@ -17,323 +17,1077 @@
 package com.android.tradefed.targetprep;
 
 import com.android.ddmlib.IDevice;
-import com.android.ddmlib.Log;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.MultiMap;
 
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.regex.Pattern;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * A {@link ITargetPreparer} that configures a device for testing based on provided {@link Option}s.
- * <p/>
+ * <p>
  * Requires a device where 'adb root' is possible, typically a userdebug build type.
- * <p/>
- * Should be performed *after* a new build is flashed.
+ * </p><p>
+ * Should be performed <strong>after</strong> a new build is flashed.
+ * </p>
  */
 @OptionClass(alias = "device-setup")
 public class DeviceSetup implements ITargetPreparer, ITargetCleaner {
 
-    private static final String LOG_TAG = "DeviceSetup";
-    private static final Pattern RELEASE_BUILD_NAME_PATTERN =
-            Pattern.compile("[A-Z]{3}\\d{2}[A-Z]?");
-    private static final String PERSIST_PREFIX = "persist.";
+    /**
+     * Enum used to record ON/OFF state with a IGNORE no-op state.
+     */
+    public enum BinaryState {
+        IGNORE,
+        ON,
+        OFF;
+    }
 
-    @Option(name="wifi-network", description="the name of wifi network to connect to.")
-    private String mWifiNetwork = null;
+    // Networking
+    @Option(name = "airplane-mode",
+            description = "Turn airplane mode on or off")
+    protected BinaryState mAirplaneMode = BinaryState.IGNORE;
+    // ON:  settings put global airplane_mode_on 1
+    //      am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true
+    // OFF: settings put global airplane_mode_on 0
+    //      am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false
 
-    @Option(name="wifi-psk", description="WPA-PSK passphrase of wifi network to connect to.")
-    private String mWifiPsk = null;
+    @Option(name = "wifi",
+            description = "Turn wifi on or off")
+    protected BinaryState mWifi = BinaryState.IGNORE;
+    // ON:  settings put global wifi_on 1
+    //      svc wifi enable
+    // OFF: settings put global wifi_off 0
+    //      svc wifi disable
 
-    @Option(name = "disconnect-wifi-after-test", description =
-            "disconnect from wifi network after test completes.")
+    @Option(name = "wifi-network",
+            description = "The SSID of the network to connect to. Will only attempt to " +
+            "connect to a network if set")
+    protected String mWifiSsid = null;
+
+    @Option(name = "wifi-psk",
+            description = "The passphrase used to connect to a secured network")
+    protected String mWifiPsk = null;
+
+    @Option(name = "wifi-watchdog",
+            description = "Turn wifi watchdog on or off")
+    protected BinaryState mWifiWatchdog = BinaryState.IGNORE;
+    // ON:  settings put global wifi_watchdog 1
+    // OFF: settings put global wifi_watchdog 0
+
+    @Option(name = "wifi-scan-always-enabled",
+            description = "Turn wifi scan always enabled on or off")
+    protected BinaryState mWifiScanAlwaysEnabled = BinaryState.IGNORE;
+    // ON:  settings put global wifi_scan_always_enabled 1
+    // OFF: settings put global wifi_scan_always_enabled 0
+
+    @Option(name = "ethernet",
+            description = "Turn ethernet on or off")
+    protected BinaryState mEthernet = BinaryState.IGNORE;
+    // ON:  ifconfig eth0 up
+    // OFF: ifconfig eth0 down
+
+    @Option(name = "bluetooth",
+            description = "Turn bluetooth on or off")
+    protected BinaryState mBluetooth = BinaryState.IGNORE;
+    // ON:  service call bluetooth_manager 6
+    // OFF: service call bluetooth_manager 8
+
+    // Screen
+    @Option(name = "screen-adaptive-brightness",
+            description = "Turn screen adaptive brightness on or off")
+    protected BinaryState mScreenAdaptiveBrightness = BinaryState.IGNORE;
+    // ON:  settings put system screen_brightness_mode 1
+    // OFF: settings put system screen_brightness_mode 0
+
+    @Option(name = "screen-brightness",
+            description = "Set the screen brightness. This is uncalibrated from product to product")
+    protected Integer mScreenBrightness = null;
+    // settings put system screen_brightness $N
+
+    @Option(name = "screen-always-on",
+            description = "Turn 'screen always on' on or off. If ON, then screen-timeout-secs " +
+            "must be unset. Will only work when the device is plugged in")
+    protected BinaryState mScreenAlwaysOn = BinaryState.ON;
+    // ON:  svc power stayon true
+    // OFF: svc power stayon false
+
+    @Option(name = "screen-timeout-secs",
+            description = "Set the screen timeout in seconds. If set, then screen-always-on must " +
+            "be OFF or DEFAULT")
+    protected Long mScreenTimeoutSecs = null;
+    // settings put system screen_off_timeout $(N * 1000)
+
+    @Option(name = "screen-ambient-mode",
+            description = "Turn screen ambient mode on or off")
+    protected BinaryState mScreenAmbientMode = BinaryState.IGNORE;
+    // ON:  settings put secure doze_enabled 1
+    // OFF: settings put secure doze_enabled 0
+
+    @Option(name = "wake-gesture",
+            description = "Turn wake gesture on or off")
+    protected BinaryState mWakeGesture = BinaryState.IGNORE;
+    // ON:  settings put secure wake_gesture_enabled 1
+    // OFF: settings put secure wake_gesture_enabled 0
+
+    @Option(name = "screen-saver",
+            description = "Turn screen saver on or off")
+    protected BinaryState mScreenSaver = BinaryState.IGNORE;
+    // ON:  settings put secure screensaver_enabled 1
+    // OFF: settings put secure screensaver_enabled 0
+
+    @Option(name = "notification-led",
+            description = "Turn the notification led on or off")
+    protected BinaryState mNotificationLed = BinaryState.IGNORE;
+    // ON:  settings put system notification_light_pulse 1
+    // OFF: settings put system notification_light_pulse 0
+
+    // Media
+    @Option(name = "trigger-media-mounted",
+            description = "Trigger a MEDIA_MOUNTED broadcast")
+    protected boolean mTriggerMediaMounted = false;
+    // am broadcast -a android.intent.action.MEDIA_MOUNTED -d file://${EXTERNAL_STORAGE}
+
+    // Location
+    @Option(name = "location-gps",
+            description = "Turn the GPS location on or off")
+    protected BinaryState mLocationGps = BinaryState.IGNORE;
+    // ON:  settings put secure location_providers_allowed +gps
+    // OFF: settings put secure location_providers_allowed -gps
+
+    @Option(name = "location-network",
+            description = "Turn the network location on or off")
+    protected BinaryState mLocationNetwork = BinaryState.IGNORE;
+    // ON:  settings put secure location_providers_allowed +network
+    // OFF: settings put secure location_providers_allowed -network
+
+    // Sensor
+    @Option(name = "auto-rotate",
+            description = "Turn auto rotate on or off")
+    protected BinaryState mAutoRotate = BinaryState.IGNORE;
+    // ON:  settings put system accelerometer_rotation 1
+    // OFF: settings put system accelerometer_rotation 0
+
+    // Power
+    @Option(name = "battery-saver-mode",
+            description = "Turn battery saver mode manually on or off. If OFF but battery is " +
+            "less battery-saver-trigger, the device will still go into battery saver mode")
+    protected BinaryState mBatterySaver = BinaryState.IGNORE;
+    // ON:  dumpsys battery set usb 0
+    //      settings put global low_power 1
+    // OFF: settings put global low_power 0
+
+    @Option(name = "battery-saver-trigger",
+            description = "Set the battery saver trigger level. Should be [1-99] to enable, or " +
+            "0 to disable automatic battery saver mode")
+    protected Integer mBatterySaverTrigger = null;
+    // settings put global low_power_trigger_level $N
+
+    @Option(name = "disable-doze",
+            description = "Disable device from going into doze mode. This option is only " +
+            "applicable for M+")
+    protected boolean mDisableDoze = false;
+    // dumpsys deviceidle disable
+
+    // Time
+    @Option(name = "auto-update-time",
+            description = "Turn auto update time on or off")
+    protected BinaryState mAutoUpdateTime = BinaryState.IGNORE;
+    // ON:  settings put system auto_time 1
+    // OFF: settings put system auto_time 0
+
+    @Option(name = "auto-update-timezone",
+            description = "Turn auto update timezone on or off")
+    protected BinaryState mAutoUpdateTimezone = BinaryState.IGNORE;
+    // ON:  settings put system auto_timezone 1
+    // OFF: settings put system auto_timezone 0
+
+    // Calling
+    @Option(name = "disable-dialing",
+            description = "Disable dialing")
+    protected boolean mDisableDialing = true;
+    // setprop ro.telephony.disable-call true"
+
+    @Option(name = "default-sim-data",
+            description = "Set the default sim card slot for data. Leave unset for single SIM " +
+            "devices")
+    protected Integer mDefaultSimData = null;
+    // settings put global multi_sim_data_call $N
+
+    @Option(name = "default-sim-voice",
+            description = "Set the default sim card slot for voice calls. Leave unset for single " +
+            "SIM devices")
+    protected Integer mDefaultSimVoice = null;
+    // settings put global multi_sim_voice_call $N
+
+    @Option(name = "default-sim-sms",
+            description = "Set the default sim card slot for SMS. Leave unset for single SIM " +
+            "devices")
+    protected Integer mDefaultSimSms = null;
+    // settings put global multi_sim_sms $N
+
+    // Audio
+    private static final boolean DEFAULT_DISABLE_AUDIO = true;
+    @Option(name = "disable-audio",
+            description = "Disable the audio")
+    protected boolean mDisableAudio = DEFAULT_DISABLE_AUDIO;
+    // setprop ro.audio.silent 1"
+
+    // Test harness
+    @Option(name = "disable",
+            description = "Disable the device setup")
+    protected boolean mDisable = false;
+
+    @Option(name = "force-skip-system-props",
+            description = "Force setup to not modify any device system properties. All other " +
+            "system property options will be ignored")
+    protected boolean mForceSkipSystemProps = false;
+
+    @Option(name = "force-skip-settings",
+            description = "Force setup to not modify any device settings. All other setting " +
+            "options will be ignored.")
+    protected boolean mForceSkipSettings = false;
+
+    @Option(name = "force-skip-run-commands",
+            description = "Force setup to not run any additional commands. All other commands " +
+            "will be ignored.")
+    protected boolean mForceSkipRunCommands = false;
+
+    @Option(name = "set-test-harness",
+            description = "Set the read-only test harness flag on boot")
+    protected boolean mSetTestHarness = true;
+    // setprop ro.monkey 1
+    // setprop ro.test_harness 1
+
+    @Option(name = "disable-dalvik-verifier",
+            description = "Disable the dalvik verifier on device. Allows package-private " +
+            "framework tests to run.")
+    protected boolean mDisableDalvikVerifier = false;
+    // setprop dalvik.vm.dexopt-flags v=n
+
+    @Option(name = "set-property",
+            description = "Set the specified property on boot. Option may be repeated but only " +
+            "the last value for a given key will be set.")
+    protected Map<String, String> mSetProps = new HashMap<>();
+
+    @Option(name = "set-system-setting",
+            description = "Change a system (non-secure) setting. Option may be repeated and all " +
+            "key/value pairs will be set in order.")
+    // Use a Multimap since it is possible for a setting to have multiple values for the same key
+    protected MultiMap<String, String> mSystemSettings = new MultiMap<>();
+
+    @Option(name = "set-secure-setting",
+            description = "Change a secure setting. Option may be repeated and all key/value " +
+            "pairs will be set in order.")
+    // Use a Multimap since it is possible for a setting to have multiple values for the same key
+    protected MultiMap<String, String> mSecureSettings = new MultiMap<>();
+
+    @Option(name = "set-global-setting",
+            description = "Change a global setting. Option may be repeated and all key/value " +
+            "pairs will be set in order.")
+    // Use a Multimap since it is possible for a setting to have multiple values for the same key
+    protected MultiMap<String, String> mGlobalSettings = new MultiMap<>();
+
+    protected List<String> mRunCommandBeforeSettings = new ArrayList<>();
+
+    @Option(name = "run-command",
+            description = "Run an adb shell command. Option may be repeated")
+    protected List<String> mRunCommandAfterSettings = new ArrayList<>();
+
+    @Option(name = "disconnect-wifi-after-test",
+            description = "Disconnect from wifi network after test completes.")
     private boolean mDisconnectWifiAfterTest = true;
 
-    @Option(name="min-external-store-space", description="the minimum amount of free space in KB" +
-            " that must be present on device's external storage.")
-    // require 500K by default. Values <=0 mean external storage is not required
-    private long mMinExternalStoreSpace = 500;
+    private static final long DEFAULT_MIN_EXTERNAL_STORAGE_KB = 500;
+    @Option(name = "min-external-storage-kb",
+            description="The minimum amount of free space in KB that must be present on device's " +
+            "external storage.")
+    protected long mMinExternalStorageKb = DEFAULT_MIN_EXTERNAL_STORAGE_KB;
 
     @Option(name = "local-data-path",
-            description = "optional local file path of test data to sync to device's external " +
+            description = "Optional local file path of test data to sync to device's external " +
             "storage. Use --remote-data-path to set remote location.")
-    private File mLocalDataFile = null;
+    protected File mLocalDataFile = null;
 
     @Option(name = "remote-data-path",
-            description = "optional file path on device's external storage to sync test data. " +
+            description = "Optional file path on device's external storage to sync test data. " +
             "Must be used with --local-data-path.")
-    private String mRemoteDataPath = null;
+    protected String mRemoteDataPath = null;
 
-    @Option(name = "force-skip-system-props", description =
-            "force setup to not modify any device system properties. " +
-            "All other system property options will be ignored.")
-    private boolean mForceNoSystemProps = false;
+    // Deprecated options follow
+    @Option(name = "min-external-store-space",
+            description = "deprecated, use option min-external-storage-kb. The minimum amount of " +
+            "free space in KB that must be present on device's external storage.")
+    @Deprecated
+    private long mDeprecatedMinExternalStoreSpace = DEFAULT_MIN_EXTERNAL_STORAGE_KB;
 
-    @Option(name="disable-dialing", description="set disable dialing property on boot.")
-    private boolean mDisableDialing = true;
+    @Option(name = "audio-silent",
+            description = "deprecated, use option disable-audio. set ro.audio.silent on boot.")
+    @Deprecated
+    private boolean mDeprecatedSetAudioSilent = DEFAULT_DISABLE_AUDIO;
 
-    @Option(name="set-test-harness", description="set the read-only test harness flag on boot. " +
-            "Requires adb root.")
-    private boolean mSetTestHarness = true;
+    @Option(name = "setprop",
+            description = "deprecated, use option set-property. set the specified property on " +
+            "boot. Format: --setprop key=value. May be repeated.")
+    @Deprecated
+    private Collection<String> mDeprecatedSetProps = new ArrayList<String>();
 
-    @Option(name="audio-silent", description="set ro.audio.silent on boot.")
-    private boolean mSetAudioSilent = true;
-
-    @Option(name="disable-dalvik-verifier", description="disable the dalvik verifier on device. "
-        + "Allows package-private framework tests to run.")
-    private boolean mDisableDalvikVerifier = false;
-
-    @Option(name="setprop", description="set the specified property on boot.  " +
-            "Format: --setprop key=value.  May be repeated.")
-    private Collection<String> mSetProps = new ArrayList<String>();
-
-    /**
-     * Sets the local data path to use
-     * <p/>
-     * Exposed for unit testing
-     */
-    void setLocalDataPath(File localPath) {
-        mLocalDataFile = localPath;
-    }
-
-    /**
-     * Sets the remote data path to use
-     * <p/>
-     * Exposed for unit testing
-     */
-    void setRemoteDataPath(String remotePath) {
-        mRemoteDataPath = remotePath;
-    }
-
-    /**
-     * Sets the wifi network ssid to setup.
-     * <p/>
-     * Exposed for unit testing
-     */
-    void setWifiNetwork(String network) {
-        mWifiNetwork = network;
-    }
-
-    /**
-     * Sets the minimum external store space
-     * <p/>
-     * Exposed for unit testing
-     */
-    void setMinExternalStoreSpace(int minKBytes) {
-        mMinExternalStoreSpace = minKBytes;
-    }
-
-    /**
-     * Adds a property to the list of properties to set
-     * <p/>
-     * Exposed for unit testing
-     */
-    void addSetProperty(String prop) {
-        mSetProps.add(prop);
-    }
+    private static final String PERSIST_PREFIX = "persist.";
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
-            DeviceNotAvailableException, BuildError {
-        Log.i(LOG_TAG, String.format("Performing setup on %s", device.getSerialNumber()));
+    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws DeviceNotAvailableException,
+            TargetSetupError {
+        if (mDisable) {
+            return;
+        }
+
+        CLog.i("Performing setup on %s", device.getSerialNumber());
 
         if (!device.enableAdbRoot()) {
-            throw new TargetSetupError(String.format("failed to enable adb root on %s",
+            throw new TargetSetupError(String.format("Failed to enable adb root on %s",
                     device.getSerialNumber()));
         }
 
-        configureSystemProperties(device);
-
+        // Convert deprecated options into current options
+        processDeprecatedOptions();
+        // Convert options into settings and run commands
+        processOptions(device);
+        // Change system props (will reboot device)
+        changeSystemProps(device);
+        // Run commands designated to be run before changing settings
+        runCommands(device, mRunCommandBeforeSettings);
+        // Change settings
         changeSettings(device);
-
-        keepScreenOn(device);
-
-        connectToWifi(device);
-
+        // Connect wifi after settings since this may take a while
+        connectWifi(device);
+        // Sync data after settings since this may take a while
         syncTestData(device);
-
+        // Run commands designated to be run after changing settings
+        runCommands(device, mRunCommandAfterSettings);
+        // Throw an error if there is not enough storage space
         checkExternalStoreSpace(device);
 
         device.clearErrorDialogs();
     }
 
     /**
-     * Configures device system properties.
-     * <p/>
-     * Device will be rebooted if any property is changed.
-     *
-     * @param device
-     * @throws TargetSetupError
-     * @throws DeviceNotAvailableException
-     */
-    private void configureSystemProperties(ITestDevice device) throws TargetSetupError,
-            DeviceNotAvailableException {
-        if (mForceNoSystemProps) {
-            return;
-        }
-        // build the local.prop file contents with properties to change
-        StringBuilder propertyBuilder = new StringBuilder();
-        if (mDisableDialing) {
-            propertyBuilder.append("ro.telephony.disable-call=true\n");
-        }
-        if (mSetTestHarness) {
-            // set both ro.monkey and ro.test_harness, for compatibility with older platforms
-            propertyBuilder.append("ro.monkey=1\n");
-            propertyBuilder.append("ro.test_harness=1\n");
-        }
-        if (mSetAudioSilent) {
-            propertyBuilder.append("ro.audio.silent=1\n");
-        }
-        if (mDisableDalvikVerifier) {
-            propertyBuilder.append("dalvik.vm.dexopt-flags = v=n\n");
-        }
-        for (String prop : mSetProps) {
-            if (prop.startsWith(PERSIST_PREFIX)) {
-                prop = prop.replace('=', ' ');
-                device.executeShellCommand("setprop " + prop);
-            } else {
-                propertyBuilder.append(prop);
-                propertyBuilder.append("\n");
-            }
-        }
-        if (propertyBuilder.length() > 0) {
-            // create a local.prop file, and push it to /data/local.prop
-            boolean result = device.pushString(propertyBuilder.toString(), "/data/local.prop");
-            if (!result) {
-                throw new TargetSetupError(String.format("Failed to push file to %s",
-                        device.getSerialNumber()));
-            }
-            // Set reasonable permissions for /data/local.prop
-            device.executeShellCommand("chmod 644 /data/local.prop");
-            Log.i(LOG_TAG, String.format(
-                    "Setup requires system property change. Reboot of %s required",
-                    device.getSerialNumber()));
-            device.reboot();
-        }
-    }
-
-    /**
-     * Change additional settings for the device. This is intended to be overridden by subclass for
-     * additional change of settings.
-     *
-     * @param device
-     * @throws DeviceNotAvailableException
-     * @throws TargetSetupError
-     */
-    protected void changeSettings(ITestDevice device) throws DeviceNotAvailableException,
-            TargetSetupError {
-        // ignore
-    }
-
-    /**
-     * @param device
-     * @throws DeviceNotAvailableException
-     */
-    private void keepScreenOn(ITestDevice device) throws DeviceNotAvailableException {
-        device.executeShellCommand("svc power stayon true");
-    }
-
-    /**
-     * Check that device external store has the required space
-     *
-     * @param device
-     * @throws DeviceNotAvailableException if device does not have required space
-     */
-    private void checkExternalStoreSpace(ITestDevice device) throws DeviceNotAvailableException {
-        if (mMinExternalStoreSpace > 0) {
-            long freeSpace = device.getExternalStoreFreeSpace();
-            if (freeSpace < mMinExternalStoreSpace) {
-                throw new DeviceNotAvailableException(String.format(
-                        "External store free space %dK is less than required %dK for device %s",
-                        freeSpace , mMinExternalStoreSpace, device.getSerialNumber()));
-            }
-        }
-    }
-
-    /**
-     * Connect to wifi network if specified
-     *
-     * @param device
-     * @throws DeviceNotAvailableException
-     * @throws TargetSetupError if failed to connect to wifi
-     */
-    private void connectToWifi(ITestDevice device) throws DeviceNotAvailableException,
-            TargetSetupError {
-        if (mWifiNetwork != null) {
-            if (!device.connectToWifiNetwork(mWifiNetwork, mWifiPsk)) {
-                throw new TargetSetupError(String.format(
-                        "Failed to connect to wifi network %s on %s", mWifiNetwork,
-                        device.getSerialNumber()));
-            }
-        }
-    }
-
-    /**
-     * Syncs a set of test data files, specified via local-data-path, to devices external storage.
-     *
-     * @param device the {@link ITestDevice} to sync data to
-     * @throws TargetSetupError if data fails to sync
-     */
-    void syncTestData(ITestDevice device) throws TargetSetupError, DeviceNotAvailableException {
-        if (mLocalDataFile != null) {
-            if (!mLocalDataFile.exists() || !mLocalDataFile.isDirectory()) {
-                throw new TargetSetupError(String.format("local-data-path %s is not a directory",
-                        mLocalDataFile.getAbsolutePath()));
-
-            }
-            String fullRemotePath = device.getIDevice().getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
-            if (fullRemotePath == null) {
-                throw new TargetSetupError(String.format(
-                        "failed to get external storage path on device %s",
-                        device.getSerialNumber()));
-            }
-            if (mRemoteDataPath != null) {
-                fullRemotePath = String.format("%s/%s", fullRemotePath, mRemoteDataPath);
-            }
-            boolean result = device.syncFiles(mLocalDataFile, fullRemotePath);
-            if (!result) {
-                // TODO: get exact error code and respond accordingly
-                throw new TargetSetupError(String.format(
-                        "failed to sync test data from local-data-path %s to %s on device %s",
-                        mLocalDataFile.getAbsolutePath(), fullRemotePath,
-                        device.getSerialNumber()));
-            }
-        }
-    }
-
-    protected boolean isReleaseBuildName(String name) {
-        return RELEASE_BUILD_NAME_PATTERN.matcher(name).matches();
-    }
-
-    /**
      * {@inheritDoc}
      */
     @Override
     public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
             throws DeviceNotAvailableException {
-        Log.i(LOG_TAG, String.format("Performing teardown on %s", device.getSerialNumber()));
+        if (mDisable) {
+            return;
+        }
 
-        if (mWifiNetwork != null && mDisconnectWifiAfterTest) {
-            disconnectFromWifi(device);
+        CLog.i("Performing teardown on %s", device.getSerialNumber());
+
+        if (e instanceof DeviceFailedToBootError) {
+            CLog.d("boot failure: skipping teardown");
+            return;
+        }
+
+        // Only try to disconnect if wifi ssid is set since isWifiEnabled() is a heavy operation
+        // which should be avoided when possible
+        if (mDisconnectWifiAfterTest && mWifiSsid != null && device.isWifiEnabled()) {
+            boolean result = device.disconnectFromWifi();
+            if (result) {
+                CLog.i("Successfully disconnected from wifi network on %s",
+                        device.getSerialNumber());
+            } else {
+                CLog.w("Failed to disconnect from wifi network on %s", device.getSerialNumber());
+            }
         }
     }
 
-    private void disconnectFromWifi(ITestDevice device) throws DeviceNotAvailableException {
-        if (device.isWifiEnabled()) {
-            if (!device.disconnectFromWifi()) {
-                CLog.w("Failed to disconnect from wifi network on %s", device.getSerialNumber());
-                return;
+    /**
+     * Processes the deprecated options converting them into the currently used options.
+     * <p>
+     * This method should be run before any other processing methods. Will throw a
+     * {@link TargetSetupError} if the deprecated option overrides a specified non-deprecated
+     * option.
+     * </p>
+     * @throws TargetSetupError if there is a conflict
+     */
+    public void processDeprecatedOptions() throws TargetSetupError {
+        if (mDeprecatedMinExternalStoreSpace != DEFAULT_MIN_EXTERNAL_STORAGE_KB) {
+            if (mMinExternalStorageKb != DEFAULT_MIN_EXTERNAL_STORAGE_KB) {
+                throw new TargetSetupError("Deprecated option min-external-store-space conflicts " +
+                        "with option min-external-storage-kb");
             }
-            CLog.i("Successfully disconnected from wifi network on %s", device.getSerialNumber());
+            mMinExternalStorageKb = mDeprecatedMinExternalStoreSpace;
         }
+
+        if (mDeprecatedSetAudioSilent != DEFAULT_DISABLE_AUDIO) {
+            if (mDisableAudio != DEFAULT_DISABLE_AUDIO) {
+                throw new TargetSetupError("Deprecated option audio-silent conflicts with " +
+                        "option disable-audio");
+            }
+            mDisableAudio = mDeprecatedSetAudioSilent;
+        }
+
+        if (!mDeprecatedSetProps.isEmpty()) {
+            if (!mSetProps.isEmpty()) {
+                throw new TargetSetupError("Deprecated option setprop conflicts with option " +
+                        "set-property ");
+            }
+            for (String prop : mDeprecatedSetProps) {
+                String[] parts = prop.split("=", 2);
+                String key = parts[0].trim();
+                String value = parts.length == 2 ? parts[1].trim() : "";
+                mSetProps.put(key, value);
+            }
+        }
+    }
+
+    /**
+     * Process all the {@link Option}s and turn them into system props, settings, or run commands.
+     * Does not run any commands on the device at this time.
+     * <p>
+     * Exposed so that children classes may override this.
+     * </p>
+     *
+     * @param device The {@link ITestDevice}
+     * @throws DeviceNotAvailableException if the device is not available
+     * @throws TargetSetupError if the {@link Option}s conflict
+     */
+    public void processOptions(ITestDevice device) throws DeviceNotAvailableException,
+            TargetSetupError {
+        setSettingForBinaryState(mWifi, mGlobalSettings, "wifi_on", "1", "0");
+        setCommandForBinaryState(mWifi, mRunCommandAfterSettings,
+                "svc wifi enable", "svc wifi disable");
+
+        setSettingForBinaryState(mWifiWatchdog, mGlobalSettings, "wifi_watchdog", "1", "0");
+
+        setSettingForBinaryState(mWifiScanAlwaysEnabled, mGlobalSettings,
+                "wifi_scan_always_enabled", "1", "0");
+
+        setCommandForBinaryState(mEthernet, mRunCommandAfterSettings,
+                "ifconfig eth0 up", "ifconfig eth0 down");
+
+        setCommandForBinaryState(mBluetooth, mRunCommandAfterSettings,
+                "service call bluetooth_manager 6", "service call bluetooth_manager 8");
+
+        if (mScreenBrightness != null && BinaryState.ON.equals(mScreenAdaptiveBrightness)) {
+            throw new TargetSetupError("Option screen-brightness cannot be set when " +
+                    "screen-adaptive-brightness is set to ON");
+        }
+
+        setSettingForBinaryState(mScreenAdaptiveBrightness, mSystemSettings,
+                "screen_brightness_mode", "1", "0");
+
+        if (mScreenBrightness != null) {
+            mSystemSettings.put("screen_brightness", Integer.toString(mScreenBrightness));
+        }
+
+        setCommandForBinaryState(mScreenAlwaysOn, mRunCommandBeforeSettings,
+                "svc power stayon true", "svc power stayon false");
+
+        if (mScreenTimeoutSecs != null) {
+            mSystemSettings.put("screen_off_timeout", Long.toString(mScreenTimeoutSecs * 1000));
+        }
+
+        setSettingForBinaryState(mScreenAmbientMode, mSecureSettings, "doze_enabled", "1", "0");
+
+        setSettingForBinaryState(mWakeGesture, mSecureSettings, "wake_gesture_enabled", "1", "0");
+
+        setSettingForBinaryState(mScreenSaver, mSecureSettings, "screensaver_enabled", "1", "0");
+
+        setSettingForBinaryState(mNotificationLed, mSystemSettings,
+                "notification_light_pulse", "1", "0");
+
+        if (mTriggerMediaMounted) {
+            mRunCommandAfterSettings.add("am broadcast -a android.intent.action.MEDIA_MOUNTED -d " +
+                    "file://${EXTERNAL_STORAGE}");
+        }
+
+        setSettingForBinaryState(mLocationGps, mSecureSettings,
+                "location_providers_allowed", "+gps", "-gps");
+
+        setSettingForBinaryState(mLocationNetwork, mSecureSettings,
+                "location_providers_allowed", "+network", "-network");
+
+        setSettingForBinaryState(mAutoRotate, mSystemSettings, "accelerometer_rotation", "1", "0");
+
+        setCommandForBinaryState(mBatterySaver, mRunCommandBeforeSettings,
+                "dumpsys battery set usb 0", null);
+        setSettingForBinaryState(mBatterySaver, mGlobalSettings, "low_power", "1", "0");
+
+        if (mBatterySaverTrigger != null) {
+            mGlobalSettings.put("low_power_trigger_level", Integer.toString(mBatterySaverTrigger));
+        }
+
+        if (mDisableDoze) {
+            mRunCommandAfterSettings.add("dumpsys deviceidle disable");
+        }
+
+        setSettingForBinaryState(mAutoUpdateTime, mSystemSettings, "auto_time", "1", "0");
+
+        setSettingForBinaryState(mAutoUpdateTimezone, mSystemSettings, "auto_timezone", "1", "0");
+
+        if (mDisableDialing) {
+            mSetProps.put("ro.telephony.disable-call", "true");
+        }
+
+        if (mDefaultSimData != null) {
+            mGlobalSettings.put("multi_sim_data_call", Integer.toString(mDefaultSimData));
+        }
+
+        if (mDefaultSimVoice != null) {
+            mGlobalSettings.put("multi_sim_voice_call", Integer.toString(mDefaultSimVoice));
+        }
+
+        if (mDefaultSimSms != null) {
+            mGlobalSettings.put("multi_sim_sms", Integer.toString(mDefaultSimSms));
+        }
+
+        if (mDisableAudio) {
+            mSetProps.put("ro.audio.silent", "1");
+        }
+
+        if (mSetTestHarness) {
+            // set both ro.monkey and ro.test_harness, for compatibility with older platforms
+            mSetProps.put("ro.monkey", "1");
+            mSetProps.put("ro.test_harness", "1");
+        }
+
+        if (mDisableDalvikVerifier) {
+            mSetProps.put("dalvik.vm.dexopt-flags", "v=n");
+        }
+    }
+
+    /**
+     * Change the system properties on the device.
+     *
+     * @param device The {@link ITestDevice}
+     * @throws DeviceNotAvailableException if the device is not available
+     * @throws TargetSetupError if there was a failure setting the system properties
+     */
+    private void changeSystemProps(ITestDevice device) throws DeviceNotAvailableException,
+            TargetSetupError {
+        if (mForceSkipSystemProps) {
+            CLog.d("Skipping system props due to force-skip-system-props");
+            return;
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, String> prop : mSetProps.entrySet()) {
+            if (prop.getKey().startsWith(PERSIST_PREFIX)) {
+                String command = String.format("setprop \"%s\" \"%s\"",
+                        prop.getKey(), prop.getValue());
+                device.executeShellCommand(command);
+            } else {
+                sb.append(String.format("%s=%s\n", prop.getKey(), prop.getValue()));
+            }
+        }
+
+        if (sb.length() == 0) {
+            return;
+        }
+
+        boolean result = device.pushString(sb.toString(), "/data/local.prop");
+        if (!result) {
+            throw new TargetSetupError(String.format("Failed to push /data/local.prop to %s",
+                    device.getSerialNumber()));
+        }
+        // Set reasonable permissions for /data/local.prop
+        device.executeShellCommand("chmod 644 /data/local.prop");
+        CLog.i("Rebooting %s due to system property change", device.getSerialNumber());
+        device.reboot();
+    }
+
+    /**
+     * Change the settings on the device.
+     * <p>
+     * Exposed so children classes may override.
+     * </p>
+     *
+     * @param device The {@link ITestDevice}
+     * @throws DeviceNotAvailableException if the device is not available
+     * @throws TargetSetupError if there was a failure setting the settings
+     */
+    public void changeSettings(ITestDevice device) throws DeviceNotAvailableException,
+            TargetSetupError {
+        if (mForceSkipSettings) {
+            CLog.d("Skipping settings due to force-skip-setttings");
+            return;
+        }
+
+        if (mSystemSettings.isEmpty() && mSecureSettings.isEmpty() && mGlobalSettings.isEmpty() &&
+                BinaryState.IGNORE.equals(mAirplaneMode)) {
+            CLog.d("No settings to change");
+            return;
+        }
+
+        if (device.getApiLevel() < 22) {
+            throw new TargetSetupError(String.format("Changing setting not supported on %s, " +
+                    "must be API 22+", device.getSerialNumber()));
+        }
+
+        // Special case airplane mode since it needs to be set before other connectivity settings
+        // For example, it is possible to enable airplane mode and then turn wifi on
+        String command = "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state %s";
+        switch (mAirplaneMode) {
+            case ON:
+                CLog.d("Changing global setting airplane_mode_on to 1");
+                device.executeShellCommand("settings put global \"airplane_mode_on\" \"1\"");
+                if (!mForceSkipRunCommands) {
+                    device.executeShellCommand(String.format(command, "true"));
+                }
+                break;
+            case OFF:
+                CLog.d("Changing global setting airplane_mode_on to 0");
+                device.executeShellCommand("settings put global \"airplane_mode_on\" \"0\"");
+                if (!mForceSkipRunCommands) {
+                    device.executeShellCommand(String.format(command, "false"));
+                }
+                break;
+            case IGNORE:
+                // No-op
+                break;
+        }
+
+        String settingCommand = "settings put %s \"%s\" \"%s\"";
+        for (String key : mSystemSettings.keySet()) {
+            for (String value : mSystemSettings.get(key)) {
+                CLog.d("Changing system setting %s to %s", key, value);
+                device.executeShellCommand(String.format(settingCommand, "system", key, value));
+            }
+        }
+        for (String key : mSecureSettings.keySet()) {
+            for (String value : mSecureSettings.get(key)) {
+                CLog.d("Changing secure setting %s to %s", key, value);
+                device.executeShellCommand(String.format(settingCommand, "secure", key, value));
+            }
+        }
+
+        for (String key : mGlobalSettings.keySet()) {
+            for (String value : mGlobalSettings.get(key)) {
+                CLog.d("Changing global setting %s to %s", key, value);
+                device.executeShellCommand(String.format(settingCommand, "global", key, value));
+            }
+        }
+    }
+
+    /**
+     * Execute additional commands on the device.
+     *
+     * @param device The {@link ITestDevice}
+     * @param commands The list of commands to run
+     * @throws DeviceNotAvailableException if the device is not available
+     * @throws TargetSetupError if there was a failure setting the settings
+     */
+    private void runCommands(ITestDevice device, List<String> commands)
+            throws DeviceNotAvailableException, TargetSetupError {
+        if (mForceSkipRunCommands) {
+            CLog.d("Skipping run commands due to force-skip-run-commands");
+            return;
+        }
+
+        for (String command : commands) {
+            device.executeShellCommand(command);
+        }
+    }
+
+    /**
+     * Connects device to Wifi if SSID is specified.
+     *
+     * @param device The {@link ITestDevice}
+     * @throws DeviceNotAvailableException if the device is not available
+     * @throws TargetSetupError if there was a failure setting the settings
+     */
+    private void connectWifi(ITestDevice device) throws DeviceNotAvailableException,
+            TargetSetupError {
+        if (mForceSkipRunCommands) {
+            CLog.d("Skipping connect wifi due to force-skip-run-commands");
+            return;
+        }
+
+        if (mWifiSsid != null) {
+            if (!device.connectToWifiNetwork(mWifiSsid, mWifiPsk)) {
+                throw new TargetSetupError(String.format(
+                        "Failed to connect to wifi network %s on %s", mWifiSsid,
+                        device.getSerialNumber()));
+            }
+        }
+    }
+
+    /**
+     * Syncs a set of test data files, specified via local-data-path, to devices external storage.
+     *
+     * @param device The {@link ITestDevice}
+     * @throws DeviceNotAvailableException if the device is not available
+     * @throws TargetSetupError if data fails to sync
+     */
+    private void syncTestData(ITestDevice device) throws DeviceNotAvailableException,
+            TargetSetupError {
+        if (mLocalDataFile == null) {
+            return;
+        }
+
+        if (!mLocalDataFile.exists() || !mLocalDataFile.isDirectory()) {
+            throw new TargetSetupError(String.format(
+                    "local-data-path %s is not a directory", mLocalDataFile.getAbsolutePath()));
+        }
+        String fullRemotePath = device.getIDevice().getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
+        if (fullRemotePath == null) {
+            throw new TargetSetupError(String.format(
+                    "failed to get external storage path on device %s", device.getSerialNumber()));
+        }
+        if (mRemoteDataPath != null) {
+            fullRemotePath = String.format("%s/%s", fullRemotePath, mRemoteDataPath);
+        }
+        boolean result = device.syncFiles(mLocalDataFile, fullRemotePath);
+        if (!result) {
+            // TODO: get exact error code and respond accordingly
+            throw new TargetSetupError(String.format(
+                    "failed to sync test data from local-data-path %s to %s on device %s",
+                    mLocalDataFile.getAbsolutePath(), fullRemotePath, device.getSerialNumber()));
+        }
+    }
+
+    /**
+     * Check that device external store has the required space
+     *
+     * @param device The {@link ITestDevice}
+     * @throws DeviceNotAvailableException if the device is not available or if the device does not
+     * have the required space
+     */
+    private void checkExternalStoreSpace(ITestDevice device) throws DeviceNotAvailableException {
+        if (mMinExternalStorageKb <= 0) {
+            return;
+        }
+
+        long freeSpace = device.getExternalStoreFreeSpace();
+        if (freeSpace < mMinExternalStorageKb) {
+            throw new DeviceNotAvailableException(String.format(
+                    "External store free space %dK is less than required %dK for device %s",
+                    freeSpace , mMinExternalStorageKb, device.getSerialNumber()));
+        }
+    }
+
+    /**
+     * Helper method to add an ON/OFF setting to a setting map.
+     *
+     * @param state The {@link BinaryState}
+     * @param settingsMap The {@link MultiMap} used to store the settings.
+     * @param setting The setting key
+     * @param onValue The value if ON
+     * @param offValue The value if OFF
+     */
+    public static void setSettingForBinaryState(BinaryState state,
+            MultiMap<String, String> settingsMap, String setting, String onValue, String offValue) {
+        switch (state) {
+            case ON:
+                settingsMap.put(setting, onValue);
+                break;
+            case OFF:
+                settingsMap.put(setting, offValue);
+                break;
+            case IGNORE:
+                // Do nothing
+                break;
+        }
+    }
+
+    /**
+     * Helper method to add an ON/OFF run command to be executed on the device.
+     *
+     * @param state The {@link BinaryState}
+     * @param commands The list of commands to add the on or off command to.
+     * @param onCommand The command to run if ON. Ignored if the command is {@code null}
+     * @param offCommand The command to run if OFF. Ignored if the command is {@code null}
+     */
+    public static void setCommandForBinaryState(BinaryState state, List<String> commands,
+            String onCommand, String offCommand) {
+        switch (state) {
+            case ON:
+                if (onCommand != null) {
+                    commands.add(onCommand);
+                }
+                break;
+            case OFF:
+                if (offCommand != null) {
+                    commands.add(offCommand);
+                }
+                break;
+            case IGNORE:
+                // Do nothing
+                break;
+        }
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setAirplaneMode(BinaryState airplaneMode) {
+        mAirplaneMode = airplaneMode;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setWifi(BinaryState wifi) {
+        mWifi = wifi;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setWifiNetwork(String wifiNetwork) {
+        mWifiSsid = wifiNetwork;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setWifiWatchdog(BinaryState wifiWatchdog) {
+        mWifiWatchdog = wifiWatchdog;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setWifiScanAlwaysEnabled(BinaryState wifiScanAlwaysEnabled) {
+        mWifiScanAlwaysEnabled = wifiScanAlwaysEnabled;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setEthernet(BinaryState ethernet) {
+        mEthernet = ethernet;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setBluetooth(BinaryState bluetooth) {
+        mBluetooth = bluetooth;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setScreenAdaptiveBrightness(BinaryState screenAdaptiveBrightness) {
+        mScreenAdaptiveBrightness = screenAdaptiveBrightness;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setScreenBrightness(Integer screenBrightness) {
+        mScreenBrightness = screenBrightness;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setScreenAlwaysOn(BinaryState screenAlwaysOn) {
+        mScreenAlwaysOn = screenAlwaysOn;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setScreenTimeoutSecs(Long screenTimeoutSecs) {
+        mScreenTimeoutSecs = screenTimeoutSecs;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setScreenAmbientMode(BinaryState screenAmbientMode) {
+        mScreenAmbientMode = screenAmbientMode;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setWakeGesture(BinaryState wakeGesture) {
+        mWakeGesture = wakeGesture;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setScreenSaver(BinaryState screenSaver) {
+        mScreenSaver = screenSaver;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setNotificationLed(BinaryState notificationLed) {
+        mNotificationLed = notificationLed;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setTriggerMediaMounted(boolean triggerMediaMounted) {
+        mTriggerMediaMounted = triggerMediaMounted;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setLocationGps(BinaryState locationGps) {
+        mLocationGps = locationGps;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setLocationNetwork(BinaryState locationNetwork) {
+        mLocationNetwork = locationNetwork;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setAutoRotate(BinaryState autoRotate) {
+        mAutoRotate = autoRotate;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setBatterySaver(BinaryState batterySaver) {
+        mBatterySaver = batterySaver;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setBatterySaverTrigger(Integer batterySaverTrigger) {
+        mBatterySaverTrigger = batterySaverTrigger;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setDisableDoze(boolean disableDoze) {
+        mDisableDoze = disableDoze;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setAutoUpdateTime(BinaryState autoUpdateTime) {
+        mAutoUpdateTime = autoUpdateTime;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setAutoUpdateTimezone(BinaryState autoUpdateTimezone) {
+        mAutoUpdateTimezone = autoUpdateTimezone;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setDisableDialing(boolean disableDialing) {
+        mDisableDialing = disableDialing;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setDefaultSimData(Integer defaultSimData) {
+        mDefaultSimData = defaultSimData;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setDefaultSimVoice(Integer defaultSimVoice) {
+        mDefaultSimVoice = defaultSimVoice;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setDefaultSimSms(Integer defaultSimSms) {
+        mDefaultSimSms = defaultSimSms;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setDisableAudio(boolean disable) {
+        mDisableAudio = disable;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setTestHarness(boolean setTestHarness) {
+        mSetTestHarness = setTestHarness;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setDisableDalvikVerifier(boolean disableDalvikVerifier) {
+        mDisableDalvikVerifier = disableDalvikVerifier;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setLocalDataPath(File path) {
+        mLocalDataFile = path;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setMinExternalStorageKb(long storageKb) {
+        mMinExternalStorageKb = storageKb;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    protected void setProperty(String key, String value) {
+        mSetProps.put(key, value);
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    @Deprecated
+    protected void setDeprecatedMinExternalStoreSpace(long storeSpace) {
+        mDeprecatedMinExternalStoreSpace = storeSpace;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    @Deprecated
+    protected void setDeprecatedAudioSilent(boolean silent) {
+        mDeprecatedSetAudioSilent = silent;
+    }
+
+    /**
+     * Exposed for unit testing
+     */
+    @Deprecated
+    protected void setDeprecatedSetProp(String prop) {
+        mDeprecatedSetProps.add(prop);
     }
 }
diff --git a/src/com/android/tradefed/targetprep/InstallApkSetup.java b/src/com/android/tradefed/targetprep/InstallApkSetup.java
index 5edc14c..cd05599 100644
--- a/src/com/android/tradefed/targetprep/InstallApkSetup.java
+++ b/src/com/android/tradefed/targetprep/InstallApkSetup.java
@@ -23,14 +23,23 @@
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.AbiFormatter;
 
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
 
 /**
  * A {@link ITargetPreparer} that installs one or more apks located on the filesystem.
+ * <p>
+ * This class should only be used for installing apks from the filesystem when all versions of the
+ * test rely on the apk being on the filesystem.  For tests which use {@link TestAppInstallSetup}
+ * to install apks from the tests zip file, use {@code --alt-dir} to specify an alternate directory
+ * on the filesystem containing the apk for other test configurations (for example, local runs
+ * where the tests zip file is not present).
+ * </p>
  */
 @OptionClass(alias = "install-apk")
 public class InstallApkSetup implements ITargetPreparer {
@@ -47,6 +56,15 @@
             importance = Importance.IF_UNSET)
     private String mForceAbi = null;
 
+    @Option(name = "install-arg",
+            description = "Additional arguments to be passed to install command, "
+                    + "including leading dash, e.g. \"-d\"")
+    private Collection<String> mInstallArgs = new ArrayList<>();
+
+    @Option(name = "post-install-cmd", description =
+            "optional post-install adb shell commands; can be repeated.")
+    private List<String> mPostInstallCmds = new ArrayList<>();
+
     /**
      * {@inheritDoc}
      */
@@ -60,18 +78,26 @@
             }
             Log.i(LOG_TAG, String.format("Installing %s on %s", apk.getName(),
                     device.getSerialNumber()));
-            String[] options = {};
             if (mForceAbi != null) {
                 String abi = AbiFormatter.getDefaultAbi(device, mForceAbi);
                 if (abi != null) {
-                    options = new String[]{String.format("--abi %s ", abi)};
+                    mInstallArgs.add(String.format("--abi %s", abi));
                 }
             }
-            String result = device.installPackage(apk, true, options);
+            String result = device.installPackage(apk, true, mInstallArgs.toArray(new String[]{}));
             if (result != null) {
                 Log.e(LOG_TAG, String.format("Failed to install %s on device %s. Reason: %s",
                         apk.getAbsolutePath(), device.getSerialNumber(), result));
             }
         }
+
+        if (mPostInstallCmds != null && !mPostInstallCmds.isEmpty()){
+            for (String cmd : mPostInstallCmds) {
+                // If the command had any output, the executeShellCommand method will log it at the
+                // VERBOSE level; so no need to do any logging from here.
+                CLog.d("About to run setup command on device %s: %s", device.getSerialNumber(), cmd);
+                device.executeShellCommand(cmd);
+            }
+        }
     }
 }
diff --git a/src/com/android/tradefed/targetprep/InstrumentationPreparer.java b/src/com/android/tradefed/targetprep/InstrumentationPreparer.java
index 0edeb48..e9a1960 100644
--- a/src/com/android/tradefed/targetprep/InstrumentationPreparer.java
+++ b/src/com/android/tradefed/targetprep/InstrumentationPreparer.java
@@ -17,6 +17,9 @@
 package com.android.tradefed.targetprep;
 
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
@@ -25,9 +28,6 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.CollectingTestListener;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestResult.TestStatus;
-import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.testtype.InstrumentationTest;
 import com.android.tradefed.util.RunUtil;
 
@@ -60,10 +60,23 @@
             description="The test method name to run.")
     private String mMethodName = null;
 
+    @Deprecated
     @Option(name = "timeout",
-            description="Aborts the test run if any test takes longer than the specified number of "
-            + "milliseconds. For no timeout, set to 0.")
-    private int mTimeout = 10 * 60 * 1000;  // default to 10 minutes
+            description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
+    private Integer mTimeout = null;
+
+    @Option(name = "shell-timeout",
+            description="The defined timeout (in milliseconds) is used as a maximum waiting time "
+                    + "when expecting the command output from the device. At any time, if the "
+                    + "shell command does not output anything for a period longer than defined "
+                    + "timeout the TF run terminates. For no timeout, set to 0.")
+    private long mShellTimeout = 10 * 60 * 1000;  // default to 10 minutes
+
+    @Option(name = "test-timeout",
+            description="Sets timeout (in milliseconds) that will be applied to each test. In the "
+                    + "event of a test timeout it will log the results and proceed with executing "
+                    + "the next test. For no timeout, set to 0.")
+    private int mTestTimeout = 10 * 60 * 1000;  // default to 10 minutes
 
     @Option(name = "instrumentation-arg",
             description = "Instrumentation arguments to provide.")
@@ -109,7 +122,13 @@
         test.setRunnerName(mRunnerName);
         test.setClassName(mClassName);
         test.setMethodName(mMethodName);
-        test.setTestTimeout(mTimeout);
+        if (mTimeout != null) {
+            CLog.w("\"timeout\" argument is deprecated and should not be used! \"shell-timeout\""
+                    + " argument value is overwritten with %d ms", mTimeout);
+            setShellTimeout(mTimeout);
+        }
+        test.setShellTimeout(mShellTimeout);
+        test.setTestTimeout(mTestTimeout);
         for (Map.Entry<String, String> entry : mInstrArgMap.entrySet()) {
             test.addInstrumentationArg(entry.getKey(), entry.getValue());
         }
@@ -164,8 +183,20 @@
         mMethodName = methodName;
     }
 
+    /**
+     * @Deprecated Use {@link #setShellTimeout(long)} or {@link #setTestTimeout(int)}
+     */
+    @Deprecated
     void setTimeout(int timeout) {
-        mTimeout = timeout;
+        setShellTimeout(timeout);
+    }
+
+    void setShellTimeout(long timeout) {
+        mShellTimeout = timeout;
+    }
+
+    void setTestTimeout(int timeout) {
+        mTestTimeout = timeout;
     }
 
     void setAttempts(int attempts) {
diff --git a/src/com/android/tradefed/targetprep/PushFilePreparer.java b/src/com/android/tradefed/targetprep/PushFilePreparer.java
index 1a9338e..7ef7ac4 100644
--- a/src/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/src/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -25,8 +25,8 @@
 import com.android.tradefed.device.ITestDevice;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Collection;
-import java.util.LinkedList;
 
 /**
  * A {@link ITargetPreparer} that attempts to push any number of files from any host path to any
@@ -35,19 +35,19 @@
  * Should be performed *after* a new build is flashed, and *after* DeviceSetup is run (if enabled)
  */
 @OptionClass(alias = "push-file")
-public class PushFilePreparer implements ITargetPreparer {
+public class PushFilePreparer implements ITargetCleaner {
     private static final String LOG_TAG = "PushFilePreparer";
 
     @Option(name="push", description=
             "A push-spec, formatted as '/path/to/srcfile.txt->/path/to/destfile.txt' or " +
             "'/path/to/srcfile.txt->/path/to/destdir/'. May be repeated.")
-    private Collection<String> mPushSpecs = new LinkedList<String>();
+    private Collection<String> mPushSpecs = new ArrayList<>();
 
     @Option(name="post-push", description=
             "A command to run on the device (with `adb shell (yourcommand)`) after all pushes " +
             "have been attempted.  Will not be run if a push fails with abort-on-push-failure " +
             "enabled.  May be repeated.")
-    private Collection<String> mPostPushCommands = new LinkedList<String>();
+    private Collection<String> mPostPushCommands = new ArrayList<>();
 
     @Option(name="abort-on-push-failure", description=
             "If false, continue if pushes fail.  If true, abort the Invocation on any failure.")
@@ -57,6 +57,17 @@
             "After pushing files, trigger a media scan of external storage on device.")
     private boolean mTriggerMediaScan = false;
 
+    @Option(name="cleanup", description = "Whether files pushed onto device should be cleaned up "
+            + "after test. Note that the preparer does not verify that files/directories have "
+            + "been deleted.")
+    private boolean mCleanup = false;
+
+    @Option(name="remount-system", description="Remounts system partition to be writable "
+            + "so that files could be pushed there too")
+    private boolean mRemount = false;
+
+    private Collection<String> mFilesPushed = null;
+
     /**
      * Set abort on failure.  Exposed for testing.
      */
@@ -92,11 +103,25 @@
     }
 
     /**
+     * Resolve relative file path via {@link IBuildInfo}
+     * @param buildInfo the build artifact information
+     * @param fileName relative file path to be resolved
+     * @return
+     */
+    public File resolveRelativeFilePath(IBuildInfo buildInfo, String fileName) {
+        return buildInfo.getFile(fileName);
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
     public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, BuildError,
             DeviceNotAvailableException {
+        mFilesPushed = new ArrayList<>();
+        if (mRemount) {
+            device.remountSystemWritable();
+        }
         for (String pushspec : mPushSpecs) {
             String[] pair = pushspec.split("->");
             if (pair.length != 2) {
@@ -107,24 +132,28 @@
                     pair[1]));
 
             File src = new File(pair[0]);
-            if (!src.exists()) {
-                src = buildInfo.getFile(pair[0]);
-                if (src == null || !src.exists()) {
-                    fail(String.format("Local source file '%s' does not exist", pair[0]));
-                    continue;
-                }
+            if (!src.isAbsolute()) {
+                src = resolveRelativeFilePath(buildInfo, pair[0]);
+            }
+            if (src == null || !src.exists()) {
+                fail(String.format("Local source file '%s' does not exist", pair[0]));
+                continue;
             }
             if (src.isDirectory()) {
                 if (!device.pushDir(src, pair[1])) {
                     fail(String.format("Failed to push local '%s' to remote '%s'", pair[0],
                             pair[1]));
                     continue;
+                } else {
+                    mFilesPushed.add(pair[1]);
                 }
             } else {
                 if (!device.pushFile(src, pair[1])) {
                     fail(String.format("Failed to push local '%s' to remote '%s'", pair[0],
                             pair[1]));
                     continue;
+                } else {
+                    mFilesPushed.add(pair[1]);
                 }
             }
         }
@@ -140,4 +169,20 @@
                     device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE)));
         }
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+            throws DeviceNotAvailableException {
+        if (!(e instanceof DeviceNotAvailableException) && mCleanup && mFilesPushed != null) {
+            if (mRemount) {
+                device.remountSystemWritable();
+            }
+            for (String devicePath : mFilesPushed) {
+                device.executeShellCommand("rm -r " + devicePath);
+            }
+        }
+    }
 }
diff --git a/src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java b/src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java
index 34acf2b..abb3c7e 100644
--- a/src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java
+++ b/src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java
@@ -36,23 +36,13 @@
     private List<String> mFiles= new ArrayList<String>();
 
     /**
-     * @param device The device to remount
-     * @throws DeviceNotAvailableException
-     */
-    private void remount(ITestDevice device) throws DeviceNotAvailableException {
-        device.enableAdbRoot();
-        device.executeAdbCommand("remount");
-        device.waitForDeviceAvailable();
-    }
-
-    /**
      * {@inheritDoc}
      */
     @Override
     public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
             DeviceNotAvailableException {
 
-        remount(device);
+        device.remountSystemWritable();
         for (String file : mFiles) {
             CLog.d("Removing system app %s from /system/app", file);
             device.executeShellCommand(String.format("rm /system/app/%s", file));
diff --git a/src/com/android/tradefed/targetprep/SdkAvdPreparer.java b/src/com/android/tradefed/targetprep/SdkAvdPreparer.java
index 07a658c..aa6b433 100644
--- a/src/com/android/tradefed/targetprep/SdkAvdPreparer.java
+++ b/src/com/android/tradefed/targetprep/SdkAvdPreparer.java
@@ -86,6 +86,11 @@
             "If unspecified, will launch generic version")
     private String mDevice = null;
 
+    @Option(name = "display", description = "which display to launch the emulator in. " +
+            "If unspecified, display will not be set. Display values should start with :" +
+            " for example for display 1 use ':1'.")
+    private String mDisplay = null;
+
     @Option(name = "abi", description = "abi to select for the avd")
     private String mAbi = null;
 
@@ -110,6 +115,9 @@
             description = "Additional argument to launch the emulator with. Can be repeated.")
     private Collection<String> mEmulatorArgs = new ArrayList<String>();
 
+    @Option(name = "verbose", description = "Use verbose for emulator output")
+    private boolean mVerbose = false;
+
     private final IRunUtil mRunUtil;
     private IDeviceManager mDeviceManager;
 
@@ -188,6 +196,9 @@
             mEmulatorBinary == null ? sdkBuild.getEmulatorToolPath() : mEmulatorBinary;
         List<String> emulatorArgs = ArrayUtil.list(emulatorBinary, "-avd", avd);
 
+        if (mDisplay != null) {
+            emulatorArgs.add(0, "DISPLAY=" + mDisplay);
+        }
         // Ensure the emulator will launch on the same port as the allocated emulator device
         Integer port = EmulatorConsole.getEmulatorPort(device.getSerialNumber());
         if (port == null) {
@@ -207,6 +218,11 @@
             emulatorArgs.add("-gpu");
             emulatorArgs.add("on");
         }
+
+        if (mVerbose) {
+            emulatorArgs.add("-verbose");
+        }
+
         for (Map.Entry<String, String> propEntry : mProps.entrySet()) {
             emulatorArgs.add("-prop");
             emulatorArgs.add(String.format("%s=%s", propEntry.getKey(), propEntry.getValue()));
diff --git a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
index 8581065..3b3ac52 100644
--- a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.targetprep;
 
-import com.android.ddmlib.Log;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.Option;
@@ -23,25 +22,34 @@
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.util.AaptParser;
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.FileUtil;
 
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * A {@link ITargetPreparer} that installs one or more apps from a
  * {@link IDeviceBuildInfo#getTestsDir()} folder onto device.
+ * <p>
+ * This preparer will look in alternate directories if the tests zip does not exist or does not
+ * contain the required apk. The search will go in order from the last alternative dir specified to
+ * the first.
+ * </p>
  */
 @OptionClass(alias = "tests-zip-app")
-public class TestAppInstallSetup implements ITargetPreparer {
+public class TestAppInstallSetup implements ITargetCleaner, IAbiReceiver {
 
-    private static final String LOG_TAG = "TestAppInstallSetup";
-
-    @Option(name = "test-file-name", description =
-        "the name of a test zip file to install on device. Can be repeated.",
-        importance = Importance.IF_UNSET)
+    @Option(name = "test-file-name",
+            description = "the name of a test zip file to install on device. Can be repeated.",
+            importance = Importance.IF_UNSET)
     private Collection<String> mTestFileNames = new ArrayList<String>();
 
     @Option(name = AbiFormatter.FORCE_ABI_STRING,
@@ -49,6 +57,31 @@
             importance = Importance.IF_UNSET)
     private String mForceAbi = null;
 
+    @Option(name = "install-arg",
+            description = "Additional arguments to be passed to install command, "
+                    + "including leading dash, e.g. \"-d\"")
+    private Collection<String> mInstallArgs = new ArrayList<>();
+
+    @Option(name = "cleanup-apks",
+            description = "Whether apks installed should be uninstalled after test. Note that the "
+                    + "preparer does not verify if the apks are successfully removed.")
+    private boolean mCleanup = false;
+
+    @Option(name = "alt-dir",
+            description = "Alternate directory to look for the apk if the apk is not in the tests "
+                    + "zip file. For each alternate dir, will look in //, //data/app, //DATA/app, "
+                    + "and //DATA/app/apk_name/. Can be repeated. Look for apks in last alt-dir "
+                    + "first.")
+    private List<File> mAltDirs = new ArrayList<>();
+
+    @Option(name = "alt-dir-behavior", description = "The order of alternate directory to be used "
+            + "when searching for apks to install")
+    private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK;
+
+    private IAbi mAbi = null;
+
+    private List<String> mPackagesInstalled = null;
+
     /**
      * Adds a file to the list of apks to install
      *
@@ -59,52 +92,138 @@
     }
 
     /**
+     * Resolve the actual apk path based on testing artifact information inside build info.
+     *
+     * @param buildInfo build artifact information
+     * @param apkFileName filename of the apk to install
+     * @return a {@link File} representing the physical apk file on host or {@code null} if the
+     *     file does not exist.
+     */
+    protected File getLocalPathForFilename(IBuildInfo buildInfo, String apkFileName)
+            throws TargetSetupError {
+        String apkBase = apkFileName.split("\\.")[0];
+
+        List<File> dirs = new ArrayList<>();
+        for (File dir : mAltDirs) {
+            dirs.add(dir);
+            // Files in tests zip file will be in DATA/app/ or DATA/app/apk_name
+            dirs.add(FileUtil.getFileForPath(dir, "DATA", "app"));
+            dirs.add(FileUtil.getFileForPath(dir, "DATA", "app", apkBase));
+            // Files in out dir will bein in uses data/app/apk_name
+            dirs.add(FileUtil.getFileForPath(dir, "data", "app", apkBase));
+        }
+        // reverse the order so ones provided via command line last can be searched first
+        Collections.reverse(dirs);
+
+        List<File> expandedTestDirs = new ArrayList<>();
+        if (buildInfo instanceof IDeviceBuildInfo) {
+            File testsDir = ((IDeviceBuildInfo)buildInfo).getTestsDir();
+            if (testsDir != null && testsDir.exists()) {
+                expandedTestDirs.add(FileUtil.getFileForPath(testsDir, "DATA", "app"));
+                expandedTestDirs.add(FileUtil.getFileForPath(testsDir, "DATA", "app", apkBase));
+            }
+        }
+        if (mAltDirBehavior == AltDirBehavior.FALLBACK) {
+            // alt dirs are appended after build artifact dirs
+            expandedTestDirs.addAll(dirs);
+            dirs = expandedTestDirs;
+        } else if (mAltDirBehavior == AltDirBehavior.OVERRIDE) {
+            dirs.addAll(expandedTestDirs);
+        } else {
+            throw new TargetSetupError("Missing handler for alt-dir-behavior: " + mAltDirBehavior);
+        }
+        if (dirs.isEmpty()) {
+            throw new TargetSetupError(
+                    "Provided buildInfo does not contain a valid tests directory and no " +
+                    "alternative directories were provided");
+        }
+
+        for (File dir : dirs) {
+            File testAppFile = new File(dir, apkFileName);
+            if (testAppFile.exists()) {
+                return testAppFile;
+            }
+        }
+        return null;
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
     public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
             DeviceNotAvailableException {
-        if (!(buildInfo instanceof IDeviceBuildInfo)) {
-            throw new IllegalArgumentException(String.format("Provided buildInfo is not a %s",
-                    IDeviceBuildInfo.class.getCanonicalName()));
-        }
         if (mTestFileNames.size() == 0) {
-            Log.i(LOG_TAG, "No test apps to install, skipping");
+            CLog.i("No test apps to install, skipping");
             return;
         }
-        File testsDir = ((IDeviceBuildInfo)buildInfo).getTestsDir();
-        if (testsDir == null || !testsDir.exists()) {
-            throw new TargetSetupError(
-                    "Provided buildInfo does not contain a valid tests directory");
+        if (mCleanup) {
+            mPackagesInstalled = new ArrayList<>();
         }
-
         for (String testAppName : mTestFileNames) {
-            File testAppFile = FileUtil.getFileForPath(testsDir, "DATA", "app", testAppName);
-            if (!testAppFile.exists()) {
-                // in addition to /data/app/TestApp.apk
-                // also check path like /data/app/TestApp/TestApp.apk
-                String[] fields = testAppName.split("\\.");
-                testAppFile = FileUtil.getFileForPath(
-                        testsDir, "DATA", "app", fields[0], testAppName);
-            }
-            if (!testAppFile.exists()) {
+            File testAppFile = getLocalPathForFilename(buildInfo, testAppName);
+            if (testAppFile == null) {
                 throw new TargetSetupError(
                     String.format("Could not find test app %s directory in extracted tests.zip",
-                            testAppFile));
+                            testAppName));
             }
-            String[] options = {};
-            if (mForceAbi != null) {
-                String abi = AbiFormatter.getDefaultAbi(device, mForceAbi);
-                if (abi != null) {
-                    options = new String[]{String.format("--abi %s ", abi)};
-                }
+            // resolve abi flags
+            if (mAbi != null && mForceAbi != null) {
+                throw new IllegalStateException("cannot specify both abi flags");
             }
-            String result = device.installPackage(testAppFile, true, options);
+            String abiName = null;
+            if (mAbi != null) {
+                abiName = mAbi.getName();
+            } else if (mForceAbi != null) {
+                abiName = AbiFormatter.getDefaultAbi(device, mForceAbi);
+            }
+            if (abiName != null) {
+                mInstallArgs.add(String.format("--abi %s", abiName));
+            }
+            CLog.d("Installing apk from %s ...", testAppFile.getAbsolutePath());
+            String result = device.installPackage(testAppFile, true,
+                    mInstallArgs.toArray(new String[]{}));
             if (result != null) {
                 throw new TargetSetupError(
                         String.format("Failed to install %s on %s. Reason: '%s'", testAppName,
                                 device.getSerialNumber(), result));
             }
+            if (mCleanup) {
+                AaptParser parser = AaptParser.parse(testAppFile);
+                if (parser == null) {
+                    throw new TargetSetupError("apk installed but AaptParser failed");
+                }
+                mPackagesInstalled.add(parser.getPackageName());
+            }
         }
     }
+
+    @Override
+    public void setAbi(IAbi abi) {
+        mAbi = abi;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+            throws DeviceNotAvailableException {
+        if (mCleanup && mPackagesInstalled != null && !(e instanceof DeviceNotAvailableException)) {
+            for (String packageName : mPackagesInstalled) {
+                String msg = device.uninstallPackage(packageName);
+                if (msg != null) {
+                    CLog.w(String.format("error uninstalling package '%s': %s",
+                            packageName, msg));
+                }
+            }
+        }
+    }
+
+    /**
+     * Set an alternate directory.
+     */
+    public void setAltDir(File altDir) {
+        mAltDirs.add(altDir);
+    }
 }
diff --git a/src/com/android/tradefed/targetprep/TestFilePushSetup.java b/src/com/android/tradefed/targetprep/TestFilePushSetup.java
index 234b748..cbf59ed 100644
--- a/src/com/android/tradefed/targetprep/TestFilePushSetup.java
+++ b/src/com/android/tradefed/targetprep/TestFilePushSetup.java
@@ -30,11 +30,17 @@
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * A {@link ITargetPreparer} that pushes one or more files/dirs from a
  * {@link IDeviceBuildInfo#getTestsDir()} folder onto device.
- *
+ * <p>
+ * This preparer will look in alternate directories if the tests zip does not exist or does not
+ * contain the required apk. The search will go in order from the last alternative dir specified to
+ * the first.
+ * </p>
  */
 @OptionClass(alias = "tests-zip-file")
 public class TestFilePushSetup implements ITargetPreparer {
@@ -48,6 +54,16 @@
             "Throw exception if the specified file is not found.")
     private boolean mThrowIfNoFile = true;
 
+    @Option(name = "alt-dir",
+            description = "Alternate directory to look for the apk if the apk is not in the tests "
+                    + "zip file. For each alternate dir, will look in // and //DATA. Can be "
+                    + "repeated. Look for apks in last alt-dir first.")
+    private List<File> mAltDirs = new ArrayList<>();
+
+    @Option(name = "alt-dir-behavior", description = "The order of alternate directory to be used "
+            + "when searching for files to push")
+    private AltDirBehavior mAltDirBehavior = AltDirBehavior.FALLBACK;
+
     /**
      * Adds a file to the list of items to push
      *
@@ -60,6 +76,54 @@
     }
 
     /**
+     * Resolve the host side path based on testing artifact information inside build info.
+     *
+     * @param buildInfo build artifact information
+     * @param fileName filename of artifacts to push
+     * @return a {@link File} representing the physical file/path on host
+     */
+    protected File getLocalPathForFilename(IBuildInfo buildInfo, String fileName)
+            throws TargetSetupError {
+        List<File> dirs = new ArrayList<>();
+        for (File dir : mAltDirs) {
+            dirs.add(dir);
+            dirs.add(FileUtil.getFileForPath(dir, "DATA"));
+        }
+        // reverse the order so ones provided via command line last can be searched first
+        Collections.reverse(dirs);
+
+        List<File> expandedTestDirs = new ArrayList<>();
+        if (buildInfo instanceof IDeviceBuildInfo) {
+            File testsDir = ((IDeviceBuildInfo)buildInfo).getTestsDir();
+            if (testsDir != null && testsDir.exists()) {
+                expandedTestDirs.add(FileUtil.getFileForPath(testsDir, "DATA"));
+            }
+        }
+        if (mAltDirBehavior == AltDirBehavior.FALLBACK) {
+            // alt dirs are appended after build artifact dirs
+            expandedTestDirs.addAll(dirs);
+            dirs = expandedTestDirs;
+        } else if (mAltDirBehavior == AltDirBehavior.OVERRIDE) {
+            dirs.addAll(expandedTestDirs);
+        } else {
+            throw new TargetSetupError("Missing handler for alt-dir-behavior: " + mAltDirBehavior);
+        }
+        if (dirs.isEmpty()) {
+            throw new TargetSetupError(
+                    "Provided buildInfo does not contain a valid tests directory and no " +
+                    "alternative directories were provided");
+        }
+
+        for (File dir : dirs) {
+            File testAppFile = new File(dir, fileName);
+            if (testAppFile.exists()) {
+                return testAppFile;
+            }
+        }
+        return null;
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
@@ -73,15 +137,10 @@
             CLog.d("No test files to push, skipping");
             return;
         }
-        File testsDir = ((IDeviceBuildInfo)buildInfo).getTestsDir();
-        if (testsDir == null || !testsDir.exists()) {
-            throw new TargetSetupError(
-                    "Provided buildInfo does not contain a valid tests directory");
-        }
         int filePushed = 0;
         for (String fileName : mTestPaths) {
-            File localFile = FileUtil.getFileForPath(testsDir, "DATA", fileName);
-            if (!localFile.exists()) {
+            File localFile = getLocalPathForFilename(buildInfo, fileName);
+            if (localFile == null) {
                 if (mThrowIfNoFile) {
                     throw new TargetSetupError(String.format(
                             "Could not find test file %s directory in extracted tests.zip",
@@ -90,15 +149,15 @@
                     continue;
                 }
             }
-            fileName = getDevicePathFromUserData(fileName);
-            CLog.d("Pushing file: %s -> %s", localFile.getAbsoluteFile(), fileName);
+            String remoteFileName = getDevicePathFromUserData(fileName);
+            CLog.d("Pushing file: %s -> %s", localFile.getAbsoluteFile(), remoteFileName);
             if (localFile.isDirectory()) {
-                device.pushDir(localFile, fileName);
+                device.pushDir(localFile, remoteFileName);
             } else if (localFile.isFile()) {
-                device.pushFile(localFile, fileName);
+                device.pushFile(localFile, remoteFileName);
             }
             // there's no recursive option for 'chown', best we can do here
-            device.executeShellCommand(String.format("chown system.system %s", fileName));
+            device.executeShellCommand(String.format("chown system.system %s", remoteFileName));
             filePushed++;
         }
         if (filePushed == 0) {
@@ -106,6 +165,21 @@
         }
     }
 
+    /**
+     * Set an alternate directory.
+     */
+    public void setAltDir(File altDir) {
+        mAltDirs.add(altDir);
+    }
+
+    /**
+     * Set the alternative directory search beahvior
+     * @param behavior
+     */
+    public void setAltDirBehavior(AltDirBehavior behavior) {
+        mAltDirBehavior = behavior;
+    }
+
     static String getDevicePathFromUserData(String path) {
         return ArrayUtil.join(FileListingService.FILE_SEPARATOR,
                 "", FileListingService.DIRECTORY_DATA, path);
diff --git a/src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java
index 3acfd93..33cc62e 100644
--- a/src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java
@@ -73,9 +73,8 @@
             throw new TargetSetupError(
                     "Provided buildInfo does not contain a valid tests directory");
         }
-        device.enableAdbRoot();
+        device.remountSystemWritable();
         device.setRecoveryMode(RecoveryMode.ONLINE);
-        device.executeAdbCommand("remount");
         device.executeShellCommand("stop");
 
         for (String testAppName : mTestFileNames) {
diff --git a/src/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java b/src/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
new file mode 100644
index 0000000..efb8d13
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2015 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.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+
+/**
+ * A {@link ITargetPreparer} that waits for datetime to be set on device
+ * <p>
+ * Optionally this preparer can force a {@link TargetSetupError} if datetime is not set within
+ * timeout, or force host datetime onto device,
+ */
+@OptionClass(alias = "wait-for-datetime")
+public class WaitForDeviceDatetimePreparer implements ITargetPreparer {
+
+    // 30s to wait for device datetime
+    private static final long DATETIME_WAIT_TIMEOUT = 30 * 1000;
+    // poll every 5s when waiting correct device datetime
+    private static final long DATETIME_CHECK_INTERVAL = 5 * 1000;
+    // allow 10s of margin for datetime difference between host/device
+    private static final long DATETIME_MARGIN = 10;
+
+    @Option(name = "force-datetime", description = "Force sync host datetime to device if device "
+            + "fails to set datetime automatically.")
+    private boolean mForceDatetime = false;
+
+    @Option(name = "datetime-wait-timeout",
+            description = "Timeout in ms to wait for correct datetime on device.")
+    private long mDatetimeWaitTimeout = DATETIME_WAIT_TIMEOUT;
+
+    @Option(name = "force-setup-error",
+            description = "Throw an TargetSetupError if correct datetime was not set. "
+                    + "Only meaningful if \"force-datetime\" is not used.")
+    private boolean mForceSetupError = false;
+
+    @Override
+    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+            BuildError, DeviceNotAvailableException {
+        if (!waitForDeviceDatetime(device, mForceDatetime)) {
+            if (mForceSetupError) {
+                throw new TargetSetupError("datetime on device is incorrect after wait timeout");
+            } else {
+                CLog.w("datetime on device is incorrect after wait timeout.");
+            }
+        }
+    }
+
+    /**
+     * Sets the timeout for waiting on valid device datetime
+     */
+    public void setDatetimeWaitTimeout(long datetimeWaitTimeout) {
+        mDatetimeWaitTimeout = datetimeWaitTimeout;
+    }
+
+    /**
+     * Sets the if datetime should be forced from host to device
+     */
+    public void setForceDatetime(boolean forceDatetime) {
+        mForceDatetime = forceDatetime;
+    }
+
+    /**
+     * Waits for a correct datetime on device, optionally force host datetime onto device
+     * @param forceDatetime
+     * @return <code>true</code> if datetime is correct or forced, <code>false</code> otherwise
+     */
+    boolean waitForDeviceDatetime(ITestDevice device, boolean forceDatetime)
+            throws DeviceNotAvailableException {
+        return waitForDeviceDatetime(device, forceDatetime,
+                mDatetimeWaitTimeout, DATETIME_CHECK_INTERVAL);
+    }
+
+    /**
+     * Waits for a correct datetime on device, optionally force host datetime onto device
+     * @param forceDatetime
+     * @param datetimeWaitTimeout
+     * @param datetimeCheckInterval
+     * @return <code>true</code> if datetime is correct or forced, <code>false</code> otherwise
+     */
+    boolean waitForDeviceDatetime(ITestDevice device, boolean forceDatetime,
+            long datetimeWaitTimeout, long datetimeCheckInterval)
+            throws DeviceNotAvailableException {
+        long start = System.currentTimeMillis();
+        while ((System.currentTimeMillis() - start) < datetimeWaitTimeout) {
+            long datetime = getDeviceDatetimeEpoch(device);
+            long now = System.currentTimeMillis() / 1000;
+            if (datetime == -1) {
+                if (forceDatetime) {
+                    throw new UnsupportedOperationException(
+                            "unexpected return from \"date\" command on device");
+                } else {
+                    return false;
+                }
+            }
+            if ((Math.abs(now - datetime) < DATETIME_MARGIN)) {
+                return true;
+            }
+            getRunUtil().sleep(datetimeCheckInterval);
+        }
+        if (forceDatetime) {
+            device.setDate(null);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Retrieve device datetime in epoch format
+     * @param device
+     * @return datetime on device in epoch format, -1 if failed
+     */
+    long getDeviceDatetimeEpoch(ITestDevice device) throws DeviceNotAvailableException {
+        String datetime = device.executeShellCommand("date '+%s'").trim();
+        try {
+            return Long.parseLong(datetime);
+        } catch (NumberFormatException nfe) {
+            CLog.v("returned datetime from device is not a number: '%s'", datetime);
+            return -1;
+        }
+    }
+
+    /**
+     * @return the {@link IRunUtil} to use
+     */
+    protected IRunUtil getRunUtil() {
+        return RunUtil.getDefault();
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/WifiPreparer.java b/src/com/android/tradefed/targetprep/WifiPreparer.java
index 99edcd8..1f576c2 100644
--- a/src/com/android/tradefed/targetprep/WifiPreparer.java
+++ b/src/com/android/tradefed/targetprep/WifiPreparer.java
@@ -80,6 +80,11 @@
             return;
         }
 
+        if (e instanceof DeviceFailedToBootError) {
+            CLog.d("boot failure: skipping wifi teardown");
+            return;
+        }
+
         if (mMonitorNetwork) {
             device.disableNetworkMonitor();
         }
diff --git a/src/com/android/tradefed/testtype/CodeCoverageTest.java b/src/com/android/tradefed/testtype/CodeCoverageTest.java
index 1736995..6547b91 100644
--- a/src/com/android/tradefed/testtype/CodeCoverageTest.java
+++ b/src/com/android/tradefed/testtype/CodeCoverageTest.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.testtype;
 
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -26,7 +27,6 @@
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.ResultForwarder;
-import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.RunUtil;
 
diff --git a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
index 15018ba..3917a75 100644
--- a/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
+++ b/src/com/android/tradefed/testtype/DeviceBatteryLevelChecker.java
@@ -71,6 +71,9 @@
             "draining processes and allow the device to charge at its fastest rate.")
     private boolean mRebootChargeDevices = false;
 
+    @Option(name = "stop-runtime", description = "Whether to stop runtime.")
+    private boolean mStopRuntime = false;
+
     Integer checkBatteryLevel(ITestDevice device) throws DeviceNotAvailableException {
         try {
             IDevice idevice = device.getIDevice();
@@ -81,20 +84,8 @@
         }
     }
 
-    private void turnScreenOffOrStopRuntime(ITestDevice device) throws DeviceNotAvailableException {
-        String output = getDevice().executeShellCommand("pm path android");
-        if (output == null || !output.contains("package:")) {
-            CLog.d("framework does not seem to be running, trying to stop it.");
-            // stop framework in case it's running some sort of runtime restart loop, and we can
-            // still charge the device
-            getDevice().executeShellCommand("stop");
-        } else {
-            output = getDevice().executeShellCommand("dumpsys power");
-            if (output.contains("mScreenOn=true")) {
-                // KEYCODE_POWER = 26
-                getDevice().executeShellCommand("input keyevent 26");
-            }
-        }
+    private void stopRuntime(ITestDevice device) throws DeviceNotAvailableException {
+        getDevice().executeShellCommand("stop");
     }
 
     /**
@@ -128,7 +119,9 @@
             mTestDevice.reboot();
         }
 
-        turnScreenOffOrStopRuntime(mTestDevice);
+        if (mStopRuntime) {
+            stopRuntime(mTestDevice);
+        }
 
         // If we're down here, it's time to hold the device until it reaches mResumeLevel
         Long lastReportTime = System.currentTimeMillis();
diff --git a/src/com/android/tradefed/testtype/FakeTest.java b/src/com/android/tradefed/testtype/FakeTest.java
index c6ea57a..a3cc8f3 100644
--- a/src/com/android/tradefed/testtype/FakeTest.java
+++ b/src/com/android/tradefed/testtype/FakeTest.java
@@ -16,7 +16,6 @@
 package com.android.tradefed.testtype;
 
 import com.android.ddmlib.testrunner.ITestRunListener;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
@@ -40,7 +39,7 @@
     @Option(name = "run", description = "Specify a new run to include.  " +
             "The key should be the unique name of the TestRun (which may be a Java class name).  " +
             "The value should specify the sequence of test results, using the characters P[ass], " +
-            "F[ail], or E[rror].  You may use run-length encoding to specify repeats, and you " +
+            "or F[ail].  You may use run-length encoding to specify repeats, and you " +
             "may use parentheses for grouping.  So \"(PF)4\" and \"((PF)2)2\" will both expand " +
             "to \"PFPFPFPF\".", importance = Importance.IF_UNSET)
     private Map<String, String> mRuns = new LinkedHashMap<String, String>();
@@ -172,7 +171,7 @@
         listener.testRunStarted(runName, spec.length());
         int i = 0;
         for (char c : spec.toCharArray()) {
-            if (c != 'P' && c != 'F' && c != 'E') {
+            if (c != 'P' && c != 'F') {
                 throw new IllegalArgumentException(String.format(
                         "Received unexpected test spec character '%c' in spec \"%s\"", c, spec));
             }
@@ -187,13 +186,9 @@
                     // no-op
                     break;
                 case 'F':
-                    listener.testFailed(TestFailure.FAILURE, test,
+                    listener.testFailed(test,
                             String.format("Test %s had a predictable boo-boo.", testName));
                     break;
-                case 'E':
-                    listener.testFailed(TestFailure.ERROR, test,
-                            String.format("Test %s had an unexpected boo-boo. Uh-oh...", testName));
-                    break;
             }
             listener.testEnded(test, EMPTY_MAP);
         }
diff --git a/src/com/android/tradefed/testtype/GTestResultParser.java b/src/com/android/tradefed/testtype/GTestResultParser.java
index 2856240..0bd06e8 100644
--- a/src/com/android/tradefed/testtype/GTestResultParser.java
+++ b/src/com/android/tradefed/testtype/GTestResultParser.java
@@ -539,16 +539,14 @@
             // If the test name of the result changed from what we started with, report that
             // the last known test failed, regardless of whether we received a pass or fail tag.
             for (ITestRunListener listener : mTestListeners) {
-                listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
-                                mCurrentTestResult.getTrace());
+                listener.testFailed(testId, mCurrentTestResult.getTrace());
             }
             // Report error as failure.
             ++mTotalNumberOfTestFailed;
         }
         else if (!testPassed) {  // test failed
             for (ITestRunListener listener : mTestListeners) {
-                listener.testFailed(ITestRunListener.TestFailure.FAILURE, testId,
-                                mCurrentTestResult.getTrace());
+                listener.testFailed(testId, mCurrentTestResult.getTrace());
             }
 
             ++mTotalNumberOfTestFailed;
@@ -624,7 +622,7 @@
                 testRunStackTrace = mCurrentTestResult.getTrace();
             }
             for (ITestRunListener listener : mTestListeners) {
-                listener.testFailed(ITestRunListener.TestFailure.ERROR, testId,
+                listener.testFailed(testId,
                         "No test results.\r\n" + testRunStackTrace);
                 listener.testEnded(testId, emptyMap);
             }
diff --git a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
index eec9c89..3209e54 100644
--- a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
+++ b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
@@ -30,6 +30,8 @@
 import com.android.tradefed.testtype.testdefs.XmlDefsTest;
 import com.android.tradefed.util.AbiFormatter;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -41,7 +43,7 @@
  * Runs all instrumentation found on current device.
  */
 @OptionClass(alias = "installed-instrumentation")
-public class InstalledInstrumentationsTest implements IDeviceTest, IResumableTest {
+public class InstalledInstrumentationsTest implements IDeviceTest, IResumableTest, IShardableTest {
 
     /** the metric key name for the test coverage target value */
     // TODO: move this to a more generic location
@@ -51,10 +53,23 @@
 
     private ITestDevice mDevice;
 
+    @Deprecated
     @Option(name = "timeout",
-            description = "Fail any test that takes longer than the specified number of "
-            + "milliseconds.")
-    private int mTestTimeout = 10 * 60 * 1000;  // default to 10 minutes
+            description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
+    private Integer mTimeout = null;
+
+    @Option(name = "shell-timeout",
+            description="The defined timeout (in milliseconds) is used as a maximum waiting time "
+                    + "when expecting the command output from the device. At any time, if the "
+                    + "shell command does not output anything for a period longer than defined "
+                    + "timeout the TF run terminates. For no timeout, set to 0.")
+    private long mShellTimeout = 10 * 60 * 1000;  // default to 10 minutes
+
+    @Option(name = "test-timeout",
+            description="Sets timeout (in milliseconds) that will be applied to each test. In the "
+                    + "event of a test timeout it will log the results and proceed with executing "
+                    + "the next test. For no timeout, set to 0.")
+    private int mTestTimeout = 5 * 60 * 1000;  // default to 5 minutes
 
     @Option(name = "size",
             description = "Restrict tests to a specific test size. " +
@@ -117,6 +132,13 @@
             "each remaining test")
     private boolean mReRunUsingTestFile = false;
 
+    @Option(name = "shards", description =
+            "Split test run into this many parallel shards")
+    private int mShards = 0;
+
+    private int mTotalShards = 0;
+    private int mShardIndex = 0;
+
     private List<InstrumentationTest> mTests = null;
 
     @Option(name = AbiFormatter.FORCE_ABI_STRING,
@@ -228,6 +250,10 @@
         listener.testRunEnded(0, coverageMetric);
     }
 
+    long getShellTimeout() {
+        return mShellTimeout;
+    }
+
     int getTestTimeout() {
         return mTestTimeout;
     }
@@ -292,13 +318,19 @@
                     if (mRunnerFilter == null || mRunnerFilter.equals(runner)) {
                         InstrumentationTest t = createInstrumentationTest();
                         try {
+                            // Copies all current argument values to the new runner that will be
+                            // used to actually run the tests.
                             OptionCopier.copyOptions(InstalledInstrumentationsTest.this, t);
                         } catch (ConfigurationException e) {
-                            CLog.e("failed to copy instrumentation options", e);
+                            CLog.e("failed to copy instrumentation options: %s", e.getMessage());
                         }
                         t.setPackageName(m.group(1));
                         t.setRunnerName(runner);
                         t.setCoverageTarget(m.group(3));
+                        if (mTotalShards > 0) {
+                            t.addInstrumentationArg("shardIndex", Integer.toString(mShardIndex));
+                            t.addInstrumentationArg("numShards", Integer.toString(mTotalShards));
+                        }
                         mTests.add(t);
                     }
                 }
@@ -309,4 +341,28 @@
             return mTests;
         }
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Collection<IRemoteTest> split() {
+        if (mShards > 1) {
+            Collection<IRemoteTest> shards = new ArrayList<>(mShards);
+            for (int index = 0; index < mShards; index++) {
+                InstalledInstrumentationsTest shard = new InstalledInstrumentationsTest();
+                try {
+                    OptionCopier.copyOptions(this, shard);
+                } catch (ConfigurationException e) {
+                    CLog.e("failed to copy instrumentation options: %s", e.getMessage());
+                }
+                shard.mShards = 0;
+                shard.mShardIndex = index;
+                shard.mTotalShards = mShards;
+                shards.add(shard);
+            }
+            return shards;
+        }
+        return null;
+    }
 }
diff --git a/src/com/android/tradefed/testtype/InstrumentationFileTest.java b/src/com/android/tradefed/testtype/InstrumentationFileTest.java
index e8dcaf2..71062cb 100644
--- a/src/com/android/tradefed/testtype/InstrumentationFileTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationFileTest.java
@@ -100,9 +100,10 @@
      */
     private void writeTestsToFileAndRun(Collection<TestIdentifier> tests,
             final ITestInvocationListener listener) throws DeviceNotAvailableException {
+        File testFile = null;
         try {
             // create and populate test file
-            File testFile = FileUtil.createTempFile(
+            testFile = FileUtil.createTempFile(
                     "tf_testFile_" + InstrumentationFileTest.class.getCanonicalName(), ".txt");
             try (BufferedWriter bw = new BufferedWriter(new FileWriter(testFile))) {
                 for (TestIdentifier testToRun : tests) {
@@ -123,8 +124,11 @@
                 reRunTestsSerially(mInstrumentationTest, listener);
             }
         } catch (IOException e) {
-            CLog.e("Failed to run tests from file, re-running tests serially", e);
+            CLog.e("Failed to run tests from file, re-running tests serially: %s", e.getMessage());
             reRunTestsSerially(mInstrumentationTest, listener);
+        } finally {
+            // clean up test file, if it was created
+            FileUtil.deleteFile(testFile);
         }
     }
 
diff --git a/src/com/android/tradefed/testtype/InstrumentationSerialTest.java b/src/com/android/tradefed/testtype/InstrumentationSerialTest.java
index 4d5bea4..ea5d913 100644
--- a/src/com/android/tradefed/testtype/InstrumentationSerialTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationSerialTest.java
@@ -20,14 +20,12 @@
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.ResultForwarder;
 
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -99,7 +97,7 @@
                 runTest(runner, listener, testToRun);
             }
         } catch (ConfigurationException e) {
-            CLog.e("Failed to create new InstrumentationTest", e);
+            CLog.e("Failed to create new InstrumentationTest: %s", e.getMessage());
         }
     }
 
@@ -164,7 +162,7 @@
         public void markTestAsFailed() {
             super.testRunStarted(mRunName, 1);
             super.testStarted(mExpectedTest);
-            super.testFailed(TestFailure.ERROR, mExpectedTest, String.format(
+            super.testFailed(mExpectedTest, String.format(
                     "Test failed to run. Test run failed due to : %s", mRunErrorMsg));
             if (mRunErrorMsg != null) {
                 super.testRunFailed(mRunErrorMsg);
diff --git a/src/com/android/tradefed/testtype/InstrumentationTest.java b/src/com/android/tradefed/testtype/InstrumentationTest.java
index 567e2d0..b1612a1 100644
--- a/src/com/android/tradefed/testtype/InstrumentationTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationTest.java
@@ -22,11 +22,11 @@
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner.TestSize;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
 import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -36,7 +36,6 @@
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.ResultForwarder;
-import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StringEscapeUtils;
@@ -63,6 +62,8 @@
     private static final String TEST_FILE_INST_ARGS_KEY = "testFile";
 
     static final String DELAY_MSEC_ARG = "delay_msec";
+    /** instrumentation test runner argument key used for individual test timeout */
+    static final String TEST_TIMEOUT_INST_ARGS_KEY = "timeout_msec";
 
     @Option(name = "package", shortName = 'p',
             description="The manifest package name of the Android test application to run.",
@@ -86,10 +87,23 @@
             "Will be ignored if --class is set.")
     private String mTestPackageName = null;
 
+    @Deprecated
     @Option(name = "timeout",
-            description="Aborts the test run if any test takes longer than the specified number of "
-            + "milliseconds. For no timeout, set to 0.")
-    private int mTestTimeout = 10 * 60 * 1000;  // default to 10 minutes
+            description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
+    private Integer mTimeout = null;
+
+    @Option(name = "shell-timeout",
+            description="The defined timeout (in milliseconds) is used as a maximum waiting time "
+                    + "when expecting the command output from the device. At any time, if the "
+                    + "shell command does not output anything for a period longer than defined "
+                    + "timeout the TF run terminates. For no timeout, set to 0.")
+    private long mShellTimeout = 10 * 60 * 1000;  // default to 10 minutes
+
+    @Option(name = "test-timeout",
+            description="Sets timeout (in milliseconds) that will be applied to each test. In the "
+                    + "event of a test timeout it will log the results and proceed with executing "
+                    + "the next test. For no timeout, set to 0.")
+    private int mTestTimeout = 5 * 60 * 1000;  // default to 5 minutes
 
     @Option(name = "size",
             description="Restrict test to a specific test size.")
@@ -311,7 +325,14 @@
     }
 
     /**
-     * Optionally, set the maximum time for each test.
+     * Optionally, set the maximum time (in milliseconds) expecting shell output from the device.
+     */
+    public void setShellTimeout(long timeout) {
+        mShellTimeout = timeout;
+    }
+
+    /**
+     * Optionally, set the maximum time (in milliseconds) for each individual test run.
      */
     public void setTestTimeout(int timeout) {
         mTestTimeout = timeout;
@@ -369,6 +390,13 @@
     }
 
     /**
+     * Get the shell timeout in ms.
+     */
+    long getShellTimeout() {
+        return mShellTimeout;
+    }
+
+    /**
      * Get the test timeout in ms.
      */
     int getTestTimeout() {
@@ -508,7 +536,7 @@
         if (mTestSize != null) {
             mRunner.setTestSize(TestSize.getTestSize(mTestSize));
         }
-        mRunner.setMaxTimeToOutputResponse(mTestTimeout, TimeUnit.MILLISECONDS);
+        addTimeoutsToRunner(mRunner);
         if (mRunName != null) {
             mRunner.setRunName(mRunName);
         }
@@ -526,6 +554,29 @@
     }
 
     /**
+     * Helper method to add test-timeout & shell-timeout timeouts to  given runner
+     */
+    private void addTimeoutsToRunner(IRemoteAndroidTestRunner runner) {
+        if (mTimeout != null) {
+            CLog.w("\"timeout\" argument is deprecated and should not be used! \"shell-timeout\""
+                    + " argument value is overwritten with %d ms", mTimeout);
+            setShellTimeout(mTimeout);
+        }
+        if (mTestTimeout < 0) {
+            throw new IllegalArgumentException(
+                    String.format("test-timeout %d cannot be negative", mTestTimeout));
+        }
+        if (mShellTimeout < mTestTimeout) {
+            CLog.w(String.format("shell-timeout %d cannot be smaller then test-timeout %d; "
+                    + "NOTE: extending shell-timeout to match test-timeout %d, please "
+                    + "consider fixing this!", mShellTimeout, mTestTimeout, mTestTimeout));
+            mShellTimeout = mTestTimeout;
+        }
+        runner.setMaxTimeToOutputResponse(mShellTimeout, TimeUnit.MILLISECONDS);
+        addInstrumentationArg(TEST_TIMEOUT_INST_ARGS_KEY, Long.toString(mTestTimeout));
+    }
+
+    /**
      * Execute test run.
      *
      * @param listener the test result listener
@@ -625,7 +676,7 @@
                 calculateRemainingTests(mRemainingTests, testTracker);
             }
         } catch (ConfigurationException e) {
-            CLog.e("Failed to create InstrumentationFileTest", e);
+            CLog.e("Failed to create InstrumentationFileTest: %s", e.getMessage());
         }
     }
 
@@ -649,7 +700,7 @@
                 calculateRemainingTests(mRemainingTests, testTracker);
             }
         } catch (ConfigurationException e) {
-            CLog.e("Failed to create InstrumentationSerialTest", e);
+            CLog.e("Failed to create InstrumentationSerialTest: %s", e.getMessage());
         }
     }
 
@@ -680,20 +731,13 @@
         if (isRerunMode()) {
             Log.d(LOG_TAG, String.format("Collecting test info for %s on device %s",
                     mPackageName, mDevice.getSerialNumber()));
-            runner.setLogOnly(true);
-            // the collecting test command can fail for large volumes of test bug 1750602. insert a
-            // small delay between each test to prevent this
-            if (mTestDelay > 0) {
-                runner.addInstrumentationArg(DELAY_MSEC_ARG, Integer.toString(mTestDelay));
-            }
-            // use a shorter timeout when collecting tests
-            runner.setMaxTimeToOutputResponse(mCollectTestsShellTimeout, TimeUnit.MILLISECONDS);
+            runner.setTestCollection(true);
             // try to collect tests multiple times, in case device is temporarily not available
             // on first attempt
             Collection<TestIdentifier>  tests = collectTestsAndRetry(runner);
-            runner.setLogOnly(false);
-            runner.setMaxTimeToOutputResponse(mTestTimeout, TimeUnit.MILLISECONDS);
-            runner.removeInstrumentationArg(DELAY_MSEC_ARG);
+            // done with "logOnly" mode, restore proper test timeout before real test execution
+            addTimeoutsToRunner(runner);
+            runner.setTestCollection(false);
             return tests;
         }
         return null;
@@ -759,8 +803,8 @@
         }
 
         @Override
-        public void testFailed(TestFailure status, TestIdentifier test, String trace) {
-            super.testFailed(status, test, trace);
+        public void testFailed(TestIdentifier test, String trace) {
+            super.testFailed(test, trace);
 
             try {
                 InputStreamSource screenSource = mDevice.getScreenshot();
@@ -790,8 +834,18 @@
         }
 
         @Override
-        public void testFailed(TestFailure status, TestIdentifier test, String trace) {
-            super.testFailed(status, test, trace);
+        public void testFailed(TestIdentifier test, String trace) {
+            super.testFailed(test, trace);
+            captureLog(test);
+        }
+
+        @Override
+        public void testAssumptionFailure(TestIdentifier test, String trace) {
+            super.testAssumptionFailure(test, trace);
+            captureLog(test);
+        }
+
+        private void captureLog(TestIdentifier test) {
             // sleep a small amount of time to ensure test failure stack trace makes it into logcat
             // capture
             RunUtil.getDefault().sleep(10);
diff --git a/src/com/android/tradefed/testtype/UiAutomatorRunner.java b/src/com/android/tradefed/testtype/UiAutomatorRunner.java
index 5137a02..f648eb9 100644
--- a/src/com/android/tradefed/testtype/UiAutomatorRunner.java
+++ b/src/com/android/tradefed/testtype/UiAutomatorRunner.java
@@ -254,6 +254,11 @@
         throw new UnsupportedOperationException("coverage mode is not supported");
     }
 
+    @Override
+    public void setTestCollection(boolean b) {
+        throw new UnsupportedOperationException("Test Collection mode is not supported");
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/testtype/UiAutomatorTest.java b/src/com/android/tradefed/testtype/UiAutomatorTest.java
index 2d3c10c..ae38299 100644
--- a/src/com/android/tradefed/testtype/UiAutomatorTest.java
+++ b/src/com/android/tradefed/testtype/UiAutomatorTest.java
@@ -210,7 +210,7 @@
         if (mJarPaths.isEmpty()) {
             String rawFileString =
                     getDevice().executeShellCommand(String.format("ls %s", SHELL_EXE_BASE));
-            String[] rawFiles = rawFileString.split("\r\n");
+            String[] rawFiles = rawFileString.split("\r?\n");
             for (String rawFile : rawFiles) {
                 if (rawFile.endsWith(".jar")) {
                     mJarPaths.add(rawFile);
@@ -308,7 +308,16 @@
         }
 
         @Override
-        public void testFailed(TestFailure status, TestIdentifier test, String trace) {
+        public void testFailed(TestIdentifier test, String trace) {
+            captureFailureLog(test);
+        }
+
+        @Override
+        public void testAssumptionFailure(TestIdentifier test, String trace) {
+            captureFailureLog(test);
+        }
+
+        private void captureFailureLog(TestIdentifier test) {
             if (mLoggingOption == LoggingOption.AFTER_FAILURE) {
                 onScreenshotAndBugreport(getDevice(), mListener, String.format("%s_%s_failure",
                         test.getClassName(), test.getTestName()));
diff --git a/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java b/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
index 1a75227..23087a0 100644
--- a/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
+++ b/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
@@ -60,9 +61,22 @@
 
     private ITestDevice mDevice;
 
+    @Deprecated
     @Option(name = "timeout",
-            description = "Fail any test that takes longer than the specified number of "
-            + "milliseconds.")
+            description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
+    private Integer mTimeout = null;
+
+    @Option(name = "shell-timeout",
+            description="The defined timeout (in milliseconds) is used as a maximum waiting time "
+                    + "when expecting the command output from the device. At any time, if the "
+                    + "shell command does not output anything for a period longer than defined "
+                    + "timeout the TF run terminates. For no timeout, set to 0.")
+    private long mShellTimeout = 10 * 60 * 1000;  // default to 10 minutes
+
+    @Option(name = "test-timeout",
+            description="Sets timeout (in milliseconds) that will be applied to each test. In the "
+                    + "event of a test timeout it will log the results and proceed with executing "
+                    + "the next test. For no timeout, set to 0.")
     private int mTestTimeout = 10 * 60 * 1000;  // default to 10 minutes
 
     @Option(name = "size",
@@ -215,6 +229,13 @@
                     test.setRerunMode(mIsRerunMode);
                     test.setResumeMode(mIsResumeMode);
                     test.setTestSize(getTestSize());
+                    if (mTimeout != null) {
+                        LogUtil.CLog
+                                .w("\"timeout\" argument is deprecated and should not be used! \"shell-timeout\""
+                                        + " argument value is overwritten with %d ms", mTimeout);
+                        setShellTimeout(mTimeout);
+                    }
+                    test.setShellTimeout(getShellTimeout());
                     test.setTestTimeout(getTestTimeout());
                     test.setCoverageTarget(def.getCoverageTarget());
                     mTests.add(test);
@@ -306,6 +327,14 @@
         return files;
     }
 
+    void setShellTimeout(long timeout) {
+        mShellTimeout = timeout;
+    }
+
+    long getShellTimeout() {
+        return mShellTimeout;
+    }
+
     int getTestTimeout() {
         return mTestTimeout;
     }
diff --git a/src/com/android/tradefed/util/AaptParser.java b/src/com/android/tradefed/util/AaptParser.java
index a3c5c0e..bd598d1 100644
--- a/src/com/android/tradefed/util/AaptParser.java
+++ b/src/com/android/tradefed/util/AaptParser.java
@@ -28,11 +28,17 @@
  */
 public class AaptParser {
     private static final Pattern PKG_PATTERN = Pattern.compile(
-            "package:\\s+name='(.*?)'\\s+versionCode='(\\d+)'\\s+versionName='(.*)'");
+            "^package:\\s+name='(.*?)'\\s+versionCode='(\\d+)'\\s+versionName='(.*?)'.*$",
+            Pattern.MULTILINE);
+    private static final Pattern LABEL_PATTERN = Pattern.compile(
+            "^application-label:'(.+?)'.*$",
+            Pattern.MULTILINE);
+    private static final int AAPT_TIMEOUT_MS = 60000;
 
     private String mPackageName;
     private String mVersionCode;
     private String mVersionName;
+    private String mLabel;
 
     // @VisibleForTesting
     AaptParser() {
@@ -42,8 +48,13 @@
         Matcher m = PKG_PATTERN.matcher(aaptOut);
         if (m.find()) {
             mPackageName = m.group(1);
+            mLabel = mPackageName;
             mVersionCode = m.group(2);
             mVersionName = m.group(3);
+            m = LABEL_PATTERN.matcher(aaptOut);
+            if (m.find()) {
+                mLabel = m.group(1);
+            }
             return true;
         }
         CLog.e("Failed to parse package and version info from 'aapt dump badging'. stdout: '%s'",
@@ -58,8 +69,8 @@
      * @return the {@link AaptParser} or <code>null</code> if failed to extract the information
      */
     public static AaptParser parse(File apkFile) {
-        CommandResult result = RunUtil.getDefault().runTimedCmd(5000, "aapt", "dump", "badging",
-                apkFile.getAbsolutePath());
+        CommandResult result = RunUtil.getDefault().runTimedCmd(AAPT_TIMEOUT_MS,
+                "aapt", "dump", "badging", apkFile.getAbsolutePath());
 
         String stderr = result.getStderr();
         if (stderr != null && stderr.length() > 0) {
@@ -87,4 +98,8 @@
     public String getVersionName() {
         return mVersionName;
     }
+
+    public String getLabel() {
+        return mLabel;
+    }
 }
diff --git a/src/com/android/tradefed/util/AbiFormatter.java b/src/com/android/tradefed/util/AbiFormatter.java
index 9ebbb31..09e1bcf 100644
--- a/src/com/android/tradefed/util/AbiFormatter.java
+++ b/src/com/android/tradefed/util/AbiFormatter.java
@@ -28,6 +28,7 @@
 public class AbiFormatter {
 
     private static final String PRODUCT_CPU_ABILIST_KEY = "ro.product.cpu.abilist";
+    private static final String PRODUCT_CPU_ABI_KEY = "ro.product.cpu.abi";
     public static final String FORCE_ABI_STRING = "force-abi";
     public static final String FORCE_ABI_DESCRIPTION = "The abi to use, can be either 32 or 64.";
 
@@ -77,7 +78,7 @@
     public static String getDefaultAbi(ITestDevice device, String bitness)
             throws DeviceNotAvailableException {
         String []abis = getSupportedAbis(device, bitness);
-        if (abis.length > 0 && abis[0].length() > 0) {
+        if (abis != null && abis.length > 0 && abis[0] != null && abis[0].length() > 0) {
             return abis[0];
         }
         return null;
@@ -93,10 +94,13 @@
     public static String[] getSupportedAbis(ITestDevice device, String bitness)
             throws DeviceNotAvailableException {
         String abiList = device.getProperty(PRODUCT_CPU_ABILIST_KEY + bitness);
-        if (abiList != null) {
+        if (abiList != null && !abiList.isEmpty()) {
             String []abis = abiList.split(",");
-            return abis;
+            if (abis.length > 0) {
+                return abis;
+            }
         }
-        return new String[0];
+        // fallback plan for before lmp, the bitness is ignored
+        return new String[]{device.getProperty(PRODUCT_CPU_ABI_KEY)};
     }
 }
diff --git a/src/com/android/tradefed/util/Alarm.java b/src/com/android/tradefed/util/Alarm.java
new file mode 100644
index 0000000..f5ddc64
--- /dev/null
+++ b/src/com/android/tradefed/util/Alarm.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2014 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.util;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A thread which waits for a period of time and then interrupts a specific other thread.
+ * Can call {@link Thread#interrupt()} to get a thread out of a blocking wait, or
+ * {@link Socket#close()} to stop a thread from blocking on a socket read or write.
+ * <p/>
+ * All time units are in milliseconds.
+ */
+public class Alarm extends Thread {
+    private final List<Thread> mInterruptThreads = new ArrayList<Thread>();
+    private final List<Socket> mInterruptSockets = new ArrayList<Socket>();
+    private final long mTimeoutTime;
+    private boolean mAlarmFired = false;
+
+    /**
+     * Constructor takes the amount of time to wait, in millis.
+     *
+     * @param timeout The amount of time to wait, in millis
+     * @throws IllegalArgumentException if {@code timeout <= 0}.
+     */
+    public Alarm(long timeout) {
+        super();
+        setDaemon(true);
+
+        if (timeout <= 0) {
+            throw new IllegalArgumentException(String.format(
+                    "Alarm timeout time %d <= 0, which is not valid.", timeout));
+        }
+
+        mTimeoutTime = timeout;
+    }
+
+    public void addThread(Thread intThread) {
+        mInterruptThreads.add(intThread);
+    }
+
+    public void addSocket(Socket intSocket) {
+        mInterruptSockets.add(intSocket);
+    }
+
+    public boolean didAlarmFire() {
+        return mAlarmFired;
+    }
+
+    @Override
+    public void run() {
+        try {
+            Thread.sleep(mTimeoutTime);
+        } catch (InterruptedException e) {
+            // Expected; return without interrupt()ing any of our InterruptThreads
+            return;
+        }
+        mAlarmFired = true;
+        for (Socket sock : mInterruptSockets) {
+            try {
+                sock.close();
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+        for (Thread thread : mInterruptThreads) {
+            thread.interrupt();
+        }
+    }
+}
+
diff --git a/src/com/android/tradefed/util/BulkEmailer.java b/src/com/android/tradefed/util/BulkEmailer.java
index 681df03..6522b85 100644
--- a/src/com/android/tradefed/util/BulkEmailer.java
+++ b/src/com/android/tradefed/util/BulkEmailer.java
@@ -49,7 +49,7 @@
     private int mInitialBurst = 0;
 
     @Option(name = "sender", description = "the sender email.", importance = Importance.NEVER)
-    private String mSender = "android.sync.battery.test@gmail.com";
+    private String mSender = "android-power-lab-external@google.com";
 
     private static final String SUBJECT = "No emails to send";
     private static final String MESSAGE = "This is a test message!";
diff --git a/src/com/android/tradefed/util/FileUtil.java b/src/com/android/tradefed/util/FileUtil.java
index be3eab3..4e59201 100644
--- a/src/com/android/tradefed/util/FileUtil.java
+++ b/src/com/android/tradefed/util/FileUtil.java
@@ -420,7 +420,7 @@
             StreamUtil.copyStreams(origStream, destStream);
         } finally {
             StreamUtil.close(origStream);
-            StreamUtil.close(destStream);
+            StreamUtil.flushAndCloseStream(destStream);
         }
     }
 
@@ -793,4 +793,16 @@
     public static void gzipFile(File file, File gzipFile) throws IOException {
         ZipUtil.gzipFile(file, gzipFile);
     }
+
+    /**
+     * Helper method to calculate md5 for a file.
+     *
+     * @param file
+     * @return md5 of the file
+     * @throws IOException
+     */
+    public static String calculateMd5(File file) throws IOException {
+        FileInputStream inputSource = new FileInputStream(file);
+        return StreamUtil.calculateMd5(inputSource);
+    }
 }
diff --git a/src/com/android/tradefed/util/IRunUtil.java b/src/com/android/tradefed/util/IRunUtil.java
index f7e837b..ef8d735 100644
--- a/src/com/android/tradefed/util/IRunUtil.java
+++ b/src/com/android/tradefed/util/IRunUtil.java
@@ -18,6 +18,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.util.List;
 
 /**
@@ -65,6 +66,15 @@
     public void setEnvVariable(String key, String value);
 
     /**
+     * Unsets an environment variable, so the system commands run without this environment variable.
+     *
+     * @param key the variable name
+     *
+     * @see {@link ProcessBuilder#environment()}
+     */
+    public void unsetEnvVariable(String key);
+
+    /**
      * Helper method to execute a system command, and aborting if it takes longer than a specified
      * time.
      *
@@ -130,6 +140,17 @@
     public Process runCmdInBackground(List<String> command) throws IOException;
 
     /**
+     * Running command with a {@link OutputStream} log the output of the command.
+     * Stdout and stderr are merged together.
+     * @param command the command to run
+     * @param output the OutputStream to save the output
+     * @return the {@link Process} running the command
+     * @throws IOException
+     */
+    public Process runCmdInBackground(List<String> command, OutputStream output)
+            throws IOException;
+
+    /**
      * Block and executes an operation, aborting if it takes longer than a specified time.
      *
      * @param timeout maximum time to wait in ms
@@ -188,4 +209,21 @@
      * @param time ms to sleep. values less than or equal to 0 will be ignored
      */
     public void sleep(long time);
+
+    /**
+     * Allows/disallows run interrupts on the current thread. If it is allowed, run operations of
+     * the current thread can be interrupted from other threads via {@link #interrupt} method.
+     *
+     * @param allow whether to allow run interrupts on the current thread.
+     */
+    public void allowInterrupt(boolean allow);
+
+    /**
+     * Interrupts the ongoing/forthcoming run operations on the given thread. The run operations on
+     * the given thread will throw {@link RunInterruptedException}.
+     *
+     * @param thread
+     * @param message the message for {@link RunInterruptedException}.
+     */
+    public void interrupt(Thread thread, String message);
 }
diff --git a/src/com/android/tradefed/util/JUnitXmlParser.java b/src/com/android/tradefed/util/JUnitXmlParser.java
index 2ea6f58..edecc76 100644
--- a/src/com/android/tradefed/util/JUnitXmlParser.java
+++ b/src/com/android/tradefed/util/JUnitXmlParser.java
@@ -16,7 +16,6 @@
 
 package com.android.tradefed.util;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.util.xml.AbstractXmlParser;
@@ -114,7 +113,7 @@
                 mTestListener.testEnded(mCurrentTest, Collections.<String, String> emptyMap());
             }
             if (FAILURE_TAG.equalsIgnoreCase(name)) {
-                mTestListener.testFailed(TestFailure.FAILURE, mCurrentTest,
+                mTestListener.testFailed(mCurrentTest,
                         mFailureContent.toString());
             }
             mFailureContent = null;
diff --git a/src/com/android/tradefed/util/RunInterruptedException.java b/src/com/android/tradefed/util/RunInterruptedException.java
new file mode 100644
index 0000000..0f40fa3
--- /dev/null
+++ b/src/com/android/tradefed/util/RunInterruptedException.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2014 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.util;
+
+
+/**
+ * Thrown when a run operation is interrupted by an external request.
+ */
+@SuppressWarnings("serial")
+public class RunInterruptedException extends RuntimeException {
+    /**
+     * Creates a {@link RunInterruptedException}.
+     */
+    public RunInterruptedException() {
+        super();
+    }
+
+    /**
+     * Creates a {@link RunInterruptedException}.
+     *
+     * @param msg a descriptive message.
+     */
+    public RunInterruptedException(String msg) {
+        super(msg);
+    }
+
+    /**
+     * Creates a {@link RunInterruptedException}.
+     *
+     * @param cause the root {@link Throwable} that caused the device to become unavailable.
+     */
+    public RunInterruptedException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Creates a {@link RunInterruptedException}.
+     *
+     * @param msg a descriptive message.
+     * @param cause the root {@link Throwable} that caused the device to become unavailable.
+     */
+    public RunInterruptedException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}
diff --git a/src/com/android/tradefed/util/RunUtil.java b/src/com/android/tradefed/util/RunUtil.java
index 2066248..73afb48 100644
--- a/src/com/android/tradefed/util/RunUtil.java
+++ b/src/com/android/tradefed/util/RunUtil.java
@@ -27,8 +27,10 @@
 import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A collection of helper methods for executing operations.
@@ -39,6 +41,14 @@
     private static IRunUtil sDefaultInstance = null;
     private File mWorkingDir = null;
     private Map<String, String> mEnvVariables = new HashMap<String, String>();
+    private Set<String> mUnsetEnvVariables = new HashSet<String>();
+    private ThreadLocal<Boolean> mIsInterruptAllowed = new ThreadLocal<Boolean>() {
+        @Override
+        protected Boolean initialValue() {
+            return Boolean.FALSE;
+        }
+    };
+    private Map<Long, String> mInterruptThreads = new HashMap<>();
 
     /**
      * Create a new {@link RunUtil} object to use.
@@ -85,6 +95,22 @@
 
     /**
      * {@inheritDoc}
+     * Environment variables may inherit from the parent process, so we need to delete
+     * the environment variable from {@link ProcessBuilder#environment()}
+     *
+     * @param key the variable name
+     * @see {@link ProcessBuilder#environment()}
+     */
+    @Override
+    public synchronized void unsetEnvVariable(String key) {
+        if (this.equals(sDefaultInstance)) {
+            throw new UnsupportedOperationException("Cannot unsetEnvVariable on default RunUtil");
+        }
+        mUnsetEnvVariables.add(key);
+    }
+
+    /**
+     * {@inheritDoc}
      */
     @Override
     public CommandResult runTimedCmd(final long timeout, final String... command) {
@@ -97,14 +123,7 @@
     }
 
     private synchronized ProcessBuilder createProcessBuilder(String... command) {
-        ProcessBuilder processBuilder = new ProcessBuilder();
-        if (mWorkingDir != null) {
-            processBuilder.directory(mWorkingDir);
-        }
-        if (!mEnvVariables.isEmpty()) {
-            processBuilder.environment().putAll(mEnvVariables);
-        }
-        return processBuilder.command(command);
+        return createProcessBuilder(Arrays.asList(command));
     }
 
     private synchronized ProcessBuilder createProcessBuilder(List<String> commandList) {
@@ -115,6 +134,10 @@
         if (!mEnvVariables.isEmpty()) {
             processBuilder.environment().putAll(mEnvVariables);
         }
+        if (!mUnsetEnvVariables.isEmpty()) {
+            // in this implementation, the unsetEnv's priority is higher than set.
+            processBuilder.environment().keySet().removeAll(mUnsetEnvVariables);
+        }
         return processBuilder.command(commandList);
     }
 
@@ -177,8 +200,23 @@
      * {@inheritDoc}
      */
     @Override
+    public Process runCmdInBackground(List<String> command, OutputStream output)
+            throws IOException {
+        CLog.v("Running %s", command);
+        Process process = createProcessBuilder(command).start();
+        inheritIO(process.getInputStream(), output);
+        inheritIO(process.getErrorStream(), output);
+        return process;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public CommandStatus runTimed(long timeout, IRunUtil.IRunnableResult runnable,
             boolean logErrors) {
+        checkInterrupted();
         RunnableNotifier runThread = new RunnableNotifier(runnable, logErrors);
         runThread.start();
         try {
@@ -190,6 +228,7 @@
                 || runThread.getStatus() == CommandStatus.EXCEPTION) {
             runThread.interrupt();
         }
+        checkInterrupted();
         return runThread.getStatus();
     }
 
@@ -273,6 +312,7 @@
      */
     @Override
     public void sleep(long time) {
+        checkInterrupted();
         if (time <= 0) {
             return;
         }
@@ -282,6 +322,37 @@
             // ignore
             CLog.d("sleep interrupted");
         }
+        checkInterrupted();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void allowInterrupt(boolean allow) {
+        CLog.d("run interrupt allowed: %s", allow);
+        mIsInterruptAllowed.set(allow);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public synchronized void interrupt(Thread thread, String message) {
+        if (message == null) {
+            throw new IllegalArgumentException("message cannot be null.");
+        }
+        mInterruptThreads.put(thread.getId(), message);
+    }
+
+    private synchronized void checkInterrupted() {
+        final long threadId = Thread.currentThread().getId();
+        if (mIsInterruptAllowed.get()) {
+            final String message = mInterruptThreads.remove(threadId);
+            if (message != null) {
+                throw new RunInterruptedException(message);
+            }
+        }
     }
 
     /**
@@ -362,16 +433,14 @@
             // Wait for process to complete.
             int rc = mProcess.waitFor();
             synchronized (this) {
-                if (mProcess != null) {
-                    // wait for stdout and stderr to be read
-                    stdoutThread.join();
-                    stderrThread.join();
-                    // Write out the streams to the result.
-                    mCommandResult.setStdout(stdOut.toString("UTF-8"));
-                    mCommandResult.setStderr(stdErr.toString("UTF-8"));
-                    stdOut.close();
-                    stdErr.close();
-                }
+                // wait for stdout and stderr to be read
+                stdoutThread.join();
+                stderrThread.join();
+                // Write out the streams to the result.
+                mCommandResult.setStdout(stdOut.toString("UTF-8"));
+                mCommandResult.setStderr(stdErr.toString("UTF-8"));
+                stdOut.close();
+                stdErr.close();
             }
 
             if (rc == 0) {
@@ -391,7 +460,7 @@
                 }
             }
         }
-    };
+    }
 
     /**
      * Helper method to redirect input stream.
diff --git a/src/com/android/tradefed/util/StreamUtil.java b/src/com/android/tradefed/util/StreamUtil.java
index 9e93703..eb80284 100644
--- a/src/com/android/tradefed/util/StreamUtil.java
+++ b/src/com/android/tradefed/util/StreamUtil.java
@@ -28,9 +28,14 @@
 import java.io.PrintStream;
 import java.io.Reader;
 import java.io.Writer;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.zip.GZIPOutputStream;
 import java.util.zip.ZipOutputStream;
 
+import javax.xml.bind.DatatypeConverter;
+
 /**
  * Utility class for managing input streams.
  */
@@ -265,6 +270,31 @@
             /** Discards the specified byte array. */
             @Override public void write(byte[] b, int off, int len) {
             }
-          };
+        };
+    }
+
+    /**
+     * Helper method to calculate md5 for a inputStream. The inputStream will be consumed and
+     * closed.
+     *
+     * @param inputSource used to create inputStream
+     * @return md5 of the stream
+     * @throws IOException
+     */
+    static String calculateMd5(InputStream inputSource) throws IOException {
+        MessageDigest md = null;
+        try {
+            md = MessageDigest.getInstance("md5");
+        } catch (NoSuchAlgorithmException e) {
+            // This should not happen
+            throw new RuntimeException(e);
+        }
+        InputStream input = new BufferedInputStream(new DigestInputStream(inputSource, md));
+        while (input.read() >= 0) {
+            // Read through the stream to update digest.
+        }
+        input.close();
+        String md5 = DatatypeConverter.printHexBinary(md.digest()).toLowerCase();
+        return md5;
     }
 }
diff --git a/src/com/android/tradefed/util/TimeVal.java b/src/com/android/tradefed/util/TimeVal.java
new file mode 100644
index 0000000..c35628a
--- /dev/null
+++ b/src/com/android/tradefed/util/TimeVal.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2014 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.util;
+
+import com.google.common.math.LongMath;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This is a sentinel type which wraps a {@code Long}.  It exists solely as a hint to the
+ * options parsing machinery that a particular value should be parsed as if it were a string
+ * representing a time value.
+ */
+@SuppressWarnings("serial")
+public class TimeVal extends Number implements Comparable<Long> {
+    private static final Pattern TIME_PATTERN =
+            Pattern.compile("(?i)" +  // case insensitive
+                    "(?:(?<d>\\d+)d)?" +  // a number followed by "d"
+                    "(?:(?<h>\\d+)h)?" +
+                    "(?:(?<m>\\d+)m)?" +
+                    "(?:(?<s>\\d+)s)?" +
+                    "(?:(?<ms>\\d+)(?:ms)?)?");  // a number followed by "ms"
+
+    private Long mValue = null;
+
+    /**
+     * Constructs a newly allocated TimeVal object that represents the specified Long argument
+     */
+    public TimeVal(Long value) {
+        mValue = value;
+    }
+
+    /**
+     * Constructs a newly allocated TimeVal object that represents the <emph>timestamp</emph>
+     * indicated by the String parameter.  The string is converted to a TimeVal in exactly the
+     * manner used by the {@see fromString(String)} method.
+     */
+    public TimeVal(String value) throws NumberFormatException {
+        mValue = fromString(value);
+    }
+
+    /**
+     * @return the wrapped {@code Long} value.
+     */
+    public Long asLong() {
+        return mValue;
+    }
+
+    /**
+     * Parses the string as a hierarchical time value
+     * <p />
+     * The default unit is millis.  The parser will accept {@code s} for seconds (1000 millis),
+     * {@code m} for minutes (60 seconds), {@code h} for hours (60 minutes), or {@code d} for days
+     * (24 hours).
+     * <p />
+     * Units may be mixed and matched, so long as each unit appears at most once, and so long as
+     * all units which do appear are listed in decreasing order of scale.  So, for instance,
+     * {@code h} may only appear before {@code m}, and may only appear after {@code d}.  As a
+     * specific example, "1d2h3m4s5ms" would be a valid time value, as would "4" or "4ms".  All
+     * embedded whitespace is discarded.
+     * <p />
+     * Do note that this method rejects overflows.  So the output number is guaranteed to be
+     * non-negative, and to fit within the {@code long} type.
+     */
+    public static long fromString(String value) throws NumberFormatException {
+        if (value == null) throw new NumberFormatException("value is null");
+
+        try {
+            value = value.replaceAll("\\s+", "");
+            Matcher m = TIME_PATTERN.matcher(value);
+            if (m.matches()) {
+                // This works by, essentially, modifying the units of timeValue, from the
+                // largest supported unit, until we've dropped down to millis.
+                long timeValue = 0;
+                timeValue = val(m.group("d"));
+
+                // 1 day == 24 hours
+                timeValue = LongMath.checkedMultiply(timeValue, 24);
+                timeValue = LongMath.checkedAdd(timeValue, val(m.group("h")));
+
+                // 1 hour == 60 minutes
+                timeValue = LongMath.checkedMultiply(timeValue, 60);
+                timeValue = LongMath.checkedAdd(timeValue, val(m.group("m")));
+
+                // 1 hour == 60 seconds
+                timeValue = LongMath.checkedMultiply(timeValue, 60);
+                timeValue = LongMath.checkedAdd(timeValue, val(m.group("s")));
+
+                // 1 second == 1000 millis
+                timeValue = LongMath.checkedMultiply(timeValue, 1000);
+                timeValue = LongMath.checkedAdd(timeValue, val(m.group("ms")));
+
+                return timeValue;
+            }
+        } catch (ArithmeticException e) {
+            throw new NumberFormatException(String.format(
+                    "Failed to parse value %s as a time value: %s", value, e.getMessage()));
+        }
+
+        throw new NumberFormatException(
+                String.format("Failed to parse value %s as a time value", value));
+    }
+
+    static long val(String str) throws NumberFormatException {
+        if (str == null) return 0;
+
+        Long value = Long.parseLong(str);
+        if (value == null) return 0;
+        return value;
+    }
+
+
+    // implementing interfaces
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public double doubleValue() {
+        return mValue.doubleValue();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public float floatValue() {
+        return mValue.floatValue();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int intValue() {
+        return mValue.intValue();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long longValue() {
+        return mValue.longValue();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int compareTo(Long other) {
+        return mValue.compareTo(other);
+    }
+}
diff --git a/src/com/android/tradefed/util/VersionParser.java b/src/com/android/tradefed/util/VersionParser.java
index fb2dc77..1f20a21 100644
--- a/src/com/android/tradefed/util/VersionParser.java
+++ b/src/com/android/tradefed/util/VersionParser.java
@@ -15,19 +15,18 @@
  */
 package com.android.tradefed.util;
 
+import com.android.tradefed.log.LogUtil.CLog;
+
 import java.io.File;
 import java.io.IOException;
 
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.FileUtil;
-
 public class VersionParser {
     private static final String DEFAULT_VERSION_FILE_NAME = "tf_version.txt";
 
     public static String fetchVersion(File file) {
         if (file.exists()) {
             try {
-                return FileUtil.readStringFromFile(file);
+                return FileUtil.readStringFromFile(file).trim();
             } catch (IOException e) {
                CLog.e(e.toString());
                return null;
diff --git a/src/com/android/tradefed/util/ZipUtil.java b/src/com/android/tradefed/util/ZipUtil.java
index 957a7e6..cc18413 100644
--- a/src/com/android/tradefed/util/ZipUtil.java
+++ b/src/com/android/tradefed/util/ZipUtil.java
@@ -163,7 +163,7 @@
      * @param relativePathSegs the relative path of file, including separators
      * @throws IOException if failed to add file to zip
      */
-    private static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
+    public static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
             throws IOException {
         relativePathSegs.add(file.getName());
         if (file.isDirectory()) {
diff --git a/src/com/android/tradefed/util/net/HttpHelper.java b/src/com/android/tradefed/util/net/HttpHelper.java
index f7098ee..e4854df 100644
--- a/src/com/android/tradefed/util/net/HttpHelper.java
+++ b/src/com/android/tradefed/util/net/HttpHelper.java
@@ -124,6 +124,21 @@
      * {@inheritDoc}
      */
     @Override
+    public void doGet(String url, OutputStream outputStream) throws IOException {
+        CLog.d("Performing GET download request for %s", url);
+        InputStream remote = null;
+        try {
+            remote = getRemoteUrlStream(new URL(url));
+            StreamUtil.copyStreams(remote, outputStream);
+        } finally {
+            StreamUtil.close(remote);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public void doGetIgnore(String url) throws IOException {
         CLog.d("Performing GET request for %s. Ignoring result.", url);
         InputStream remote = null;
@@ -240,7 +255,7 @@
      * Runnable for making requests with
      * {@link IRunUtil#runEscalatingTimedRetry(long, long, long, long, IRunnableResult)}.
      */
-    private abstract class RequestRunnable implements IRunnableResult {
+    public abstract class RequestRunnable implements IRunnableResult {
         private String mResponse = null;
         private Exception mException = null;
         private final String mUrl;
@@ -499,7 +514,7 @@
     /**
      * Get {@link IRunUtil} to use. Exposed so unit tests can mock.
      */
-    IRunUtil getRunUtil() {
+    public IRunUtil getRunUtil() {
         return RunUtil.getDefault();
     }
 }
diff --git a/src/com/android/tradefed/util/net/IHttpHelper.java b/src/com/android/tradefed/util/net/IHttpHelper.java
index c1bde53..49c3fb7 100644
--- a/src/com/android/tradefed/util/net/IHttpHelper.java
+++ b/src/com/android/tradefed/util/net/IHttpHelper.java
@@ -20,6 +20,7 @@
 import com.android.tradefed.util.MultiMap;
 
 import java.io.IOException;
+import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.net.URL;
 
@@ -83,6 +84,17 @@
     public String doGet(String url) throws IOException, DataSizeException;
 
     /**
+     * Performs a GET HTTP request method for a given URL and streams result to a
+     * {@link OutputStream}.
+     *
+     * @see #doGet(String)
+     * @param url the URL
+     * @param outputStream stream of the response data
+     * @throws IOException if failed to retrieve data
+     */
+    public void doGet(String url, OutputStream outputStream) throws IOException;
+
+    /**
      * Performs {{@link #doGet(String)} retrying upon failure.
      *
      * @see IRunUtil#runEscalatingTimedRetry(long, long, long, long,
diff --git a/tests/Android.mk b/tests/Android.mk
index 31f1748..df45f09 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -26,7 +26,7 @@
 LOCAL_MODULE := tradefed-tests
 LOCAL_MODULE_TAGS := optional
 LOCAL_STATIC_JAVA_LIBRARIES := easymock
-LOCAL_JAVA_LIBRARIES := tradefed ddmlib-prebuilt
+LOCAL_JAVA_LIBRARIES := tradefed
 
 include $(BUILD_HOST_JAVA_LIBRARY)
 
diff --git a/tests/res/config/tf/acceptance.xml b/tests/res/config/tf/acceptance.xml
new file mode 100644
index 0000000..ae396a4
--- /dev/null
+++ b/tests/res/config/tf/acceptance.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<configuration
+    description="Run the tradefed stress tests on a userdebug build with single iteration">
+
+    <test class="com.android.tradefed.device.TestDeviceStressTest">
+        <option name="iterations" value="1" />
+    </test>
+</configuration>
diff --git a/tests/res/testCmdFiles/basic.txt b/tests/res/testCmdFiles/basic.txt
new file mode 100644
index 0000000..39c3afd
--- /dev/null
+++ b/tests/res/testCmdFiles/basic.txt
@@ -0,0 +1,13 @@
+MACRO f0 = 0
+MACRO f1 = 1
+MACRO f2 = 2
+MACRO f3 = 3
+
+LONG MACRO f
+f0()
+f1()
+f2()
+f3()
+END MACRO
+
+recharge --max-battery f() --noisy-dry-run
diff --git a/tests/res/testCmdFiles/missing-begin-macro.txt b/tests/res/testCmdFiles/missing-begin-macro.txt
new file mode 100644
index 0000000..b5ad227
--- /dev/null
+++ b/tests/res/testCmdFiles/missing-begin-macro.txt
@@ -0,0 +1,15 @@
+MACRO f0 = 0
+MACRO f1 = 1
+MACRO f2 = 2
+MACRO f3 = 3
+
+#LONG MACRO f
+f0()
+f1()
+f2()
+f3()
+# Note that "END MACRO" by itself is potentially a completely valid command.
+# This will actually fail because the macro f() is now undefined.
+END MACRO
+
+recharge --max-battery f() --noisy-dry-run
diff --git a/tests/res/testCmdFiles/missing-end-macro.txt b/tests/res/testCmdFiles/missing-end-macro.txt
new file mode 100644
index 0000000..7f24bac
--- /dev/null
+++ b/tests/res/testCmdFiles/missing-end-macro.txt
@@ -0,0 +1,13 @@
+MACRO f0 = 0
+MACRO f1 = 1
+MACRO f2 = 2
+MACRO f3 = 3
+
+LONG MACRO f
+f0()
+f1()
+f2()
+f3()
+#END MACRO
+
+recharge --max-battery f() --noisy-dry-run
diff --git a/tests/res/testCmdFiles/missing-macro-def.txt b/tests/res/testCmdFiles/missing-macro-def.txt
new file mode 100644
index 0000000..defb8d2
--- /dev/null
+++ b/tests/res/testCmdFiles/missing-macro-def.txt
@@ -0,0 +1,13 @@
+MACRO f0 = 0
+MACRO f1 = 1
+MACRO f2 = 2
+#MACRO f3 = 3
+
+LONG MACRO f
+f0()
+f1()
+f2()
+f3()
+END MACRO
+
+recharge --max-battery f() --noisy-dry-run
diff --git a/tests/res/testconfigs/depend-template-include-config.xml b/tests/res/testconfigs/depend-template-include-config.xml
new file mode 100644
index 0000000..028b9c2
--- /dev/null
+++ b/tests/res/testconfigs/depend-template-include-config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<configuration
+    description="test template-based config inclusion with templated targets">
+
+    <template-include name="dep-target" />
+
+    <test class="com.android.tradefed.config.StubOptionTest" >
+        <option name="option" value="valueFromDependTemplateIncludeConfig" />
+    </test>
+
+</configuration>
diff --git a/tests/res/testconfigs/include-template-config-with-default.xml b/tests/res/testconfigs/include-template-config-with-default.xml
new file mode 100644
index 0000000..ed4c674
--- /dev/null
+++ b/tests/res/testconfigs/include-template-config-with-default.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<configuration
+    description="Make sure that we can include a template-include-containing config if it has a default resolution set.">
+
+    <include name="template-include-config-with-default" />
+
+    <test class="com.android.tradefed.config.StubOptionTest" >
+        <option name="option" value="valueFromIncludeTemplateConfigWithDefault" />
+    </test>
+
+</configuration>
diff --git a/tests/res/testconfigs/include-template-config.xml b/tests/res/testconfigs/include-template-config.xml
new file mode 100644
index 0000000..5654045
--- /dev/null
+++ b/tests/res/testconfigs/include-template-config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<configuration
+    description="test to demonstrate that including a template-include-containing config doesn't work">
+
+    <include name="template-include-config" />
+
+    <test class="com.android.tradefed.config.StubOptionTest" >
+        <option name="option" value="valueFromIncludeTemplateConfig" />
+    </test>
+
+</configuration>
diff --git a/tests/res/testconfigs/template-include-config-with-default.xml b/tests/res/testconfigs/template-include-config-with-default.xml
new file mode 100644
index 0000000..4af14e5
--- /dev/null
+++ b/tests/res/testconfigs/template-include-config-with-default.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<configuration
+    description="test template-based config inclusion with default values">
+
+    <template-include name="target" default="test-config" />
+
+    <test class="com.android.tradefed.config.StubOptionTest" >
+        <option name="option" value="valueFromTemplateIncludeWithDefaultConfig" />
+    </test>
+
+</configuration>
diff --git a/tests/res/testconfigs/template-include-config.xml b/tests/res/testconfigs/template-include-config.xml
new file mode 100644
index 0000000..d8b3f7f
--- /dev/null
+++ b/tests/res/testconfigs/template-include-config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<configuration
+    description="test template-based config inclusion">
+
+    <template-include name="target" />
+
+    <test class="com.android.tradefed.config.StubOptionTest" >
+        <option name="option" value="valueFromTemplateIncludeConfig" />
+    </test>
+
+</configuration>
diff --git a/tests/src/com/android/tradefed/FuncTests.java b/tests/src/com/android/tradefed/FuncTests.java
index 422d84f..4435f22 100644
--- a/tests/src/com/android/tradefed/FuncTests.java
+++ b/tests/src/com/android/tradefed/FuncTests.java
@@ -17,6 +17,7 @@
 
 import com.android.tradefed.build.FileDownloadCacheFuncTest;
 import com.android.tradefed.command.CommandSchedulerFuncTest;
+import com.android.tradefed.command.remote.RemoteManagerFuncTest;
 import com.android.tradefed.device.TestDeviceFuncTest;
 import com.android.tradefed.targetprep.DeviceSetupFuncTest;
 import com.android.tradefed.testtype.DeviceTestSuite;
@@ -41,6 +42,8 @@
         this.addTestSuite(FileDownloadCacheFuncTest.class);
         // command
         this.addTestSuite(CommandSchedulerFuncTest.class);
+        // command.remote
+        this.addTestSuite(RemoteManagerFuncTest.class);
         // device
         this.addTestSuite(TestDeviceFuncTest.class);
         // targetprep
diff --git a/tests/src/com/android/tradefed/TfTestLauncher.java b/tests/src/com/android/tradefed/TfTestLauncher.java
index 2946883..cdd2b00 100644
--- a/tests/src/com/android/tradefed/TfTestLauncher.java
+++ b/tests/src/com/android/tradefed/TfTestLauncher.java
@@ -15,12 +15,11 @@
  */
 package com.android.tradefed;
 
-import com.android.ddmlib.Log;
-import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IFolderBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IRemoteTest;
@@ -56,6 +55,7 @@
     @Option(name = "config-name", description = "the config that runs the TF tests")
     private String mConfigName;
 
+    private static final String TF_GLOBAL_CONFIG = "TF_GLOBAL_CONFIG";
     /**
      * {@inheritDoc}
      */
@@ -90,19 +90,18 @@
             args.add(mBuildInfo.getBuildFlavor());
         }
 
-        CommandResult result = getRunUtil().runTimedCmd(mMaxTfRunTimeMin * 60 * 1000,
+        IRunUtil runUtil = new RunUtil();
+        // clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file
+        runUtil.unsetEnvVariable(TF_GLOBAL_CONFIG);
+        CommandResult result = runUtil.runTimedCmd(mMaxTfRunTimeMin * 60 * 1000,
                 args.toArray(new String[0]));
         if (result.getStatus().equals(CommandStatus.SUCCESS)) {
-            Log.logAndDisplay(LogLevel.INFO, "TfTestLauncher",
-                    String.format("Successfully ran TF tests for build %s. stdout: %s\n, stderr: %s",
-                    mBuildInfo.getBuildId(), result.getStdout(), result.getStderr()));
-
+            CLog.d("Successfully ran TF tests for build %s", mBuildInfo.getBuildId());
         } else {
-            Log.logAndDisplay(LogLevel.INFO, "TfTestLauncher",
-                    String.format("Failed to run TF tests for build %s. stdout: %s\n, stderr: %s",
-                    mBuildInfo.getBuildId(), result.getStdout(),
-                    result.getStderr()));
+            CLog.w("Failed ran TF tests for build %s, status %s",
+                    mBuildInfo.getBuildId(), result.getStatus());
         }
+        CLog.v("TF tests output:\nstdout: %s\nstderror\n", result.getStdout(), result.getStderr());
     }
 
     IRunUtil getRunUtil() {
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 3e3fc4c..ca85966 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -26,7 +26,7 @@
 import com.android.tradefed.command.CommandFileParserTest;
 import com.android.tradefed.command.CommandSchedulerTest;
 import com.android.tradefed.command.ConsoleTest;
-import com.android.tradefed.command.remote.RemoteManagerTest;
+import com.android.tradefed.command.VerifyTest;
 import com.android.tradefed.command.remote.RemoteOperationTest;
 import com.android.tradefed.config.ArgsOptionParserTest;
 import com.android.tradefed.config.ConfigurationDefTest;
@@ -42,6 +42,7 @@
 import com.android.tradefed.device.DeviceStateMonitorTest;
 import com.android.tradefed.device.DeviceUtilStatsMonitorTest;
 import com.android.tradefed.device.DumpsysPackageReceiverTest;
+import com.android.tradefed.device.FastbootHelperTest;
 import com.android.tradefed.device.ManagedDeviceListTest;
 import com.android.tradefed.device.ReconnectingRecoveryTest;
 import com.android.tradefed.device.TestDeviceTest;
@@ -53,6 +54,8 @@
 import com.android.tradefed.log.TerribleFailureEmailHandlerTest;
 import com.android.tradefed.result.BugreportCollectorTest;
 import com.android.tradefed.result.CollectingTestListenerTest;
+import com.android.tradefed.result.ConsoleResultReporterTest;
+import com.android.tradefed.result.DeviceFileReporterTest;
 import com.android.tradefed.result.EmailResultReporterTest;
 import com.android.tradefed.result.FailureEmailResultReporterTest;
 import com.android.tradefed.result.FileSystemLogSaverTest;
@@ -104,6 +107,7 @@
 import com.android.tradefed.util.RegexTrieTest;
 import com.android.tradefed.util.RunUtilTest;
 import com.android.tradefed.util.SizeLimitedOutputStreamTest;
+import com.android.tradefed.util.net.HttpHelperTest;
 import com.android.tradefed.util.net.HttpMultipartPostTest;
 import com.android.tradefed.util.xml.AndroidManifestWriterTest;
 
@@ -132,9 +136,9 @@
         addTestSuite(CommandFileParserTest.class);
         addTestSuite(CommandSchedulerTest.class);
         addTestSuite(ConsoleTest.class);
+        addTestSuite(VerifyTest.class);
 
         // command.remote
-        addTestSuite(RemoteManagerTest.class);
         addTestSuite(RemoteOperationTest.class);
 
         // config
@@ -150,11 +154,12 @@
         // device
         addTestSuite(CpuStatsCollectorTest.class);
         addTestSuite(DeviceManagerTest.class);
-        addTestSuite(ManagedDeviceListTest.class);
         addTestSuite(DeviceSelectionOptionsTest.class);
         addTestSuite(DeviceStateMonitorTest.class);
         addTestSuite(DeviceUtilStatsMonitorTest.class);
         addTestSuite(DumpsysPackageReceiverTest.class);
+        addTestSuite(FastbootHelperTest.class);
+        addTestSuite(ManagedDeviceListTest.class);
         addTestSuite(ReconnectingRecoveryTest.class);
         addTestSuite(TestDeviceTest.class);
         addTestSuite(WaitDeviceRecoveryTest.class);
@@ -170,7 +175,9 @@
 
         // result
         addTestSuite(BugreportCollectorTest.class);
+        addTestSuite(ConsoleResultReporterTest.class);
         addTestSuite(CollectingTestListenerTest.class);
+        addTestSuite(DeviceFileReporterTest.class);
         addTestSuite(EmailResultReporterTest.class);
         addTestSuite(FailureEmailResultReporterTest.class);
         addTestSuite(FileSystemLogSaverTest.class);
@@ -221,6 +228,7 @@
         addTestSuite(ConditionPriorityBlockingQueueTest.class);
         addTestSuite(EmailTest.class);
         addTestSuite(FileUtilTest.class);
+        addTestSuite(HttpHelperTest.class);
         addTestSuite(HttpMultipartPostTest.class);
         addTestSuite(JUnitXmlParserTest.class);
         addTestSuite(MultiMapTest.class);
diff --git a/tests/src/com/android/tradefed/command/CommandFileParserTest.java b/tests/src/com/android/tradefed/command/CommandFileParserTest.java
index dbb5d0a..a9ffd15 100644
--- a/tests/src/com/android/tradefed/command/CommandFileParserTest.java
+++ b/tests/src/com/android/tradefed/command/CommandFileParserTest.java
@@ -61,20 +61,20 @@
         assertParsedData(expectedArgs);
     }
 
-    @SuppressWarnings("unchecked")
-    private void assertParsedData(List<String>... expectedCommands) throws IOException,
+    @SafeVarargs
+    private final void assertParsedData(List<String>... expectedCommands) throws IOException,
             ConfigurationException {
         assertParsedData(mCommandFile, mMockFile, expectedCommands);
     }
 
-    @SuppressWarnings("unchecked")
-    private void assertParsedData(CommandFileParser parser, List<String>... expectedCommands)
+    @SafeVarargs
+    private final void assertParsedData(CommandFileParser parser, List<String>... expectedCommands)
             throws IOException, ConfigurationException {
         assertParsedData(parser, mMockFile, expectedCommands);
     }
 
-    @SuppressWarnings("unchecked")
-    private void assertParsedData(CommandFileParser parser, File file,
+    @SafeVarargs
+    private final void assertParsedData(CommandFileParser parser, File file,
             List<String>... expectedCommands) throws IOException, ConfigurationException {
         List<CommandLine> data = parser.parseFile(file);
         assertEquals(expectedCommands.length, data.size());
diff --git a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
index 3446206..9faf93e 100644
--- a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
@@ -39,6 +39,8 @@
 
 import org.easymock.EasyMock;
 import org.easymock.IAnswer;
+import org.json.JSONArray;
+import org.json.JSONException;
 import org.junit.Assert;
 
 import java.io.File;
@@ -104,6 +106,11 @@
             }
 
             @Override
+            void checkInvocations() {
+                // ignore
+            }
+
+            @Override
             CommandFileParser createCommandFileParser() {
                 return mMockCmdFileParser;
             }
@@ -171,6 +178,20 @@
     }
 
     /**
+     * Test {@link CommandScheduler#addCommand(String[])} when json help mode is specified
+     */
+    public void testAddConfig_configJsonHelp() throws ConfigurationException, JSONException {
+        String[] args = new String[] {};
+        mCommandOptions.setJsonHelpMode(true);
+        setCreateConfigExpectations(args, 1);
+        // expect
+        EasyMock.expect(mMockConfiguration.getJsonCommandUsage()).andReturn(new JSONArray());
+        replayMocks();
+        mScheduler.addCommand(args);
+        verifyMocks();
+    }
+
+    /**
      * Test {@link CommandScheduler#run()} when one config has been added
      */
     public void testRun_oneConfig() throws Throwable {
@@ -551,14 +572,13 @@
         mMockManager.setNumDevices(0);
         String[] cmdFile1Args = new String[] {"fromFile1"};
         setCreateConfigExpectations(cmdFile1Args, 1);
-
+        setCreateConfigExpectations(cmdFile1Args, 1);
         mMockConfiguration.validateOptions();
-        EasyMock.expectLastCall().times(1);
+        EasyMock.expectLastCall().times(2);
 
         final List<CommandLine> cmdFileContent1 = Arrays.asList(new CommandLine(
                 Arrays.asList("fromFile1")));
         mMockCmdFileParser = new CommandFileParser() {
-            boolean firstCall = true;
             @Override
             public List<CommandLine> parseFile(File cmdFile) {
                 return cmdFileContent1;
@@ -575,7 +595,8 @@
         // now attempt to add the same command file
         mScheduler.addCommandFile("mycmd.txt", Collections.<String>emptyList());
 
-        // ensure no effect
+        // expect reload
+        // ensure same state as before
         cmds = mScheduler.getCommandTrackers();
         assertEquals(1, cmds.size());
         Assert.assertArrayEquals(cmdFile1Args, cmds.get(0).getArgs());
diff --git a/tests/src/com/android/tradefed/command/VerifyTest.java b/tests/src/com/android/tradefed/command/VerifyTest.java
new file mode 100644
index 0000000..a0b4e82
--- /dev/null
+++ b/tests/src/com/android/tradefed/command/VerifyTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2015 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.command;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.util.FileUtil;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link Verify}
+ */
+public class VerifyTest extends TestCase {
+    private static final String TEST_CMD_FILE_PATH = "/testCmdFiles";
+    private final Verify mVerify;
+
+    public VerifyTest() throws ConfigurationException {
+        mVerify = new Verify();
+
+        OptionSetter option = new OptionSetter(mVerify);
+        option.setOptionValue("quiet", "true");
+    }
+
+    /**
+     * Extract an embedded command file into a temporary file, which we can feed to the
+     * CommandFileParser
+     */
+    private File extractTestCmdFile(String name) throws IOException {
+        final InputStream cmdFileStream = getClass().getResourceAsStream(
+                String.format("%s/%s.txt", TEST_CMD_FILE_PATH, name));
+        final String tmpFileName = String.format("VerifyTest_%s_", name);
+        File tmpFile = FileUtil.createTempFile(tmpFileName, ".txt");
+        try {
+            FileUtil.writeToFile(cmdFileStream, tmpFile);
+        } catch (Throwable t) {
+            // Clean up tmpFile, if it was created.
+            FileUtil.deleteFile(tmpFile);
+            throw t;
+        }
+
+        return tmpFile;
+    }
+
+    /**
+     * Assert that the specified command file parses correctly, and clean up any temporary files
+     */
+    private void assertGoodCmdFile(String name) throws IOException {
+        File cmdFile = extractTestCmdFile(name);
+        try {
+            assertTrue(mVerify.runVerify(cmdFile));
+        } finally {
+            FileUtil.deleteFile(cmdFile);
+        }
+    }
+
+    /**
+     * Assert that the specified command file does not parse correctly, and clean up any temporary
+     * files
+     */
+    private void assertBadCmdFile(String name) throws IOException {
+        File cmdFile = extractTestCmdFile(name);
+        try {
+            assertFalse(mVerify.runVerify(cmdFile));
+        } finally {
+            FileUtil.deleteFile(cmdFile);
+        }
+    }
+
+    public void testBasic() throws IOException {
+        assertGoodCmdFile("basic");
+    }
+
+    public void testMissingMacroDef() throws IOException {
+        assertBadCmdFile("missing-macro-def");
+    }
+
+    public void testMissingBeginMacro() throws IOException {
+        assertBadCmdFile("missing-begin-macro");
+    }
+
+    public void testMissingEndMacro() throws IOException {
+        assertBadCmdFile("missing-end-macro");
+    }
+}
diff --git a/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java b/tests/src/com/android/tradefed/command/remote/RemoteManagerFuncTest.java
similarity index 99%
rename from tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
rename to tests/src/com/android/tradefed/command/remote/RemoteManagerFuncTest.java
index b8e1f50..bef004d 100644
--- a/tests/src/com/android/tradefed/command/remote/RemoteManagerTest.java
+++ b/tests/src/com/android/tradefed/command/remote/RemoteManagerFuncTest.java
@@ -37,7 +37,7 @@
 /**
  * Unit tests for {@link RemoteManager}.
  */
-public class RemoteManagerTest extends TestCase {
+public class RemoteManagerFuncTest extends TestCase {
 
     private IDeviceManager mMockDeviceManager;
     private RemoteManager mRemoteMgr;
diff --git a/tests/src/com/android/tradefed/config/ArgsOptionParserTest.java b/tests/src/com/android/tradefed/config/ArgsOptionParserTest.java
index af38b77..c53808a 100644
--- a/tests/src/com/android/tradefed/config/ArgsOptionParserTest.java
+++ b/tests/src/com/android/tradefed/config/ArgsOptionParserTest.java
@@ -195,6 +195,8 @@
         private String mNullImmutableOption = null;
     }
 
+
+    // SECTION: option update rule validation
     /**
      * Verify that {@link OptionUpdateRule}s work properly when the update compares to greater-than
      * the default value.
@@ -314,32 +316,34 @@
         }
     }
 
+
+    // SECTION: tests for #parse(...)
     /**
     * Test passing an empty argument list for an object that has one option specified.
     * <p/>
     * Expected that the option field should retain its default value.
     */
-   public void testParse_noArg() throws ConfigurationException {
-       OneOptionSource object = new OneOptionSource();
-       ArgsOptionParser parser = new ArgsOptionParser(object);
-       parser.parse(new String[] {});
-       assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
-   }
+    public void testParse_noArg() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        parser.parse(new String[] {});
+        assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
+    }
 
-   /**
-    * Test passing an single argument for an object that has one option specified.
-    */
-   public void testParse_oneArg() throws ConfigurationException {
-       OneOptionSource object = new OneOptionSource();
-       ArgsOptionParser parser = new ArgsOptionParser(object);
-       final String expectedValue = "set";
-       parser.parse(new String[] {"--my_option", expectedValue});
-       assertEquals(expectedValue, object.mMyOption);
-   }
+    /**
+     * Test passing an single argument for an object that has one option specified.
+     */
+    public void testParse_oneArg() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String expectedValue = "set";
+        parser.parse(new String[] {"--my_option", expectedValue});
+        assertEquals(expectedValue, object.mMyOption);
+    }
 
-   /**
-    * Test passing an single argument for an object that has one option specified.
-    */
+    /**
+     * Test passing an single argument for an object that has one option specified.
+     */
     public void testParse_oneMapArg() throws ConfigurationException {
         MapOptionSource object = new MapOptionSource();
         ArgsOptionParser parser = new ArgsOptionParser(object);
@@ -546,6 +550,245 @@
         }
     }
 
+
+    // SECTION: tests for #parseBestEffort(...)
+    /**
+     * Test passing an single argument for an object that has one option specified.
+     */
+    public void testParseBestEffort_oneArg() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String option = "--my_option";
+        final String value = "set";
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {option, value});
+        assertEquals(value, object.mMyOption);
+        assertEquals(0, leftovers.size());
+    }
+
+    /**
+     * Make sure that overwriting arguments works as expected.
+     */
+    public void testParseBestEffort_oneArg_overwrite() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String option = "--my_option";
+        final String value1 = "set";
+        final String value2 = "game";
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {option, value1, option, value2});
+        assertEquals(value2, object.mMyOption);
+        assertEquals(0, leftovers.size());
+    }
+
+    /**
+     * Test passing a usable argument followed by an unusable one.
+     */
+    public void testParseBestEffort_oneArg_oneLeftover() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String expectedValue = "set";
+        final String leftoverOption = "--no_exist";
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {"--my_option", expectedValue, leftoverOption});
+        assertEquals(expectedValue, object.mMyOption);
+        assertEquals(1, leftovers.size());
+        assertEquals(leftoverOption, leftovers.get(0));
+    }
+
+    /**
+     * Test passing an unusable argument followed by a usable one.  Basically verifies that the
+     * parse attempt stops wholesale, and doesn't merely skip the unusable arg.
+     */
+    public void testParseBestEffort_oneLeftover_oneArg() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String goodOption = "--my_option";
+        final String value = "set";
+        final String badOption = "--no_exist";
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {badOption, goodOption, value});
+        assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
+        assertEquals(3, leftovers.size());
+        assertEquals(badOption, leftovers.get(0));
+        assertEquals(goodOption, leftovers.get(1));
+        assertEquals(value, leftovers.get(2));
+    }
+
+    /**
+     * Make sure that parsing stops when a bare option prefix, "--", is encountered.  That prefix
+     * should _not_ be returned as one of the leftover args.
+     */
+    public void testParseBestEffort_manualStop() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String goodOption = "--my_option";
+        final String value = "set";
+        final String badOption = "--no_exist";
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {"--", badOption, goodOption, value});
+        assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
+        assertEquals(3, leftovers.size());
+        assertEquals(badOption, leftovers.get(0));
+        assertEquals(goodOption, leftovers.get(1));
+        assertEquals(value, leftovers.get(2));
+    }
+
+    /**
+     * Make sure that parsing stops when a bare word is encountered.  Unlike a bare option prefix,
+     * the bare word _should_ be returned as one of the leftover args.
+     */
+    public void testParseBestEffort_bareWord() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String goodOption = "--my_option";
+        final String value = "set";
+        final String badOption = "--no_exist";
+        final String bareWord = "configName";
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {bareWord, badOption, goodOption, value});
+        assertEquals(OneOptionSource.DEFAULT_VALUE, object.mMyOption);
+        assertEquals(4, leftovers.size());
+        assertEquals(bareWord, leftovers.get(0));
+        assertEquals(badOption, leftovers.get(1));
+        assertEquals(goodOption, leftovers.get(2));
+        assertEquals(value, leftovers.get(3));
+    }
+
+    /**
+     * Make sure that parsing stops when a bare option prefix, "--", is encountered.  That prefix
+     * should _not_ be returned as one of the leftover args.
+     */
+    public void testParseBestEffort_oneArg_manualStop() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String option = "--my_option";
+        final String value1 = "set";
+        final String value2 = "game";
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {option, value1, "--", option, value2});
+        assertEquals(value1, object.mMyOption);
+        assertEquals(2, leftovers.size());
+        assertEquals(option, leftovers.get(0));
+        assertEquals(value2, leftovers.get(1));
+    }
+
+    /**
+     * Make sure that parsing stops when a bare word is encountered.  Unlike a bare option prefix,
+     * the bare word _should_ be returned as one of the leftover args.
+     */
+    public void testParseBestEffort_oneArg_bareWord() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String option = "--my_option";
+        final String value1 = "set";
+        final String value2 = "game";
+        final String bareWord = "configName";
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {option, value1, bareWord, option, value2});
+        assertEquals(value1, object.mMyOption);
+        assertEquals(3, leftovers.size());
+        assertEquals(bareWord, leftovers.get(0));
+        assertEquals(option, leftovers.get(1));
+        assertEquals(value2, leftovers.get(2));
+    }
+
+    /**
+     * Test passing an single argument for an object that has one option specified.
+     */
+    public void testParseBestEffort_oneArg_twoLeftovers() throws ConfigurationException {
+        OneOptionSource object = new OneOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String expectedValue = "set";
+        final String leftover1 = "--no_exist";
+        final String leftover2 = "--me_neither";
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {"--my_option", expectedValue, leftover1, leftover2});
+        assertEquals(expectedValue, object.mMyOption);
+        assertEquals(2, leftovers.size());
+        assertEquals(leftover1, leftovers.get(0));
+        assertEquals(leftover2, leftovers.get(1));
+    }
+
+    /**
+     * Make sure that map option parsing works as expected.
+     */
+    public void testParseBestEffort_mapOption() throws ConfigurationException {
+        MapOptionSource object = new MapOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String option = "--my_option";
+        final String key = "123";  // Integer is the key type
+        final String value = "true";  // Boolean is the value type
+        final Integer expKey = 123;
+        final Boolean expValue = Boolean.TRUE;
+
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {option, key, value});
+
+        assertEquals(0, leftovers.size());
+        assertNotNull(object.mMyOption);
+        assertEquals(1, object.mMyOption.size());
+        assertTrue(object.mMyOption.containsKey(expKey));
+        assertEquals(expValue, object.mMyOption.get(expKey));
+    }
+
+    /**
+     * Make sure that we backtrack the appropriate amount when a Map option parse fails in the
+     * middle
+     */
+    public void testParseBestEffort_mapOption_missingValue() throws ConfigurationException {
+        MapOptionSource object = new MapOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String option = "--my_option";
+        final String key = "123";  // Integer is the key type
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {option, key});
+        assertTrue(object.mMyOption.isEmpty());
+        assertEquals(2, leftovers.size());
+        assertEquals(option, leftovers.get(0));
+        assertEquals(key, leftovers.get(1));
+    }
+
+    /**
+     * Make sure that we backtrack the appropriate amount when a Map option parse fails in the
+     * middle
+     */
+    public void testParseBestEffort_mapOption_badValue() throws ConfigurationException {
+        MapOptionSource object = new MapOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String option = "--my_option";
+        final String key = "123";  // Integer is the key type
+        final String value = "notBoolean";  // Boolean is the value type
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {option, key, value});
+        assertTrue(object.mMyOption.isEmpty());
+        assertEquals(3, leftovers.size());
+        assertEquals(option, leftovers.get(0));
+        assertEquals(key, leftovers.get(1));
+        assertEquals(value, leftovers.get(2));
+    }
+
+    /**
+     * Make sure that we backtrack the appropriate amount when a Map option parse fails in the
+     * middle
+     */
+    public void testParseBestEffort_mapOption_badKey() throws ConfigurationException {
+        MapOptionSource object = new MapOptionSource();
+        ArgsOptionParser parser = new ArgsOptionParser(object);
+        final String option = "--my_option";
+        final String key = "NotANumber";  // Integer is the key type
+        final String value = "true";  // Boolean is the value type
+        final List<String> leftovers = parser.parseBestEffort(
+                new String[] {option, key, value});
+        assertTrue(object.mMyOption.isEmpty());
+        assertEquals(3, leftovers.size());
+        assertEquals(option, leftovers.get(0));
+        assertEquals(key, leftovers.get(1));
+        assertEquals(value, leftovers.get(2));
+    }
+
+
+    // SECTION: help-related tests
     /**
      * Test that help text is displayed for all fields
      */
@@ -578,6 +821,8 @@
         assertFalse(help.contains(ImportantOptionSource.UNIMPORTANT_OPTION_NAME));
     }
 
+
+    // SECTION: mandatory option tests
     public void testMandatoryOption_noDefault() throws Exception {
         MandatoryOptionSourceNoDefault object = new MandatoryOptionSourceNoDefault();
         ArgsOptionParser parser = new ArgsOptionParser(object);
diff --git a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
index fb37036..5bb5e07 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.config;
 
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.ConfigurationFactory.ConfigId;
 import com.android.tradefed.log.ILeveledLogOutput;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.FileUtil;
@@ -28,7 +29,9 @@
 import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Unit tests for {@link ConfigurationFactory}
@@ -82,6 +85,104 @@
         assertConfigValid(TEST_CONFIG);
     }
 
+    private Map<String, String> buildMap(String... args) {
+        if ((args.length % 2) != 0) {
+            throw new IllegalArgumentException(String.format(
+                "Expected an even number of args; got %d", args.length));
+        }
+
+        final Map<String, String> map = new HashMap<String, String>(args.length / 2);
+        for (int i = 0; i < args.length; i += 2) {
+            map.put(args[i], args[i + 1]);
+        }
+
+        return map;
+    }
+
+    /**
+     * Make sure that ConfigId behaves in the right way to serve as a hash key
+     */
+    public void testConfigId_equals() {
+        final ConfigId config1a = new ConfigId("one");
+        final ConfigId config1b = new ConfigId("one");
+        final ConfigId config2 = new ConfigId("two");
+        final ConfigId config3a = new ConfigId("one", buildMap("target", "foo"));
+        final ConfigId config3b = new ConfigId("one", buildMap("target", "foo"));
+        final ConfigId config4 = new ConfigId("two", buildMap("target", "bar"));
+
+        assertEquals(config1a, config1b);
+        assertEquals(config3a, config3b);
+
+        // Check for false equivalences, and don't depend on #equals being commutative
+        assertFalse(config1a.equals(config2));
+        assertFalse(config1a.equals(config3a));
+        assertFalse(config1a.equals(config4));
+
+        assertFalse(config2.equals(config1a));
+        assertFalse(config2.equals(config3a));
+        assertFalse(config2.equals(config4));
+
+        assertFalse(config3a.equals(config1a));
+        assertFalse(config3a.equals(config2));
+        assertFalse(config3a.equals(config4));
+
+        assertFalse(config4.equals(config1a));
+        assertFalse(config4.equals(config2));
+        assertFalse(config4.equals(config3a));
+    }
+
+    /**
+     * Make sure that ConfigId behaves in the right way to serve as a hash key
+     */
+    public void testConfigId_hashKey() {
+        final Map<ConfigId, String> map = new HashMap<>();
+        final ConfigId config1a = new ConfigId("one");
+        final ConfigId config1b = new ConfigId("one");
+        final ConfigId config2 = new ConfigId("two");
+        final ConfigId config3a = new ConfigId("one", buildMap("target", "foo"));
+        final ConfigId config3b = new ConfigId("one", buildMap("target", "foo"));
+        final ConfigId config4 = new ConfigId("two", buildMap("target", "bar"));
+
+        // Make sure that keys config1a and config1b behave identically
+        map.put(config1a, "1a");
+        assertEquals("1a", map.get(config1a));
+        assertEquals("1a", map.get(config1b));
+
+        map.put(config1b, "1b");
+        assertEquals("1b", map.get(config1a));
+        assertEquals("1b", map.get(config1b));
+
+        assertFalse(map.containsKey(config2));
+        assertFalse(map.containsKey(config3a));
+        assertFalse(map.containsKey(config4));
+
+        // Make sure that keys config3a and config3b behave identically
+        map.put(config3a, "3a");
+        assertEquals("3a", map.get(config3a));
+        assertEquals("3a", map.get(config3b));
+
+        map.put(config3b, "3b");
+        assertEquals("3b", map.get(config3a));
+        assertEquals("3b", map.get(config3b));
+
+        assertEquals(2, map.size());
+        assertFalse(map.containsKey(config2));
+        assertFalse(map.containsKey(config4));
+
+        // It's unlikely for these to fail if the above tests all passed, but just fill everything
+        // out for completeness
+        map.put(config2, "2");
+        map.put(config4, "4");
+
+        assertEquals(4, map.size());
+        assertEquals("1b", map.get(config1a));
+        assertEquals("1b", map.get(config1b));
+        assertEquals("2", map.get(config2));
+        assertEquals("3b", map.get(config3a));
+        assertEquals("3b", map.get(config3b));
+        assertEquals("4", map.get(config4));
+    }
+
     /**
      * Test specifying a config xml by file path
      */
@@ -238,7 +339,6 @@
 
     /**
      * Test loading a config that includes another config.
-     * Also ensure options are set correctly.
      */
     public void testCreateConfigurationFromArgs_includeConfig() throws Exception {
         IConfiguration config = mFactory.createConfigurationFromArgs(
@@ -252,6 +352,181 @@
     }
 
     /**
+     * Test loading a config that uses the "default" attribute of a template-include tag to include
+     * another config.
+     */
+    public void testCreateConfigurationFromArgs_defaultTemplateInclude_default() throws Exception {
+        // The default behavior is to include test-config directly.  Nesting is such that innermost
+        // elements come first.
+        IConfiguration config = mFactory.createConfigurationFromArgs(
+                new String[]{"template-include-config-with-default"});
+        assertEquals(2, config.getTests().size());
+        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
+        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
+        StubOptionTest innerConfig = (StubOptionTest) config.getTests().get(0);
+        StubOptionTest outerConfig = (StubOptionTest) config.getTests().get(1);
+        assertEquals("valueFromTestConfig", innerConfig.mOption);
+        assertEquals("valueFromTemplateIncludeWithDefaultConfig", outerConfig.mOption);
+    }
+
+    /**
+     * Test using {@code <include>} to load a config that uses the "default" attribute of a
+     * template-include tag to include a third config.
+     */
+    public void testCreateConfigurationFromArgs_includeTemplateIncludeWithDefault() throws Exception {
+        // The default behavior is to include test-config directly.  Nesting is such that innermost
+        // elements come first.
+        IConfiguration config = mFactory.createConfigurationFromArgs(
+                new String[]{"include-template-config-with-default"});
+        assertEquals(3, config.getTests().size());
+        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
+        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
+        assertTrue(config.getTests().get(2) instanceof StubOptionTest);
+        StubOptionTest innerConfig = (StubOptionTest) config.getTests().get(0);
+        StubOptionTest middleConfig = (StubOptionTest) config.getTests().get(1);
+        StubOptionTest outerConfig = (StubOptionTest) config.getTests().get(2);
+        assertEquals("valueFromTestConfig", innerConfig.mOption);
+        assertEquals("valueFromTemplateIncludeWithDefaultConfig", middleConfig.mOption);
+        assertEquals("valueFromIncludeTemplateConfigWithDefault", outerConfig.mOption);
+    }
+
+    /**
+     * Test loading a config that uses the "default" attribute of a template-include tag to include
+     * another config.  In this case, we override the default attribute on the commandline.
+     */
+    public void testCreateConfigurationFromArgs_defaultTemplateInclude_alternate() throws Exception {
+        IConfiguration config = mFactory.createConfigurationFromArgs(
+                new String[]{"template-include-config-with-default", "--template:map", "target",
+                "include-config"});
+        assertEquals(3, config.getTests().size());
+        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
+        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
+        assertTrue(config.getTests().get(2) instanceof StubOptionTest);
+
+        StubOptionTest innerConfig = (StubOptionTest) config.getTests().get(0);
+        StubOptionTest middleConfig = (StubOptionTest) config.getTests().get(1);
+        StubOptionTest outerConfig = (StubOptionTest) config.getTests().get(2);
+
+        assertEquals("valueFromTestConfig", innerConfig.mOption);
+        assertEquals("valueFromIncludeConfig", middleConfig.mOption);
+        assertEquals("valueFromTemplateIncludeWithDefaultConfig", outerConfig.mOption);
+    }
+
+    /**
+     * Test loading a config that uses template-include to include another config.
+     */
+    public void testCreateConfigurationFromArgs_templateInclude() throws Exception {
+        IConfiguration config = mFactory.createConfigurationFromArgs(
+                new String[]{"template-include-config", "--template:map", "target",
+                "test-config"});
+        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
+        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
+        StubOptionTest fromTestConfig = (StubOptionTest) config.getTests().get(0);
+        StubOptionTest fromTemplateIncludeConfig = (StubOptionTest) config.getTests().get(1);
+        assertEquals("valueFromTestConfig", fromTestConfig.mOption);
+        assertEquals("valueFromTemplateIncludeConfig", fromTemplateIncludeConfig.mOption);
+    }
+
+    /**
+     * Make sure that we throw a useful error when template-include usage is underspecified.
+     */
+    public void testCreateConfigurationFromArgs_templateInclude_unspecified() throws Exception {
+        final String configName = "template-include-config";
+        try {
+            mFactory.createConfigurationFromArgs(new String[]{configName});
+            fail ("ConfigurationException not thrown");
+        } catch (ConfigurationException e) {
+            // Make sure that we get the expected error message
+            final String msg = e.getMessage();
+            assertNotNull(msg);
+
+            assertTrue(String.format("Error message does not mention the name of the broken " +
+                    "config.  msg was: %s", msg), msg.contains(configName));
+
+            // Error message should help people to resolve the problem
+            assertTrue(String.format("Error message should help user to resolve the " +
+                    "template-include.  msg was: %s", msg),
+                    msg.contains(String.format("--template:map %s", "target")));
+            assertTrue(String.format("Error message should mention the ability to specify a " +
+                    "default resolution.  msg was: %s", msg),
+                    msg.contains(String.format("'default'", configName)));
+        }
+    }
+
+    /**
+     * Make sure that we throw a useful error when template-include mentions a target configuration
+     * that doesn't exist.
+     */
+    public void testCreateConfigurationFromArgs_templateInclude_missing() throws Exception {
+        final String configName = "template-include-config";
+        final String includeName = "no-exist";
+
+        try {
+            mFactory.createConfigurationFromArgs(
+                    new String[]{configName, "--template:map", "target", includeName});
+            fail ("ConfigurationException not thrown");
+        } catch (ConfigurationException e) {
+            // Make sure that we get the expected error message
+            final String msg = e.getMessage();
+            assertNotNull(msg);
+
+            assertTrue(String.format("Error message does not mention the name of the broken " +
+                    "config.  msg was: %s", msg), msg.contains(configName));
+            assertTrue(String.format("Error message does not mention the name of the missing " +
+                    "include target.  msg was: %s", msg), msg.contains(includeName));
+        }
+    }
+
+    /**
+     * A limitation of the current implementation is that template args are only passed to the
+     * outermost configuration.  This unit test codifies the expectation that an inner
+     * {@code <template-include>} tag that doesn't have a default resolution set will fail.
+     */
+    public void testCreateConfigurationFromArgs_templateInclude_dependent() throws Exception {
+        final String configName = "depend-template-include-config";
+        final String depTargetName = "template-include-config";
+        final String targetName = "test-config";
+        final String expError = String.format(
+                "Failed to parse config xml '%s'. Reason: " +
+                ConfigurationXmlParser.ConfigHandler.INNER_TEMPLATE_INCLUDE_ERROR,
+                configName, configName, depTargetName);
+
+        try {
+            mFactory.createConfigurationFromArgs(new String[]{configName,
+                    "--template:map", "dep-target", depTargetName,
+                    "--template:map", "target", targetName});
+            fail ("ConfigurationException not thrown");
+        } catch (ConfigurationException e) {
+            // Make sure that we get the expected error message
+            assertEquals(expError, e.getMessage());
+        }
+    }
+
+    /**
+     * A limitation of the current implementation is that template args are only passed to the
+     * outermost configuration.  This unit test codifies the expectation that an inner
+     * {@code <template-include>} tag that doesn't have a default resolution set will fail.
+     */
+    public void testCreateConfigurationFromArgs_include_dependent() throws Exception {
+        final String configName = "include-template-config";
+        final String targetName = "test-config";
+        final String failedTargetName = "template-include-config";
+        final String expError = String.format(
+                "Failed to parse config xml '%s'. Reason: " +
+                ConfigurationXmlParser.ConfigHandler.INNER_TEMPLATE_INCLUDE_ERROR,
+                configName, configName, failedTargetName);
+
+        try {
+            mFactory.createConfigurationFromArgs(new String[]{configName,
+                    "--template:map", "target", targetName});
+            fail ("ConfigurationException not thrown");
+        } catch (ConfigurationException e) {
+            // Make sure that we get the expected error message
+            assertEquals(expError, e.getMessage());
+        }
+    }
+
+    /**
      * Test loading a config that tries to include itself
      */
     public void testCreateConfigurationFromArgs_recursiveInclude() throws Exception {
@@ -264,6 +539,18 @@
     }
 
     /**
+    * Test loading a config that tries to include a non-bundled config
+    */
+   public void testCreateConfigurationFromArgs_nonBundledInclude() throws Exception {
+       try {
+           mFactory.createConfigurationFromArgs(new String[] {"non-bundled-config"});
+           fail("ConfigurationException not thrown");
+       } catch (ConfigurationException e) {
+           // expected
+       }
+   }
+
+    /**
      * Test loading a config that has a circular include
      */
     public void testCreateConfigurationFromArgs_circularInclude() throws Exception {
diff --git a/tests/src/com/android/tradefed/config/ConfigurationTest.java b/tests/src/com/android/tradefed/config/ConfigurationTest.java
index bd0f3c9..ad7c3f0 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationTest.java
@@ -32,6 +32,9 @@
 import junit.framework.TestCase;
 
 import org.easymock.EasyMock;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
@@ -352,4 +355,74 @@
                 usageString.contains("serial"));
 
     }
+
+    /**
+     * Basic test for {@link Configuration#getJsonCommandUsage()}.
+     */
+    public void testGetJsonCommandUsage() throws ConfigurationException, JSONException {
+        TestConfigObject testConfigObject = new TestConfigObject();
+        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
+
+        // General validation of usage elements
+        JSONArray usage = mConfig.getJsonCommandUsage();
+        JSONObject jsonConfigObject = null;
+        for (int i = 0; i < usage.length(); i++) {
+            JSONObject optionClass = usage.getJSONObject(i);
+
+            // Each element should contain 'name', 'class', and 'fields' values
+            assertTrue("Usage element does not contain a 'name' value", optionClass.has("name"));
+            assertTrue("Usage element does not contain a 'class' value", optionClass.has("class"));
+            assertTrue("Usage element does not contain a 'fields' value",
+                    optionClass.has("fields"));
+
+            // General validation of each field
+            JSONArray fields = optionClass.getJSONArray("fields");
+            for (int j = 0; j < fields.length(); j++) {
+                JSONObject field = fields.getJSONObject(j);
+
+                // Each field should at least have 'name', 'description', 'mandatory', and
+                // 'javaClass' values
+                assertTrue("Option field does not have a 'name' value", field.has("name"));
+                assertTrue("Option field does not have a 'description' value",
+                        field.has("description"));
+                assertTrue("Option field does not have a 'mandatory' value",
+                        field.has("mandatory"));
+                assertTrue("Option field does not have a 'javaClass' value",
+                        field.has("javaClass"));
+            }
+
+            // The only elements should either be built-in types, or the configuration object we
+            // added.
+            String name = optionClass.getString("name");
+            if (name.equals(CONFIG_OBJECT_TYPE_NAME)) {
+                // The object we added should only appear once
+                assertNull("Duplicate JSON usage element", jsonConfigObject);
+                jsonConfigObject = optionClass;
+            } else {
+                assertTrue(String.format("Unexpected JSON usage element: %s", name),
+                    Configuration.isBuiltInObjType(name));
+            }
+        }
+
+        // Verify that the configuration element we added has the expected values
+        assertNotNull("Missing JSON usage element", jsonConfigObject);
+        JSONArray fields = jsonConfigObject.getJSONArray("fields");
+        JSONObject jsonOptionField = null;
+        JSONObject jsonAltOptionField = null;
+        for (int i = 0; i < fields.length(); i++) {
+            JSONObject field = fields.getJSONObject(i);
+
+            if (OPTION_NAME.equals(field.getString("name"))) {
+                assertNull("Duplicate option field", jsonOptionField);
+                jsonOptionField = field;
+            } else if (ALT_OPTION_NAME.equals(field.getString("name"))) {
+                assertNull("Duplication option field", jsonAltOptionField);
+                jsonAltOptionField = field;
+            }
+        }
+        assertNotNull(jsonOptionField);
+        assertEquals(OPTION_DESCRIPTION, jsonOptionField.getString("description"));
+        assertNotNull(jsonAltOptionField);
+        assertEquals(OPTION_DESCRIPTION, jsonAltOptionField.getString("description"));
+    }
 }
diff --git a/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java b/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java
index 7614008..5ba3f9e 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java
@@ -49,7 +49,7 @@
             "</configuration>";
         final String configName = "config";
         ConfigurationDef configDef = new ConfigurationDef(configName);
-        xmlParser.parse(configDef, configName, getStringAsStream(normalConfig));
+        xmlParser.parse(configDef, configName, getStringAsStream(normalConfig), null);
         assertEquals(configName, configDef.getName());
         assertEquals("desc", configDef.getDescription());
         assertEquals("junit.framework.TestCase", configDef.getObjectClassMap().get("test").get(0));
@@ -69,7 +69,7 @@
             "</configuration>";
         final String configName = "config";
         ConfigurationDef configDef = new ConfigurationDef(configName);
-        xmlParser.parse(configDef, configName, getStringAsStream(normalConfig));
+        xmlParser.parse(configDef, configName, getStringAsStream(normalConfig), null);
         assertEquals(configName, configDef.getName());
         assertEquals("desc", configDef.getDescription());
         assertEquals("junit.framework.TestCase", configDef.getObjectClassMap().get("test").get(0));
@@ -93,7 +93,7 @@
             "</configuration>";
         final String configName = "config";
         ConfigurationDef configDef = new ConfigurationDef(configName);
-        xmlParser.parse(configDef, configName, getStringAsStream(normalConfig));
+        xmlParser.parse(configDef, configName, getStringAsStream(normalConfig), null);
 
         assertEquals(configName, configDef.getName());
         assertEquals("desc", configDef.getDescription());
@@ -114,7 +114,7 @@
         final String config =
             "<object name=\"foo\" />";
         try {
-            xmlParser.parse(new ConfigurationDef("foo"), "foo", getStringAsStream(config));
+            xmlParser.parse(new ConfigurationDef("foo"), "foo", getStringAsStream(config), null);
             fail("ConfigurationException not thrown");
         } catch (ConfigurationException e) {
             // expected
@@ -128,7 +128,7 @@
         final String config =
             "<option name=\"foo\" />";
         try {
-            xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config));
+            xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config), null);
             fail("ConfigurationException not thrown");
         } catch (ConfigurationException e) {
             // expected
@@ -142,7 +142,7 @@
         final String config =
             "<object type=\"foo\" class=\"junit.framework.TestCase\" />";
         ConfigurationDef configDef = new ConfigurationDef("name");
-        xmlParser.parse(configDef, "name", getStringAsStream(config));
+        xmlParser.parse(configDef, "name", getStringAsStream(config), null);
         assertEquals("junit.framework.TestCase", configDef.getObjectClassMap().get("foo").get(0));
     }
 
@@ -151,11 +151,11 @@
      */
     public void testParse_include() throws ConfigurationException {
         String includedName = "includeme";
-        ConfigurationDef configDef = new ConfigurationDef("name");
-        mMockLoader.loadIncludedConfiguration(EasyMock.eq(configDef), EasyMock.eq(includedName));
+        ConfigurationDef configDef = new ConfigurationDef("foo");
+        mMockLoader.loadIncludedConfiguration(EasyMock.eq(configDef), EasyMock.eq("foo"), EasyMock.eq(includedName));
         EasyMock.replay(mMockLoader);
         final String config = "<include name=\"includeme\" />";
-        xmlParser.parse(configDef, "name", getStringAsStream(config));
+        xmlParser.parse(configDef, "foo", getStringAsStream(config), null);
     }
 
     /**
@@ -165,12 +165,12 @@
         String includedName = "non-existent";
         ConfigurationDef parent = new ConfigurationDef("name");
         ConfigurationException exception = new ConfigurationException("I don't exist");
-        mMockLoader.loadIncludedConfiguration(parent, includedName);
+        mMockLoader.loadIncludedConfiguration(parent, "name", includedName);
         EasyMock.expectLastCall().andThrow(exception);
         EasyMock.replay(mMockLoader);
         final String config = String.format("<include name=\"%s\" />", includedName);
         try {
-            xmlParser.parse(parent, "name", getStringAsStream(config));
+            xmlParser.parse(parent, "name", getStringAsStream(config), null);
             fail("ConfigurationException not thrown");
         } catch (ConfigurationException e) {
             // expected
@@ -183,7 +183,7 @@
     public void testParse_badTag() throws ConfigurationException {
         final String config = "<blah name=\"foo\" />";
         try {
-            xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config));
+            xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config), null);
             fail("ConfigurationException not thrown");
         } catch (ConfigurationException e) {
             // expected
@@ -196,7 +196,7 @@
     public void testParse_xml() throws ConfigurationException {
         final String config = "blah";
         try {
-            xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config));
+            xmlParser.parse(new ConfigurationDef("name"), "name", getStringAsStream(config), null);
             fail("ConfigurationException not thrown");
         } catch (ConfigurationException e) {
             // expected
diff --git a/tests/src/com/android/tradefed/config/OptionSetterTest.java b/tests/src/com/android/tradefed/config/OptionSetterTest.java
index 623ed61..e9edcf9 100644
--- a/tests/src/com/android/tradefed/config/OptionSetterTest.java
+++ b/tests/src/com/android/tradefed/config/OptionSetterTest.java
@@ -17,6 +17,8 @@
 package com.android.tradefed.config;
 
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.TimeVal;
 
 import junit.framework.TestCase;
 
@@ -110,6 +112,9 @@
         @Option(name = "string_string_map")
         private Map<String, String> mStringMap = new HashMap<String, String>();
 
+        @Option(name = "string_string_multimap")
+        private MultiMap<String, String> mStringMultiMap = new MultiMap<String, String>();
+
         @Option(name = "string")
         private String mString = null;
 
@@ -143,6 +148,15 @@
         @Option(name = "longObj")
         private Long mLongObj = null;
 
+        @Option(name = "timeValLong", isTimeVal = true)
+        private long mTimeValLong = 0;
+
+        @Option(name = "timeValLongObj", isTimeVal = true)
+        private Long mTimeValLongObj = null;
+
+        @Option(name = "timeVal")
+        private TimeVal mTimeVal = null;
+
         @Option(name = "float")
         private float mFloat = 0;
 
@@ -511,6 +525,29 @@
     }
 
     /**
+     * Test {@link OptionSetter#setOptionValue(String, String)} for a MultiMap.
+     */
+    public void testSetOptionValue_multimap() throws ConfigurationException, IOException {
+        AllTypesOptionSource optionSource = new AllTypesOptionSource();
+        final String expectedKey = "stringkey";
+        final String expectedValue1 = "stringvalue1";
+        final String expectedValue2 = "stringvalue2";
+
+        // Actually set the key/value pair
+        OptionSetter parser = new OptionSetter(optionSource);
+        parser.setOptionMapValue("string_string_multimap", expectedKey, expectedValue1);
+        parser.setOptionMapValue("string_string_multimap", expectedKey, expectedValue2);
+
+        assertEquals(1, optionSource.mStringMultiMap.size());
+        assertNotNull(optionSource.mStringMultiMap.get(expectedKey));
+
+        Collection<String> values = optionSource.mStringMultiMap.get(expectedKey);
+        assertEquals(2, values.size());
+        assertTrue(values.contains(expectedValue1));
+        assertTrue(values.contains(expectedValue2));
+    }
+
+    /**
      * Test {@link OptionSetter#setOptionValue(String, String)} for a boolean.
      */
     public void testSetOptionValue_boolean() throws ConfigurationException {
@@ -642,6 +679,41 @@
     }
 
     /**
+     * Test {@link OptionSetter#setOptionValue(String, String)} for a long that represents a time
+     * value.
+     */
+    public void testSetOptionValue_timeValLong() throws ConfigurationException {
+        AllTypesOptionSource optionSource = new AllTypesOptionSource();
+        assertSetOptionValue(optionSource, "timeValLong", "2H 45s");
+        assertTrue(1000 * (45 + 60 * 60 * 2) == optionSource.mTimeValLong);
+        assertSetOptionValue(optionSource, "timeValLong", "12345");
+        assertTrue(12345 == optionSource.mTimeValLong);
+    }
+
+    /**
+     * Test {@link OptionSetter#setOptionValue(String, String)} for a Long that represents a time
+     * value.
+     */
+    public void testSetOptionValue_timeValLongObj() throws ConfigurationException {
+        AllTypesOptionSource optionSource = new AllTypesOptionSource();
+        assertSetOptionValue(optionSource, "timeValLongObj", "2H 45s");
+        assertTrue(1000 * (45 + 60 * 60 * 2) == optionSource.mTimeValLongObj);
+        assertSetOptionValue(optionSource, "timeValLongObj", "12345");
+        assertTrue(12345 == optionSource.mTimeValLongObj);
+    }
+
+    /**
+     * Test {@link OptionSetter#setOptionValue(String, String)} for a TimeVal.
+     */
+    public void testSetOptionValue_timeVal() throws ConfigurationException {
+        AllTypesOptionSource optionSource = new AllTypesOptionSource();
+        assertSetOptionValue(optionSource, "timeVal", "2H 45s");
+        assertTrue(1000 * (45 + 60 * 60 * 2) == optionSource.mTimeVal.asLong());
+        assertSetOptionValue(optionSource, "timeVal", "12345");
+        assertTrue(12345 == optionSource.mTimeVal.asLong());
+    }
+
+    /**
      * Test {@link OptionSetter#setOptionValue(String, String)} for a float.
      */
     public void testSetOptionValue_float() throws ConfigurationException {
diff --git a/tests/src/com/android/tradefed/device/DeviceManagerTest.java b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
index 47b01d1..0c4dfc4 100644
--- a/tests/src/com/android/tradefed/device/DeviceManagerTest.java
+++ b/tests/src/com/android/tradefed/device/DeviceManagerTest.java
@@ -36,7 +36,6 @@
 
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.Collection;
 import java.util.List;
 
 /**
@@ -214,6 +213,10 @@
             }
 
             @Override
+            void startDeviceRecoverer() {
+            }
+
+            @Override
             IDeviceStateMonitor createStateMonitor(IDevice device) {
                 return mMockStateMonitor;
             }
@@ -305,6 +308,7 @@
                 .andReturn(new DeviceEventResponse(DeviceAllocationState.Available, true));
         EasyMock.expect(mMockTestDevice.handleAllocationEvent(DeviceEvent.ALLOCATE_REQUEST))
                 .andReturn(new DeviceEventResponse(DeviceAllocationState.Allocated, true));
+        mMockTestDevice.stopEmulatorOutput();
         replayMocks();
         DeviceManager manager = createDeviceManagerNoInit();
         manager.setMaxEmulators(1);
@@ -552,27 +556,6 @@
     // TODO: add test for fastboot state changes
 
     /**
-     * Verify the 'fastboot devices' output parsing
-     */
-    public void testParseDevicesOnFastboot() {
-        Collection<String> deviceSerials = DeviceManager.parseDevicesOnFastboot(
-                "04035EEB0B01F01C        fastboot\n" +
-                "HT99PP800024    fastboot\n" +
-                "????????????    fastboot");
-        assertEquals(2, deviceSerials.size());
-        assertTrue(deviceSerials.contains("04035EEB0B01F01C"));
-        assertTrue(deviceSerials.contains("HT99PP800024"));
-    }
-
-    /**
-     * Verify the 'fastboot devices' output parsing when empty
-     */
-    public void testParseDevicesOnFastboot_empty() {
-        Collection<String> deviceSerials = DeviceManager.parseDevicesOnFastboot("");
-        assertEquals(0, deviceSerials.size());
-    }
-
-    /**
      * Test normal success case for {@link DeviceManager#connectToTcpDevice(String)}
      */
     public void testConnectToTcpDevice() throws Exception {
diff --git a/tests/src/com/android/tradefed/device/FastbootHelperTest.java b/tests/src/com/android/tradefed/device/FastbootHelperTest.java
new file mode 100644
index 0000000..f61bad5
--- /dev/null
+++ b/tests/src/com/android/tradefed/device/FastbootHelperTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014 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.tradefed.util.IRunUtil;
+
+import junit.framework.TestCase;
+
+import org.easymock.EasyMock;
+
+import java.util.Collection;
+
+/**
+ * Unit tests for {@link FastBootHelper}.
+ */
+public class FastbootHelperTest extends TestCase {
+
+    private IRunUtil mMockRunUtil;
+    private FastbootHelper mFastbootHelper;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mMockRunUtil = EasyMock.createMock(IRunUtil.class);
+        mFastbootHelper = new FastbootHelper(mMockRunUtil);
+    }
+
+    /**
+     * Verify the 'fastboot devices' output parsing
+     */
+    public void testParseDevicesOnFastboot() {
+        Collection<String> deviceSerials = mFastbootHelper.parseDevices(
+                "04035EEB0B01F01C        fastboot\n" +
+                "HT99PP800024    fastboot\n" +
+                "????????????    fastboot");
+        assertEquals(2, deviceSerials.size());
+        assertTrue(deviceSerials.contains("04035EEB0B01F01C"));
+        assertTrue(deviceSerials.contains("HT99PP800024"));
+    }
+
+    /**
+     * Verify the 'fastboot devices' output parsing when empty
+     */
+    public void testParseDevicesOnFastboot_empty() {
+        Collection<String> deviceSerials = mFastbootHelper.parseDevices("");
+        assertEquals(0, deviceSerials.size());
+    }
+
+}
diff --git a/tests/src/com/android/tradefed/device/StubTestDevice.java b/tests/src/com/android/tradefed/device/StubTestDevice.java
index 28838d7..76f9d2c 100644
--- a/tests/src/com/android/tradefed/device/StubTestDevice.java
+++ b/tests/src/com/android/tradefed/device/StubTestDevice.java
@@ -21,14 +21,12 @@
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
 import com.android.ddmlib.testrunner.ITestRunListener;
 import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.IWifiHelper;
-import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.targetprep.TargetSetupError;
 import com.android.tradefed.util.CommandResult;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
@@ -77,6 +75,13 @@
     }
 
     @Override
+    public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
+            Collection<ITestRunListener> listeners) throws DeviceNotAvailableException {
+        // ignore
+        return true;
+    }
+
+    @Override
     public boolean runInstrumentationTests(IRemoteAndroidTestRunner runner,
             ITestRunListener... listeners) throws DeviceNotAvailableException {
         // ignore
@@ -84,6 +89,13 @@
     }
 
     @Override
+    public boolean runInstrumentationTestsAsUser(IRemoteAndroidTestRunner runner, int userId,
+            ITestRunListener... listeners) throws DeviceNotAvailableException {
+        // ignore
+        return true;
+    }
+
+    @Override
     public boolean pullFile(String remoteFilePath, File localFile)
             throws DeviceNotAvailableException {
         return false;
@@ -313,8 +325,8 @@
      */
     @Override
     public boolean waitForBootComplete(long timeOut) throws DeviceNotAvailableException {
-      // ignore
-      return false;
+        // ignore
+        return false;
     }
 
     /**
@@ -412,6 +424,11 @@
     }
 
     @Override
+    public void clearLastConnectedWifiNetwork() {
+        // ignore
+    }
+
+    @Override
     public boolean connectToWifiNetwork(String wifiSsid, String wifiPsk)
             throws DeviceNotAvailableException {
         // ignore
@@ -451,6 +468,7 @@
         // ignore
         return false;
     }
+
     /**
      * {@inheritDoc}
      */
@@ -464,6 +482,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public boolean checkConnectivity() throws DeviceNotAvailableException {
         return false;
     }
@@ -500,6 +519,16 @@
      * {@inheritDoc}
      */
     @Override
+    public String installPackageForUser(File packageFile, boolean reinstall, int userId,
+            String... extraArgs) throws DeviceNotAvailableException {
+        // ignore
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public String uninstallPackage(String packageName) throws DeviceNotAvailableException {
         // ignore
         return null;
@@ -743,6 +772,7 @@
      * {@inheritDoc}
      */
     @Override
+    @Deprecated
     public String getPropertySync(String name) throws DeviceNotAvailableException {
         // ignore
         return null;
@@ -860,4 +890,102 @@
     @Override
     public void setDate(Date date) throws DeviceNotAvailableException {
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isMultiUserSupported() throws DeviceNotAvailableException {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int createUser(String name) throws DeviceNotAvailableException {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean removeUser(int userId) throws DeviceNotAvailableException {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean startUser(int userId) throws DeviceNotAvailableException {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void stopUser(int userId) throws DeviceNotAvailableException {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public InputStreamSource getEmulatorOutput() {
+        return new ByteArrayInputStreamSource(new byte[0]);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void stopEmulatorOutput() {
+        // ignore
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
+            String... extraArgs) throws DeviceNotAvailableException {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String installPackageForUser(File packageFile, boolean reinstall,
+            boolean grantPermissions, int userId, String... extraArgs)
+            throws DeviceNotAvailableException {
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void remountSystemWritable() {
+        // no-op
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
index 781ab23..a579c2d 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
@@ -18,6 +18,7 @@
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.Log;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.TestAppConstants;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.CollectingTestListener;
@@ -698,6 +699,6 @@
                 TestAppConstants.UITESTAPP_PACKAGE, getDevice().getIDevice());
         CollectingTestListener uilistener = new CollectingTestListener();
         getDevice().runInstrumentationTests(uirunner, uilistener);
-        return TestAppConstants.UI_TOTAL_TESTS == uilistener.getNumPassedTests();
+        return TestAppConstants.UI_TOTAL_TESTS == uilistener.getNumTestsInState(TestStatus.PASSED);
     }
 }
diff --git a/tests/src/com/android/tradefed/device/TestDeviceStressTest.java b/tests/src/com/android/tradefed/device/TestDeviceStressTest.java
index 2231635..f54035b 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceStressTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceStressTest.java
@@ -18,6 +18,7 @@
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.Log;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.TestAppConstants;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.result.CollectingTestListener;
@@ -143,7 +144,7 @@
                 TestAppConstants.UITESTAPP_PACKAGE, getDevice().getIDevice());
         CollectingTestListener uilistener = new CollectingTestListener();
         getDevice().runInstrumentationTests(uirunner, uilistener);
-        return TestAppConstants.UI_TOTAL_TESTS == uilistener.getNumPassedTests();
+        return TestAppConstants.UI_TOTAL_TESTS == uilistener.getNumTestsInState(TestStatus.PASSED);
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index 524874f..7ca4e32 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -22,6 +22,7 @@
 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.device.ITestDevice.MountPointInfo;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -36,6 +37,7 @@
 
 import org.easymock.EasyMock;
 import org.easymock.IAnswer;
+import org.easymock.IExpectationSetters;
 
 import java.io.File;
 import java.io.IOException;
@@ -44,6 +46,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -51,6 +54,7 @@
  */
 public class TestDeviceTest extends TestCase {
 
+    private static final String MOCK_DEVICE_SERIAL = "serial";
     private IDevice mMockIDevice;
     private IShellOutputReceiver mMockReceiver;
     private TestDevice mTestDevice;
@@ -92,7 +96,7 @@
     protected void setUp() throws Exception {
         super.setUp();
         mMockIDevice = EasyMock.createMock(IDevice.class);
-        EasyMock.expect(mMockIDevice.getSerialNumber()).andReturn("serial").anyTimes();
+        EasyMock.expect(mMockIDevice.getSerialNumber()).andReturn(MOCK_DEVICE_SERIAL).anyTimes();
         mMockReceiver = EasyMock.createMock(IShellOutputReceiver.class);
         mMockRecovery = EasyMock.createMock(IDeviceRecovery.class);
         mMockStateMonitor = EasyMock.createMock(IDeviceStateMonitor.class);
@@ -157,10 +161,7 @@
         CommandResult adbResult = new CommandResult();
         adbResult.setStatus(CommandStatus.SUCCESS);
         adbResult.setStdout("restarting adbd as root");
-        EasyMock.expect(
-                mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("adb"),
-                        EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("root"))).andReturn(
-                adbResult);
+        setExecuteAdbCommandExpectations(adbResult, "root");
         EasyMock.expect(mMockStateMonitor.waitForDeviceNotAvailable(EasyMock.anyLong())).andReturn(
                 Boolean.TRUE);
         EasyMock.expect(mMockStateMonitor.waitForDeviceOnline()).andReturn(
@@ -168,6 +169,19 @@
     }
 
     /**
+     * COnfigure EasMock expectations for a successful adb command call
+     * @param command the adb command to execute
+     * @param result the {@link CommandResult} expected from the adb command execution
+     * @throws Exception
+     */
+    private void setExecuteAdbCommandExpectations(CommandResult result, String command)
+            throws Exception {
+        EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(),
+                EasyMock.eq("adb"), EasyMock.eq("-s"), EasyMock.eq(MOCK_DEVICE_SERIAL),
+                EasyMock.eq(command))).andReturn(result);
+    }
+
+    /**
      * Test that {@link TestDevice#enableAdbRoot()} reattempts adb root
      */
     public void testEnableAdbRoot_rootRetry() throws Exception {
@@ -176,16 +190,10 @@
         injectShellResponse("id", "uid=0(root) gid=0(root)");
         CommandResult adbBadResult = new CommandResult(CommandStatus.SUCCESS);
         adbBadResult.setStdout("");
-        EasyMock.expect(
-                mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("adb"),
-                        EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("root"))).andReturn(
-                adbBadResult);
+        setExecuteAdbCommandExpectations(adbBadResult, "root");
         CommandResult adbResult = new CommandResult(CommandStatus.SUCCESS);
         adbResult.setStdout("restarting adbd as root");
-        EasyMock.expect(
-                mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("adb"),
-                        EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("root"))).andReturn(
-                adbResult);
+        setExecuteAdbCommandExpectations(adbResult, "root");
         EasyMock.expect(mMockStateMonitor.waitForDeviceNotAvailable(EasyMock.anyLong())).andReturn(
                 Boolean.TRUE).times(2);
         EasyMock.expect(mMockStateMonitor.waitForDeviceOnline()).andReturn(
@@ -289,9 +297,7 @@
     public void testGetProductType_adb() throws Exception {
         EasyMock.expect(mMockIDevice.getProperty("ro.hardware")).andReturn(null);
         final String expectedOutput = "nexusone";
-        SettableFuture<String> f = SettableFuture.create();
-        f.set(expectedOutput);
-        EasyMock.expect(mMockIDevice.getSystemProperty("ro.hardware")).andReturn(f);
+        injectSystemProperty("ro.hardware", expectedOutput);
         EasyMock.replay(mMockIDevice);
         assertEquals(expectedOutput, mTestDevice.getProductType());
     }
@@ -302,11 +308,7 @@
      */
     public void testGetProductType_adbFail() throws Exception {
         EasyMock.expect(mMockIDevice.getProperty(EasyMock.<String>anyObject())).andStubReturn(null);
-        SettableFuture<String> f = SettableFuture.create();
-        f.set(null);
-        EasyMock.expect(mMockIDevice.getSystemProperty("ro.hardware"))
-                .andReturn(f)
-                .times(3);
+        injectSystemProperty("ro.hardware", null).times(3);
         EasyMock.replay(mMockIDevice);
         try {
             mTestDevice.getProductType();
@@ -544,6 +546,18 @@
     /**
      * Unit test for {@link TestDevice#getExternalStoreFreeSpace()}.
      * <p/>
+     * Verify that the coreutils-like output of 'adb shell df' command is parsed correctly.
+     */
+    public void testGetExternalStoreFreeSpace_toybox() throws Exception {
+        final String dfOutput =
+            "Filesystem      1K-blocks	Used  Available Use% Mounted on\n" +
+            "/dev/fuse        11585536    1316348   10269188  12% /mnt/sdcard";
+        assertGetExternalStoreFreeSpace(dfOutput, 10269188);
+    }
+
+    /**
+     * Unit test for {@link TestDevice#getExternalStoreFreeSpace()}.
+     * <p/>
      * Verify behavior when 'df' command returns unexpected content
      */
     public void testGetExternalStoreFreeSpace_badOutput() throws Exception {
@@ -705,7 +719,7 @@
             }
         };
         EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
-                EasyMock.eq("-s"),EasyMock.eq("serial"), EasyMock.eq("foo"))).andAnswer(
+                EasyMock.eq("-s"),EasyMock.eq(MOCK_DEVICE_SERIAL), EasyMock.eq("foo"))).andAnswer(
                         blockResult);
 
         // expect
@@ -751,15 +765,16 @@
     public void testExecuteFastbootCommand_recovery() throws UnsupportedOperationException,
            DeviceNotAvailableException {
         CommandResult result = new CommandResult(CommandStatus.EXCEPTION);
-        EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
-                EasyMock.eq("-s"),EasyMock.eq("serial"), EasyMock.eq("foo"))).andReturn(result);
+        EasyMock.expect(mMockRunUtil.runTimedCmd(
+                EasyMock.anyLong(), EasyMock.eq("fastboot"), EasyMock.eq("-s"),
+                EasyMock.eq(MOCK_DEVICE_SERIAL), EasyMock.eq("foo"))).andReturn(result);
         mMockRecovery.recoverDeviceBootloader((IDeviceStateMonitor)EasyMock.anyObject());
         CommandResult successResult = new CommandResult(CommandStatus.SUCCESS);
         successResult.setStderr("");
         successResult.setStdout("");
         // now expect a successful retry
         EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
-                EasyMock.eq("-s"),EasyMock.eq("serial"), EasyMock.eq("foo"))).andReturn(
+                EasyMock.eq("-s"),EasyMock.eq(MOCK_DEVICE_SERIAL), EasyMock.eq("foo"))).andReturn(
                         successResult);
         replayMocks();
         mTestDevice.executeFastbootCommand("foo");
@@ -814,9 +829,7 @@
      * Simple test for {@link TestDevice#switchToAdbUsb()}
      */
     public void testSwitchToAdbUsb() throws Exception  {
-        EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("adb"),
-                EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("usb"))).andReturn(
-                        new CommandResult(CommandStatus.SUCCESS));
+        setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "usb");
         replayMocks();
         mTestDevice.switchToAdbUsb();
         verifyMocks();
@@ -870,6 +883,259 @@
     }
 
     /**
+     * Test that isRuntimePermissionSupported returns correct result for device reporting LRX22F
+     * build attributes
+     * @throws Exception
+     */
+    public void testRuntimePermissionSupportedLmpRelease() throws Exception {
+        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "REL");
+        injectSystemProperty(TestDevice.BUILD_ID_PROP, "1642709");
+        replayMocks();
+        assertFalse(mTestDevice.isRuntimePermissionSupported());
+    }
+
+    /**
+     * Test that isRuntimePermissionSupported returns correct result for device reporting LMP MR1
+     * dev build attributes
+     * @throws Exception
+     */
+    public void testRuntimePermissionSupportedLmpMr1Dev() throws Exception {
+        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "REL");
+        injectSystemProperty(TestDevice.BUILD_ID_PROP, "1844090");
+        replayMocks();
+        assertFalse(mTestDevice.isRuntimePermissionSupported());
+    }
+
+    /**
+     * Test that isRuntimePermissionSupported returns correct result for device reporting random
+     * dev build attributes
+     * @throws Exception
+     */
+    public void testRuntimePermissionSupportedRandom1() throws Exception {
+        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "YADDA");
+        injectSystemProperty(TestDevice.BUILD_ID_PROP, "XYZ");
+        replayMocks();
+        assertFalse(mTestDevice.isRuntimePermissionSupported());
+    }
+
+    /**
+     * Test that isRuntimePermissionSupported returns correct result for device reporting random
+     * mnc dev build attributes
+     * @throws Exception
+     */
+    public void testRuntimePermissionSupportedMncLocal() throws Exception {
+        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "MNC");
+        injectSystemProperty(TestDevice.BUILD_ID_PROP, "eng.foo.20150414.190304");
+        replayMocks();
+        assertTrue(mTestDevice.isRuntimePermissionSupported());
+    }
+
+    /**
+     * Test that isRuntimePermissionSupported returns correct result for device reporting random
+     * dev build attributes
+     * @throws Exception
+     */
+    public void testRuntimePermissionSupportedNonMncLocal() throws Exception {
+        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "LMP");
+        injectSystemProperty(TestDevice.BUILD_ID_PROP, "eng.foo.20150414.190304");
+        replayMocks();
+        assertFalse(mTestDevice.isRuntimePermissionSupported());
+    }
+
+    /**
+     * Test that isRuntimePermissionSupported returns correct result for device reporting early MNC
+     * dev build attributes
+     * @throws Exception
+     */
+    public void testRuntimePermissionSupportedEarlyMnc() throws Exception {
+        setMockIDeviceRuntimePermissionNotSupported();
+        replayMocks();
+        assertFalse(mTestDevice.isRuntimePermissionSupported());
+    }
+
+    /**
+     * Test that isRuntimePermissionSupported returns correct result for device reporting early MNC
+     * dev build attributes
+     * @throws Exception
+     */
+    public void testRuntimePermissionSupportedMncPostSwitch() throws Exception {
+        setMockIDeviceRuntimePermissionSupported();
+        replayMocks();
+        assertTrue(mTestDevice.isRuntimePermissionSupported());
+    }
+
+    /**
+     * Convenience method for setting up mMockIDevice to not support runtime permission
+     */
+    private void setMockIDeviceRuntimePermissionNotSupported() {
+        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "MNC");
+        injectSystemProperty(TestDevice.BUILD_ID_PROP, "1816412");
+    }
+
+    /**
+     * Convenience method for setting up mMockIDevice to support runtime permission
+     */
+    private void setMockIDeviceRuntimePermissionSupported() {
+        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "MNC");
+        injectSystemProperty(TestDevice.BUILD_ID_PROP, "1844452");
+    }
+
+    /**
+     * Test default installPackage on device not supporting runtime permission has expected
+     * list of args
+     * @throws Exception
+     */
+    public void testInstallPackage_default_runtimePermissionNotSupported() throws Exception {
+        final String apkFile = "foo.apk";
+        setMockIDeviceRuntimePermissionNotSupported();
+        EasyMock.expect(mMockIDevice.installPackage(
+                EasyMock.contains(apkFile), EasyMock.eq(true))).andReturn(null);
+        replayMocks();
+        assertNull(mTestDevice.installPackage(new File(apkFile), true));
+    }
+
+    /**
+     * Test default installPackage on device supporting runtime permission has expected list of args
+     * @throws Exception
+     */
+    public void testInstallPackage_default_runtimePermissionSupported() throws Exception {
+        final String apkFile = "foo.apk";
+        setMockIDeviceRuntimePermissionSupported();
+        EasyMock.expect(mMockIDevice.installPackage(
+                EasyMock.contains(apkFile), EasyMock.eq(true), EasyMock.eq("-g"))).andReturn(null);
+        replayMocks();
+        assertNull(mTestDevice.installPackage(new File(apkFile), true));
+    }
+
+    /**
+     * Test default installPackageForUser on device not supporting runtime permission has expected
+     * list of args
+     * @throws Exception
+     */
+    public void testinstallPackageForUser_default_runtimePermissionNotSupported() throws Exception {
+        final String apkFile = "foo.apk";
+        int uid = 123;
+        setMockIDeviceRuntimePermissionNotSupported();
+        EasyMock.expect(mMockIDevice.installPackage(
+                EasyMock.contains(apkFile), EasyMock.eq(true),
+                EasyMock.eq("--user"), EasyMock.eq(Integer.toString(uid)))).andReturn(null);
+        replayMocks();
+        assertNull(mTestDevice.installPackageForUser(new File(apkFile), true, uid));
+    }
+
+    /**
+     * Test default installPackageForUser on device supporting runtime permission has expected
+     * list of args
+     * @throws Exception
+     */
+    public void testinstallPackageForUser_default_runtimePermissionSupported() throws Exception {
+        final String apkFile = "foo.apk";
+        int uid = 123;
+        setMockIDeviceRuntimePermissionSupported();
+        EasyMock.expect(mMockIDevice.installPackage(
+                EasyMock.contains(apkFile), EasyMock.eq(true), EasyMock.eq("-g"),
+                EasyMock.eq("--user"), EasyMock.eq(Integer.toString(uid)))).andReturn(null);
+        replayMocks();
+        assertNull(mTestDevice.installPackageForUser(new File(apkFile), true, uid));
+    }
+
+    /**
+     * Test runtime permission variant of installPackage throws exception on unsupported device
+     * platform
+     * @throws Exception
+     */
+    public void testInstallPackage_throw() throws Exception {
+        final String apkFile = "foo.apk";
+        setMockIDeviceRuntimePermissionNotSupported();
+        replayMocks();
+        try {
+            mTestDevice.installPackage(new File(apkFile), true, true);
+        } catch (UnsupportedOperationException uoe) {
+            // ignore, exception thrown here is expected
+            return;
+        }
+        fail("installPackage did not throw IllegalArgumentException");
+    }
+
+    /**
+     * Test runtime permission variant of installPackage has expected list of args on a supported
+     * device when granting
+     * @throws Exception
+     */
+    public void testInstallPackage_grant_runtimePermissionSupported() throws Exception {
+        final String apkFile = "foo.apk";
+        setMockIDeviceRuntimePermissionSupported();
+        EasyMock.expect(mMockIDevice.installPackage(
+                EasyMock.contains(apkFile), EasyMock.eq(true), EasyMock.eq("-g"))).andReturn(null);
+        replayMocks();
+        assertNull(mTestDevice.installPackage(new File(apkFile), true, true));
+    }
+
+    /**
+     * Test runtime permission variant of installPackage has expected list of args on a supported
+     * device when not granting
+     * @throws Exception
+     */
+    public void testInstallPackage_noGrant_runtimePermissionSupported() throws Exception {
+        final String apkFile = "foo.apk";
+        setMockIDeviceRuntimePermissionSupported();
+        EasyMock.expect(mMockIDevice.installPackage(
+                EasyMock.contains(apkFile), EasyMock.eq(true))).andReturn(null);
+        replayMocks();
+        assertNull(mTestDevice.installPackage(new File(apkFile), true, false));
+    }
+
+    /**
+     * Test grant permission variant of installPackageForUser throws exception on unsupported
+     * device platform
+     * @throws Exception
+     */
+    public void testInstallPackageForUser_throw() throws Exception {
+        final String apkFile = "foo.apk";
+        setMockIDeviceRuntimePermissionNotSupported();
+        replayMocks();
+        try {
+            mTestDevice.installPackageForUser(new File(apkFile), true, true, 123);
+        } catch (UnsupportedOperationException uoe) {
+            // ignore, exception thrown here is expected
+            return;
+        }
+        fail("installPackage did not throw IllegalArgumentException");
+    }
+
+    /**
+     * Test grant permission variant of installPackageForUser has expected list of args on a
+     * supported device when granting
+     * @throws Exception
+     */
+    public void testInstallPackageForUser_grant_runtimePermissionSupported() throws Exception {
+        final String apkFile = "foo.apk";
+        int uid = 123;
+        setMockIDeviceRuntimePermissionSupported();
+        EasyMock.expect(mMockIDevice.installPackage(
+                EasyMock.contains(apkFile), EasyMock.eq(true), EasyMock.eq("-g"),
+                EasyMock.eq("--user"), EasyMock.eq(Integer.toString(uid)))).andReturn(null);
+        replayMocks();
+        assertNull(mTestDevice.installPackageForUser(new File(apkFile), true, true, uid));
+    }
+
+    /**
+     * Test grant permission variant of installPackageForUser has expected list of args on a
+     * supported device when not granting
+     * @throws Exception
+     */
+    public void testInstallPackageForUser_noGrant_runtimePermissionSupported() throws Exception {
+        final String apkFile = "foo.apk";
+        int uid = 123;
+        setMockIDeviceRuntimePermissionSupported();
+        EasyMock.expect(mMockIDevice.installPackage(
+                EasyMock.contains(apkFile), EasyMock.eq(true),
+                EasyMock.eq("--user"), EasyMock.eq(Integer.toString(uid)))).andReturn(null);
+        replayMocks();
+        assertNull(mTestDevice.installPackageForUser(new File(apkFile), true, false, uid));
+    }
+
+    /**
      * Helper method to build a response to a executeShellCommand call
      *
      * @param expectedCommand the shell command to expect or null to skip verification of command
@@ -887,7 +1153,6 @@
      * @param response the response to simulate
      * @param asStub whether to set a single expectation or a stub expectation
      */
-    @SuppressWarnings("unchecked")
     private void injectShellResponse(final String expectedCommand, final String response,
             boolean asStub) throws Exception {
         IAnswer<Object> shellAnswer = new IAnswer<Object>() {
@@ -918,15 +1183,37 @@
     }
 
     /**
-     * Test normal success case for {@link TestDevice#reboot()}
+     * Helper method to inject a response to {@link TestDevice#getProperty(String)} calls
+     * @param property property name
+     * @param value property value
+     * @return preset {@link IExpectationSetters} returned by {@link EasyMock} where further
+     * expectations can be added
      */
-    public void testReboot() throws Exception {
+    private IExpectationSetters<Future<String>> injectSystemProperty(
+            final String property, final String value) {
+        SettableFuture<String> valueResponse = SettableFuture.create();
+        valueResponse.set(value);
+        return EasyMock.expect(mMockIDevice.getSystemProperty(property)).andReturn(valueResponse);
+    }
+
+    /**
+     * Helper method to build response to a reboot call
+     * @throws Exception
+     */
+    private void setRebootExpectations() throws Exception {
         EasyMock.expect(mMockStateMonitor.waitForDeviceOnline()).andReturn(
                 mMockIDevice);
         setEnableAdbRootExpectations();
         setEncryptedUnsupportedExpectations();
         EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable(EasyMock.anyLong())).andReturn(
                 mMockIDevice);
+    }
+
+    /**
+     * Test normal success case for {@link TestDevice#reboot()}
+     */
+    public void testReboot() throws Exception {
+        setRebootExpectations();
         replayMocks();
         mTestDevice.reboot();
         verifyMocks();
@@ -1143,7 +1430,7 @@
      * Simple test for {@link TestDevice#handleAllocationEvent(DeviceEvent)}
      */
     public void testHandleAllocationEvent() {
-        EasyMock.expect(mMockIDevice.getSerialNumber()).andStubReturn("serial");
+        EasyMock.expect(mMockIDevice.getSerialNumber()).andStubReturn(MOCK_DEVICE_SERIAL);
         EasyMock.replay(mMockIDevice);
 
         assertEquals(DeviceAllocationState.Unknown, mTestDevice.getAllocationState());
@@ -1169,5 +1456,283 @@
         assertNotNull(mTestDevice.handleAllocationEvent(DeviceEvent.FREE_UNKNOWN));
         assertEquals(DeviceAllocationState.Unknown, mTestDevice.getAllocationState());
     }
+
+    public void testGetPingLoss() throws Exception {
+        final String pingCommand = "ping -c 1 -w 5 -s 1024 www.google.com";
+        injectShellResponse(pingCommand, ArrayUtil.join("\r\n",
+                "PING www.google.com (0.0.0.0) 1024(1052) bytes of data.",
+                "1032 bytes from www.google.com (0.0.0.0):" +
+                        "icmp_seq=1 ttl=52 time=74.1 ms",
+                "",
+                "--- www.google.com ping statistics ---",
+                "2 packets transmitted, 1 received, 50% packet loss, time 0ms"
+                ));
+        replayMocks();
+        assertEquals(50, mTestDevice.getPingLoss());
+    }
+
+    public void testCheckConnectivity() throws Exception {
+        final String pingCommand = "ping -c 1 -w 5 -s 1024 www.google.com";
+        injectShellResponse(pingCommand, ArrayUtil.join("\r\n",
+                "PING www.google.com (0.0.0.0) 1024(1052) bytes of data.",
+                "1032 bytes from www.google.com (0.0.0.0):" +
+                        "icmp_seq=1 ttl=52 time=74.1 ms",
+                "",
+                "--- www.google.com ping statistics ---",
+                "1 packets transmitted, 1 received, 0% packet loss, time 0ms"
+                ));
+        replayMocks();
+        assertTrue(mTestDevice.checkConnectivity());
+    }
+
+    public void testCheckConnectivity_NoConnectivity() throws Exception {
+        final String pingCommand = "ping -c 1 -w 5 -s 1024 www.google.com";
+        injectShellResponse(pingCommand, ArrayUtil.join("\r\n",
+                "PING www.google.com (0.0.0.0) 1024(1052) bytes of data.",
+                "",
+                "--- www.google.com ping statistics ---",
+                "1 packets transmitted, 0 received, 100% packet loss, time 0ms"
+                ));
+        replayMocks();
+        assertFalse(mTestDevice.checkConnectivity());
+    }
+
+    /**
+     * Test that a single user is handled by {@link TestDevice#listUsers()}.
+     */
+    public void testListUsers_oneUser() throws Exception {
+        final String listUsersCommand = "pm list users";
+        injectShellResponse(listUsersCommand, ArrayUtil.join("\r\n",
+                "Users:",
+                "UserInfo{0:Foo:13} running"));
+        replayMocks();
+        ArrayList<Integer> actual = mTestDevice.listUsers();
+        assertNotNull(actual);
+        assertEquals(1, actual.size());
+        assertEquals(0, actual.get(0).intValue());
+    }
+
+    /**
+     * Test that invalid output is handled by {@link TestDevice#listUsers()}.
+     */
+    public void testListUsers_invalidOutput() throws Exception {
+        final String listUsersCommand = "pm list users";
+        injectShellResponse(listUsersCommand, "not really what we are looking for");
+        replayMocks();
+        ArrayList<Integer> actual = mTestDevice.listUsers();
+        assertNull(actual);
+    }
+
+    /**
+     * Test that multiple user is handled by {@link TestDevice#listUsers()}.
+     */
+    public void testListUsers_multiUsers() throws Exception {
+        final String listUsersCommand = "pm list users";
+        injectShellResponse(listUsersCommand, ArrayUtil.join("\r\n",
+                "Users:",
+                "UserInfo{0:Foo:13} running",
+                "UserInfo{3:FooBar:14}"));
+        replayMocks();
+        ArrayList<Integer> actual = mTestDevice.listUsers();
+        assertNotNull(actual);
+        assertEquals(2, actual.size());
+        assertEquals(0, actual.get(0).intValue());
+        assertEquals(3, actual.get(1).intValue());
+    }
+
+    /**
+     * Test that multi user output is handled by {@link TestDevice#getMaxNumberOfUsersSupported()}.
+     */
+    public void testMaxNumberOfUsersSupported() throws Exception {
+        final String getMaxUsersCommand = "pm get-max-users";
+        injectShellResponse(getMaxUsersCommand, "Maximum supported users: 4");
+        replayMocks();
+        assertEquals(4, mTestDevice.getMaxNumberOfUsersSupported());
+    }
+
+    /**
+     * Test that invalid output is handled by {@link TestDevice#getMaxNumberOfUsersSupported()}.
+     */
+    public void testMaxNumberOfUsersSupported_invalid() throws Exception {
+        final String getMaxUsersCommand = "pm get-max-users";
+        injectShellResponse(getMaxUsersCommand, "not the output we expect");
+        replayMocks();
+        assertEquals(0, mTestDevice.getMaxNumberOfUsersSupported());
+    }
+
+    /**
+     * Test that single user output is handled by {@link TestDevice#getMaxNumberOfUsersSupported()}.
+     */
+    public void testIsMultiUserSupported_singleUser() throws Exception {
+        final String getMaxUsersCommand = "pm get-max-users";
+        injectShellResponse(getMaxUsersCommand, "Maximum supported users: 1");
+        replayMocks();
+        assertFalse(mTestDevice.isMultiUserSupported());
+    }
+
+    /**
+     * Test that {@link TestDevice#isMultiUserSupported()} works.
+     */
+    public void testIsMultiUserSupported() throws Exception {
+        final String getMaxUsersCommand = "pm get-max-users";
+        injectShellResponse(getMaxUsersCommand, "Maximum supported users: 4");
+        replayMocks();
+        assertTrue(mTestDevice.isMultiUserSupported());
+    }
+
+    /**
+     * Test that invalid output is handled by {@link TestDevice#isMultiUserSupported()}.
+     */
+    public void testIsMultiUserSupported_invalidOutput() throws Exception {
+        final String getMaxUsersCommand = "pm get-max-users";
+        injectShellResponse(getMaxUsersCommand, "not the output we expect");
+        replayMocks();
+        assertFalse(mTestDevice.isMultiUserSupported());
+    }
+
+    /**
+     * Test that successful user creation is handled by {@link TestDevice#createUser()}.
+     */
+    public void testCreateUser() throws Exception {
+        final String createUserCommand = "pm create-user foo";
+        injectShellResponse(createUserCommand, "Success: created user id 10");
+        replayMocks();
+        assertEquals(10, mTestDevice.createUser("foo"));
+    }
+
+    /**
+     * Test that a failure to create a user is handled by {@link TestDevice#createUser()}.
+     */
+    public void testCreateUser_failed() throws Exception {
+        final String createUserCommand = "pm create-user foo";
+        injectShellResponse(createUserCommand, "Error");
+        replayMocks();
+        try {
+            mTestDevice.createUser("foo");
+            fail("IllegalStateException not thrown");
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+    }
+
+    /**
+     * Test that successful user removal is handled by {@link TestDevice#removeUser()}.
+     */
+    public void testRemoveUser() throws Exception {
+        final String removeUserCommand = "pm remove-user 10";
+        injectShellResponse(removeUserCommand, "Success: removed user\n");
+        replayMocks();
+        assertTrue(mTestDevice.removeUser(10));
+    }
+
+    /**
+     * Test that a failure to remove a user is handled by {@link TestDevice#removeUser()}.
+     */
+    public void testRemoveUser_failed() throws Exception {
+        final String removeUserCommand = "pm remove-user 10";
+        injectShellResponse(removeUserCommand, "Error: couldn't remove user id 10");
+        replayMocks();
+        assertFalse(mTestDevice.removeUser(10));
+    }
+
+    /**
+     * Test that trying to run a test with a user with
+     * {@link TestDevice#runInstrumentationTestsAsUser(IRemoteAndroidTestRunner, int, Collection)}
+     * fails if the {@link IRemoteAndroidTestRunner} is not an instance of
+     * {@link RemoteAndroidTestRunner}.
+     */
+    public void testrunInstrumentationTestsAsUser_failed() throws Exception {
+        IRemoteAndroidTestRunner mockRunner = EasyMock.createMock(IRemoteAndroidTestRunner.class);
+        EasyMock.expect(mockRunner.getPackageName()).andStubReturn("com.example");
+        Collection<ITestRunListener> listeners = new ArrayList<ITestRunListener>();
+        EasyMock.replay(mockRunner);
+        try {
+            mTestDevice.runInstrumentationTestsAsUser(mockRunner, 12, listeners);
+            fail("IllegalStateException not thrown.");
+        } catch (IllegalStateException e) {
+            //expected
+        }
+    }
+
+     /**
+     * Test that successful user start is handled by {@link TestDevice#startUser()}.
+     */
+    public void testStartUser() throws Exception {
+        final String startUserCommand = "am start-user 10";
+        injectShellResponse(startUserCommand, "Success: user started\n");
+        replayMocks();
+        assertTrue(mTestDevice.startUser(10));
+    }
+
+    /**
+     * Test that a failure to start user is handled by {@link TestDevice#startUser()}.
+     */
+    public void testStartUser_failed() throws Exception {
+        final String startUserCommand = "am start-user 10";
+        injectShellResponse(startUserCommand, "Error: could not start user\n");
+        replayMocks();
+        assertFalse(mTestDevice.startUser(10));
+    }
+
+    /**
+     * Test that remount works as expected on a device not supporting dm verity
+     * @throws Exception
+     */
+    public void testRemount_verityUnsupported() throws Exception {
+        injectSystemProperty("partition.system.verified", "");
+        setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+        replayMocks();
+        mTestDevice.remountSystemWritable();
+        verifyMocks();
+    }
+
+    /**
+     * Test that remount works as expected on a device supporting dm verity v1
+     * @throws Exception
+     */
+    public void testRemount_veritySupportedV1() throws Exception {
+        injectSystemProperty("partition.system.verified", "1");
+        setExecuteAdbCommandExpectations(
+                new CommandResult(CommandStatus.SUCCESS), "disable-verity");
+        setRebootExpectations();
+        setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+        replayMocks();
+        mTestDevice.remountSystemWritable();
+        verifyMocks();
+    }
+
+    /**
+     * Test that remount works as expected on a device supporting dm verity v2
+     * @throws Exception
+     */
+    public void testRemount_veritySupportedV2() throws Exception {
+        injectSystemProperty("partition.system.verified", "2");
+        setExecuteAdbCommandExpectations(
+                new CommandResult(CommandStatus.SUCCESS), "disable-verity");
+        setRebootExpectations();
+        setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+        replayMocks();
+        mTestDevice.remountSystemWritable();
+        verifyMocks();
+    }
+
+    /**
+     * Test that remount works as expected on a device supporting dm verity but with unknown version
+     * @throws Exception
+     */
+    public void testRemount_veritySupportedNonNumerical() throws Exception {
+        injectSystemProperty("partition.system.verified", "foo");
+        setExecuteAdbCommandExpectations(
+                new CommandResult(CommandStatus.SUCCESS), "disable-verity");
+        setRebootExpectations();
+        setExecuteAdbCommandExpectations(new CommandResult(CommandStatus.SUCCESS), "remount");
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable()).andReturn(mMockIDevice);
+        replayMocks();
+        mTestDevice.remountSystemWritable();
+        verifyMocks();
+    }
 }
 
diff --git a/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java b/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java
index c4fa088..b44e052 100644
--- a/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java
+++ b/tests/src/com/android/tradefed/device/WaitDeviceRecoveryTest.java
@@ -65,6 +65,7 @@
         EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
         EasyMock.expect(mMockMonitor.waitForDeviceAvailable(EasyMock.anyLong())).andReturn(
                 mMockDevice);
+        EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(mMockDevice);
         replayMocks();
         mRecovery.recoverDevice(mMockMonitor, false);
         verifyMocks();
@@ -133,6 +134,7 @@
         EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
         EasyMock.expect(mMockMonitor.waitForDeviceAvailable(EasyMock.anyLong())).andReturn(
                 mMockDevice);
+        EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(mMockDevice);
         replayMocks();
         mRecovery.recoverDevice(mMockMonitor, false);
         verifyMocks();
@@ -152,6 +154,10 @@
                 Boolean.TRUE);
         EasyMock.expect(mMockMonitor.waitForDeviceBootloader(EasyMock.anyLong())).andReturn(
                 Boolean.TRUE).times(2);
+        EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
+                EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("getvar"),
+                EasyMock.eq("product"))).
+                andReturn(new CommandResult(CommandStatus.SUCCESS));
         replayMocks();
         mRecovery.recoverDeviceBootloader(mMockMonitor);
         verifyMocks();
@@ -174,6 +180,10 @@
                 Boolean.TRUE);
         EasyMock.expect(mMockMonitor.waitForDeviceBootloader(EasyMock.anyLong())).andReturn(
                 Boolean.TRUE).times(2);
+        EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), EasyMock.eq("fastboot"),
+                EasyMock.eq("-s"), EasyMock.eq("serial"), EasyMock.eq("getvar"),
+                EasyMock.eq("product"))).
+                andReturn(new CommandResult(CommandStatus.SUCCESS));
         replayMocks();
         mRecovery.recoverDeviceBootloader(mMockMonitor);
         verifyMocks();
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index ecfe742..3deeb0e 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -341,9 +341,8 @@
         mMockPreparer.setUp(mMockDevice, mMockBuildInfo);
         EasyMock.expectLastCall().andThrow(exception);
         setupMockFailureListeners(exception);
-
         EasyMock.expect(mMockDevice.getBugreport())
-                .andReturn(new ByteArrayInputStreamSource(new byte[0]));
+            .andReturn(new ByteArrayInputStreamSource(new byte[0]));
         setupInvokeWithBuild();
         replayMocks(test);
         mTestInvocation.invoke(mMockDevice, mStubConfiguration, new StubRescheduler());
@@ -365,6 +364,7 @@
 
         EasyMock.expect(mMockBuildProvider.getBuild()).andReturn(mMockBuildInfo);
         resumeListener.invocationStarted(mMockBuildInfo);
+        mMockDevice.clearLastConnectedWifiNetwork();
         mMockDevice.setOptions((TestDeviceOptions)EasyMock.anyObject());
         mMockBuildInfo.setDeviceSerial(SERIAL);
         mMockDevice.startLogcat();
@@ -397,11 +397,13 @@
         EasyMock.expect(mockRescheduler.scheduleConfig(EasyMock.capture(capturedConfig)))
                 .andReturn(Boolean.TRUE);
         mMockBuildProvider.cleanUp(mMockBuildInfo);
+        mMockDevice.clearLastConnectedWifiNetwork();
         mMockDevice.stopLogcat();
 
         mMockLogger.init();
         mMockLogSaver.invocationStarted(mMockBuildInfo);
         // now set resumed invocation expectations
+        mMockDevice.clearLastConnectedWifiNetwork();
         mMockDevice.setOptions((TestDeviceOptions)EasyMock.anyObject());
         mMockBuildInfo.setDeviceSerial(SERIAL);
         mMockDevice.startLogcat();
@@ -428,6 +430,7 @@
         EasyMock.expect(resumeListener.getSummary()).andReturn(null);
         mMockBuildInfo.cleanUp();
         mMockLogger.closeLog();
+        mMockDevice.clearLastConnectedWifiNetwork();
         mMockDevice.stopLogcat();
 
         EasyMock.replay(mockRescheduler, resumeListener, resumableTest, mMockPreparer,
@@ -499,13 +502,14 @@
         EasyMock.expectLastCall().andThrow(exception);
         ITargetCleaner mockCleaner = EasyMock.createMock(ITargetCleaner.class);
         mockCleaner.setUp(mMockDevice, mMockBuildInfo);
-        // tearDown should NOT be called
-        // mockCleaner.tearDown(mMockDevice, mMockBuildInfo, null);
+        EasyMock.expectLastCall();
+        mockCleaner.tearDown(mMockDevice, mMockBuildInfo, exception);
+        EasyMock.expectLastCall();
+        EasyMock.replay(mockCleaner);
         mStubConfiguration.getTargetPreparers().add(mockCleaner);
         setupMockFailureListeners(exception);
         mMockBuildProvider.buildNotTested(mMockBuildInfo);
         setupNormalInvoke(test);
-        EasyMock.replay(mockCleaner);
         try {
             mTestInvocation.invoke(mMockDevice, mStubConfiguration, new StubRescheduler());
             fail("DeviceNotAvailableException not thrown");
@@ -601,8 +605,10 @@
      * Set up expected calls that occur on every invoke, regardless of result
      */
     private void setupInvoke() {
+        mMockDevice.clearLastConnectedWifiNetwork();
         mMockDevice.setOptions((TestDeviceOptions)EasyMock.anyObject());
         mMockDevice.startLogcat();
+        mMockDevice.clearLastConnectedWifiNetwork();
         mMockDevice.stopLogcat();
     }
 
@@ -643,6 +649,12 @@
         mMockTestListener.invocationStarted(mMockBuildInfo);
         mMockSummaryListener.invocationStarted(mMockBuildInfo);
 
+        // invocationFailed
+        if (!status.equals(InvocationStatus.SUCCESS)) {
+            mMockTestListener.invocationFailed(EasyMock.eq(throwable));
+            mMockSummaryListener.invocationFailed(EasyMock.eq(throwable));
+        }
+
         if (throwable instanceof BuildError) {
             EasyMock.expect(mMockLogSaver.saveLogData(
                     EasyMock.eq(TestInvocation.BUILD_ERROR_BUGREPORT_NAME),
@@ -654,12 +666,6 @@
                     EasyMock.eq(LogDataType.BUGREPORT), (InputStreamSource)EasyMock.anyObject());
         }
 
-        // invocationFailed
-        if (!status.equals(InvocationStatus.SUCCESS)) {
-            mMockTestListener.invocationFailed(EasyMock.eq(throwable));
-            mMockSummaryListener.invocationFailed(EasyMock.eq(throwable));
-        }
-
         // saveAndZipLogData (mMockLogFileSaver)
         EasyMock.expect(mMockLogSaver.saveLogData( EasyMock.eq(TestInvocation.DEVICE_LOG_NAME),
                 EasyMock.eq(LogDataType.LOGCAT), (InputStream)EasyMock.anyObject())
diff --git a/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java b/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java
index 5c6034f..777e498 100644
--- a/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java
+++ b/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java
@@ -33,6 +33,7 @@
     private IEmail mMockEmail;
     private TerribleFailureEmailHandler mWtfEmailHandler;
     private final static String MOCK_HOST_NAME = "myhostname.mydomain.com";
+    private long mCurrentTimeMillis;
 
     @Override
     protected void setUp() throws Exception {
@@ -43,7 +44,13 @@
             protected String getLocalHostName() {
                 return MOCK_HOST_NAME;
             }
+
+            @Override
+            protected long getCurrentTimeMillis() {
+                return mCurrentTimeMillis;
+            }
         };
+        mCurrentTimeMillis = System.currentTimeMillis();
     }
 
     /**
@@ -103,6 +110,44 @@
     }
 
     /**
+     * Test that no email is attempted to be sent if it is too adjacent to the previous failure.
+     */
+    public void testOnTerribleFailure_adjacentFailures() throws IllegalArgumentException,
+            IOException {
+        mMockEmail.send(EasyMock.<Message>anyObject());
+        mWtfEmailHandler.setMinEmailInterval(60000);
+
+        EasyMock.replay(mMockEmail);
+        mWtfEmailHandler.addDestination("user@domain.com");
+        boolean retValue = mWtfEmailHandler.onTerribleFailure("something terrible happened", null);
+        assertTrue(retValue);
+        mCurrentTimeMillis += 30000;
+        retValue = mWtfEmailHandler.onTerribleFailure("something terrible happened again", null);
+        assertFalse(retValue);
+        EasyMock.verify(mMockEmail);
+    }
+
+    /**
+     * Test that the second email is attempted to be sent if it is not adjacent to the previous
+     * failure.
+     */
+    public void testOnTerribleFailure_notAdjacentFailures() throws IllegalArgumentException,
+            IOException {
+        mMockEmail.send(EasyMock.<Message>anyObject());
+        mMockEmail.send(EasyMock.<Message>anyObject());
+        mWtfEmailHandler.setMinEmailInterval(60000);
+
+        EasyMock.replay(mMockEmail);
+        mWtfEmailHandler.addDestination("user@domain.com");
+        boolean retValue = mWtfEmailHandler.onTerribleFailure("something terrible happened", null);
+        assertTrue(retValue);
+        mCurrentTimeMillis += 90000;
+        retValue = mWtfEmailHandler.onTerribleFailure("something terrible happened again", null);
+        assertTrue(retValue);
+        EasyMock.verify(mMockEmail);
+    }
+
+    /**
      * Test that the generated email message actually contains the sender and
      * destination email addresses.
      */
diff --git a/tests/src/com/android/tradefed/result/BugreportCollectorTest.java b/tests/src/com/android/tradefed/result/BugreportCollectorTest.java
index 9b87328..c807d3b 100644
--- a/tests/src/com/android/tradefed/result/BugreportCollectorTest.java
+++ b/tests/src/com/android/tradefed/result/BugreportCollectorTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.result;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.result.BugreportCollector.Filter;
@@ -294,7 +293,7 @@
         final TestIdentifier test = new TestIdentifier("FooTest", testName);
         mCollector.testStarted(test);
         if (shouldFail) {
-            mCollector.testFailed(TestFailure.FAILURE, test, STACK_TRACE);
+            mCollector.testFailed(test, STACK_TRACE);
         }
         mCollector.testEnded(test, testMetrics);
         mCollector.testRunEnded(0, runMetrics);
@@ -314,8 +313,7 @@
         final TestIdentifier test = new TestIdentifier("FooTest", testName);
         listener.testStarted(EasyMock.eq(test));
         if (shouldFail) {
-            listener.testFailed((TestFailure)EasyMock.anyObject(), EasyMock.eq(test),
-                    EasyMock.eq(STACK_TRACE));
+            listener.testFailed(EasyMock.eq(test), EasyMock.eq(STACK_TRACE));
         }
         listener.testEnded(EasyMock.eq(test), (Map<String, String>)EasyMock.anyObject());
         listener.testRunEnded(EasyMock.anyInt(), (Map<String, String>)EasyMock.anyObject());
diff --git a/tests/src/com/android/tradefed/result/CollectingTestListenerTest.java b/tests/src/com/android/tradefed/result/CollectingTestListenerTest.java
index cfdfa69..c9063a4 100644
--- a/tests/src/com/android/tradefed/result/CollectingTestListenerTest.java
+++ b/tests/src/com/android/tradefed/result/CollectingTestListenerTest.java
@@ -15,10 +15,10 @@
  */
 package com.android.tradefed.result;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.result.TestResult.TestStatus;
 
 import junit.framework.TestCase;
 
@@ -87,7 +87,7 @@
         final TestIdentifier test1 = injectTestRun("run1", "testFoo1", METRIC_VALUE);
         final TestIdentifier test2 = injectTestRun("run2", "testFoo2", METRIC_VALUE2);
         assertEquals(2, mCollectingTestListener.getNumTotalTests());
-        assertEquals(2, mCollectingTestListener.getNumPassedTests());
+        assertEquals(2, mCollectingTestListener.getNumTestsInState(TestStatus.PASSED));
         assertEquals(2, mCollectingTestListener.getRunResults().size());
         Iterator<TestRunResult> runIter = mCollectingTestListener.getRunResults().iterator();
         final TestRunResult runResult1 = runIter.next();
@@ -114,10 +114,10 @@
         final TestIdentifier test1 = injectTestRun("run", "testFoo1", METRIC_VALUE);
         final TestIdentifier test2 = injectTestRun("run", "testFoo2", METRIC_VALUE2);
         assertEquals(2, mCollectingTestListener.getNumTotalTests());
-        assertEquals(2, mCollectingTestListener.getNumPassedTests());
+        assertEquals(2, mCollectingTestListener.getNumTestsInState(TestStatus.PASSED));
         assertEquals(1, mCollectingTestListener.getRunResults().size());
         TestRunResult runResult = mCollectingTestListener.getCurrentRunResults();
-        assertEquals(2, runResult.getNumPassedTests());
+        assertEquals(2, runResult.getNumTestsInState(TestStatus.PASSED));
         assertTrue(runResult.getCompletedTests().contains(test1));
         assertTrue(runResult.getCompletedTests().contains(test2));
     }
@@ -130,12 +130,12 @@
         injectTestRun("run", "testFoo1", METRIC_VALUE);
         injectTestRun("run", "testFoo1", METRIC_VALUE2, true);
         assertEquals(1, mCollectingTestListener.getNumTotalTests());
-        assertEquals(0, mCollectingTestListener.getNumPassedTests());
-        assertEquals(1, mCollectingTestListener.getNumFailedTests());
+        assertEquals(0, mCollectingTestListener.getNumTestsInState(TestStatus.PASSED));
+        assertEquals(1, mCollectingTestListener.getNumTestsInState(TestStatus.FAILURE));
         assertEquals(1, mCollectingTestListener.getRunResults().size());
         TestRunResult runResult = mCollectingTestListener.getCurrentRunResults();
-        assertEquals(0, runResult.getNumPassedTests());
-        assertEquals(1, runResult.getNumFailedTests());
+        assertEquals(0, runResult.getNumTestsInState(TestStatus.PASSED));
+        assertEquals(1, runResult.getNumTestsInState(TestStatus.FAILURE));
         assertEquals(1, runResult.getNumTests());
     }
 
@@ -147,7 +147,7 @@
         mCollectingTestListener.testRunStarted("run", 1);
         mCollectingTestListener.testStarted(new TestIdentifier("FooTest", "incomplete"));
         mCollectingTestListener.testRunEnded(0, Collections.EMPTY_MAP);
-        assertEquals(1, mCollectingTestListener.getNumIncompleteTests());
+        assertEquals(1, mCollectingTestListener.getNumTestsInState(TestStatus.INCOMPLETE));
     }
 
     /**
@@ -233,7 +233,7 @@
         final TestIdentifier test = new TestIdentifier("FooTest", testName);
         mCollectingTestListener.testStarted(test);
         if (failtest) {
-            mCollectingTestListener.testFailed(TestFailure.FAILURE, test, "trace");
+            mCollectingTestListener.testFailed(test, "trace");
         }
         mCollectingTestListener.testEnded(test, testMetrics);
         mCollectingTestListener.testRunEnded(0, runMetrics);
diff --git a/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java b/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
new file mode 100644
index 0000000..9a7cce9
--- /dev/null
+++ b/tests/src/com/android/tradefed/result/ConsoleResultReporterTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2015 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.result;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import junit.framework.TestCase;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link ConsoleResultReporter}
+ */
+public class ConsoleResultReporterTest extends TestCase {
+    private static final Map<String, String> EMPTY_MAP = Collections.emptyMap();
+
+    /**
+     * Test the results printed for an empty invocation
+     */
+    public void testGetInvocationSummary_empty() {
+        ConsoleResultReporter reporter = new ConsoleResultReporter();
+        reporter.invocationStarted(null);
+        reporter.invocationEnded(0);
+        assertEquals("No test results\n", reporter.getInvocationSummary());
+    }
+
+    /**
+     * Test that run metrics are sorted by key
+     */
+    public void testGetInvocationSummary_test_run_metrics() {
+        ConsoleResultReporter reporter = new ConsoleResultReporter();
+        reporter.invocationStarted(null);
+        reporter.testRunStarted("Test Run", 0);
+        Map<String, String> metrics = new HashMap<>();
+        metrics.put("key2", "value2");
+        metrics.put("key1", "value1");
+        reporter.testRunEnded(0, metrics);
+        reporter.invocationEnded(0);
+        assertEquals(
+                "Test results:\n" +
+                "Test Run:\n" +
+                "  key1: value1\n" +
+                "  key2: value2\n",
+                reporter.getInvocationSummary());
+    }
+
+    /**
+     * Test that test metrics are sorted by key
+     */
+    public void testGetInvocationSummary_test_metrics() {
+        ConsoleResultReporter reporter = new ConsoleResultReporter();
+        reporter.invocationStarted(null);
+        reporter.testRunStarted("Test Run", 1);
+        TestIdentifier testId = new TestIdentifier("class", "method");
+        reporter.testStarted(testId);
+        Map<String, String> metrics = new HashMap<>();
+        metrics.put("key2", "value2");
+        metrics.put("key1", "value1");
+        reporter.testEnded(testId, metrics);
+        reporter.testRunEnded(0, EMPTY_MAP);
+        reporter.invocationEnded(0);
+        assertEquals(
+                "Test results:\n" +
+                "Test Run: 1 Test, 1 Passed, 0 Failed, 0 Ignored\n" +
+                "  class#method: PASSED\n" +
+                "    key1: value1\n" +
+                "    key2: value2\n",
+                reporter.getInvocationSummary());
+    }
+
+    /**
+     * Test that logs are printed, favoring url.
+     */
+    public void testGetInvocationSummary_logs() {
+        ConsoleResultReporter reporter = new ConsoleResultReporter();
+        reporter.invocationStarted(null);
+        reporter.testLogSaved(null, null, null, new LogFile("/path/to/log1", "http://log1"));
+        reporter.testLogSaved(null, null, null, new LogFile("/path/to/log2", null));
+        reporter.invocationEnded(0);
+        assertEquals(
+                "Test results:\n" +
+                "Log Files:\n" +
+                "  http://log1\n" +
+                "  /path/to/log2\n",
+                reporter.getInvocationSummary());
+    }
+
+    /**
+     * Inclusive test to test that all test runs are printed, and metrics and logs are printed with
+     * it.
+     */
+    public void testGetInvocationSummary_all() {
+        ConsoleResultReporter reporter = new ConsoleResultReporter();
+        reporter.invocationStarted(null);
+
+        reporter.testRunStarted("Test Run 1", 3);
+
+        TestIdentifier run1test1Id = new TestIdentifier("class1", "method1");
+        reporter.testStarted(run1test1Id);
+        reporter.testFailed(run1test1Id, "trace");
+        Map<String, String> run1Test1Metrics = new HashMap<>();
+        run1Test1Metrics.put("run1_test1_key1", "run1_test1_value1");
+        run1Test1Metrics.put("run1_test1_key2", "run1_test1_value2");
+        reporter.testEnded(run1test1Id, run1Test1Metrics);
+
+        TestIdentifier run1test2Id = new TestIdentifier("class1", "method2");
+        reporter.testStarted(run1test2Id);
+        Map<String, String> run1Test2Metrics = new HashMap<>();
+        run1Test2Metrics.put("run1_test2_key1", "run1_test2_value1");
+        run1Test2Metrics.put("run1_test2_key2", "run1_test2_value2");
+        reporter.testEnded(run1test2Id, run1Test2Metrics);
+
+        TestIdentifier run1test3Id = new TestIdentifier("class1", "method3");
+        reporter.testStarted(run1test3Id);
+        reporter.testAssumptionFailure(run1test3Id, "trace");
+        Map<String, String> run1Test3Metrics = new HashMap<>();
+        run1Test3Metrics.put("run1_test3_key1", "run1_test3_value1");
+        run1Test3Metrics.put("run1_test3_key2", "run1_test3_value2");
+        reporter.testEnded(run1test3Id, run1Test3Metrics);
+
+        TestIdentifier run1test4Id = new TestIdentifier("class1", "method4");
+        reporter.testStarted(run1test4Id);
+        reporter.testIgnored(run1test4Id);
+        reporter.testEnded(run1test4Id, EMPTY_MAP);
+
+        Map<String, String> run1Metrics = new HashMap<>();
+        run1Metrics.put("run1_key1", "run1_value2");
+        run1Metrics.put("run1_key2", "run1_value1");
+        reporter.testRunEnded(0, run1Metrics);
+
+        reporter.testRunStarted("Test Run 2", 4);
+        TestIdentifier run2test1Id = new TestIdentifier("class2", "method1");
+        reporter.testStarted(run2test1Id);
+        reporter.testFailed(run2test1Id, "trace");
+        reporter.testEnded(run2test1Id, EMPTY_MAP);
+
+        TestIdentifier run2test2Id = new TestIdentifier("class2", "method2");
+        reporter.testStarted(run2test2Id);
+        reporter.testEnded(run2test2Id, EMPTY_MAP);
+
+        TestIdentifier run2test3Id = new TestIdentifier("class2", "method3");
+        reporter.testStarted(run2test3Id);
+        reporter.testAssumptionFailure(run2test3Id, "trace");
+        reporter.testEnded(run2test3Id, EMPTY_MAP);
+
+        TestIdentifier run2test4Id = new TestIdentifier("class2", "method4");
+        reporter.testStarted(run2test4Id);
+        reporter.testIgnored(run2test4Id);
+        reporter.testEnded(run2test4Id, EMPTY_MAP);
+        reporter.testRunEnded(0, EMPTY_MAP);
+
+        reporter.testRunStarted("Test Run 3", 0);
+        Map<String, String> run3Metrics = new HashMap<>();
+        run3Metrics.put("run3_key1", "run3_value1");
+        run3Metrics.put("run3_key2", "run3_value2");
+        reporter.testRunEnded(0, run3Metrics);
+
+        reporter.testRunStarted("Test Run 4", 0);
+        reporter.testRunEnded(0, EMPTY_MAP);
+
+        reporter.testLogSaved(null, null, null, new LogFile("/path/to/log1", "http://log1"));
+        reporter.testLogSaved(null, null, null, new LogFile("/path/to/log2", null));
+        reporter.invocationEnded(0);
+        assertEquals(
+                "Test results:\n" +
+                "Test Run 1: 4 Tests, 1 Passed, 2 Failed, 1 Ignored\n" +
+                "  class1#method1: FAILURE\n" +
+                "    run1_test1_key1: run1_test1_value1\n" +
+                "    run1_test1_key2: run1_test1_value2\n" +
+                "  class1#method2: PASSED\n" +
+                "    run1_test2_key1: run1_test2_value1\n" +
+                "    run1_test2_key2: run1_test2_value2\n" +
+                "  class1#method3: ASSUMPTION_FAILURE\n" +
+                "    run1_test3_key1: run1_test3_value1\n" +
+                "    run1_test3_key2: run1_test3_value2\n" +
+                "  class1#method4: IGNORED\n" +
+                "  run1_key1: run1_value2\n" +
+                "  run1_key2: run1_value1\n" +
+                "\n" +
+                "Test Run 2: 4 Tests, 1 Passed, 2 Failed, 1 Ignored\n" +
+                "  class2#method1: FAILURE\n" +
+                "  class2#method2: PASSED\n" +
+                "  class2#method3: ASSUMPTION_FAILURE\n" +
+                "  class2#method4: IGNORED\n" +
+                "\n" +
+                "Test Run 3:\n" +
+                "  run3_key1: run3_value1\n" +
+                "  run3_key2: run3_value2\n" +
+                "\n" +
+                "Test Run 4: No results\n" +
+                "\n" +
+                "Log Files:\n" +
+                "  http://log1\n" +
+                "  /path/to/log2\n",
+                reporter.getInvocationSummary());
+    }
+}
diff --git a/tests/src/com/android/tradefed/result/DeviceFileReporterTest.java b/tests/src/com/android/tradefed/result/DeviceFileReporterTest.java
index 67a683e..0ddd95d 100644
--- a/tests/src/com/android/tradefed/result/DeviceFileReporterTest.java
+++ b/tests/src/com/android/tradefed/result/DeviceFileReporterTest.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.result;
 
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.ArrayUtil;
 
 import junit.framework.TestCase;
 
@@ -23,6 +24,8 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Unit tests for {@link DeviceFileReporter}
@@ -35,9 +38,31 @@
     // Used to control what ISS is returned
     InputStreamSource mDfrIss = null;
 
+    @SuppressWarnings("serial")
+    private static class FakeFile extends File {
+        private final String mName;
+        private final long mSize;
+
+        FakeFile(String name, long size) {
+            super(name);
+            mName = name;
+            mSize = size;
+        }
+        @Override
+        public String toString() {
+            return mName;
+        }
+        @Override
+        public long length() {
+            return mSize;
+        }
+    }
+
     @Override
     public void setUp() throws Exception {
         mDevice = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(mDevice.getSerialNumber()).andStubReturn("serial");
+
         mListener = EasyMock.createMock(ITestInvocationListener.class);
         dfr = new DeviceFileReporter(mDevice, mListener) {
             @Override
@@ -50,14 +75,15 @@
     public void testSimple() throws Exception {
         final String result = "/data/tombstones/tombstone_00\r\n";
         final String filename = "/data/tombstones/tombstone_00";
+        final String tombstone = "What do you want on your tombstone?";
         dfr.addPatterns("/data/tombstones/*");
 
         EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
                 .andReturn(result);
         // This gets passed verbatim to createIssForFile above
-        EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename))).andReturn(null);
+        EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename)))
+                .andReturn(new FakeFile(filename, tombstone.length()));
 
-        final String tombstone = "What do you want on your tombstone?";
         mDfrIss = new ByteArrayInputStreamSource(tombstone.getBytes());
         // FIXME: use captures here to make sure we get the string back out
         mListener.testLog(EasyMock.eq(filename), EasyMock.eq(LogDataType.UNKNOWN),
@@ -68,6 +94,175 @@
         verifyMocks();
     }
 
+    public void testLineEnding_LF() throws Exception {
+        final String[] filenames = {"/data/tombstones/tombstone_00",
+                "/data/tombstones/tombstone_01",
+                "/data/tombstones/tombstone_02",
+                "/data/tombstones/tombstone_03",
+                "/data/tombstones/tombstone_04"};
+        String result = ArrayUtil.join("\n", (Object[])filenames);
+        final String tombstone = "What do you want on your tombstone?";
+        dfr.addPatterns("/data/tombstones/*");
+
+        EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+                .andReturn(result);
+        mDfrIss = new ByteArrayInputStreamSource(tombstone.getBytes());
+        // This gets passed verbatim to createIssForFile above
+        for (String filename : filenames) {
+            EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename))).andReturn(
+                    new FakeFile(filename, tombstone.length()));
+
+            // FIXME: use captures here to make sure we get the string back out
+            mListener.testLog(EasyMock.eq(filename), EasyMock.eq(LogDataType.UNKNOWN),
+                    EasyMock.eq(mDfrIss));
+        }
+        replayMocks();
+        dfr.run();
+        verifyMocks();
+    }
+
+    public void testLineEnding_CRLF() throws Exception {
+        final String[] filenames = {"/data/tombstones/tombstone_00",
+                "/data/tombstones/tombstone_01",
+                "/data/tombstones/tombstone_02",
+                "/data/tombstones/tombstone_03",
+                "/data/tombstones/tombstone_04"};
+        String result = ArrayUtil.join("\r\n", (Object[])filenames);
+        final String tombstone = "What do you want on your tombstone?";
+        dfr.addPatterns("/data/tombstones/*");
+
+        EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+                .andReturn(result);
+        mDfrIss = new ByteArrayInputStreamSource(tombstone.getBytes());
+        // This gets passed verbatim to createIssForFile above
+        for (String filename : filenames) {
+            EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename))).andReturn(
+                    new FakeFile(filename, tombstone.length()));
+
+            // FIXME: use captures here to make sure we get the string back out
+            mListener.testLog(EasyMock.eq(filename), EasyMock.eq(LogDataType.UNKNOWN),
+                    EasyMock.eq(mDfrIss));
+        }
+        replayMocks();
+        dfr.run();
+        verifyMocks();
+    }
+
+    /**
+     * Make sure that the Reporter behaves as expected when a file is matched by multiple patterns
+     */
+    public void testRepeat_skip() throws Exception {
+        final String result1 = "/data/files/file.png\r\n";
+        final String result2 = "/data/files/file.png\r\n/data/files/file.xml\r\n";
+        final String pngFilename = "/data/files/file.png";
+        final String xmlFilename = "/data/files/file.xml";
+        final Map<String, LogDataType> patMap = new HashMap<>(2);
+        patMap.put("/data/files/*.png", LogDataType.PNG);
+        patMap.put("/data/files/*", LogDataType.UNKNOWN);
+
+        final String pngContents = "This is PNG data";
+        final String xmlContents = "<!-- This is XML data -->";
+        final InputStreamSource pngIss = new ByteArrayInputStreamSource(pngContents.getBytes());
+        final InputStreamSource xmlIss = new ByteArrayInputStreamSource(xmlContents.getBytes());
+
+        dfr = new DeviceFileReporter(mDevice, mListener) {
+            @Override
+            InputStreamSource createIssForFile(File file) throws IOException {
+                if (file.toString().endsWith(".png")) {
+                    return pngIss;
+                } else if (file.toString().endsWith(".xml")) {
+                    return xmlIss;
+                }
+                throw new IOException ("unknown fake file");
+            }
+        };
+        dfr.addPatterns(patMap);
+        dfr.setInferUnknownDataTypes(false);
+
+        // Set file listing pulling, and reporting expectations
+        // Expect that we go through the entire process for the PNG file, and then go through
+        // the entire process again for the XML file
+        EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+                .andReturn(result1);
+        EasyMock.expect(mDevice.pullFile(EasyMock.eq(pngFilename)))
+                .andReturn(new FakeFile(pngFilename, pngContents.length()));
+        mListener.testLog(EasyMock.eq(pngFilename), EasyMock.eq(LogDataType.PNG),
+                EasyMock.eq(pngIss));
+
+        EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+                .andReturn(result2);
+        EasyMock.expect(mDevice.pullFile(EasyMock.eq(xmlFilename)))
+                .andReturn(new FakeFile(xmlFilename, xmlContents.length()));
+        mListener.testLog(EasyMock.eq(xmlFilename), EasyMock.eq(LogDataType.UNKNOWN),
+                EasyMock.eq(xmlIss));
+
+        replayMocks();
+        dfr.run();
+        verifyMocks();
+        // FIXME: use captures here to make sure we get the string back out
+    }
+
+    /**
+     * Make sure that the Reporter behaves as expected when a file is matched by multiple patterns
+     */
+    public void testRepeat_noSkip() throws Exception {
+        final String result1 = "/data/files/file.png\r\n";
+        final String result2 = "/data/files/file.png\r\n/data/files/file.xml\r\n";
+        final String pngFilename = "/data/files/file.png";
+        final String xmlFilename = "/data/files/file.xml";
+        final Map<String, LogDataType> patMap = new HashMap<>(2);
+        patMap.put("/data/files/*.png", LogDataType.PNG);
+        patMap.put("/data/files/*", LogDataType.UNKNOWN);
+
+        final String pngContents = "This is PNG data";
+        final String xmlContents = "<!-- This is XML data -->";
+        final InputStreamSource pngIss = new ByteArrayInputStreamSource(pngContents.getBytes());
+        final InputStreamSource xmlIss = new ByteArrayInputStreamSource(xmlContents.getBytes());
+
+        dfr = new DeviceFileReporter(mDevice, mListener) {
+            @Override
+            InputStreamSource createIssForFile(File file) throws IOException {
+                if (file.toString().endsWith(".png")) {
+                    return pngIss;
+                } else if (file.toString().endsWith(".xml")) {
+                    return xmlIss;
+                }
+                throw new IOException ("unknown fake file");
+            }
+        };
+        dfr.addPatterns(patMap);
+        dfr.setInferUnknownDataTypes(false);
+        // this should cause us to see three pulls instead of two
+        dfr.setSkipRepeatFiles(false);
+
+        // Set file listing pulling, and reporting expectations
+        // Expect that we go through the entire process for the PNG file, and then go through
+        // the entire process again for the PNG file (again) and the XML file
+        EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+                .andReturn(result1);
+        EasyMock.expect(mDevice.pullFile(EasyMock.eq(pngFilename)))
+                .andReturn(new FakeFile(pngFilename, pngContents.length()));
+        mListener.testLog(EasyMock.eq(pngFilename), EasyMock.eq(LogDataType.PNG),
+                EasyMock.eq(pngIss));
+
+        // Note that the PNG file is picked up with the UNKNOWN data type this time
+        EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+                .andReturn(result2);
+        EasyMock.expect(mDevice.pullFile(EasyMock.eq(pngFilename)))
+                .andReturn(new FakeFile(pngFilename, pngContents.length()));
+        mListener.testLog(EasyMock.eq(pngFilename), EasyMock.eq(LogDataType.UNKNOWN),
+                EasyMock.eq(pngIss));
+        EasyMock.expect(mDevice.pullFile(EasyMock.eq(xmlFilename)))
+                .andReturn(new FakeFile(xmlFilename, xmlContents.length()));
+        mListener.testLog(EasyMock.eq(xmlFilename), EasyMock.eq(LogDataType.UNKNOWN),
+                EasyMock.eq(xmlIss));
+
+        replayMocks();
+        dfr.run();
+        verifyMocks();
+        // FIXME: use captures here to make sure we get the string back out
+    }
+
     /**
      * Make sure that we correctly handle the case where a file doesn't exist while matching the
      * exact name.
@@ -94,6 +289,7 @@
         final String result = "/data/tombstones/tombstone_00\r\n/data/tombstones/tombstone_01\r\n";
         final String filename1 = "/data/tombstones/tombstone_00";
         final String filename2 = "/data/tombstones/tombstone_01";
+        final String tombstone = "What do you want on your tombstone?";
         dfr.addPatterns("/data/tombstones/*");
 
         // Search the filesystem
@@ -102,8 +298,8 @@
 
         // Log the first file
         // This gets passed verbatim to createIssForFile above
-        EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename1))).andReturn(null);
-        final String tombstone = "What do you want on your tombstone?";
+        EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename1)))
+                .andReturn(new FakeFile(filename1, tombstone.length()));
         mDfrIss = new ByteArrayInputStreamSource(tombstone.getBytes());
         // FIXME: use captures here to make sure we get the string back out
         mListener.testLog(EasyMock.eq(filename1), EasyMock.eq(LogDataType.UNKNOWN),
@@ -111,7 +307,8 @@
 
         // Log the second file
         // This gets passed verbatim to createIssForFile above
-        EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename2))).andReturn(null);
+        EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename2)))
+                .andReturn(new FakeFile(filename2, tombstone.length()));
         // FIXME: use captures here to make sure we get the string back out
         mListener.testLog(EasyMock.eq(filename2), EasyMock.eq(LogDataType.UNKNOWN),
                 EasyMock.eq(mDfrIss));
@@ -121,6 +318,40 @@
         verifyMocks();
     }
 
+    /**
+     * Make sure that data type inference works as expected
+     */
+    public void testInferDataTypes() throws Exception {
+        final String result = "/data/files/file.png\r\n/data/files/file.xml\r\n" +
+                "/data/files/file.zip\r\n";
+        final String[] filenames = {"/data/files/file.png", "/data/files/file.xml",
+                "/data/files/file.zip"};
+        final LogDataType[] expTypes = {LogDataType.PNG, LogDataType.XML, LogDataType.ZIP};
+        dfr.addPatterns("/data/files/*");
+
+        final String contents = "these are file contents";
+        mDfrIss = new ByteArrayInputStreamSource(contents.getBytes());
+
+        EasyMock.expect(mDevice.executeShellCommand((String)EasyMock.anyObject()))
+                .andReturn(result);
+        // This gets passed verbatim to createIssForFile above
+        for (int i = 0; i < filenames.length; ++i) {
+            final String filename = filenames[i];
+            final LogDataType expType = expTypes[i];
+            EasyMock.expect(mDevice.pullFile(EasyMock.eq(filename)))
+                    .andReturn(new FakeFile(filename, contents.length()));
+
+            // FIXME: use captures here to make sure we get the string back out
+            mListener.testLog(EasyMock.eq(filename), EasyMock.eq(expType),
+                    EasyMock.eq(mDfrIss));
+        }
+
+        replayMocks();
+        dfr.run();
+        verifyMocks();
+    }
+
+
     private void replayMocks() {
         EasyMock.replay(mDevice, mListener);
     }
diff --git a/tests/src/com/android/tradefed/result/JUnitToInvocationResultForwarderTest.java b/tests/src/com/android/tradefed/result/JUnitToInvocationResultForwarderTest.java
index 39b6847..3adc047 100644
--- a/tests/src/com/android/tradefed/result/JUnitToInvocationResultForwarderTest.java
+++ b/tests/src/com/android/tradefed/result/JUnitToInvocationResultForwarderTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.result;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 
 import junit.framework.AssertionFailedError;
@@ -46,24 +45,12 @@
     }
 
     /**
-     * Test method for {@link JUnitToInvocationResultForwarder#addError(Test, Throwable)}.
-     */
-    public void testAddError() {
-       final Throwable t = new Throwable();
-       mListener.testFailed(EasyMock.eq(TestFailure.ERROR),
-               EasyMock.eq(new TestIdentifier(JUnitToInvocationResultForwarderTest.class.getName(),
-                       "testAddError")), (String)EasyMock.anyObject());
-       EasyMock.replay(mListener);
-       mForwarder.addError(this, t);
-    }
-
-    /**
      * Test method for
      * {@link JUnitToInvocationResultForwarder#addFailure(Test, AssertionFailedError)}.
      */
     public void testAddFailure() {
         final AssertionFailedError a = new AssertionFailedError();
-        mListener.testFailed(EasyMock.eq(TestFailure.FAILURE),
+        mListener.testFailed(
                 EasyMock.eq(new TestIdentifier(JUnitToInvocationResultForwarderTest.class.getName(),
                         "testAddFailure")), (String)EasyMock.anyObject());
         EasyMock.replay(mListener);
diff --git a/tests/src/com/android/tradefed/result/XmlResultReporterTest.java b/tests/src/com/android/tradefed/result/XmlResultReporterTest.java
index b22e2b5..0181f1b 100644
--- a/tests/src/com/android/tradefed/result/XmlResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/XmlResultReporterTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.result;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.IBuildInfo;
@@ -139,7 +138,7 @@
         mResultReporter.invocationStarted(new BuildInfo());
         mResultReporter.testRunStarted("run", 1);
         mResultReporter.testStarted(testId);
-        mResultReporter.testFailed(TestFailure.FAILURE, testId, trace);
+        mResultReporter.testFailed(testId, trace);
         mResultReporter.testEnded(testId, emptyMap);
         mResultReporter.testRunEnded(3, emptyMap);
         mResultReporter.invocationEnded(1);
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java b/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
index 4a112e6..be5172c 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
@@ -56,6 +56,7 @@
         mMockDevice = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("foo").anyTimes();
         mMockBuildInfo = new DeviceBuildInfo("0", "", "");
+        mMockBuildInfo.setDeviceImageFile(new File("foo"), "0");
         mMockBuildInfo.setBuildFlavor("flavor");
         mDeviceFlashPreparer = new DeviceFlashPreparer() {
             @Override
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
index 013c61d..da734da 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
@@ -22,14 +22,15 @@
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.targetprep.DeviceSetup.BinaryState;
 import com.android.tradefed.util.FileUtil;
 
 import junit.framework.TestCase;
 
+import org.easymock.Capture;
 import org.easymock.EasyMock;
 
 import java.io.File;
-import java.io.IOException;
 
 /**
  * Unit tests for {@link DeviceSetup}.
@@ -53,7 +54,6 @@
         EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("foo").anyTimes();
         mMockBuildInfo = new DeviceBuildInfo("0", "", "");
         mDeviceSetup = new DeviceSetup();
-        mDeviceSetup.setMinExternalStoreSpace(-1);
         mTmpDir = FileUtil.createTempDir("tmp");
     }
 
@@ -70,27 +70,647 @@
      * Simple normal case test for {@link DeviceSetup#setUp(ITestDevice, IBuildInfo)}.
      */
     public void testSetup() throws Exception {
-        doSetupExpectations();
+        Capture<String> setPropCapture = new Capture<>();
+        doSetupExpectations(true, setPropCapture);
+        doCheckExternalStoreSpaceExpectations();
         EasyMock.replay(mMockDevice);
+
         mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+
+        String setProp = setPropCapture.getValue();
+        assertTrue("Set prop doesn't contain ro.telephony.disable-call=true",
+                setProp.contains("ro.telephony.disable-call=true\n"));
+        assertTrue("Set prop doesn't contain ro.audio.silent=1",
+                setProp.contains("ro.audio.silent=1\n"));
+        assertTrue("Set prop doesn't contain ro.test_harness=1",
+                setProp.contains("ro.test_harness=1\n"));
+        assertTrue("Set prop doesn't contain ro.monkey=1",
+                setProp.contains("ro.monkey=1\n"));
     }
 
-    /**
-     * Set EasyMock expectations for a normal setup call
-     */
-    private void doSetupExpectations() throws TargetSetupError, DeviceNotAvailableException {
-        EasyMock.expect(mMockDevice.enableAdbRoot()).andReturn(Boolean.TRUE);
-        mMockDevice.postBootSetup();
-        EasyMock.expect(mMockDevice.clearErrorDialogs()).andReturn(Boolean.TRUE);
-        EasyMock.expect(mMockDevice.executeShellCommand("getprop dev.bootcomplete")).andReturn("1");
-        // expect push of local.prop file to change system properties
-        EasyMock.expect(mMockDevice.pushString((String)EasyMock.anyObject(),
-                EasyMock.contains("local.prop"))).andReturn(Boolean.TRUE);
-        mMockDevice.reboot();
-        // expect a bunch of shell commands - no need to verify which ones
-        EasyMock.expect(mMockDevice.executeShellCommand((String)EasyMock.anyObject())).
-                andReturn("").anyTimes();
-        EasyMock.expect(mMockDevice.getProperty("ro.build.id")).andReturn("IMM76K");
+    public void testSetup_airplane_mode_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true,
+                "settings put global \"airplane_mode_on\" \"1\"",
+                "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setAirplaneMode(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_airplane_mode_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true,
+                "settings put global \"airplane_mode_on\" \"0\"",
+                "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setAirplaneMode(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_wifi_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true,
+                "settings put global \"wifi_on\" \"1\"",
+                "svc wifi enable");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWifi(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_wifi_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true,
+                "settings put global \"wifi_on\" \"0\"",
+                "svc wifi disable");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWifi(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_wifi_watchdog_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put global \"wifi_watchdog\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWifiWatchdog(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_wifi_watchdog_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put global \"wifi_watchdog\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWifiWatchdog(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_wifi_scan_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put global \"wifi_scan_always_enabled\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWifiScanAlwaysEnabled(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_wifi_scan_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put global \"wifi_scan_always_enabled\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWifiScanAlwaysEnabled(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_ethernet_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(false, "ifconfig eth0 up");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setEthernet(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_ethernet_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(false, "ifconfig eth0 down");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setEthernet(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_bluetooth_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(false, "service call bluetooth_manager 6");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setBluetooth(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_bluetooth_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(false, "service call bluetooth_manager 8");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setBluetooth(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_adaptive_on() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"screen_brightness_mode\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenAdaptiveBrightness(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_adaptive_off() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"screen_brightness_mode\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenAdaptiveBrightness(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_brightness() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"screen_brightness\" \"50\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenBrightness(50);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_stayon_default() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations(false /* Expect no screen on command */, new Capture<String>());
+        doCheckExternalStoreSpaceExpectations();
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenAlwaysOn(BinaryState.IGNORE);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_stayon_off() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations(false /* Expect no screen on command */, new Capture<String>());
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(false, "svc power stayon false");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenAlwaysOn(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_timeout() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations(false /* Expect no screen on command */, new Capture<String>());
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"screen_off_timeout\" \"5000\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenAlwaysOn(BinaryState.IGNORE);
+        mDeviceSetup.setScreenTimeoutSecs(5l);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_ambient_on() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put secure \"doze_enabled\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenAmbientMode(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_ambient_off() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put secure \"doze_enabled\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenAmbientMode(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_wake_gesture_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put secure \"wake_gesture_enabled\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWakeGesture(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_wake_gesture_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put secure \"wake_gesture_enabled\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWakeGesture(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_saver_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put secure \"screensaver_enabled\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenSaver(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_screen_saver_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put secure \"screensaver_enabled\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setScreenSaver(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_notification_led_on() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"notification_light_pulse\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setNotificationLed(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_notification_led_off() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"notification_light_pulse\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setNotificationLed(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_trigger_media_mounted() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(false, "am broadcast -a android.intent.action.MEDIA_MOUNTED " +
+                "-d file://${EXTERNAL_STORAGE}");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setTriggerMediaMounted(true);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_location_gps_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put secure \"location_providers_allowed\" \"+gps\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setLocationGps(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_location_gps_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put secure \"location_providers_allowed\" \"-gps\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setLocationGps(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_location_network_on() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true,
+                "settings put secure \"location_providers_allowed\" \"+network\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setLocationNetwork(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_location_network_off() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true,
+                "settings put secure \"location_providers_allowed\" \"-network\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setLocationNetwork(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_rotate_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"accelerometer_rotation\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setAutoRotate(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_rotate_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"accelerometer_rotation\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setAutoRotate(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_battery_saver_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true,
+                "dumpsys battery set usb 0",
+                "settings put global \"low_power\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setBatterySaver(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_battery_saver_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put global \"low_power\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setBatterySaver(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_battery_saver_trigger() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put global \"low_power_trigger_level\" \"50\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setBatterySaverTrigger(50);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_disable_doze() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(false, "dumpsys deviceidle disable");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setDisableDoze(true);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_update_time_on() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"auto_time\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setAutoUpdateTime(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_update_time_off() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"auto_time\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setAutoUpdateTime(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_update_timezone_on() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"auto_timezone\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setAutoUpdateTimezone(BinaryState.ON);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_update_timezone_off() throws DeviceNotAvailableException,
+            TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put system \"auto_timezone\" \"0\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setAutoUpdateTimezone(BinaryState.OFF);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_no_disable_dialing() throws DeviceNotAvailableException,
+            TargetSetupError {
+        Capture<String> setPropCapture = new Capture<>();
+        doSetupExpectations(true, setPropCapture);
+        doCheckExternalStoreSpaceExpectations();
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setDisableDialing(false);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+
+        assertFalse("Set prop contains ro.telephony.disable-call=true",
+                setPropCapture.getValue().contains("ro.telephony.disable-call=true\n"));
+    }
+
+    public void testSetup_sim_data() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put global \"multi_sim_data_call\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setDefaultSimData(1);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_sim_voice() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put global \"multi_sim_voice_call\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setDefaultSimVoice(1);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_sim_sms() throws DeviceNotAvailableException, TargetSetupError {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doCommandsExpectations(true, "settings put global \"multi_sim_sms\" \"1\"");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setDefaultSimSms(1);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    public void testSetup_no_disable_audio() throws DeviceNotAvailableException, TargetSetupError {
+        Capture<String> setPropCapture = new Capture<>();
+        doSetupExpectations(true, setPropCapture);
+        doCheckExternalStoreSpaceExpectations();
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setDisableAudio(false);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+
+        assertFalse("Set prop contains ro.audio.silent=1",
+                setPropCapture.getValue().contains("ro.audio.silent=1\n"));
+    }
+
+    public void testSetup_no_test_harness() throws DeviceNotAvailableException, TargetSetupError {
+        Capture<String> setPropCapture = new Capture<>();
+        doSetupExpectations(true, setPropCapture);
+        doCheckExternalStoreSpaceExpectations();
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setTestHarness(false);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+
+        String setProp = setPropCapture.getValue();
+        assertFalse("Set prop contains ro.test_harness=1",
+                setProp.contains("ro.test_harness=1\n"));
+        assertFalse("Set prop contains ro.monkey=1",
+                setProp.contains("ro.monkey=1\n"));
+    }
+
+    public void testSetup_disalbe_dalvik_verifier() throws DeviceNotAvailableException,
+            TargetSetupError {
+        Capture<String> setPropCapture = new Capture<>();
+        doSetupExpectations(true, setPropCapture);
+        doCheckExternalStoreSpaceExpectations();
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setDisableDalvikVerifier(true);
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+
+        String setProp = setPropCapture.getValue();
+        assertTrue("Set prop doesn't contain dalvik.vm.dexopt-flags=v=n",
+                setProp.contains("dalvik.vm.dexopt-flags=v=n\n"));
     }
 
     /**
@@ -98,7 +718,7 @@
      */
     public void testSetup_freespace() throws Exception {
         doSetupExpectations();
-        mDeviceSetup.setMinExternalStoreSpace(500);
+        mDeviceSetup.setMinExternalStorageKb(500);
         EasyMock.expect(mMockDevice.getExternalStoreFreeSpace()).andReturn(1L);
         EasyMock.replay(mMockDevice);
         try {
@@ -131,23 +751,12 @@
      */
     public void testSetup_syncData() throws Exception {
         doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
         doSyncDataExpectations(true);
 
         EasyMock.replay(mMockDevice, mMockIDevice);
         mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
-    }
-
-    /**
-     * Perform common EasyMock expect operations for a setUp call which syncs local data
-     */
-    private void doSyncDataExpectations(boolean result) throws IOException,
-            DeviceNotAvailableException {
-        mDeviceSetup.setLocalDataPath(mTmpDir);
-        EasyMock.expect(mMockDevice.getIDevice()).andReturn(mMockIDevice);
-        String mntPoint = "/sdcard";
-        EasyMock.expect(mMockIDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE)).andReturn(
-                mntPoint);
-        EasyMock.expect(mMockDevice.syncFiles(mTmpDir, mntPoint)).andReturn(result);
+        EasyMock.verify(mMockDevice, mMockIDevice);
     }
 
     /**
@@ -166,20 +775,80 @@
         }
     }
 
-    public void testBuildName() {
-        assertTrue("failed to verify IML74K", mDeviceSetup.isReleaseBuildName("IML74K"));
-        assertTrue("failed to verify GRJ90", mDeviceSetup.isReleaseBuildName("GRJ90"));
-        assertFalse("failed to reject MASTER", mDeviceSetup.isReleaseBuildName("MASTER"));
-        assertFalse("failed to reject 123456", mDeviceSetup.isReleaseBuildName("123456"));
-        assertFalse("failed to reject empty string", mDeviceSetup.isReleaseBuildName(""));
-        assertFalse("failed to reject random stuff", mDeviceSetup.isReleaseBuildName("!@#$%^&*("));
+    @SuppressWarnings("deprecation")
+    public void testSetup_legacy() throws DeviceNotAvailableException, TargetSetupError {
+        Capture<String> setPropCapture = new Capture<>();
+        doSetupExpectations(true, setPropCapture);
+        doCheckExternalStoreSpaceExpectations();
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setDeprecatedAudioSilent(false);
+        mDeviceSetup.setDeprecatedMinExternalStoreSpace(1000);
+        mDeviceSetup.setDeprecatedSetProp("key=value");
+        mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+
+        EasyMock.verify(mMockDevice);
+
+        String setProp = setPropCapture.getValue();
+        assertTrue("Set prop doesn't contain ro.telephony.disable-call=true",
+                setProp.contains("ro.telephony.disable-call=true\n"));
+        assertTrue("Set prop doesn't contain ro.test_harness=1",
+                setProp.contains("ro.test_harness=1\n"));
+        assertTrue("Set prop doesn't contain ro.monkey=1",
+                setProp.contains("ro.monkey=1\n"));
+        assertTrue("Set prop doesn't contain key=value",
+                setProp.contains("key=value\n"));
+    }
+
+    @SuppressWarnings("deprecation")
+    public void testSetup_legacy_storage_conflict() throws DeviceNotAvailableException {
+        doSetupExpectations();
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setMinExternalStorageKb(1000);
+        mDeviceSetup.setDeprecatedMinExternalStoreSpace(1000);
+        try {
+            mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+            fail("TargetSetupError expected");
+        } catch (TargetSetupError e) {
+            // Expected
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    public void testSetup_legacy_silent_conflict() throws DeviceNotAvailableException {
+        doSetupExpectations();
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setDisableAudio(false);
+        mDeviceSetup.setDeprecatedAudioSilent(false);
+        try {
+            mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+            fail("TargetSetupError expected");
+        } catch (TargetSetupError e) {
+            // Expected
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    public void testSetup_legacy_setprop_conflict() throws DeviceNotAvailableException {
+        doSetupExpectations();
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setProperty("key", "value");
+        mDeviceSetup.setDeprecatedSetProp("key=value");
+        try {
+            mDeviceSetup.setUp(mMockDevice, mMockBuildInfo);
+            fail("TargetSetupError expected");
+        } catch (TargetSetupError e) {
+            // Expected
+        }
     }
 
     public void testTearDown() throws Exception {
-        EasyMock.expect(mMockDevice.isWifiEnabled()).andReturn(Boolean.TRUE);
         EasyMock.replay(mMockDevice);
-
         mDeviceSetup.tearDown(mMockDevice, mMockBuildInfo, null);
+        EasyMock.verify(mMockDevice);
     }
 
     public void testTearDown_disconnectFromWifi() throws Exception {
@@ -187,7 +856,58 @@
         EasyMock.expect(mMockDevice.disconnectFromWifi()).andReturn(Boolean.TRUE);
         mDeviceSetup.setWifiNetwork("wifi_network");
         EasyMock.replay(mMockDevice);
-
         mDeviceSetup.tearDown(mMockDevice, mMockBuildInfo, null);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Set EasyMock expectations for a normal setup call
+     */
+    private void doSetupExpectations() throws DeviceNotAvailableException {
+        doSetupExpectations(true, new Capture<String>());
+    }
+
+    /**
+     * Set EasyMock expectations for a normal setup call
+     */
+    private void doSetupExpectations(boolean screenOn, Capture<String> setPropCapture)
+            throws DeviceNotAvailableException {
+        EasyMock.expect(mMockDevice.enableAdbRoot()).andReturn(Boolean.TRUE);
+        EasyMock.expect(mMockDevice.clearErrorDialogs()).andReturn(Boolean.TRUE);
+        // expect push of local.prop file to change system properties
+        EasyMock.expect(mMockDevice.pushString(EasyMock.capture(setPropCapture),
+                EasyMock.contains("local.prop"))).andReturn(Boolean.TRUE);
+        EasyMock.expect(mMockDevice.executeShellCommand(
+                EasyMock.matches("chmod 644 .*local.prop"))).andReturn("");
+        mMockDevice.reboot();
+        if (screenOn) {
+            EasyMock.expect(mMockDevice.executeShellCommand("svc power stayon true")).andReturn("");
+        }
+    }
+
+    /**
+     * Perform common EasyMock expect operations for a setUp call which syncs local data
+     */
+    private void doSyncDataExpectations(boolean result) throws DeviceNotAvailableException {
+        mDeviceSetup.setLocalDataPath(mTmpDir);
+        EasyMock.expect(mMockDevice.getIDevice()).andReturn(mMockIDevice);
+        String mntPoint = "/sdcard";
+        EasyMock.expect(mMockIDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE)).andReturn(
+                mntPoint);
+        EasyMock.expect(mMockDevice.syncFiles(mTmpDir, mntPoint)).andReturn(result);
+    }
+
+    private void doCheckExternalStoreSpaceExpectations() throws DeviceNotAvailableException {
+        EasyMock.expect(mMockDevice.getExternalStoreFreeSpace()).andReturn(1000l);
+    }
+
+    private void doCommandsExpectations(boolean settings, String... commands)
+            throws DeviceNotAvailableException {
+        if (settings) {
+            EasyMock.expect(mMockDevice.getApiLevel()).andReturn(22);
+        }
+        for (String command : commands) {
+            EasyMock.expect(mMockDevice.executeShellCommand(command)).andReturn("");
+        }
     }
 }
diff --git a/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java b/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java
index 031528d..ef9ef7a 100644
--- a/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.tradefed.targetprep;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.build.DeviceBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
@@ -61,8 +60,8 @@
             public void run(ITestInvocationListener listener) {
                 listener.testRunStarted(packageName, 1);
                 listener.testStarted(test);
-                listener.testEnded(test, Collections.EMPTY_MAP);
-                listener.testRunEnded(0, Collections.EMPTY_MAP);
+                listener.testEnded(test, Collections.<String, String>emptyMap());
+                listener.testRunEnded(0, Collections.<String, String>emptyMap());
             }
         };
         mInstrumentationPreparer = new InstrumentationPreparer() {
@@ -75,7 +74,7 @@
         mInstrumentationPreparer.setUp(mMockDevice, mMockBuildInfo);
     }
 
-    public void testRun_testError() throws Exception {
+    public void testRun_testFailed() throws Exception {
         final String packageName = "packageName";
         final TestIdentifier test = new TestIdentifier("FooTest", "testFoo");
         mMockITest = new InstrumentationTest() {
@@ -83,9 +82,9 @@
             public void run(ITestInvocationListener listener) {
                 listener.testRunStarted(packageName, 1);
                 listener.testStarted(test);
-                listener.testFailed(TestFailure.ERROR, test, null);
-                listener.testEnded(test, Collections.EMPTY_MAP);
-                listener.testRunEnded(0, Collections.EMPTY_MAP);
+                listener.testFailed(test, null);
+                listener.testEnded(test, Collections.<String, String>emptyMap());
+                listener.testRunEnded(0, Collections.<String, String>emptyMap());
             }
         };
         mInstrumentationPreparer = new InstrumentationPreparer() {
diff --git a/tests/src/com/android/tradefed/targetprep/TestFilePushSetupFuncTest.java b/tests/src/com/android/tradefed/targetprep/TestFilePushSetupFuncTest.java
index 2c181f1..7cfec7d 100644
--- a/tests/src/com/android/tradefed/targetprep/TestFilePushSetupFuncTest.java
+++ b/tests/src/com/android/tradefed/targetprep/TestFilePushSetupFuncTest.java
@@ -22,6 +22,7 @@
 import com.android.tradefed.device.StubTestDevice;
 import com.android.tradefed.util.FakeTestsZipFolder;
 import com.android.tradefed.util.FakeTestsZipFolder.ItemType;
+import com.google.common.io.Files;
 
 import junit.framework.TestCase;
 
@@ -39,6 +40,9 @@
     private Map<String, ItemType> mFiles;
     private List<String> mDeviceLocationList;
     private FakeTestsZipFolder mFakeTestsZipFolder;
+    private File mAltDirFile1, mAltDirFile2;
+    private static final String ALT_FILENAME1 = "foobar";
+    private static final String ALT_FILENAME2 = "barfoo";
 
     @Override
     protected void setUp() throws Exception {
@@ -47,12 +51,18 @@
         mFiles.put("app/AndroidCommonTests.apk", ItemType.FILE);
         mFiles.put("app/GalleryTests.apk", ItemType.FILE);
         mFiles.put("testinfo", ItemType.DIRECTORY);
+        mFiles.put(ALT_FILENAME1, ItemType.FILE);
         mFakeTestsZipFolder = new FakeTestsZipFolder(mFiles);
         assertTrue(mFakeTestsZipFolder.createItems());
         mDeviceLocationList = new ArrayList<String>();
         for (String file : mFiles.keySet()) {
             mDeviceLocationList.add(TestFilePushSetup.getDevicePathFromUserData(file));
         }
+        File tmpBase = Files.createTempDir();
+        mAltDirFile1 = new File(tmpBase, ALT_FILENAME1);
+        assertTrue("failed to create temp file", mAltDirFile1.createNewFile());
+        mAltDirFile2 = new File(tmpBase, ALT_FILENAME2);
+        assertTrue("failed to create temp file", mAltDirFile2.createNewFile());
     }
 
     public void testSetup() throws TargetSetupError, BuildError, DeviceNotAvailableException {
@@ -86,9 +96,55 @@
         assertTrue(mDeviceLocationList.isEmpty());
     }
 
+    /**
+     * Test that the artifact path resolution logic correctly uses alt dir as override
+     * @throws Exception
+     */
+    public void testAltDirOverride() throws Exception {
+        TestFilePushSetup setup = new TestFilePushSetup();
+        setup.setAltDirBehavior(AltDirBehavior.OVERRIDE);
+        DeviceBuildInfo info = new DeviceBuildInfo();
+        info.setTestsDir(mFakeTestsZipFolder.getBasePath(), "0");
+        setup.setAltDir(mAltDirFile1.getParentFile());
+        File apk = setup.getLocalPathForFilename(info, ALT_FILENAME1);
+        assertEquals(mAltDirFile1.getAbsolutePath(), apk.getAbsolutePath());
+    }
+
+    /**
+     * Test that the artifact path resolution logic correctly favors the one in test dir
+     * @throws Exception
+     */
+    public void testAltDirNoFallback() throws Exception {
+        TestFilePushSetup setup = new TestFilePushSetup();
+        DeviceBuildInfo info = new DeviceBuildInfo();
+        info.setTestsDir(mFakeTestsZipFolder.getBasePath(), "0");
+        setup.setAltDir(mAltDirFile1.getParentFile());
+        File apk = setup.getLocalPathForFilename(info, ALT_FILENAME1);
+        File apkInTestDir = new File(
+                new File(mFakeTestsZipFolder.getBasePath(), "DATA"), ALT_FILENAME1);
+        assertEquals(apkInTestDir.getAbsolutePath(), apk.getAbsolutePath());
+    }
+
+    /**
+     * Test that the artifact path resolution logic correctly falls back to alt dir
+     * @throws Exception
+     */
+    public void testAltDirFallback() throws Exception {
+        TestFilePushSetup setup = new TestFilePushSetup();
+        DeviceBuildInfo info = new DeviceBuildInfo();
+        info.setTestsDir(mFakeTestsZipFolder.getBasePath(), "0");
+        setup.setAltDir(mAltDirFile2.getParentFile());
+        File apk = setup.getLocalPathForFilename(info, ALT_FILENAME2);
+        assertEquals(mAltDirFile2.getAbsolutePath(), apk.getAbsolutePath());
+    }
+
     @Override
     protected void tearDown() throws Exception {
         mFakeTestsZipFolder.cleanUp();
+        File tmpDir = mAltDirFile1.getParentFile();
+        mAltDirFile1.delete();
+        mAltDirFile2.delete();
+        tmpDir.delete();
         super.tearDown();
     }
 }
diff --git a/tests/src/com/android/tradefed/testtype/DeviceTestCaseTest.java b/tests/src/com/android/tradefed/testtype/DeviceTestCaseTest.java
index b99a6a1..423e1d8 100644
--- a/tests/src/com/android/tradefed/testtype/DeviceTestCaseTest.java
+++ b/tests/src/com/android/tradefed/testtype/DeviceTestCaseTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.testtype;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -101,7 +100,7 @@
         final TestIdentifier test1 = new TestIdentifier(MockAbortTest.class.getName(), "test1");
         listener.testRunStarted(MockAbortTest.class.getName(), 1);
         listener.testStarted(test1);
-        listener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(test1),
+        listener.testFailed(EasyMock.eq(test1),
                 EasyMock.contains(MockAbortTest.EXCEP_MSG));
         listener.testEnded(test1, Collections.EMPTY_MAP);
         listener.testRunFailed(EasyMock.contains(MockAbortTest.EXCEP_MSG));
diff --git a/tests/src/com/android/tradefed/testtype/DeviceTestSuiteTest.java b/tests/src/com/android/tradefed/testtype/DeviceTestSuiteTest.java
index 0d8680a..229e911 100644
--- a/tests/src/com/android/tradefed/testtype/DeviceTestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/DeviceTestSuiteTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.testtype;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -84,7 +83,7 @@
         final TestIdentifier test1 = new TestIdentifier(MockAbortTest.class.getName(), "test1");
         listener.testRunStarted(DeviceTestSuite.class.getName(), 1);
         listener.testStarted(test1);
-        listener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(test1),
+        listener.testFailed(EasyMock.eq(test1),
                 EasyMock.contains(MockAbortTest.EXCEP_MSG));
         listener.testEnded(test1, Collections.EMPTY_MAP);
         listener.testRunFailed(EasyMock.contains(MockAbortTest.EXCEP_MSG));
diff --git a/tests/src/com/android/tradefed/testtype/FakeTestTest.java b/tests/src/com/android/tradefed/testtype/FakeTestTest.java
index ad620b3..2d05367 100644
--- a/tests/src/com/android/tradefed/testtype/FakeTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/FakeTestTest.java
@@ -16,7 +16,6 @@
 
 package com.android.tradefed.testtype;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -164,31 +163,17 @@
         EasyMock.verify(mListener);
     }
 
-    public void testRun_simpleError() throws Exception {
-        final String name = "com.moo.cow";
-        mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(1));
-        testErrorExpectations(mListener, name, 1);
-        mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
-
-        EasyMock.replay(mListener);
-        mOption.setOptionMapValue("run", name, "E");
-        mTest.run(mListener);
-        EasyMock.verify(mListener);
-    }
-
     public void testRun_basicSequence() throws Exception {
         final String name = "com.moo.cow";
         int i = 1;
-        mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(5));
+        mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(3));
         testPassExpectations(mListener, name, i++);
         testFailExpectations(mListener, name, i++);
-        testErrorExpectations(mListener, name, i++);
-        testFailExpectations(mListener, name, i++);
         testPassExpectations(mListener, name, i++);
         mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
 
         EasyMock.replay(mListener);
-        mOption.setOptionMapValue("run", name, "PFEFP");
+        mOption.setOptionMapValue("run", name, "PFP");
         mTest.run(mListener);
         EasyMock.verify(mListener);
     }
@@ -212,7 +197,7 @@
     public void testRun_recursiveParens() throws Exception {
         final String name = "com.moo.cow";
         int i = 1;
-        mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(11));
+        mListener.testRunStarted(EasyMock.eq(name), EasyMock.eq(8));
         testPassExpectations(mListener, name, i++);
         testFailExpectations(mListener, name, i++);
 
@@ -225,14 +210,10 @@
         testPassExpectations(mListener, name, i++);
         testFailExpectations(mListener, name, i++);
 
-        testErrorExpectations(mListener, name, i++);
-        testErrorExpectations(mListener, name, i++);
-        testErrorExpectations(mListener, name, i++);
-
         mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
 
         EasyMock.replay(mListener);
-        mOption.setOptionMapValue("run", name, "((PF)2)2E3");
+        mOption.setOptionMapValue("run", name, "((PF)2)2");
         mTest.run(mListener);
         EasyMock.verify(mListener);
     }
@@ -240,16 +221,14 @@
     public void testMultiRun() throws Exception {
         final String name1 = "com.moo.cow";
         int i = 1;
-        mListener.testRunStarted(EasyMock.eq(name1), EasyMock.eq(3));
+        mListener.testRunStarted(EasyMock.eq(name1), EasyMock.eq(2));
         testPassExpectations(mListener, name1, i++);
         testFailExpectations(mListener, name1, i++);
-        testErrorExpectations(mListener, name1, i++);
         mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
 
         final String name2 = "com.quack.duck";
         i = 1;
-        mListener.testRunStarted(EasyMock.eq(name2), EasyMock.eq(3));
-        testErrorExpectations(mListener, name2, i++);
+        mListener.testRunStarted(EasyMock.eq(name2), EasyMock.eq(2));
         testFailExpectations(mListener, name2, i++);
         testPassExpectations(mListener, name2, i++);
         mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
@@ -261,8 +240,8 @@
         mListener.testRunEnded(EasyMock.eq(0l), EasyMock.<Map<String, String>>anyObject());
 
         EasyMock.replay(mListener);
-        mOption.setOptionMapValue("run", name1, "PFE");
-        mOption.setOptionMapValue("run", name2, "EFP");
+        mOption.setOptionMapValue("run", name1, "PF");
+        mOption.setOptionMapValue("run", name2, "FP");
         mOption.setOptionMapValue("run", name3, "");
         mTest.run(mListener);
         EasyMock.verify(mListener);
@@ -281,18 +260,7 @@
         final String name = String.format("testMethod%d", idx);
         final TestIdentifier test = new TestIdentifier(klass, name);
         l.testStarted(test);
-        l.testFailed(EasyMock.eq(TestFailure.FAILURE), EasyMock.eq(test),
-                EasyMock.<String>anyObject());
-        l.testEnded(EasyMock.eq(test), EasyMock.<Map<String, String>>anyObject());
-    }
-
-    private void testErrorExpectations(ITestInvocationListener l, String klass,
-            int idx) {
-        final String name = String.format("testMethod%d", idx);
-        final TestIdentifier test = new TestIdentifier(klass, name);
-        l.testStarted(test);
-        l.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(test),
-                EasyMock.<String>anyObject());
+        l.testFailed(EasyMock.eq(test), EasyMock.<String>anyObject());
         l.testEnded(EasyMock.eq(test), EasyMock.<Map<String, String>>anyObject());
     }
 }
diff --git a/tests/src/com/android/tradefed/testtype/GTestFuncTest.java b/tests/src/com/android/tradefed/testtype/GTestFuncTest.java
index ae00761..b930b02 100644
--- a/tests/src/com/android/tradefed/testtype/GTestFuncTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestFuncTest.java
@@ -17,7 +17,6 @@
 package com.android.tradefed.testtype;
 
 import com.android.ddmlib.Log;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -80,7 +79,7 @@
             mMockListener.testStarted(id);
 
             if (testName.endsWith("Fail")) {
-              mMockListener.testFailed(EasyMock.eq(TestFailure.FAILURE),
+              mMockListener.testFailed(
                       EasyMock.eq(id),
                       EasyMock.isA(String.class));
             }
@@ -103,7 +102,7 @@
         mGTest.setModuleName(NATIVE_TESTAPP_MODULE_NAME);
         mMockListener.testRunStarted(NATIVE_TESTAPP_MODULE_NAME, 1);
         mMockListener.testStarted(EasyMock.eq(testId));
-        mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(testId),
+        mMockListener.testFailed(EasyMock.eq(testId),
                 EasyMock.isA(String.class));
         mMockListener.testEnded(EasyMock.eq(testId), EasyMock.eq(emptyMap));
         mMockListener.testRunFailed((String)EasyMock.anyObject());
diff --git a/tests/src/com/android/tradefed/testtype/GTestResultParserTest.java b/tests/src/com/android/tradefed/testtype/GTestResultParserTest.java
index 09aa151..cb50d59 100644
--- a/tests/src/com/android/tradefed/testtype/GTestResultParserTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestResultParserTest.java
@@ -170,7 +170,7 @@
                     (Map<String, String>)EasyMock.anyObject());
         // test failure
         mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
-        mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.FAILURE),
+        mockRunListener.testFailed(
                 (TestIdentifier)EasyMock.anyObject(), (String)EasyMock.anyObject());
         mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
                 (Map<String, String>)EasyMock.anyObject());
@@ -182,13 +182,13 @@
         }
         // 2 consecutive test failures
         mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
-        mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.FAILURE),
+        mockRunListener.testFailed(
                 (TestIdentifier)EasyMock.anyObject(), (String)EasyMock.anyObject());
         mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
                 (Map<String, String>)EasyMock.anyObject());
 
         mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
-        mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.FAILURE),
+        mockRunListener.testFailed(
                 (TestIdentifier)EasyMock.anyObject(), EasyMock.matches(MESSAGE_OUTPUT));
         mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
                 (Map<String, String>)EasyMock.anyObject());
@@ -223,7 +223,7 @@
                     (Map<String, String>)EasyMock.anyObject());
         // test failure
         mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
-        mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.ERROR),
+        mockRunListener.testFailed(
                 (TestIdentifier)EasyMock.anyObject(), (String)EasyMock.anyObject());
         mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
                 (Map<String, String>)EasyMock.anyObject());
@@ -235,7 +235,7 @@
         }
         // another test error
         mockRunListener.testStarted((TestIdentifier)EasyMock.anyObject());
-        mockRunListener.testFailed(EasyMock.eq(ITestRunListener.TestFailure.ERROR),
+        mockRunListener.testFailed(
                 (TestIdentifier)EasyMock.anyObject(), (String)EasyMock.anyObject());
         mockRunListener.testEnded((TestIdentifier)EasyMock.anyObject(),
                 (Map<String, String>)EasyMock.anyObject());
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java
index 45eda48..e546056 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationSerialTestTest.java
@@ -15,28 +15,19 @@
  */
 package com.android.tradefed.testtype;
 
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.util.FileUtil;
 
 import junit.framework.TestCase;
 
 import org.easymock.EasyMock;
 
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 
 /**
  * Unit tests for {@link InstrumentationSerialTest}.
@@ -143,7 +134,7 @@
 
         // now expect test to be marked as failed
         mMockListener.testStarted(test);
-        mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(test),
+        mMockListener.testFailed(EasyMock.eq(test),
                 EasyMock.contains(runFailureMsg));
         mMockListener.testEnded(test, Collections.EMPTY_MAP);
 
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestFuncTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestFuncTest.java
index 137a158..04e8d5c 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestFuncTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestFuncTest.java
@@ -17,8 +17,8 @@
 package com.android.tradefed.testtype;
 
 import com.android.ddmlib.Log;
-import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.TestAppConstants;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.result.CollectingTestListener;
@@ -49,7 +49,7 @@
         mInstrumentationTest.setPackageName(TestAppConstants.TESTAPP_PACKAGE);
         mInstrumentationTest.setDevice(getDevice());
         // use no timeout by default
-        mInstrumentationTest.setTestTimeout(-1);
+        mInstrumentationTest.setShellTimeout(-1);
         // set to no rerun by default
         mInstrumentationTest.setRerunMode(false);
         mMockListener = EasyMock.createStrictMock(ITestInvocationListener.class);
@@ -88,7 +88,7 @@
         mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
         mMockListener.testStarted(EasyMock.eq(expectedTest));
         // TODO: add stricter checking on stackTrace
-        mMockListener.testFailed(EasyMock.eq(TestFailure.FAILURE), EasyMock.eq(expectedTest),
+        mMockListener.testFailed(EasyMock.eq(expectedTest),
                 (String)EasyMock.anyObject());
         mMockListener.testEnded(EasyMock.eq(expectedTest),
                     (Map<String, String>)EasyMock.anyObject());
@@ -110,7 +110,7 @@
         mInstrumentationTest.setMethodName(TestAppConstants.CRASH_TEST_METHOD);
         mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
         mMockListener.testStarted(EasyMock.eq(expectedTest));
-        mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(expectedTest),
+        mMockListener.testFailed(EasyMock.eq(expectedTest),
                 (String)EasyMock.anyObject());
         mMockListener.testEnded(EasyMock.eq(expectedTest),
                     (Map<String, String>)EasyMock.anyObject());
@@ -132,10 +132,10 @@
                 TestAppConstants.TIMEOUT_TEST_METHOD);
         mInstrumentationTest.setClassName(TestAppConstants.TESTAPP_CLASS);
         mInstrumentationTest.setMethodName(TestAppConstants.TIMEOUT_TEST_METHOD);
-        mInstrumentationTest.setTestTimeout(timeout);
+        mInstrumentationTest.setShellTimeout(timeout);
         mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
         mMockListener.testStarted(EasyMock.eq(expectedTest));
-        mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(expectedTest),
+        mMockListener.testFailed(EasyMock.eq(expectedTest),
                 (String)EasyMock.anyObject());
         mMockListener.testEnded(EasyMock.eq(expectedTest),
                 (Map<String, String>)EasyMock.anyObject());
@@ -159,7 +159,7 @@
         mInstrumentationTest.setMethodName(TestAppConstants.TIMEOUT_TEST_METHOD);
         mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
         mMockListener.testStarted(EasyMock.eq(expectedTest));
-        mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(expectedTest),
+        mMockListener.testFailed(EasyMock.eq(expectedTest),
                 (String)EasyMock.anyObject());
         mMockListener.testEnded(EasyMock.eq(expectedTest),
                     (Map<String, String>)EasyMock.anyObject());
@@ -194,7 +194,8 @@
         CollectingTestListener uilistener = new CollectingTestListener();
         uiTest.run(uilistener);
         assertFalse(uilistener.hasFailedTests());
-        assertEquals(TestAppConstants.UI_TOTAL_TESTS, uilistener.getNumPassedTests());
+        assertEquals(TestAppConstants.UI_TOTAL_TESTS,
+                uilistener.getNumTestsInState(TestStatus.PASSED));
     }
 
     /**
@@ -212,7 +213,7 @@
         mInstrumentationTest.setMethodName(TestAppConstants.TIMEOUT_TEST_METHOD);
         mMockListener.testRunStarted(TestAppConstants.TESTAPP_PACKAGE, 1);
         mMockListener.testStarted(EasyMock.eq(expectedTest));
-        mMockListener.testFailed(EasyMock.eq(TestFailure.ERROR), EasyMock.eq(expectedTest),
+        mMockListener.testFailed(EasyMock.eq(expectedTest),
                 (String)EasyMock.anyObject());
         mMockListener.testEnded(EasyMock.eq(expectedTest),
                     (Map<String, String>)EasyMock.anyObject());
@@ -251,7 +252,8 @@
         CollectingTestListener uilistener = new CollectingTestListener();
         uiTest.run(uilistener);
         assertFalse(uilistener.hasFailedTests());
-        assertEquals(TestAppConstants.UI_TOTAL_TESTS, uilistener.getNumPassedTests());
+        assertEquals(TestAppConstants.UI_TOTAL_TESTS,
+                uilistener.getNumTestsInState(TestStatus.PASSED));
     }
 
     /**
@@ -265,11 +267,12 @@
         // run all tests in class
         mInstrumentationTest.setClassName(TestAppConstants.TESTAPP_CLASS);
         mInstrumentationTest.setRerunMode(true);
-        mInstrumentationTest.setTestTimeout(1000);
+        mInstrumentationTest.setShellTimeout(1000);
         CollectingTestListener listener = new CollectingTestListener();
         mInstrumentationTest.run(listener);
         assertEquals(TestAppConstants.TOTAL_TEST_CLASS_TESTS, listener.getNumTotalTests());
-        assertEquals(TestAppConstants.TOTAL_TEST_CLASS_PASSED_TESTS, listener.getNumPassedTests());
+        assertEquals(TestAppConstants.TOTAL_TEST_CLASS_PASSED_TESTS,
+                listener.getNumTestsInState(TestStatus.PASSED));
     }
 
     /**
@@ -283,7 +286,7 @@
         mInstrumentationTest.setClassName(TestAppConstants.CRASH_ON_INIT_TEST_CLASS);
         mInstrumentationTest.setMethodName(TestAppConstants.CRASH_ON_INIT_TEST_METHOD);
         mInstrumentationTest.setRerunMode(true);
-        mInstrumentationTest.setTestTimeout(1000);
+        mInstrumentationTest.setShellTimeout(1000);
         CollectingTestListener listener = new CollectingTestListener();
         mInstrumentationTest.run(listener);
         assertEquals(0, listener.getNumTotalTests());
@@ -303,7 +306,7 @@
 
         mInstrumentationTest.setClassName(TestAppConstants.HANG_ON_INIT_TEST_CLASS);
         mInstrumentationTest.setRerunMode(true);
-        mInstrumentationTest.setTestTimeout(1000);
+        mInstrumentationTest.setShellTimeout(1000);
         mInstrumentationTest.setCollectsTestsShellTimeout(2 * 1000);
         CollectingTestListener listener = new CollectingTestListener();
         mInstrumentationTest.run(listener);
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
index 0f44ab8..e7cb6d8 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
@@ -39,6 +39,7 @@
 public class InstrumentationTestTest extends TestCase {
 
     private static final int TEST_TIMEOUT = 0;
+    private static final long SHELL_TIMEOUT = 0;
     private static final String TEST_PACKAGE_VALUE = "com.foo";
     private static final String TEST_RUNNER_VALUE = ".FooRunner";
     private static final TestIdentifier TEST1 = new TestIdentifier("Test", "test1");
@@ -107,15 +108,18 @@
                 return mMockRemoteRunner;
             }
         };
-       mInstrumentationTest.setPackageName(TEST_PACKAGE_VALUE);
-       mInstrumentationTest.setRunnerName(TEST_RUNNER_VALUE);
-       mInstrumentationTest.setDevice(mMockTestDevice);
-       // default to no rerun, for simplicity
-       mInstrumentationTest.setRerunMode(false);
-       // default to no timeout for simplicity
-       mInstrumentationTest.setTestTimeout(TEST_TIMEOUT);
-       mMockRemoteRunner.setMaxTimeToOutputResponse(0, TimeUnit.MILLISECONDS);
-       mInstrumentationTest.setCollectsTestsShellTimeout(COLLECT_TESTS_SHELL_TIMEOUT);
+        mInstrumentationTest.setPackageName(TEST_PACKAGE_VALUE);
+        mInstrumentationTest.setRunnerName(TEST_RUNNER_VALUE);
+        mInstrumentationTest.setDevice(mMockTestDevice);
+        // default to no rerun, for simplicity
+        mInstrumentationTest.setRerunMode(false);
+        // default to no timeout for simplicity
+        mInstrumentationTest.setTestTimeout(TEST_TIMEOUT);
+        mInstrumentationTest.setShellTimeout(SHELL_TIMEOUT);
+        mMockRemoteRunner.setMaxTimeToOutputResponse(SHELL_TIMEOUT, TimeUnit.MILLISECONDS);
+        mMockRemoteRunner.addInstrumentationArg(InstrumentationTest.TEST_TIMEOUT_INST_ARGS_KEY,
+                Long.toString(SHELL_TIMEOUT));
+        mInstrumentationTest.setCollectsTestsShellTimeout(COLLECT_TESTS_SHELL_TIMEOUT);
     }
 
     /**
@@ -227,12 +231,8 @@
      */
     public void testRun_rerunEmpty() throws Exception {
         mInstrumentationTest.setRerunMode(true);
-        // expect log only mode run first to collect tests
-        mMockRemoteRunner.setLogOnly(true);
-        mMockRemoteRunner.addInstrumentationArg(InstrumentationTest.DELAY_MSEC_ARG,
-                Long.toString(mInstrumentationTest.getTestDelay()));
-        mMockRemoteRunner.setMaxTimeToOutputResponse(
-                COLLECT_TESTS_SHELL_TIMEOUT, TimeUnit.MILLISECONDS);
+        // expect test collection mode run first to collect tests
+        mMockRemoteRunner.setTestCollection(true);
         // collect tests run
         CollectTestAnswer collectTestResponse = new CollectTestAnswer() {
             @Override
@@ -244,9 +244,8 @@
         };
         setCollectTestsExpectations(collectTestResponse);
         // expect normal mode to be turned back on
-        mMockRemoteRunner.setLogOnly(false);
-        mMockRemoteRunner.removeInstrumentationArg(InstrumentationTest.DELAY_MSEC_ARG);
-        mMockRemoteRunner.setMaxTimeToOutputResponse(0, TimeUnit.MILLISECONDS);
+        mMockRemoteRunner.setMaxTimeToOutputResponse(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+        mMockRemoteRunner.setTestCollection(false);
 
         // note: expect run to not be reported
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice, mMockListener);
@@ -296,7 +295,9 @@
             }
         };
         setRerunExpectations(firstRunResponse);
-        mMockRemoteRunner.setMaxTimeToOutputResponse(0, TimeUnit.MILLISECONDS);
+        mMockRemoteRunner.setMaxTimeToOutputResponse(SHELL_TIMEOUT, TimeUnit.MILLISECONDS);
+        mMockRemoteRunner.addInstrumentationArg(InstrumentationTest.TEST_TIMEOUT_INST_ARGS_KEY,
+                Long.toString(SHELL_TIMEOUT));
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice, mMockListener);
         try {
             mInstrumentationTest.run(mMockListener);
@@ -308,23 +309,32 @@
         EasyMock.verify(mMockRemoteRunner, mMockTestDevice, mMockListener);
     }
 
-
+    /**
+     * Test that IllegalArgumentException is thrown when attempting run with negative timeout args.
+     */
+    public void testRun_negativeTimeouts() throws Exception {
+        mInstrumentationTest.setShellTimeout(-1);
+        mInstrumentationTest.setTestTimeout(-2);
+        EasyMock.replay(mMockRemoteRunner);
+        try {
+            mInstrumentationTest.run(mMockListener);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
 
     /**
      * Set EasyMock expectations for a run that fails.
      *
-     * @param firstRunResponse the behavior callback of the first run. It should perform callbacks
+     * @param firstRunAnswer the behavior callback of the first run. It should perform callbacks
      * on listeners to indicate only TEST1 was run
      */
     private void setRerunExpectations(RunTestAnswer firstRunAnswer)
             throws DeviceNotAvailableException {
         mInstrumentationTest.setRerunMode(true);
-        // expect log only mode run first to collect tests
-        mMockRemoteRunner.setLogOnly(true);
-        mMockRemoteRunner.addInstrumentationArg(InstrumentationTest.DELAY_MSEC_ARG,
-                Long.toString(mInstrumentationTest.getTestDelay()));
-        mMockRemoteRunner.setMaxTimeToOutputResponse(
-                COLLECT_TESTS_SHELL_TIMEOUT, TimeUnit.MILLISECONDS);
+        // expect test collection mode run first to collect tests
+        mMockRemoteRunner.setTestCollection(true);
         CollectTestAnswer collectTestAnswer = new CollectTestAnswer() {
             @Override
             public Boolean answer(IRemoteAndroidTestRunner runner, ITestRunListener listener) {
@@ -340,10 +350,9 @@
         };
         setCollectTestsExpectations(collectTestAnswer);
 
-        // now expect second run with log only mode off
-        mMockRemoteRunner.setLogOnly(false);
-        mMockRemoteRunner.removeInstrumentationArg(InstrumentationTest.DELAY_MSEC_ARG);
+        // now expect second run with test collection mode off
         mMockRemoteRunner.setMaxTimeToOutputResponse(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+        mMockRemoteRunner.setTestCollection(false);
         setRunTestExpectations(firstRunAnswer);
 
         // now expect second run to run remaining test
diff --git a/tests/src/com/android/tradefed/util/AaptParserTest.java b/tests/src/com/android/tradefed/util/AaptParserTest.java
index b357222..d825c40 100644
--- a/tests/src/com/android/tradefed/util/AaptParserTest.java
+++ b/tests/src/com/android/tradefed/util/AaptParserTest.java
@@ -22,13 +22,31 @@
  */
 public class AaptParserTest extends TestCase {
 
-    public void testParsePackageName() {
+    public void testParsePackageNameVersionLabel() {
         AaptParser p = new AaptParser();
         p.parse("package: name='com.android.foo' versionCode='13' versionName='2.3'\n" +
             "sdkVersion:'5'\n" +
+            "application-label:'Foo'\n" +
+            "application-label-fr:'Faa'\n"+
             "uses-permission:'android.permission.INTERNET'");
         assertEquals("com.android.foo", p.getPackageName());
         assertEquals("13", p.getVersionCode());
         assertEquals("2.3", p.getVersionName());
+        assertEquals("Foo", p.getLabel());
+    }
+
+    public void testParseVersionMultipleFieldsNoLabel() {
+        AaptParser p = new AaptParser();
+        p.parse("package: name='com.android.foo' versionCode='217173' versionName='1.7173' " +
+                "platformBuildVersionName=''\n" +
+                "install-location:'preferExternal'\n" +
+                "sdkVersion:'10'\n" +
+                "targetSdkVersion:'21'\n" +
+                "uses-permission: name='android.permission.INTERNET'\n" +
+                "uses-permission: name='android.permission.ACCESS_NETWORK_STATE'\n");
+        assertEquals("com.android.foo", p.getPackageName());
+        assertEquals("217173", p.getVersionCode());
+        assertEquals("1.7173", p.getVersionName());
+        assertEquals("com.android.foo", p.getLabel());
     }
 }
diff --git a/tests/src/com/android/tradefed/util/AbiFormatterTest.java b/tests/src/com/android/tradefed/util/AbiFormatterTest.java
index 520b222..67dea05 100644
--- a/tests/src/com/android/tradefed/util/AbiFormatterTest.java
+++ b/tests/src/com/android/tradefed/util/AbiFormatterTest.java
@@ -48,6 +48,7 @@
         ITestDevice device = EasyMock.createMock(ITestDevice.class);
 
         EasyMock.expect(device.getProperty("ro.product.cpu.abilist32")).andReturn(null);
+        EasyMock.expect(device.getProperty("ro.product.cpu.abi")).andReturn(null);
         EasyMock.replay(device);
         assertEquals(null, AbiFormatter.getDefaultAbi(device, "32"));
 
@@ -59,7 +60,35 @@
 
         EasyMock.reset(device);
         EasyMock.expect(device.getProperty(EasyMock.eq("ro.product.cpu.abilist64"))).andReturn("");
+        EasyMock.expect(device.getProperty("ro.product.cpu.abi")).andReturn(null);
         EasyMock.replay(device);
         assertEquals(null, AbiFormatter.getDefaultAbi(device, "64"));
     }
+
+    /**
+     * Verify that {@link AbiFormatter#getSupportedAbis} works as expected.
+     */
+    public void testGetSupportedAbis() throws Exception {
+        ITestDevice device = EasyMock.createMock(ITestDevice.class);
+
+        EasyMock.expect(device.getProperty("ro.product.cpu.abilist32")).andReturn("abi1,abi2");
+        EasyMock.replay(device);
+        String[] supportedAbiArray = AbiFormatter.getSupportedAbis(device, "32");
+        assertEquals("abi1", supportedAbiArray[0]);
+        assertEquals("abi2", supportedAbiArray[1]);
+
+        EasyMock.reset(device);
+        EasyMock.expect(device.getProperty("ro.product.cpu.abilist32")).andReturn(null);
+        EasyMock.expect(device.getProperty("ro.product.cpu.abi")).andReturn("abi");
+        EasyMock.replay(device);
+        supportedAbiArray = AbiFormatter.getSupportedAbis(device, "32");
+        assertEquals("abi", supportedAbiArray[0]);
+
+        EasyMock.reset(device);
+        EasyMock.expect(device.getProperty("ro.product.cpu.abilist")).andReturn("");
+        EasyMock.expect(device.getProperty("ro.product.cpu.abi")).andReturn("abi");
+        EasyMock.replay(device);
+        supportedAbiArray = AbiFormatter.getSupportedAbis(device, "");
+        assertEquals("abi", supportedAbiArray[0]);
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/FileUtilFuncTest.java b/tests/src/com/android/tradefed/util/FileUtilFuncTest.java
index 21c9f4f..a7dc5ab 100644
--- a/tests/src/com/android/tradefed/util/FileUtilFuncTest.java
+++ b/tests/src/com/android/tradefed/util/FileUtilFuncTest.java
@@ -293,6 +293,22 @@
         }
     }
 
+    /**
+     * Verify {@link FileUtil#calculateMd5(File)} works.
+     * @throws IOException
+     */
+    public void testCalculateMd5() throws IOException {
+        final String source = "testtesttesttesttest";
+        final String md5 = "f317f682fafe0309c6a423af0b4efa59";
+        File tmpFile = FileUtil.createTempFile("testCalculateMd5", ".txt");
+        try {
+            FileUtil.writeToFile(source, tmpFile);
+            String actualMd5 = FileUtil.calculateMd5(tmpFile);
+            assertEquals(md5, actualMd5);
+        } finally {
+            FileUtil.deleteFile(tmpFile);
+        }
+    }
 
     // Assertions
     private String assertUnixPerms(File file, String expPerms) {
diff --git a/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java b/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java
index 3a29a04..00d098b 100644
--- a/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java
+++ b/tests/src/com/android/tradefed/util/JUnitXmlParserTest.java
@@ -17,9 +17,9 @@
 package com.android.tradefed.util;
 
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestRunResult;
 import com.android.tradefed.result.CollectingTestListener;
-import com.android.tradefed.result.TestResult;
-import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
 
 import junit.framework.TestCase;
@@ -61,7 +61,7 @@
     public void testParse() throws ParseException, IOException {
         mParser.parse(extractTestXml("JUnitXmlParserTest_testParse.xml"));
         assertEquals(3, mListener.getNumTotalTests());
-        assertEquals(1, mListener.getNumFailedTests());
+        assertEquals(1, mListener.getNumAllFailedTests());
         TestRunResult runData = mListener.getCurrentRunResults();
         assertEquals("null", runData.getName());
         assertTrue(runData.getTestResults().containsKey(new TestIdentifier("PassTest", "testPass")));
diff --git a/tests/src/com/android/tradefed/util/RunUtilFuncTest.java b/tests/src/com/android/tradefed/util/RunUtilFuncTest.java
index 749cde3..83669ca 100644
--- a/tests/src/com/android/tradefed/util/RunUtilFuncTest.java
+++ b/tests/src/com/android/tradefed/util/RunUtilFuncTest.java
@@ -132,4 +132,25 @@
             StreamUtil.close(s);
         }
     }
+
+    /**
+     * Test case for {@link RunUtil#unsetEnvVariable(String key)}
+     */
+    public void testUnsetEnvVariable() {
+        long timeout = 200;
+        RunUtil runUtil = new RunUtil();
+        runUtil.setEnvVariable("bar", "foo");
+        // FIXME: this test case is not ideal, as it will only work on platforms that support
+        // printenv
+        CommandResult result = runUtil.runTimedCmd(timeout, "printenv", "bar");
+        assertTrue(result.getStatus() == CommandStatus.SUCCESS);
+        assertTrue("foo".equals(result.getStdout().trim()));
+
+        // remove env variable
+        runUtil.unsetEnvVariable("bar");
+        // printenv with non-exist variable will fail
+        result = runUtil.runTimedCmd(timeout, "printenv", "bar");
+        assertTrue(result.getStatus() == CommandStatus.FAILED);
+        assertTrue("".equals(result.getStdout().trim()));
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/RunUtilTest.java b/tests/src/com/android/tradefed/util/RunUtilTest.java
index 0d682b7..0bcdcb9 100644
--- a/tests/src/com/android/tradefed/util/RunUtilTest.java
+++ b/tests/src/com/android/tradefed/util/RunUtilTest.java
@@ -118,6 +118,19 @@
     }
 
     /**
+     * Verify that calling {@link RunUtil#unsetEnvVariable(String)} is not allowed on default
+     * instance.
+     */
+    public void testUnsetEnvVariable_default() {
+        try {
+            RunUtil.getDefault().unsetEnvVariable("foo");
+            fail("could unset env var on RunUtil.getDefault()");
+        } catch (Exception e) {
+            // expected
+        }
+    }
+
+    /**
      * Test that {@link RunUtil#runEscalatingTimedRetry()} fails when operation continually fails,
      * and that the maxTime variable is respected.
      */
@@ -158,4 +171,56 @@
         assertEquals(maxTime, mSleepTime);
         EasyMock.verify(mockRunnable);
     }
+
+    /**
+     * Test a success case for {@link RunUtil#interrupt}.
+     */
+    public void testInterrupt() {
+        final String message = "it is alright now";
+        mRunUtil.allowInterrupt(true);
+        mRunUtil.interrupt(Thread.currentThread(), message);
+        try{
+            mRunUtil.sleep(1);
+            fail("RunInterruptedException was expected, but not thrown.");
+        } catch (final RunInterruptedException e) {
+            assertEquals(message, e.getMessage());
+        }
+    }
+
+    /**
+     * Test whether a {@link RunUtil#interrupt} call is respected when called while interrupts are
+     * not allowed.
+     */
+    public void testInterrupt_delayed() {
+        final String message = "it is alright now";
+        mRunUtil.allowInterrupt(false);
+        mRunUtil.interrupt(Thread.currentThread(), message);
+        mRunUtil.sleep(1);
+        mRunUtil.allowInterrupt(true);
+        try{
+            mRunUtil.sleep(1);
+            fail("RunInterruptedException was expected, but not thrown.");
+        } catch (final RunInterruptedException e) {
+            assertEquals(message, e.getMessage());
+        }
+    }
+
+    /**
+     * Test whether a {@link RunUtil#interrupt} call is respected when called multiple times.
+     */
+    public void testInterrupt_multiple() {
+        final String message1 = "it is alright now";
+        final String message2 = "without a fight";
+        final String message3 = "rock this town";
+        mRunUtil.allowInterrupt(true);
+        mRunUtil.interrupt(Thread.currentThread(), message1);
+        mRunUtil.interrupt(Thread.currentThread(), message2);
+        mRunUtil.interrupt(Thread.currentThread(), message3);
+        try{
+            mRunUtil.sleep(1);
+            fail("RunInterruptedException was expected, but not thrown.");
+        } catch (final RunInterruptedException e) {
+            assertEquals(message3, e.getMessage());
+        }
+    }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/tradefed/util/StreamUtilTest.java b/tests/src/com/android/tradefed/util/StreamUtilTest.java
index 5473acf..8033943 100644
--- a/tests/src/com/android/tradefed/util/StreamUtilTest.java
+++ b/tests/src/com/android/tradefed/util/StreamUtilTest.java
@@ -21,6 +21,7 @@
 import junit.framework.TestCase;
 
 import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.io.InputStream;
 
 /**
@@ -82,5 +83,17 @@
                 new ByteArrayInputStream(contents.getBytes()));
         assertEquals(contents, output);
     }
+
+    /**
+     * Verify that {@link StreamUtil#calculateMd5(InputStream)} works as expected.
+     * @throws IOException
+     */
+    public void testCalculateMd5() throws IOException {
+        final String source = "testtesttesttesttest";
+        final String md5 = "f317f682fafe0309c6a423af0b4efa59";
+        ByteArrayInputStream inputSource = new ByteArrayInputStream(source.getBytes());
+        String actualMd5 = StreamUtil.calculateMd5(inputSource);
+        assertEquals(md5, actualMd5);
+    }
 }
 
diff --git a/tests/src/com/android/tradefed/util/TimeValTest.java b/tests/src/com/android/tradefed/util/TimeValTest.java
new file mode 100644
index 0000000..75770a9
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/TimeValTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2014 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.util;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link TimeVal}.
+ */
+public class TimeValTest extends TestCase {
+
+    public void testBasic() {
+        assertEquals(0, TimeVal.fromString("0"));
+        assertEquals(1, TimeVal.fromString("1"));
+
+        assertEquals(1000, TimeVal.fromString("1000"));
+        assertEquals(1000, TimeVal.fromString("1000ms"));
+        assertEquals(1000, TimeVal.fromString("1000Ms"));
+        assertEquals(1000, TimeVal.fromString("1000mS"));
+        assertEquals(1000, TimeVal.fromString("1000MS"));
+
+        assertEquals(1000, TimeVal.fromString("1s"));
+        assertEquals(1000, TimeVal.fromString("1S"));
+
+        assertEquals(1000 * 60, TimeVal.fromString("1m"));
+        assertEquals(1000 * 60, TimeVal.fromString("1M"));
+
+        assertEquals(1000 * 3600, TimeVal.fromString("1h"));
+        assertEquals(1000 * 3600, TimeVal.fromString("1H"));
+
+        assertEquals(1000 * 86400, TimeVal.fromString("1d"));
+        assertEquals(1000 * 86400, TimeVal.fromString("1D"));
+    }
+
+    public void testComposition() {
+        assertEquals(1303, TimeVal.fromString("1s303"));
+        assertEquals(1000 * 60 + 303, TimeVal.fromString("1m303"));
+        assertEquals(1000 * 3600 + 303, TimeVal.fromString("1h303ms"));
+        assertEquals(1000 * 86400 + 303, TimeVal.fromString("1d303MS"));
+
+        assertEquals(4 * 1000 * 86400 + 5 * 1000 * 3600 + 303, TimeVal.fromString("4D5h303mS"));
+        assertEquals(5 + 1000 * (4 + 60 * (3 + 60 * (2 + 24 * 1))),
+                TimeVal.fromString("1d2h3m4s5ms"));
+    }
+
+    public void testWhitespace() {
+        assertEquals(1, TimeVal.fromString("1 ms"));
+        assertEquals(1, TimeVal.fromString("  1  ms  "));
+
+        assertEquals(1002, TimeVal.fromString("1s2"));
+        assertEquals(1002, TimeVal.fromString("1s2 ms"));
+        assertEquals(1002, TimeVal.fromString(" 1s2ms"));
+        assertEquals(1002, TimeVal.fromString("1s 2 ms"));
+        // This is non-ideal, but is a side-effect of discarding all whitespace prior to parsing
+        assertEquals(3 * 1000 * 60 + 1002, TimeVal.fromString(" 3 m 1 s 2 m s"));
+
+        assertEquals(5 + 1000 * (4 + 60 * (3 + 60 * (2 + 24 * 1))),
+                TimeVal.fromString("1d 2h 3m 4s 5ms"));
+    }
+
+    public void testInvalid() {
+        assertInvalid("-1");
+        assertInvalid("1m1h");
+        assertInvalid("+5");
+    }
+
+    /**
+     * Because of TimeVal's multiplication features, it can be easier for a user to unexpectedly
+     * trigger an overflow without realizing that their value has become quite so large.  Here,
+     * we verify that various kinds of overflows and ensure that we detect them.
+     */
+    public void testOverflow() {
+        // 2**63 - 1
+        assertEquals(Long.MAX_VALUE, TimeVal.fromString("9223372036854775807"));
+        // 2**63
+        assertInvalid("9223372036854775808");
+
+        // (2**63 - 1).divmod(1000 * 86400) = [106751991167, 25975807] (days, msecs)
+        // This should be the greatest number of days that does not overflow
+        assertEquals(106751991167L * 1000L * 86400L, TimeVal.fromString("106751991167d"));
+        // This should be 2**63 - 1
+        assertEquals(Long.MAX_VALUE, TimeVal.fromString("106751991167d 25975807ms"));
+        // Adding 1 more ms should cause an overflow.
+        assertInvalid("106751991167d 25975808ms");
+
+        // 2**64 + 1 should be a positive value after an overflow.  Make sure we can still detect
+        // this non-negative overflow
+        long l = 1<<62;
+        l *= 2;
+        l *= 2;
+        l += 1;
+        assertTrue(l > 0);
+        // 2**64 + 1 == 18446744073709551617
+        assertInvalid("18446744073709551617ms");
+    }
+
+    private void assertInvalid(String input) {
+        try {
+            final long val = TimeVal.fromString(input);
+            fail(String.format("Did not reject input: %s.  Produced value: %d", input, val));
+        } catch (NumberFormatException e) {
+            // expected
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/net/HttpHelperTest.java b/tests/src/com/android/tradefed/util/net/HttpHelperTest.java
index 0d0cc41..ce417e6 100644
--- a/tests/src/com/android/tradefed/util/net/HttpHelperTest.java
+++ b/tests/src/com/android/tradefed/util/net/HttpHelperTest.java
@@ -126,6 +126,18 @@
     }
 
     /**
+     * Normal case test for {@link HttpHelper#doGet(String, OutputStream)}
+     */
+    public void testDoGetStream() throws IOException, DataSizeException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+        mHelper.doGet(TEST_URL_STRING, out);
+        StreamUtil.flushAndCloseStream(out);
+
+        assertEquals(TEST_DATA, out.toString());
+    }
+
+    /**
      * Normal case test for {@link HttpHelper#doGetWithRetry(String)}.
      */
     public void testDoGetWithRetry() throws IOException, DataSizeException {
diff --git a/tradefed.sh b/tradefed.sh
index 3b63625..804e9d2 100755
--- a/tradefed.sh
+++ b/tradefed.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# Copyright (C) 2010 The Android Open Source Project
+# Copyright (C) 2015 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.
@@ -14,80 +14,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# A helper script that launches TradeFederation from the current build
-# environment.
+# A helper script that launches Trade Federation
 
-checkPath() {
-    if ! type -P $1 &> /dev/null; then
-        echo "Unable to find $1 in path."
-        exit
-    fi;
-}
-
-checkFile() {
-    if [ ! -f "$1" ]; then
-        echo "Unable to locate $1"
-        exit
-    fi;
-}
-
+shdir=`dirname $0`/
+source "${shdir}/script_help.sh"
+# At this point, we're guaranteed to have the right Java version, and the following
+# env variables will be set, if appropriate:
+# JAVA_VERSION, RDBG_FLAG, TF_PATH, TRADEFED_OPTS
 checkPath adb
-checkPath java
 
-# check java version
-java_version_string=$(java -version 2>&1)
-JAVA_VERSION=$(echo "$java_version_string" | grep '[ "]1\.[67][\. "$$]')
-if [ "${JAVA_VERSION}" == "" ]; then
-    echo "Wrong java version. 1.7 is required."
-    exit
-else
-    # We have 1.6 or 1.7.  Now print a warning if the version was 1.6
-    java_version_16=$(echo "$java_version_string" | grep '[ "]1\.6[\. "$$]')
-    if [ "${java_version_16}" != "" ]; then
-        echo "DEPRECATION WARNING: Please update your java to version 1.7."
-        echo
-    fi
-fi
 
-# check debug flag and set up remote debugging
-if [ -n "${TF_DEBUG}" ]; then
-    if [ -z "${TF_DEBUG_PORT}" ]; then
-        TF_DEBUG_PORT=10088
-    fi
-    RDBG_FLAG="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=${TF_DEBUG_PORT}"
-fi
-
-# first try to find TF jars in same dir as this script
-CUR_DIR=$(dirname "$0")
-if [ -f "${CUR_DIR}/tradefed.jar" ]; then
-    tf_path="${CUR_DIR}/*"
-elif [ ! -z "${ANDROID_HOST_OUT}" ]; then
-    # in an Android build env, tradefed.jar should be in
-    # $ANDROID_HOST_OUT/tradefed/
-    if [ -f "${ANDROID_HOST_OUT}/tradefed/tradefed.jar" ]; then
-        # We intentionally pass the asterisk through without shell expansion
-        tf_path="${ANDROID_HOST_OUT}/tradefed/*"
-        # ddmlib-prebuilt is in the framework subdir
-        ddmlib_path="${ANDROID_HOST_OUT}/framework/ddmlib-prebuilt.jar"
-    fi
-fi
-
-if [ -z "${tf_path}" ]; then
-    echo "ERROR: Could not find tradefed jar files"
-    exit
-fi
-
-# set any host specific options
-# file format for file at $TRADEFED_OPTS_FILE is one line per host with the following format:
-# <hostname>=<options>
-# for example:
-# hostname.domain.com=-Djava.io.tmpdir=/location/on/disk -Danother=false ...
-# hostname2.domain.com=-Djava.io.tmpdir=/different/location -Danother=true ...
-if [ -e "${TRADEFED_OPTS_FILE}" ]; then
-    # pull the line for this host and take everything after the first =
-    export TRADEFED_OPTS=`grep "^$HOSTNAME=" "$TRADEFED_OPTS_FILE" | cut -d '=' -f 2-`
-fi
-
-# Note: must leave ${RDBG_FLAG} unquoted so that it goes away when unset
-java ${RDBG_FLAG} -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow $TRADEFED_OPTS \
-  -cp "${ddmlib_path}:${tf_path}" com.android.tradefed.command.Console "$@"
+# Note: must leave $RDBG_FLAG and $TRADEFED_OPTS unquoted so that they go away when unset
+java $RDBG_FLAG -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow $TRADEFED_OPTS \
+  -cp "${TF_PATH}" com.android.tradefed.command.Console "$@"
diff --git a/tradefed_win.bat b/tradefed_win.bat
new file mode 100755
index 0000000..165516e
--- /dev/null
+++ b/tradefed_win.bat
@@ -0,0 +1,103 @@
+@echo off

+

+:: Copyright (C) 2014 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.

+

+:: A helper script that launches TradeFederation from the current build

+:: environment.

+

+setlocal EnableDelayedExpansion

+call:checkCommand adb

+call:checkCommand java

+

+:: check java version

+set JAVA_VERSION=

+

+for /f "delims=" %%j in ('java -version 2^>^&1 ^| findstr /i """1.7"') do (

+    set JAVA_VERSION=7

+)

+

+if "%JAVA_VERSION%" == "" (

+    echo "Wrong java version. 1.7 is required."

+    exit /B

+)

+

+:: check debug flag and set up remote debugging

+if not "%TF_DEBUG%"=="" (

+    if "%TF_DEBUG_PORT%" == "" (

+        set TF_DEBUG_PORT=10088

+    )

+    set RDBG_FLAG=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=!TF_DEBUG_PORT!

+)

+

+:: first try to find TF jars in same dir as this script

+set CUR_DIR=%CD%

+

+if exist "%CUR_DIR%\tradefed.jar" (

+    set tf_path="%CUR_DIR%\*"

+) else (

+    if not "%ANDROID_HOST_OUT%" == "" (

+        if exist "%ANDROID_HOST_OUT%\tradefed\tradefed.jar" (

+            set tf_path="%ANDROID_HOST_OUT%\tradefed\*"

+        )

+    )

+)

+

+if "%tf_path%" == "" (

+    echo "ERROR: Could not find tradefed jar files"

+    exit /B

+)

+

+:: set any host specific options

+:: file format for file at $TRADEFED_OPTS_FILE is one line per host with the following format:

+:: <hostname>=<options>

+:: for example:

+:: hostname.domain.com=-Djava.io.tmpdir=/location/on/disk -Danother=false ...

+:: hostname2.domain.com=-Djava.io.tmpdir=/different/location -Danother=true ...

+if exist "%TRADEFED_OPTS_FILE%" (

+    call:commandResult "hostname" HOST_NAME

+    call:commandResult "findstr /i /b "%HOST_NAME%" "%TRADEFED_OPTS_FILE%"" TRADEFED_OPTS

+:: delete the hostname part

+    set TRADEFED_OPTS=!TRADEFED_OPTS:%HOST_NAME%=!

+:: delete the first =

+    set TRADEFED_OPTS=!TRADEFED_OPTS:~1!

+)

+

+java %RDBG_FLAG% -XX:+HeapDumpOnOutOfMemoryError ^

+-XX:-OmitStackTraceInFastThrow %TRADEFED_OPTS% -cp %tf_path% com.android.tradefed.command.Console %*

+

+endlocal

+::end of file

+goto:eof

+

+:: check command exist or not

+:: if command not exist, exit

+:checkCommand

+for /f "delims=" %%i in ('where %~1') do (

+    if %%i == "" (

+        echo %~1 not exist

+        exit /B

+    )

+    goto:eof

+)

+goto:eof

+

+:: get the command result

+:: usage: call:commandResult "command" result

+:commandResult

+for /f "delims=" %%i in ('%~1') do (

+    set %~2=%%i

+    goto:eof

+)

+goto:eof

diff --git a/util-apps/WifiUtil/AndroidManifest.xml b/util-apps/WifiUtil/AndroidManifest.xml
index 83fa0a4..353b57c 100644
--- a/util-apps/WifiUtil/AndroidManifest.xml
+++ b/util-apps/WifiUtil/AndroidManifest.xml
@@ -19,6 +19,7 @@
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 
     <uses-sdk android:minSdkVersion="3"
               android:targetSdkVersion="7" />
diff --git a/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java b/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
index d8b6c8a..16115dc 100644
--- a/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
+++ b/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
@@ -42,6 +42,7 @@
 
     private static final String TAG = WifiConnector.class.getSimpleName();
     private static final long DEFAULT_TIMEOUT = 30 * 1000;
+    private static final long DEFAULT_WAIT_TIME = 5 * 1000;
     private static final long POLL_TIME = 1000;
 
     private Context mContext;
@@ -190,6 +191,15 @@
                 }
             }, "failed to enable wifi");
 
+        // Wait for some seconds to let wifi to be stable. This increases the chance of success for
+        // subsequent operations.
+        try {
+            Thread.sleep(DEFAULT_WAIT_TIME);
+        } catch (InterruptedException e) {
+            throw new WifiException(String.format("failed to sleep for %d ms", DEFAULT_WAIT_TIME),
+                    e);
+        }
+
         removeAllNetworks(false);
 
         final int networkId = addNetwork(ssid, psk);
diff --git a/verify.sh b/verify.sh
new file mode 100755
index 0000000..7484194
--- /dev/null
+++ b/verify.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# Copyright (C) 2015 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.
+
+# A helper script that launches Trade Federation's "Verify" entrypoint, to perform
+# standalone command file verification
+
+shdir=`dirname $0`/
+source "${shdir}/script_help.sh"
+# At this point, we're guaranteed to have the right Java version, and the following
+# env variables will be set, if appropriate:
+# JAVA_VERSION, RDBG_FLAG, TF_PATH, TRADEFED_OPTS
+
+
+# Note: must leave $RDBG_FLAG and $TRADEFED_OPTS unquoted so that they go away when unset
+java $RDBG_FLAG -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow $TRADEFED_OPTS \
+  -cp "${TF_PATH}" com.android.tradefed.command.Verify "$@"