Merge "Update PushFilePreparer to new interface"
diff --git a/atest/TEST_MAPPING b/atest/TEST_MAPPING
index cebe409..32e6a6d 100644
--- a/atest/TEST_MAPPING
+++ b/atest/TEST_MAPPING
@@ -1,6 +1,32 @@
 // Below lists the TEST_MAPPING tests to do ASuite unittests to make sure
 // the expectation of ASuite are still good.
 {
+  "mainline-presubmit": [
+    {
+      "name": "HelloWorldTests"
+    },
+    {
+      "name": "hello_world_test",
+      "host": true
+    },
+    {
+      "name": "CtsApacheHttpLegacy27ApiSignatureTestCases"
+    },
+    {
+      "name": "ziparchive-tests"
+    },
+    {
+      "name": "CtsDpiTestCases",
+      "options": [
+        {
+          "include-filter": "android.dpi.cts.ConfigurationScreenLayoutTest"
+        }
+      ]
+    },
+    {
+      "name": "CtsSampleHostTestCases"
+    }
+  ],
   "presubmit": [
     {
       // Host side ATest unittests.
diff --git a/device_build_interfaces/com/android/tradefed/device/StubLocalAndroidVirtualDevice.java b/device_build_interfaces/com/android/tradefed/device/StubLocalAndroidVirtualDevice.java
index 5066999..5ce00ec 100644
--- a/device_build_interfaces/com/android/tradefed/device/StubLocalAndroidVirtualDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/StubLocalAndroidVirtualDevice.java
@@ -21,9 +21,9 @@
  * A placeholder {@link IDevice} used by {@link DeviceManager} to allocate when {@link
  * DeviceSelectionOptions#localVirtualDeviceRequested()} is <code>true</code>
  */
-public class StubLocalAndroidVirtualDevice extends StubDevice {
+public class StubLocalAndroidVirtualDevice extends TcpDevice {
 
     public StubLocalAndroidVirtualDevice(String serial) {
-        super(serial, false);
+        super(serial);
     }
 }
diff --git a/proto/test_record.proto b/proto/test_record.proto
index cbbfafc..58e7147 100644
--- a/proto/test_record.proto
+++ b/proto/test_record.proto
@@ -94,4 +94,22 @@
 
   // A stacktrace.
   string trace = 2;
+
+  // A more detailed failure status description.
+  FailureStatus failure_status = 3;
+}
+
+// A Fail TestStatus can be associated with a more granular failure status that helps understanding
+// the context.
+enum FailureStatus {
+  // The test in progress was the reason for the failure.
+  TEST_FAILURE = 0;
+  // A timeout condition on the operation in progress occurred.
+  TIMED_OUT = 1;
+  // The test in progress was cancelled.
+  CANCELLED = 2;
+  // A failure attributed to something not functioning properly.
+  INFRA_FAILURE = 10;
+  // System under test crashed and caused the test to fail.
+  SYSTEM_UNDER_TEST_CRASHED = 20;
 }
\ No newline at end of file
diff --git a/src/com/android/tradefed/config/RetryConfigurationFactory.java b/src/com/android/tradefed/config/RetryConfigurationFactory.java
index bbc8c0d..fc9ae95 100644
--- a/src/com/android/tradefed/config/RetryConfigurationFactory.java
+++ b/src/com/android/tradefed/config/RetryConfigurationFactory.java
@@ -49,9 +49,9 @@
         RetryRescheduler retryRunner = (RetryRescheduler) rerunner;
         retryRunner.setConfiguration(retryConfig);
         try {
-            retryRunner.run(null);
+            retryRunner.run(null, null);
             return retryRunner.getRetryConfiguration();
-        } catch (Exception e) {
+        } catch (Throwable e) {
             throw new ConfigurationException(e.getMessage(), e);
         }
     }
diff --git a/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java b/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
index cce0bbf..66a92fb 100644
--- a/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
@@ -20,6 +20,7 @@
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.device.cloud.GceAvdInfo;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.FileInputStreamSource;
@@ -36,6 +37,7 @@
 import com.android.tradefed.util.ZipUtil;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.common.net.HostAndPort;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -43,7 +45,9 @@
 import java.util.List;
 
 /** The class for local virtual devices running on TradeFed host. */
-public class LocalAndroidVirtualDevice extends TestDevice implements ITestLoggerReceiver {
+public class LocalAndroidVirtualDevice extends RemoteAndroidDevice implements ITestLoggerReceiver {
+
+    private static final int INVALID_PORT = 0;
 
     // Environment variables.
     private static final String ANDROID_HOST_OUT = "ANDROID_HOST_OUT";
@@ -53,13 +57,8 @@
     // The name of the GZIP file containing launch_cvd and stop_cvd.
     private static final String CVD_HOST_PACKAGE_NAME = "cvd-host_package.tar.gz";
 
-    // The port of cuttlefish instance 1.
-    private static final int CUTTLEFISH_FIRST_HOST_PORT = 6520;
-
     private static final String ACLOUD_CVD_TEMP_DIR_NAME = "acloud_cvd_temp";
-    private static final String INSTANCE_DIR_NAME_PREFIX = "instance_home_";
     private static final String CUTTLEFISH_RUNTIME_DIR_NAME = "cuttlefish_runtime";
-    private static final String INSTANCE_NAME_PREFIX = "local-instance-";
 
     private ITestLogger mTestLogger = null;
 
@@ -68,11 +67,7 @@
     private boolean mShouldDeleteHostPackageDir = false;
     private File mImageDir = null;
 
-    // The data for restoring the stub device at tear-down.
-    private String mOriginalSerialNumber = null;
-
-    // A positive integer for acloud to identify this device.
-    private int mInstanceId = -1;
+    private GceAvdInfo mGceAvdInfo = null;
 
     public LocalAndroidVirtualDevice(
             IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor) {
@@ -86,27 +81,58 @@
         // The setup method in super class does not require the device to be online.
         super.preInvocationSetup(info, testResourceBuildInfos);
 
-        // TODO(b/133211308): multiple instances
-        mInstanceId = 1;
-        replaceStubDevice("127.0.0.1:" + (CUTTLEFISH_FIRST_HOST_PORT + mInstanceId - 1));
-
         createTempDirs((IDeviceBuildInfo) info);
 
-        CommandResult result = acloudCreate(info.getBuildFlavor(), getOptions());
+        CommandResult result = null;
+        File report = null;
+        try {
+            report = FileUtil.createTempFile("report", ".json");
+            result = acloudCreate(info.getBuildFlavor(), report, getOptions());
+            loadAvdInfo(report);
+        } catch (IOException ex) {
+            throw new TargetSetupError(
+                    "Cannot create acloud report file.", ex, getDeviceDescriptor());
+        } finally {
+            FileUtil.deleteFile(report);
+        }
         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
             throw new TargetSetupError(
-                    String.format("Cannot launch virtual device. stderr:\n%s", result.getStderr()),
+                    String.format("Cannot execute acloud command. stderr:\n%s", result.getStderr()),
                     getDeviceDescriptor());
         }
+
+        HostAndPort hostAndPort = mGceAvdInfo.hostAndPort();
+        replaceStubDevice(hostAndPort.toString());
+
+        RecoveryMode previousMode = getRecoveryMode();
+        try {
+            setRecoveryMode(RecoveryMode.NONE);
+            if (!adbTcpConnect(hostAndPort.getHost(), Integer.toString(hostAndPort.getPort()))) {
+                throw new TargetSetupError(
+                        String.format("Cannot connect to %s.", hostAndPort), getDeviceDescriptor());
+            }
+            waitForDeviceAvailable();
+        } finally {
+            setRecoveryMode(previousMode);
+        }
     }
 
     /** Execute common tear-down procedure and stop the virtual device. */
     @Override
     public void postInvocationTearDown(Throwable exception) {
         TestDeviceOptions options = getOptions();
+        HostAndPort hostAndPort = getHostAndPortFromAvdInfo();
+        String instanceName = (mGceAvdInfo != null ? mGceAvdInfo.instanceName() : null);
         try {
-            if (!options.shouldSkipTearDown() && mHostPackageDir != null) {
-                CommandResult result = acloudDelete(options);
+            if (!options.shouldSkipTearDown() && hostAndPort != null) {
+                if (!adbTcpDisconnect(
+                        hostAndPort.getHost(), Integer.toString(hostAndPort.getPort()))) {
+                    CLog.e("Cannot disconnect from %s", hostAndPort.toString());
+                }
+            }
+
+            if (!options.shouldSkipTearDown() && instanceName != null) {
+                CommandResult result = acloudDelete(instanceName, options);
                 if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
                     CLog.e("Cannot stop the virtual device.");
                 }
@@ -114,7 +140,9 @@
                 CLog.i("Skip stopping the virtual device.");
             }
 
-            reportInstanceLogs();
+            if (instanceName != null) {
+                reportInstanceLogs(instanceName);
+            }
         } finally {
             restoreStubDevice();
 
@@ -122,10 +150,15 @@
                 deleteTempDirs();
             } else {
                 CLog.i(
-                        "Skip deleting the temporary directories.\nHost package: %s\nImage: %s\n",
-                        mHostPackageDir, mImageDir);
+                        "Skip deleting the temporary directories.\n"
+                                + "Address: %s\nName: %s\nHost package: %s\nImage: %s",
+                        hostAndPort, instanceName, mHostPackageDir, mImageDir);
+                mHostPackageDir = null;
+                mImageDir = null;
             }
 
+            mGceAvdInfo = null;
+
             super.postInvocationTearDown(exception);
         }
     }
@@ -202,38 +235,14 @@
             throw new TargetSetupError(
                     "Unexpected device type: " + device.getClass(), getDeviceDescriptor());
         }
-        mOriginalSerialNumber = device.getSerialNumber();
         setIDevice(new StubLocalAndroidVirtualDevice(newSerialNumber));
         setFastbootEnabled(false);
     }
 
-    /**
-     * Set this device to be offline and associate it with a {@link StubLocalAndroidVirtualDevice}.
-     */
+    /** Restore the {@link StubLocalAndroidVirtualDevice} with the initial serial number. */
     private void restoreStubDevice() {
-        if (mOriginalSerialNumber == null) {
-            CLog.w("Skip restoring the stub device.");
-            return;
-        }
-        setIDevice(new StubLocalAndroidVirtualDevice(mOriginalSerialNumber));
+        setIDevice(new StubLocalAndroidVirtualDevice(getInitialSerial()));
         setFastbootEnabled(false);
-        mOriginalSerialNumber = null;
-    }
-
-    private String getInstanceDirName() {
-        return INSTANCE_DIR_NAME_PREFIX + mInstanceId;
-    }
-
-    private File getInstanceDir() {
-        return FileUtil.getFileForPath(
-                getTmpDir(),
-                ACLOUD_CVD_TEMP_DIR_NAME,
-                getInstanceDirName(),
-                CUTTLEFISH_RUNTIME_DIR_NAME);
-    }
-
-    private String getInstanceName() {
-        return INSTANCE_NAME_PREFIX + mInstanceId;
     }
 
     private static void addLogLevelToAcloudCommand(List<String> command, LogLevel logLevel) {
@@ -244,7 +253,7 @@
         }
     }
 
-    private CommandResult acloudCreate(String buildFlavor, TestDeviceOptions options) {
+    private CommandResult acloudCreate(String buildFlavor, File report, TestDeviceOptions options) {
         CommandResult result = null;
 
         File acloud = options.getAvdDriverBinary();
@@ -262,6 +271,7 @@
                             options.getGceCmdTimeout(),
                             acloud,
                             buildFlavor,
+                            report,
                             options.getGceDriverLogLevel(),
                             options.getGceDriverParams());
             if (CommandStatus.SUCCESS.equals(result.getStatus())) {
@@ -275,18 +285,16 @@
     }
 
     private CommandResult acloudCreate(
-            long timeout, File acloud, String buildFlavor, LogLevel logLevel, List<String> args) {
+            long timeout,
+            File acloud,
+            String buildFlavor,
+            File report,
+            LogLevel logLevel,
+            List<String> args) {
         IRunUtil runUtil = createRunUtil();
         // The command creates the instance directory under TMPDIR.
         runUtil.setEnvVariable(TMPDIR, getTmpDir().getAbsolutePath());
-        // The command finds bin/launch_cvd in ANDROID_HOST_OUT.
-        runUtil.setEnvVariable(ANDROID_HOST_OUT, mHostPackageDir.getAbsolutePath());
         runUtil.setEnvVariable(TARGET_PRODUCT, buildFlavor);
-        // TODO(b/141349771): Size of sockaddr_un->sun_path is 108, which may be too small for this
-        // path.
-        if (new File(getInstanceDir(), "launcher_monitor.sock").getAbsolutePath().length() > 108) {
-            CLog.w("Length of instance path is too long for launch_cvd.");
-        }
 
         List<String> command =
                 new ArrayList<String>(
@@ -294,9 +302,13 @@
                                 acloud.getAbsolutePath(),
                                 "create",
                                 "--local-instance",
-                                Integer.toString(mInstanceId),
                                 "--local-image",
                                 mImageDir.getAbsolutePath(),
+                                "--local-tool",
+                                mHostPackageDir.getAbsolutePath(),
+                                "--report_file",
+                                report.getAbsolutePath(),
+                                "--no-autoconnect",
                                 "--yes",
                                 "--skip-pre-run-check"));
         addLogLevelToAcloudCommand(command, logLevel);
@@ -308,7 +320,47 @@
         return result;
     }
 
-    private CommandResult acloudDelete(TestDeviceOptions options) {
+    /**
+     * Get valid host and port from mGceAvdInfo.
+     *
+     * @return {@link HostAndPort} if the port is valid; null otherwise.
+     */
+    private HostAndPort getHostAndPortFromAvdInfo() {
+        if (mGceAvdInfo == null) {
+            return null;
+        }
+        HostAndPort hostAndPort = mGceAvdInfo.hostAndPort();
+        if (hostAndPort == null
+                || !hostAndPort.hasPort()
+                || hostAndPort.getPort() == INVALID_PORT) {
+            return null;
+        }
+        return hostAndPort;
+    }
+
+    /** Initialize instance name, host address, and port from an acloud report file. */
+    private void loadAvdInfo(File report) throws TargetSetupError {
+        mGceAvdInfo = GceAvdInfo.parseGceInfoFromFile(report, getDeviceDescriptor(), INVALID_PORT);
+        if (mGceAvdInfo == null) {
+            throw new TargetSetupError("Cannot read acloud report file.", getDeviceDescriptor());
+        }
+
+        if (Strings.isNullOrEmpty(mGceAvdInfo.instanceName())) {
+            throw new TargetSetupError("No instance name in acloud report.", getDeviceDescriptor());
+        }
+
+        if (getHostAndPortFromAvdInfo() == null) {
+            throw new TargetSetupError("No port in acloud report.", getDeviceDescriptor());
+        }
+
+        if (!GceAvdInfo.GceStatus.SUCCESS.equals(mGceAvdInfo.getStatus())) {
+            throw new TargetSetupError(
+                    "Cannot launch virtual device: " + mGceAvdInfo.getErrors(),
+                    getDeviceDescriptor());
+        }
+    }
+
+    private CommandResult acloudDelete(String instanceName, TestDeviceOptions options) {
         File acloud = options.getAvdDriverBinary();
         if (acloud == null || !acloud.isFile()) {
             CLog.e("Specified AVD driver binary is not a file.");
@@ -324,8 +376,9 @@
                         Arrays.asList(
                                 acloud.getAbsolutePath(),
                                 "delete",
+                                "--local-only",
                                 "--instance-names",
-                                getInstanceName()));
+                                instanceName));
         addLogLevelToAcloudCommand(command, options.getGceDriverLogLevel());
 
         CommandResult result =
@@ -335,24 +388,29 @@
         return result;
     }
 
-    private void reportInstanceLogs() {
+    private void reportInstanceLogs(String instanceName) {
         if (mTestLogger == null) {
             return;
         }
-        reportInstanceLog("kernel.log", LogDataType.KERNEL_LOG);
-        reportInstanceLog("logcat", LogDataType.LOGCAT);
-        reportInstanceLog("launcher.log", LogDataType.TEXT);
-        reportInstanceLog("cuttlefish_config.json", LogDataType.TEXT);
+        File instanceDir =
+                FileUtil.getFileForPath(
+                        getTmpDir(),
+                        ACLOUD_CVD_TEMP_DIR_NAME,
+                        instanceName,
+                        CUTTLEFISH_RUNTIME_DIR_NAME);
+        reportInstanceLog(new File(instanceDir, "kernel.log"), LogDataType.KERNEL_LOG);
+        reportInstanceLog(new File(instanceDir, "logcat"), LogDataType.LOGCAT);
+        reportInstanceLog(new File(instanceDir, "launcher.log"), LogDataType.TEXT);
+        reportInstanceLog(new File(instanceDir, "cuttlefish_config.json"), LogDataType.TEXT);
     }
 
-    private void reportInstanceLog(String fileName, LogDataType type) {
-        File file = new File(getInstanceDir(), fileName);
+    private void reportInstanceLog(File file, LogDataType type) {
         if (file.exists()) {
             try (InputStreamSource source = new FileInputStreamSource(file)) {
-                mTestLogger.testLog(fileName, type, source);
+                mTestLogger.testLog(file.getName(), type, source);
             }
         } else {
-            CLog.w("%s doesn't exist.", fileName);
+            CLog.w("%s doesn't exist.", file.getAbsolutePath());
         }
     }
 
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index 8d5c71e..5387567 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -27,6 +27,7 @@
 import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.util.AaptParser;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.KeyguardControllerState;
@@ -194,6 +195,9 @@
                 packageFile.getAbsolutePath(), extraArgs.toString(), getSerialNumber());
         performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
                 installAction, MAX_RETRY_ATTEMPTS);
+        List<File> packageFiles = new ArrayList();
+        packageFiles.add(packageFile);
+        allowLegacyStorageForApps(packageFiles);
         return response[0];
     }
 
@@ -332,6 +336,9 @@
                 };
         performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
                 installAction, MAX_RETRY_ATTEMPTS);
+        List<File> packageFiles = new ArrayList();
+        packageFiles.add(packageFile);
+        allowLegacyStorageForApps(packageFiles);
         return response[0];
     }
 
@@ -404,9 +411,47 @@
                 String.format("install %s", packageFiles.toString()),
                 installAction,
                 MAX_RETRY_ATTEMPTS);
+        allowLegacyStorageForApps(packageFiles);
         return response[0];
     }
 
+    /**
+     * Allows Legacy External Storage access for apps that request for it.
+     *
+     * <p>Apps that request for legacy external storage access are granted the access by setting
+     * MANAGE_EXTERNAL_STORAGE App Op. This gives the app File manager privileges, File managers
+     * have legacy external storage access.
+     *
+     * @param appFiles List of Files. Apk Files of the apps that are installed.
+     */
+    private void allowLegacyStorageForApps(List<File> appFiles) throws DeviceNotAvailableException {
+        for (File appFile : appFiles) {
+            AaptParser aaptParser = AaptParser.parse(appFile);
+            if (aaptParser != null
+                    && aaptParser.getTargetSdkVersion() > 29
+                    && aaptParser.isRequestingLegacyStorage()) {
+                // Set the MANAGE_EXTERNAL_STORAGE App Op to MODE_ALLOWED (Code = 0)
+                // for all users.
+                ArrayList<Integer> userIds = listUsers();
+                for (int userId : userIds) {
+                    CommandResult setFileManagerAppOpResult =
+                            executeShellV2Command(
+                                    "appops set --user "
+                                            + userId
+                                            + " --uid "
+                                            + aaptParser.getPackageName()
+                                            + " MANAGE_EXTERNAL_STORAGE 0");
+                    if (!CommandStatus.SUCCESS.equals(setFileManagerAppOpResult.getStatus())) {
+                        CLog.e(
+                                "Failed to set MANAGE_EXTERNAL_STORAGE App Op to"
+                                        + " allow legacy external storage for: %s ; stderr: %s",
+                                aaptParser.getPackageName(), setFileManagerAppOpResult.getStderr());
+                    }
+                }
+            }
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public String installPackages(
diff --git a/src/com/android/tradefed/result/ResultForwarder.java b/src/com/android/tradefed/result/ResultForwarder.java
index fa64e72..750b125 100644
--- a/src/com/android/tradefed/result/ResultForwarder.java
+++ b/src/com/android/tradefed/result/ResultForwarder.java
@@ -206,6 +206,19 @@
         }
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void testRunFailed(FailureDescription failure) {
+        for (ITestInvocationListener listener : mListeners) {
+            try {
+                listener.testRunFailed(failure);
+            } catch (RuntimeException e) {
+                CLog.e("Exception while invoking %s#testRunFailed", listener.getClass().getName());
+                CLog.e(e);
+            }
+        }
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -268,6 +281,19 @@
 
     /** {@inheritDoc} */
     @Override
+    public void testFailed(TestDescription test, FailureDescription failure) {
+        for (ITestInvocationListener listener : mListeners) {
+            try {
+                listener.testFailed(test, failure);
+            } catch (RuntimeException e) {
+                CLog.e("Exception while invoking %s#testFailed", listener.getClass().getName());
+                CLog.e(e);
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) {
         testEnded(test, System.currentTimeMillis(), testMetrics);
     }
diff --git a/src/com/android/tradefed/testtype/TfTestLauncher.java b/src/com/android/tradefed/testtype/TfTestLauncher.java
index 2c49459..d940b4b 100644
--- a/src/com/android/tradefed/testtype/TfTestLauncher.java
+++ b/src/com/android/tradefed/testtype/TfTestLauncher.java
@@ -42,8 +42,10 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.regex.Pattern;
 
 /**
@@ -60,6 +62,12 @@
             + "sub process. Run will be slightly slower because of the overhead.")
     private boolean mEnableCoverage = false;
 
+    @Option(name = "include-coverage", description = "Patterns to include in the code coverage.")
+    private Set<String> mIncludeCoverage = new LinkedHashSet<>();
+
+    @Option(name = "exclude-coverage", description = "Patterns to exclude in the code coverage.")
+    private Set<String> mExcludeCoverage = new LinkedHashSet<>();
+
     @Option(
         name = "hprof-heap-memory",
         description =
@@ -240,10 +248,19 @@
      * @param destfile destination file where the report will be put.
      */
     private void addCoverageArgs(File jacocoAgent, List<String> args, File destfile) {
-        String javaagent = String.format("-javaagent:%s=destfile=%s,"
-                + "includes=com.android.tradefed*:com.google.android.tradefed*",
-                jacocoAgent.getAbsolutePath(),
-                destfile.getAbsolutePath());
+        if (mIncludeCoverage.isEmpty() && mExcludeCoverage.isEmpty()) {
+            mIncludeCoverage.add("com.android.tradefed*");
+            mIncludeCoverage.add("com.google.android.tradefed*");
+        }
+        String includeFilter = String.join(":", mIncludeCoverage);
+        String javaagent =
+                String.format(
+                        "-javaagent:%s=destfile=%s," + "includes=%s",
+                        jacocoAgent.getAbsolutePath(), destfile.getAbsolutePath(), includeFilter);
+        if (!mExcludeCoverage.isEmpty()) {
+            String excludeFilter = String.join(":", mExcludeCoverage);
+            javaagent += ",excludes=" + excludeFilter;
+        }
         args.add(javaagent);
     }
 
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index d2dab70..cd3296e 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -102,12 +102,11 @@
     private List<String> mModuleArgs = new ArrayList<>();
 
     @Option(
-        name = TEST_ARG_OPTION,
-        description =
-                "the arguments to pass to a test. The expected format is"
-                        + "\"<test-class>:<arg-name>:[<arg-key>:=]<arg-value>\"",
-        importance = Importance.ALWAYS
-    )
+            name = TEST_ARG_OPTION,
+            description =
+                    "The arguments to pass to a test or its preparers. The expected format is"
+                            + "\"<test-class>:<arg-name>:[<arg-key>:=]<arg-value>\"",
+            importance = Importance.ALWAYS)
     private List<String> mTestArgs = new ArrayList<>();
 
     @Option(
diff --git a/src/com/android/tradefed/testtype/suite/ModuleListener.java b/src/com/android/tradefed/testtype/suite/ModuleListener.java
index 3dad444..f78d76f 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleListener.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleListener.java
@@ -111,15 +111,20 @@
     }
 
     /** Helper to log the test passed if it didn't fail. */
-    private void logTestPassed(String testName) {
+    private void logTestPassed(TestDescription testName) {
         if (!mTestFailed && !mCollectTestsOnly) {
+            String runName = "";
+            // Only print the run name in addition to test case fully qualified if different.
+            if (!testName.getClassName().startsWith(getCurrentRunResults().getName())) {
+                runName = getCurrentRunResults().getName() + " ";
+            }
+            String runAndTestCase = String.format("%s%s", runName, testName.toString());
             CLog.logAndDisplay(
                     LogLevel.INFO,
-                    "[%d/%d] %s %s pass",
+                    "[%d/%d] %s pass",
                     mTestsRan,
                     getExpectedTests(),
-                    getCurrentRunResults().getName(),
-                    testName);
+                    runAndTestCase);
         }
         mTestsRan++;
     }
@@ -133,7 +138,7 @@
     /** {@inheritDoc} */
     @Override
     public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
-        logTestPassed(test.toString());
+        logTestPassed(test);
         super.testEnded(test, endTime, testMetrics);
     }
 
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index 0f40af6..c4cd8cc 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -64,7 +64,7 @@
 public class SuiteModuleLoader {
 
     public static final String CONFIG_EXT = ".config";
-    private Map<String, List<OptionDef>> mTestOptions = new HashMap<>();
+    private Map<String, List<OptionDef>> mTestOrPreparerOptions = new HashMap<>();
     private Map<String, List<OptionDef>> mModuleOptions = new HashMap<>();
     private boolean mIncludeAll;
     private Map<String, List<SuiteTestFilter>> mIncludeFilters = new HashMap<>();
@@ -93,7 +93,7 @@
         mIncludeFilters = includeFilters;
         mExcludeFilters = excludeFilters;
 
-        parseArgs(testArgs, mTestOptions);
+        parseArgs(testArgs, mTestOrPreparerOptions);
         parseArgs(moduleArgs, mModuleOptions);
     }
 
@@ -620,6 +620,10 @@
         // Set target preparers
         List<ITargetPreparer> preparers = config.getTargetPreparers();
         for (ITargetPreparer preparer : preparers) {
+            String className = preparer.getClass().getName();
+            if (mTestOrPreparerOptions.containsKey(className)) {
+                config.injectOptionValues(mTestOrPreparerOptions.get(className));
+            }
             if (preparer instanceof IAbiReceiver) {
                 ((IAbiReceiver) preparer).setAbi(abi);
             }
@@ -629,8 +633,8 @@
         List<IRemoteTest> tests = config.getTests();
         for (IRemoteTest test : tests) {
             String className = test.getClass().getName();
-            if (mTestOptions.containsKey(className)) {
-                config.injectOptionValues(mTestOptions.get(className));
+            if (mTestOrPreparerOptions.containsKey(className)) {
+                config.injectOptionValues(mTestOrPreparerOptions.get(className));
             }
             addFiltersToTest(test, abi, fullId, mIncludeFilters, mExcludeFilters);
             if (test instanceof IAbiReceiver) {
diff --git a/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java b/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
index ad77eee..0709915 100644
--- a/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
+++ b/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
@@ -29,10 +29,9 @@
 
     public static final String INSTANT_APP_FAMILY = "instant_app_family";
     public static final String MULTI_ABI_FAMILY = "multi_abi_family";
+    public static final String SECONDARY_USER_FAMILY = "secondary_user_family";
     public static final String[] FAMILY_LIST =
-            new String[] {
-                INSTANT_APP_FAMILY, MULTI_ABI_FAMILY,
-            };
+            new String[] {INSTANT_APP_FAMILY, MULTI_ABI_FAMILY, SECONDARY_USER_FAMILY};
 
     private final String mName;
     /** Defines whether several module parameters are associated and mutually exclusive. */
diff --git a/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java b/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java
index 96538ec..c1a10d7 100644
--- a/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java
+++ b/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java
@@ -22,6 +22,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ILogSaverListener;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -29,7 +30,6 @@
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
-import com.android.tradefed.testtype.IInvocationContextReceiver;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.TimeUtil;
 
@@ -41,15 +41,13 @@
 import java.util.Map.Entry;
 
 /** Special runner that replays the results given to it. */
-public final class ResultsPlayer
-        implements IRemoteTest, IInvocationContextReceiver, IConfigurationReceiver {
+public final class ResultsPlayer implements IRemoteTest, IConfigurationReceiver {
 
     private class ReplayModuleHolder {
         public IInvocationContext mModuleContext;
         public List<Entry<TestDescription, TestResult>> mResults = new ArrayList<>();
     }
 
-    private IInvocationContext mContext;
     private Map<TestRunResult, ReplayModuleHolder> mModuleResult;
     private IConfiguration mConfiguration;
 
@@ -59,11 +57,12 @@
     }
 
     @Override
-    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+    public void run(TestInformation testInfo, ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
         // Very first thing of the retry is to check whether all devices are available, this avoids
         // use wasting time replaying result for an invocation that will fail right after during
         // the re-run.
-        for (ITestDevice device : mContext.getDevices()) {
+        for (ITestDevice device : testInfo.getContext().getDevices()) {
             if (device.getIDevice() instanceof StubDevice) {
                 continue;
             }
@@ -83,9 +82,11 @@
 
             IInvocationContext moduleContext = holder.mModuleContext;
             if (moduleContext != null) {
-                for (String deviceName : mContext.getDeviceConfigNames()) {
-                    moduleContext.addAllocatedDevice(deviceName, mContext.getDevice(deviceName));
-                    moduleContext.addDeviceBuildInfo(deviceName, mContext.getBuildInfo(deviceName));
+                for (String deviceName : testInfo.getContext().getDeviceConfigNames()) {
+                    moduleContext.addAllocatedDevice(
+                            deviceName, testInfo.getContext().getDevice(deviceName));
+                    moduleContext.addDeviceBuildInfo(
+                            deviceName, testInfo.getContext().getBuildInfo(deviceName));
                 }
                 listener.testModuleStarted(moduleContext);
             }
@@ -141,12 +142,6 @@
 
     /** {@inheritDoc} */
     @Override
-    public void setInvocationContext(IInvocationContext invocationContext) {
-        mContext = invocationContext;
-    }
-
-    /** {@inheritDoc} */
-    @Override
     public void setConfiguration(IConfiguration configuration) {
         mConfiguration = configuration;
     }
diff --git a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
index e75267b..24d6c07 100644
--- a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
+++ b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
@@ -15,6 +15,8 @@
  */
 package com.android.tradefed.testtype.suite.retry;
 
+import static org.junit.Assert.assertNull;
+
 import com.android.annotations.VisibleForTesting;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
@@ -25,6 +27,7 @@
 import com.android.tradefed.config.Option.Importance;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.invoker.IRescheduler;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.FileLogger;
 import com.android.tradefed.log.ILeveledLogOutput;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -109,7 +112,13 @@
     private IConfiguration mRescheduledConfiguration;
 
     @Override
-    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+    public void run(
+            TestInformation testInfo /* do not use - should be null */,
+            ITestInvocationListener listener /* do not use - should be null */)
+            throws DeviceNotAvailableException {
+        assertNull(testInfo);
+        assertNull(listener);
+
         // Get the re-loader for previous results
         Object loader = mConfiguration.getConfigurationObject(PREVIOUS_LOADER_NAME);
         if (loader == null) {
diff --git a/src/com/android/tradefed/util/AaptParser.java b/src/com/android/tradefed/util/AaptParser.java
index 58a8b39..b20652e 100644
--- a/src/com/android/tradefed/util/AaptParser.java
+++ b/src/com/android/tradefed/util/AaptParser.java
@@ -37,10 +37,15 @@
             Pattern.MULTILINE);
     private static final Pattern SDK_PATTERN = Pattern.compile(
             "^sdkVersion:'(\\d+)'", Pattern.MULTILINE);
+    private static final Pattern TARGET_SDK_PATTERN =
+            Pattern.compile("^targetSdkVersion:'(\\d+)'", Pattern.MULTILINE);
     /** Patterns for native code are not always present, so the list may stay empty. */
     private static final Pattern NATIVE_CODE_PATTERN =
             Pattern.compile("native-code: '(.*?)'( '.*?')*");
 
+    private static final Pattern REQUEST_LEGACY_STORAGE_PATTERN =
+            Pattern.compile("requestLegacyExternalStorage.*=\\(.*\\)(.*)", Pattern.MULTILINE);
+
     private static final Pattern ALT_NATIVE_CODE_PATTERN =
             Pattern.compile("alt-native-code: '(.*)'");
     private static final int AAPT_TIMEOUT_MS = 60000;
@@ -52,6 +57,8 @@
     private List<String> mNativeCode = new ArrayList<>();
     private String mLabel;
     private int mSdkVersion = INVALID_SDK;
+    private int mTargetSdkVersion = 10000;
+    private boolean mRequestLegacyStorage = false;
 
     // @VisibleForTesting
     AaptParser() {
@@ -73,6 +80,10 @@
             if (m.find()) {
                 mSdkVersion = Integer.parseInt(m.group(1));
             }
+            m = TARGET_SDK_PATTERN.matcher(aaptOut);
+            if (m.find()) {
+                mTargetSdkVersion = Integer.parseInt(m.group(1));
+            }
             m = NATIVE_CODE_PATTERN.matcher(aaptOut);
             if (m.find()) {
                 for (int i = 1; i <= m.groupCount(); i++) {
@@ -96,6 +107,16 @@
         return false;
     }
 
+    boolean parseXmlTree(String aaptOut) {
+        Matcher m = REQUEST_LEGACY_STORAGE_PATTERN.matcher(aaptOut);
+        if (m.find()) {
+            // 0xffffffff is true and 0x0 is false
+            mRequestLegacyStorage = m.group(1).equals("0xffffffff");
+        }
+        // REQUEST_LEGACY_STORAGE_PATTERN may or may not be present
+        return true;
+    }
+
     /**
      * Parse info from the apk.
      *
@@ -118,17 +139,38 @@
         if (stderr != null && !stderr.isEmpty()) {
             CLog.e("aapt dump badging stderr: %s", stderr);
         }
-
-        if (CommandStatus.SUCCESS.equals(result.getStatus())) {
-            AaptParser p = new AaptParser();
-            if (p.parse(result.getStdout()))
-                return p;
+        AaptParser p = new AaptParser();
+        if (!CommandStatus.SUCCESS.equals(result.getStatus()) || !p.parse(result.getStdout())) {
+            CLog.e(
+                    "Failed to run aapt on %s. stdout: %s",
+                    apkFile.getAbsoluteFile(), result.getStdout());
             return null;
         }
-        CLog.e(
-                "Failed to run aapt on %s. stdout: %s",
-                apkFile.getAbsoluteFile(), result.getStdout());
-        return null;
+        result =
+                RunUtil.getDefault()
+                        .runTimedCmdRetry(
+                                AAPT_TIMEOUT_MS,
+                                0L,
+                                2,
+                                "aapt",
+                                "dump",
+                                "xmltree",
+                                apkFile.getAbsolutePath(),
+                                "AndroidManifest.xml");
+
+        stderr = result.getStderr();
+        if (stderr != null && !stderr.isEmpty()) {
+            CLog.e("aapt dump xmltree AndroidManifest.xml stderr: %s", stderr);
+        }
+
+        if (!CommandStatus.SUCCESS.equals(result.getStatus())
+                || !p.parseXmlTree(result.getStdout())) {
+            CLog.e(
+                    "Failed to run aapt on %s. stdout: %s",
+                    apkFile.getAbsoluteFile(), result.getStdout());
+            return null;
+        }
+        return p;
     }
 
     public String getPackageName() {
@@ -154,4 +196,17 @@
     public int getSdkVersion() {
         return mSdkVersion;
     }
+
+    public int getTargetSdkVersion() {
+        return mTargetSdkVersion;
+    }
+
+    /**
+     * Check if the app is requesting legacy storage.
+     *
+     * @return boolean return true if requestLegacyExternalStorage is true in AndroidManifest.xml
+     */
+    public boolean isRequestingLegacyStorage() {
+        return mRequestLegacyStorage;
+    }
 }
diff --git a/test_result_interfaces/Android.bp b/test_result_interfaces/Android.bp
index 9a8cc0b..6d6d6b1 100644
--- a/test_result_interfaces/Android.bp
+++ b/test_result_interfaces/Android.bp
@@ -20,6 +20,7 @@
     ],
     libs: [
         "ddmlib-prebuilt",
+        "jsr305",
         "tradefed-common-util",
         "tradefed-protos",
     ],
diff --git a/test_result_interfaces/com/android/tradefed/result/FailureDescription.java b/test_result_interfaces/com/android/tradefed/result/FailureDescription.java
new file mode 100644
index 0000000..4018ace
--- /dev/null
+++ b/test_result_interfaces/com/android/tradefed/result/FailureDescription.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2020 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.result.proto.TestRecordProto;
+
+import javax.annotation.Nullable;
+
+/**
+ * The class describing a failure information in Trade Federation. This class contains the debugging
+ * information and context of the failure that helps understanding the issue.
+ */
+public class FailureDescription {
+    // The error message generated from the failure
+    private String mErrorMessage;
+    // Optional: The category of the failure
+    private @Nullable TestRecordProto.FailureStatus mFailureStatus;
+
+    FailureDescription() {}
+
+    /**
+     * Set the {@link com.android.tradefed.result.proto.TestRecordProto.FailureStatus} associated
+     * with the failure.
+     */
+    public FailureDescription setFailureStatus(TestRecordProto.FailureStatus status) {
+        mFailureStatus = status;
+        return this;
+    }
+
+    /** Returns the FailureStatus associated with the failure. Can be null. */
+    public @Nullable TestRecordProto.FailureStatus getFailureStatus() {
+        return mFailureStatus;
+    }
+
+    /** Returns the error message associated with the failure. */
+    public String getErrorMessage() {
+        return mErrorMessage;
+    }
+
+    @Override
+    public String toString() {
+        // For backward compatibility of result interface, toString falls back to the simple message
+        return mErrorMessage;
+    }
+
+    /**
+     * Create a {@link FailureDescription} based on the error message generated from the failure.
+     *
+     * @param errorMessage The error message from the failure.
+     * @return the created {@link FailureDescription}
+     */
+    public static FailureDescription create(String errorMessage) {
+        return create(errorMessage, null);
+    }
+
+    /**
+     * Create a {@link FailureDescription} based on the error message generated from the failure.
+     *
+     * @param errorMessage The error message from the failure.
+     * @param status The status associated with the failure.
+     * @return the created {@link FailureDescription}
+     */
+    public static FailureDescription create(
+            String errorMessage, @Nullable TestRecordProto.FailureStatus status) {
+        FailureDescription info = new FailureDescription();
+        info.mErrorMessage = errorMessage;
+        info.mFailureStatus = status;
+        return info;
+    }
+}
diff --git a/test_result_interfaces/com/android/tradefed/result/ITestLifeCycleReceiver.java b/test_result_interfaces/com/android/tradefed/result/ITestLifeCycleReceiver.java
index a4f86cc..99f563f 100644
--- a/test_result_interfaces/com/android/tradefed/result/ITestLifeCycleReceiver.java
+++ b/test_result_interfaces/com/android/tradefed/result/ITestLifeCycleReceiver.java
@@ -88,6 +88,15 @@
     public default void testRunFailed(String errorMessage) {}
 
     /**
+     * Reports test run failed to complete due to a failure described by {@link FailureDescription}.
+     *
+     * @param failure {@link FailureDescription} describing the failure and its context.
+     */
+    public default void testRunFailed(FailureDescription failure) {
+        testRunFailed(failure.toString());
+    }
+
+    /**
      * Reports end of test run.
      *
      * @param elapsedTimeMillis device reported elapsed time, in milliseconds
@@ -145,6 +154,18 @@
     public default void testFailed(TestDescription test, String trace) {}
 
     /**
+     * Reports the failure of a individual test case.
+     *
+     * <p>Will be called between testStarted and testEnded.
+     *
+     * @param test identifies the test
+     * @param failure {@link FailureDescription} describing the failure and its context.
+     */
+    public default void testFailed(TestDescription test, FailureDescription failure) {
+        testFailed(test, failure.toString());
+    }
+
+    /**
      * Called when an atomic test flags that it assumes a condition that is false
      *
      * @param test identifies the test
diff --git a/test_result_interfaces/com/android/tradefed/result/MultiFailureDescription.java b/test_result_interfaces/com/android/tradefed/result/MultiFailureDescription.java
new file mode 100644
index 0000000..55ad8dc
--- /dev/null
+++ b/test_result_interfaces/com/android/tradefed/result/MultiFailureDescription.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 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.result.proto.TestRecordProto.FailureStatus;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * Collect multiple {@link FailureDescription} in one holder. This can be used to carry all the
+ * failures description when several attempts on the same test case or run are made, each resulting
+ * in a failure.
+ */
+public final class MultiFailureDescription extends FailureDescription {
+
+    private List<FailureDescription> mFailures = new ArrayList<>();
+
+    public MultiFailureDescription(List<FailureDescription> failures) {
+        super();
+        mFailures.addAll(failures);
+    }
+
+    public MultiFailureDescription(FailureDescription... failures) {
+        super();
+        mFailures.addAll(Arrays.asList(failures));
+    }
+
+    /**
+     * Add another failure to an existing {@link MultiFailureDescription}.
+     *
+     * @param failure The additional failure
+     * @return The current {@link MultiFailureDescription}.
+     */
+    public MultiFailureDescription addFailure(FailureDescription failure) {
+        mFailures.add(failure);
+        return this;
+    }
+
+    /**
+     * Returns the list of {@link FailureDescription} tracked by the {@link
+     * MultiFailureDescription}.
+     */
+    public List<FailureDescription> getFailures() {
+        return mFailures;
+    }
+
+    @Override
+    public @Nullable FailureStatus getFailureStatus() {
+        throw new UnsupportedOperationException(
+                "Cannot call #getFailureStatus on MultiFailureDescription");
+    }
+
+    @Override
+    public String getErrorMessage() {
+        throw new UnsupportedOperationException(
+                "Cannot call #getErrorMessage on MultiFailureDescription");
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb =
+                new StringBuilder(String.format("There were %d failures:", mFailures.size()));
+        for (FailureDescription f : mFailures) {
+            sb.append(String.format("\n  %s", f.toString()));
+        }
+        return sb.toString();
+    }
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index ecd6680..c299b28 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -160,6 +160,7 @@
 import com.android.tradefed.result.DeviceFileReporterTest;
 import com.android.tradefed.result.DeviceUnavailEmailResultReporterTest;
 import com.android.tradefed.result.EmailResultReporterTest;
+import com.android.tradefed.result.FailureDescriptionTest;
 import com.android.tradefed.result.FailureEmailResultReporterTest;
 import com.android.tradefed.result.FileSystemLogSaverTest;
 import com.android.tradefed.result.InvocationFailureEmailResultReporterTest;
@@ -170,6 +171,7 @@
 import com.android.tradefed.result.LogFileSaverTest;
 import com.android.tradefed.result.LogcatCrashResultForwarderTest;
 import com.android.tradefed.result.MetricsXMLResultReporterTest;
+import com.android.tradefed.result.MultiFailureDescriptionTest;
 import com.android.tradefed.result.SnapshotInputStreamSourceTest;
 import com.android.tradefed.result.SubprocessResultsReporterTest;
 import com.android.tradefed.result.TestDescriptionTest;
@@ -589,6 +591,7 @@
     DeviceFileReporterTest.class,
     DeviceUnavailEmailResultReporterTest.class,
     EmailResultReporterTest.class,
+    FailureDescriptionTest.class,
     FailureEmailResultReporterTest.class,
     FileSystemLogSaverTest.class,
     InvocationFailureEmailResultReporterTest.class,
@@ -599,6 +602,7 @@
     LogcatCrashResultForwarderTest.class,
     LogFileSaverTest.class,
     MetricsXMLResultReporterTest.class,
+    MultiFailureDescriptionTest.class,
     SnapshotInputStreamSourceTest.class,
     SubprocessResultsReporterTest.class,
     TestDescriptionTest.class,
diff --git a/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java b/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java
index f1eee0b..b40597d 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationXmlParserTest.java
@@ -303,7 +303,7 @@
      * Map)}. when presented a device tag with a used name, should merge them.
      */
     @Test
-    public void testParse_deviceTagSameName() {
+    public void testParse_deviceTagSameName() throws Exception {
         final String normalConfig =
             "<configuration description=\"desc\" >\n" +
             "  <device name=\"device1\">\n" +
@@ -318,15 +318,11 @@
             "</configuration>";
         final String configName = "config";
         ConfigurationDef configDef = new ConfigurationDef(configName);
-        try {
-            xmlParser.parse(configDef, configName, getStringAsStream(normalConfig), null);
-            assertTrue(configDef.getObjectClassMap().get(Configuration.DEVICE_NAME).size() == 2);
-            assertTrue("{device1}opName".equals(configDef.getOptionList().get(0).name));
-            assertEquals("{device2}opName3", configDef.getOptionList().get(1).name);
-            assertTrue("{device1}opName2".equals(configDef.getOptionList().get(2).name));
-        } catch(ConfigurationException unExpected) {
-            fail("No exception should have been thrown.");
-        }
+        xmlParser.parse(configDef, configName, getStringAsStream(normalConfig), null);
+        assertTrue(configDef.getObjectClassMap().get(Configuration.DEVICE_NAME).size() == 2);
+        assertTrue("{device1}opName".equals(configDef.getOptionList().get(0).name));
+        assertEquals("{device2}opName3", configDef.getOptionList().get(1).name);
+        assertTrue("{device1}opName2".equals(configDef.getOptionList().get(2).name));
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java b/tests/src/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
index 28335ef..c123833 100644
--- a/tests/src/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
@@ -41,6 +41,7 @@
 import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
 import org.easymock.Capture;
 import org.easymock.EasyMock;
+import org.easymock.IAnswer;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -61,22 +62,27 @@
             super(device, stateMonitor, allocationMonitor);
         }
 
-        IDevice currentDevice = null;
-        IRunUtil currentRunUtil = null;
+        IRunUtil currentRunUtil;
+        boolean expectToConnect;
 
         @Override
-        public IDevice getIDevice() {
-            return currentDevice;
+        public boolean adbTcpConnect(String host, String port) {
+            Assert.assertTrue("Unexpected method call to adbTcpConnect.", expectToConnect);
+            Assert.assertEquals(IP_ADDRESS, host);
+            Assert.assertEquals(PORT, port);
+            return true;
         }
 
         @Override
-        public void setIDevice(IDevice device) {
-            currentDevice = device;
+        public boolean adbTcpDisconnect(String host, String port) {
+            Assert.assertEquals(IP_ADDRESS, host);
+            Assert.assertEquals(PORT, port);
+            return true;
         }
 
         @Override
-        public void setDeviceState(TestDeviceState state) {
-            Assert.assertEquals(TestDeviceState.NOT_AVAILABLE, state);
+        public void waitForDeviceAvailable() {
+            Assert.assertTrue("Unexpected method call to waitForDeviceAvailable.", expectToConnect);
         }
 
         @Override
@@ -94,9 +100,44 @@
     }
 
     private static final String STUB_SERIAL_NUMBER = "local-virtual-device-0";
-    private static final String ONLINE_SERIAL_NUMBER = "127.0.0.1:6520";
+    private static final String IP_ADDRESS = "127.0.0.1";
+    private static final String PORT = "6520";
+    private static final String ONLINE_SERIAL_NUMBER = IP_ADDRESS + ":" + PORT;
+    private static final String INSTANCE_NAME = "local-instance-1";
     private static final String BUILD_FLAVOR = "cf_x86_phone-userdebug";
     private static final long ACLOUD_TIMEOUT = 12345;
+    private static final String SUCCESS_REPORT_STRING =
+            String.format(
+                    "{"
+                            + " \"command\": \"create\","
+                            + " \"data\": {"
+                            + "  \"devices\": ["
+                            + "   {"
+                            + "    \"ip\": \"%s\","
+                            + "    \"instance_name\": \"%s\""
+                            + "   }"
+                            + "  ]"
+                            + " },"
+                            + " \"errors\": [],"
+                            + " \"status\": \"SUCCESS\""
+                            + "}",
+                    ONLINE_SERIAL_NUMBER, INSTANCE_NAME);
+    private static final String FAILURE_REPORT_STRING =
+            String.format(
+                    "{"
+                            + " \"command\": \"create\","
+                            + " \"data\": {"
+                            + "  \"devices_failing_boot\": ["
+                            + "   {"
+                            + "    \"ip\": \"%s\","
+                            + "    \"instance_name\": \"%s\""
+                            + "   }"
+                            + "  ]"
+                            + " },"
+                            + " \"errors\": [],"
+                            + " \"status\": \"BOOT_FAIL\""
+                            + "}",
+                    ONLINE_SERIAL_NUMBER, INSTANCE_NAME);
 
     // Temporary files.
     private File mAcloud;
@@ -104,12 +145,7 @@
     private File mHostPackageTarGzip;
     private File mTmpDir;
 
-    // The initial stub device.
-    private StubLocalAndroidVirtualDevice mStubLocalAvd;
-
-    // Mock objects
-    private IDeviceStateMonitor mMockDeviceStateMonitor;
-    private IDeviceMonitor mMockDeviceMonitor;
+    // Mock object.
     private IDeviceBuildInfo mMockDeviceBuildInfo;
 
     // The object under test.
@@ -123,20 +159,22 @@
         createHostPackage(mHostPackageTarGzip);
         mTmpDir = FileUtil.createTempDir("LocalAvdTmp");
 
-        mStubLocalAvd = new StubLocalAndroidVirtualDevice(STUB_SERIAL_NUMBER);
-
-        mMockDeviceStateMonitor = EasyMock.createMock(IDeviceStateMonitor.class);
-        mMockDeviceMonitor = EasyMock.createMock(IDeviceMonitor.class);
         mMockDeviceBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
         EasyMock.expect(mMockDeviceBuildInfo.getDeviceImageFile()).andReturn(mImageZip);
         EasyMock.expect(mMockDeviceBuildInfo.getFile(EasyMock.eq("cvd-host_package.tar.gz")))
                 .andReturn(mHostPackageTarGzip);
         EasyMock.expect(mMockDeviceBuildInfo.getBuildFlavor()).andReturn(BUILD_FLAVOR);
+        IDeviceStateMonitor mockDeviceStateMonitor = EasyMock.createMock(IDeviceStateMonitor.class);
+        mockDeviceStateMonitor.setIDevice(EasyMock.anyObject());
+        EasyMock.expectLastCall().anyTimes();
+        IDeviceMonitor mockDeviceMonitor = EasyMock.createMock(IDeviceMonitor.class);
+        EasyMock.replay(mMockDeviceBuildInfo, mockDeviceStateMonitor, mockDeviceMonitor);
 
         mLocalAvd =
                 new TestableLocalAndroidVirtualDevice(
-                        mStubLocalAvd, mMockDeviceStateMonitor, mMockDeviceMonitor);
-        mLocalAvd.setIDevice(mStubLocalAvd);
+                        new StubLocalAndroidVirtualDevice(STUB_SERIAL_NUMBER),
+                        mockDeviceStateMonitor,
+                        mockDeviceMonitor);
         TestDeviceOptions options = mLocalAvd.getOptions();
         options.setGceCmdTimeout(ACLOUD_TIMEOUT);
         options.setAvdDriverBinary(mAcloud);
@@ -178,35 +216,54 @@
         }
     }
 
-    private void replayAllMocks(Object... mocks) {
-        EasyMock.replay(mocks);
-        EasyMock.replay(mMockDeviceStateMonitor, mMockDeviceMonitor, mMockDeviceBuildInfo);
-    }
-
     private IRunUtil mockAcloudCreate(
-            CommandStatus status, Capture<String> hostPackageDir, Capture<String> imageDir) {
+            CommandStatus status,
+            String reportString,
+            Capture<String> reportFile,
+            Capture<String> hostPackageDir,
+            Capture<String> imageDir) {
         IRunUtil runUtil = EasyMock.createMock(IRunUtil.class);
         runUtil.setEnvVariable(EasyMock.eq("TMPDIR"), EasyMock.eq(mTmpDir.getAbsolutePath()));
-        runUtil.setEnvVariable(EasyMock.eq("ANDROID_HOST_OUT"), EasyMock.capture(hostPackageDir));
         runUtil.setEnvVariable(EasyMock.eq("TARGET_PRODUCT"), EasyMock.eq(BUILD_FLAVOR));
 
-        CommandResult result = new CommandResult(status);
-        result.setStderr("acloud create");
-        result.setStdout("acloud create");
+        IAnswer<CommandResult> writeToReportFile =
+                new IAnswer() {
+                    @Override
+                    public CommandResult answer() throws Throwable {
+                        Object[] args = EasyMock.getCurrentArguments();
+                        for (int index = 0; index < args.length; index++) {
+                            if ("--report_file".equals(args[index])) {
+                                index++;
+                                File file = new File((String) args[index]);
+                                FileUtil.writeToFile(reportString, file);
+                            }
+                        }
+
+                        CommandResult result = new CommandResult(status);
+                        result.setStderr("acloud create");
+                        result.setStdout("acloud create");
+                        return result;
+                    }
+                };
+
         EasyMock.expect(
                         runUtil.runTimedCmd(
                                 EasyMock.eq(ACLOUD_TIMEOUT),
-                                EasyMock.startsWith(mAcloud.getAbsolutePath()),
+                                EasyMock.eq(mAcloud.getAbsolutePath()),
                                 EasyMock.eq("create"),
                                 EasyMock.eq("--local-instance"),
-                                EasyMock.eq("1"),
                                 EasyMock.eq("--local-image"),
                                 EasyMock.capture(imageDir),
+                                EasyMock.eq("--local-tool"),
+                                EasyMock.capture(hostPackageDir),
+                                EasyMock.eq("--report_file"),
+                                EasyMock.capture(reportFile),
+                                EasyMock.eq("--no-autoconnect"),
                                 EasyMock.eq("--yes"),
                                 EasyMock.eq("--skip-pre-run-check"),
                                 EasyMock.eq("-vv"),
                                 EasyMock.eq("-test")))
-                .andReturn(result);
+                .andAnswer(writeToReportFile);
 
         return runUtil;
     }
@@ -221,10 +278,11 @@
         EasyMock.expect(
                         runUtil.runTimedCmd(
                                 EasyMock.eq(ACLOUD_TIMEOUT),
-                                EasyMock.startsWith(mAcloud.getAbsolutePath()),
+                                EasyMock.eq(mAcloud.getAbsolutePath()),
                                 EasyMock.eq("delete"),
+                                EasyMock.eq("--local-only"),
                                 EasyMock.eq("--instance-names"),
-                                EasyMock.eq("local-instance-1"),
+                                EasyMock.eq(INSTANCE_NAME),
                                 EasyMock.eq("-vv")))
                 .andReturn(result);
 
@@ -269,10 +327,16 @@
     @Test
     public void testPreinvocationSetupSuccess()
             throws DeviceNotAvailableException, IOException, TargetSetupError {
+        Capture<String> reportFile = new Capture<String>();
         Capture<String> hostPackageDir = new Capture<String>();
         Capture<String> imageDir = new Capture<String>();
         IRunUtil acloudCreateRunUtil =
-                mockAcloudCreate(CommandStatus.SUCCESS, hostPackageDir, imageDir);
+                mockAcloudCreate(
+                        CommandStatus.SUCCESS,
+                        SUCCESS_REPORT_STRING,
+                        reportFile,
+                        hostPackageDir,
+                        imageDir);
 
         IRunUtil acloudDeleteRunUtil = mockAcloudDelete(CommandStatus.SUCCESS);
 
@@ -281,11 +345,12 @@
         IDevice mockOnlineDevice = EasyMock.createMock(IDevice.class);
         EasyMock.expect(mockOnlineDevice.getSerialNumber()).andReturn(ONLINE_SERIAL_NUMBER);
 
-        replayAllMocks(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger, mockOnlineDevice);
+        EasyMock.replay(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger, mockOnlineDevice);
 
         // Test setUp.
         mLocalAvd.setTestLogger(testLogger);
         mLocalAvd.currentRunUtil = acloudCreateRunUtil;
+        mLocalAvd.expectToConnect = true;
         mLocalAvd.preInvocationSetup(mMockDeviceBuildInfo, null);
 
         Assert.assertEquals(ONLINE_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
@@ -300,34 +365,42 @@
         // Create the logs and configuration that the local AVD object expects.
         File runtimeDir =
                 FileUtil.getFileForPath(
-                        mTmpDir, "acloud_cvd_temp", "instance_home_1", "cuttlefish_runtime");
+                        mTmpDir, "acloud_cvd_temp", INSTANCE_NAME, "cuttlefish_runtime");
         Assert.assertTrue(runtimeDir.mkdirs());
         createEmptyFiles(
                 runtimeDir, "kernel.log", "logcat", "launcher.log", "cuttlefish_config.json");
 
         // Test tearDown.
         mLocalAvd.currentRunUtil = acloudDeleteRunUtil;
+        mLocalAvd.expectToConnect = false;
         mLocalAvd.postInvocationTearDown(null);
 
         assertFinalDeviceState(mLocalAvd.getIDevice());
 
+        Assert.assertFalse(new File(reportFile.getValue()).exists());
         Assert.assertFalse(capturedHostPackageDir.exists());
         Assert.assertFalse(capturedImageDir.exists());
     }
 
-    /** Test that the device cannot boot within timeout. */
+    /** Test that the acloud command reports failure. */
     @Test
-    public void testPreInvocationSetupTimeout() throws DeviceNotAvailableException {
+    public void testPreInvocationSetupBootFailure() throws DeviceNotAvailableException {
+        Capture<String> reportFile = new Capture<String>();
         Capture<String> hostPackageDir = new Capture<String>();
         Capture<String> imageDir = new Capture<String>();
         IRunUtil acloudCreateRunUtil =
-                mockAcloudCreate(CommandStatus.TIMED_OUT, hostPackageDir, imageDir);
+                mockAcloudCreate(
+                        CommandStatus.SUCCESS,
+                        FAILURE_REPORT_STRING,
+                        reportFile,
+                        hostPackageDir,
+                        imageDir);
 
         IRunUtil acloudDeleteRunUtil = mockAcloudDelete(CommandStatus.FAILED);
 
         ITestLogger testLogger = EasyMock.createMock(ITestLogger.class);
 
-        replayAllMocks(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger);
+        EasyMock.replay(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger);
 
         // Test setUp.
         TargetSetupError expectedException = null;
@@ -340,7 +413,7 @@
             expectedException = e;
         }
 
-        Assert.assertEquals(ONLINE_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
+        Assert.assertEquals(STUB_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
 
         File capturedHostPackageDir = new File(hostPackageDir.getValue());
         File capturedImageDir = new File(imageDir.getValue());
@@ -353,23 +426,23 @@
 
         assertFinalDeviceState(mLocalAvd.getIDevice());
 
+        Assert.assertFalse(new File(reportFile.getValue()).exists());
         Assert.assertFalse(capturedHostPackageDir.exists());
         Assert.assertFalse(capturedImageDir.exists());
     }
 
-    /** Test that the device fails to boot. */
+    /** Test that the acloud command fails, and the report is empty. */
     @Test
     public void testPreInvocationSetupFailure() throws DeviceNotAvailableException {
+        Capture<String> reportFile = new Capture<String>();
         Capture<String> hostPackageDir = new Capture<String>();
         Capture<String> imageDir = new Capture<String>();
         IRunUtil acloudCreateRunUtil =
-                mockAcloudCreate(CommandStatus.FAILED, hostPackageDir, imageDir);
-
-        IRunUtil acloudDeleteRunUtil = mockAcloudDelete(CommandStatus.FAILED);
+                mockAcloudCreate(CommandStatus.FAILED, "", reportFile, hostPackageDir, imageDir);
 
         ITestLogger testLogger = EasyMock.createMock(ITestLogger.class);
 
-        replayAllMocks(acloudCreateRunUtil, acloudDeleteRunUtil, testLogger);
+        EasyMock.replay(acloudCreateRunUtil, testLogger);
 
         // Test setUp.
         TargetSetupError expectedException = null;
@@ -382,7 +455,7 @@
             expectedException = e;
         }
 
-        Assert.assertEquals(ONLINE_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
+        Assert.assertEquals(STUB_SERIAL_NUMBER, mLocalAvd.getIDevice().getSerialNumber());
 
         File capturedHostPackageDir = new File(hostPackageDir.getValue());
         File capturedImageDir = new File(imageDir.getValue());
@@ -390,11 +463,12 @@
         Assert.assertTrue(capturedImageDir.isDirectory());
 
         // Test tearDown.
-        mLocalAvd.currentRunUtil = acloudDeleteRunUtil;
+        mLocalAvd.currentRunUtil = null;
         mLocalAvd.postInvocationTearDown(expectedException);
 
         assertFinalDeviceState(mLocalAvd.getIDevice());
 
+        Assert.assertFalse(new File(reportFile.getValue()).exists());
         Assert.assertFalse(capturedHostPackageDir.exists());
         Assert.assertFalse(capturedImageDir.exists());
     }
diff --git a/tests/src/com/android/tradefed/device/metric/BugreportzOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/BugreportzOnFailureCollectorTest.java
index fb4b2d5..330b2c5 100644
--- a/tests/src/com/android/tradefed/device/metric/BugreportzOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/BugreportzOnFailureCollectorTest.java
@@ -56,7 +56,7 @@
     public void testCollect() throws Exception {
         TestDescription test = new TestDescription("class", "test");
         mMockListener.testStarted(EasyMock.eq(test), EasyMock.anyLong());
-        mMockListener.testFailed(EasyMock.eq(test), EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.eq(test), (String) EasyMock.anyObject());
         mMockListener.testEnded(
                 EasyMock.eq(test),
                 EasyMock.anyLong(),
diff --git a/tests/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollectorTest.java
index 9d610e3..5eb295e 100644
--- a/tests/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollectorTest.java
@@ -117,7 +117,7 @@
                 .testRunStarted(
                         Mockito.eq("runName"), Mockito.eq(1), Mockito.eq(0), Mockito.anyLong());
         Mockito.verify(mMockListener).testStarted(Mockito.eq(test), Mockito.anyLong());
-        Mockito.verify(mMockListener).testFailed(Mockito.eq(test), Mockito.any());
+        Mockito.verify(mMockListener).testFailed(Mockito.eq(test), (String) Mockito.any());
         Mockito.verify(mMockListener)
                 .testLog(
                         Mockito.eq("class#test-debug-hostlog-on-failure"),
@@ -152,7 +152,7 @@
                 .testRunStarted(
                         Mockito.eq("runName"), Mockito.eq(1), Mockito.eq(0), Mockito.anyLong());
         Mockito.verify(mMockListener).testStarted(Mockito.eq(test), Mockito.anyLong());
-        Mockito.verify(mMockListener).testFailed(Mockito.eq(test), Mockito.any());
+        Mockito.verify(mMockListener).testFailed(Mockito.eq(test), (String) Mockito.any());
         // No file is logged
         Mockito.verify(mMockListener, never())
                 .testLog(
diff --git a/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
index ca86faa..b338ed2 100644
--- a/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
@@ -111,7 +111,7 @@
                 EasyMock.eq("runName"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         TestDescription test = new TestDescription("class", "test");
         mMockListener.testStarted(EasyMock.eq(test), EasyMock.anyLong());
-        mMockListener.testFailed(EasyMock.eq(test), EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.eq(test), (String) EasyMock.anyObject());
         mMockListener.testEnded(
                 EasyMock.eq(test),
                 EasyMock.anyLong(),
@@ -152,7 +152,7 @@
                 EasyMock.eq("runName"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         TestDescription test = new TestDescription("class", "test");
         mMockListener.testStarted(EasyMock.eq(test), EasyMock.anyLong());
-        mMockListener.testFailed(EasyMock.eq(test), EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.eq(test), (String) EasyMock.anyObject());
         mMockListener.testEnded(
                 EasyMock.eq(test),
                 EasyMock.anyLong(),
@@ -201,7 +201,7 @@
         TestDescription test = new TestDescription("class", "test");
         TestDescription test2 = new TestDescription("class2", "test2");
         mMockListener.testStarted(EasyMock.eq(test), EasyMock.anyLong());
-        mMockListener.testFailed(EasyMock.eq(test), EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.eq(test), (String) EasyMock.anyObject());
         mMockListener.testEnded(
                 EasyMock.eq(test),
                 EasyMock.anyLong(),
@@ -221,7 +221,7 @@
         mMockListener.testRunStarted(
                 EasyMock.eq("runName2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         mMockListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
-        mMockListener.testFailed(EasyMock.eq(test2), EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.eq(test2), (String) EasyMock.anyObject());
         mMockListener.testEnded(
                 EasyMock.eq(test2),
                 EasyMock.anyLong(),
diff --git a/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java
index 683a466..20776d0 100644
--- a/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java
@@ -59,7 +59,7 @@
     public void testCollect() throws Exception {
         TestDescription test = new TestDescription("class", "test");
         mMockListener.testStarted(EasyMock.eq(test), EasyMock.anyLong());
-        mMockListener.testFailed(EasyMock.eq(test), EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.eq(test), (String) EasyMock.anyObject());
         mMockListener.testEnded(
                 EasyMock.eq(test),
                 EasyMock.anyLong(),
diff --git a/tests/src/com/android/tradefed/log/LogRegistryTest.java b/tests/src/com/android/tradefed/log/LogRegistryTest.java
index b43ff87..deda135 100644
--- a/tests/src/com/android/tradefed/log/LogRegistryTest.java
+++ b/tests/src/com/android/tradefed/log/LogRegistryTest.java
@@ -105,10 +105,10 @@
     }
 
     /**
-     * Tests for ensuring new threads spawned without an explicit ThreadGroup will inherit the
-     * same logger as the parent's logger.
+     * Tests for ensuring new threads spawned without an explicit ThreadGroup will inherit the same
+     * logger as the parent's logger.
      */
-    public void testThreadedLogging() {
+    public void testThreadedLogging() throws Exception {
         final String testMessage = "Another test message!";
         final ILeveledLogOutput mockLogger = EasyMock.createMock(ILeveledLogOutput.class);
 
@@ -129,11 +129,9 @@
                 secondThread.start();
                 try {
                     secondThread.join();  // threaded, but force serialization for testing
-                }
-                catch (InterruptedException ie) {
-                    fail("Thread was unexpectedly interrupted.");
-                }
-                finally {
+                } catch (InterruptedException ie) {
+                    throw new RuntimeException(ie);
+                } finally {
                     mLogRegistry.unregisterLogger();
                 }
             }
@@ -151,12 +149,6 @@
         ThreadGroup tg = new ThreadGroup("TestThreadGroup");
         Thread firstThread = new Thread(tg, new FirstThread());
         firstThread.start();
-
-        try {
-            firstThread.join();  // threaded, but force serialization for testing
-        }
-        catch (InterruptedException ie) {
-            fail("Thread was unexpectedly interrupted.");
-        }
+        firstThread.join(); // threaded, but force serialization for testing
     }
 }
diff --git a/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java b/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java
index 777e498..5d74abc 100644
--- a/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java
+++ b/tests/src/com/android/tradefed/log/TerribleFailureEmailHandlerTest.java
@@ -67,15 +67,10 @@
     }
 
     /**
-     * Test that onTerribleFailure catches IllegalArgumentException when Mailer
-     * state is incorrect
+     * Test that onTerribleFailure catches IllegalArgumentException when Mailer state is incorrect
      */
-    public void testOnTerribleFailure_catchesIllegalArgumentException() {
-        try {
-            mMockEmail.send(EasyMock.<Message> anyObject());
-        } catch (IOException e) {
-            fail("IOException escaped the method under test - should never happen");
-        }
+    public void testOnTerribleFailure_catchesIllegalArgumentException() throws IOException {
+        mMockEmail.send(EasyMock.<Message>anyObject());
         EasyMock.expectLastCall().andThrow(new IllegalArgumentException("Mailer state illegal"));
         EasyMock.replay(mMockEmail);
 
@@ -84,15 +79,9 @@
         assertFalse(retValue);
     }
 
-    /**
-     * Test that onTerribleFailure catches IOException
-     */
-    public void testOnTerribleFailure_catchesIOException() {
-        try {
-            mMockEmail.send(EasyMock.<Message> anyObject());
-        } catch (IOException e) {
-            fail("IOException escaped the method under test - should never happen");
-        }
+    /** Test that onTerribleFailure catches IOException */
+    public void testOnTerribleFailure_catchesIOException() throws IOException {
+        mMockEmail.send(EasyMock.<Message>anyObject());
         EasyMock.expectLastCall().andThrow(new IOException("Mailer had an IO Exception"));
         EasyMock.replay(mMockEmail);
 
diff --git a/tests/src/com/android/tradefed/result/FailureDescriptionTest.java b/tests/src/com/android/tradefed/result/FailureDescriptionTest.java
new file mode 100644
index 0000000..da94866
--- /dev/null
+++ b/tests/src/com/android/tradefed/result/FailureDescriptionTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link FailureDescription}. */
+@RunWith(JUnit4.class)
+public class FailureDescriptionTest {
+
+    private FailureDescription mFailureDescription;
+
+    @Test
+    public void testCreation() {
+        mFailureDescription = FailureDescription.create("error message");
+        assertNull(mFailureDescription.getFailureStatus());
+        assertEquals("error message", mFailureDescription.toString());
+        mFailureDescription.setFailureStatus(FailureStatus.TEST_FAILURE);
+        assertEquals(FailureStatus.TEST_FAILURE, mFailureDescription.getFailureStatus());
+    }
+}
diff --git a/tests/src/com/android/tradefed/result/MultiFailureDescriptionTest.java b/tests/src/com/android/tradefed/result/MultiFailureDescriptionTest.java
new file mode 100644
index 0000000..3b86019
--- /dev/null
+++ b/tests/src/com/android/tradefed/result/MultiFailureDescriptionTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 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 static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MultiFailureDescription}. */
+@RunWith(JUnit4.class)
+public class MultiFailureDescriptionTest {
+
+    private MultiFailureDescription mMultiFailureDesc;
+
+    @Test
+    public void testCreation() {
+        FailureDescription failure1 = FailureDescription.create("error message 1");
+        FailureDescription failure2 = FailureDescription.create("error message 2");
+        mMultiFailureDesc = new MultiFailureDescription(failure1, failure2);
+        assertEquals(2, mMultiFailureDesc.getFailures().size());
+        assertEquals(
+                "There were 2 failures:\n" + "  error message 1\n" + "  error message 2",
+                mMultiFailureDesc.toString());
+    }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java b/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java
index ac6b032..7c05067 100644
--- a/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstrumentationPreparerTest.java
@@ -99,7 +99,7 @@
                     public void run(TestInformation testInfo, ITestInvocationListener listener) {
                         listener.testRunStarted(packageName, 1);
                         listener.testStarted(test);
-                        listener.testFailed(test, null);
+                        listener.testFailed(test, "error");
                         listener.testEnded(test, new HashMap<String, Metric>());
                         listener.testRunEnded(0, new HashMap<String, Metric>());
                     }
diff --git a/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java b/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java
index 58788c9..9fbfa13 100644
--- a/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java
@@ -267,7 +267,7 @@
                                         "%s/test1 --benchmark_list_tests=true", nativeTestPath)))
                 .andReturn("method1\nmethod2\nmethod3");
         mMockInvocationListener.testRunStarted(test1, 3);
-        mMockInvocationListener.testRunFailed(EasyMock.anyObject());
+        mMockInvocationListener.testRunFailed((String) EasyMock.anyObject());
         // Even with exception testrunEnded is expected.
         mMockInvocationListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
diff --git a/tests/src/com/android/tradefed/testtype/HostTestTest.java b/tests/src/com/android/tradefed/testtype/HostTestTest.java
index 544991b..c119c26 100644
--- a/tests/src/com/android/tradefed/testtype/HostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/HostTestTest.java
@@ -1323,7 +1323,7 @@
                 EasyMock.eq(test1),
                 EasyMock.contains("MultipleFailureException: There were 2 errors:"));
         mListener.testEnded(EasyMock.eq(test1), (HashMap<String, Metric>) EasyMock.anyObject());
-        mListener.testRunFailed(EasyMock.anyObject());
+        mListener.testRunFailed((String) EasyMock.anyObject());
         mListener.testRunEnded(EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
         EasyMock.replay(mListener);
         try {
@@ -2186,7 +2186,7 @@
         mListener.testRunStarted(EasyMock.anyObject(), EasyMock.eq(1));
         TestDescription tid = new TestDescription(JUnit4FailedBefore.class.getName(), "test1");
         mListener.testStarted(EasyMock.eq(tid));
-        mListener.testFailed(EasyMock.eq(tid), EasyMock.anyObject());
+        mListener.testFailed(EasyMock.eq(tid), (String) EasyMock.anyObject());
         mListener.testEnded(EasyMock.eq(tid), (HashMap<String, Metric>) EasyMock.anyObject());
         mListener.testRunEnded(EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
 
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
index a44cf0a..09c5ef4 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
@@ -673,7 +672,7 @@
             }
         } catch (IOException e) {
             // fail if the file is corrupt in any way
-            fail("failed reading test file");
+            throw new RuntimeException(e);
         }
     }
 
diff --git a/tests/src/com/android/tradefed/testtype/NoisyDryRunTestTest.java b/tests/src/com/android/tradefed/testtype/NoisyDryRunTestTest.java
index a277317..2c3d663 100644
--- a/tests/src/com/android/tradefed/testtype/NoisyDryRunTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/NoisyDryRunTestTest.java
@@ -141,7 +141,7 @@
         mMockListener.testRunStarted("com.android.tradefed.testtype.NoisyDryRunTest_parseFile", 1);
         mMockListener.testStarted(anyObject());
         mMockListener.testEnded(anyObject(), EasyMock.<HashMap<String, Metric>>anyObject());
-        mMockListener.testFailed(anyObject(), anyObject());
+        mMockListener.testFailed(anyObject(), (String) anyObject());
         mMockListener.testRunEnded(EasyMock.eq(0l), EasyMock.<HashMap<String, Metric>>anyObject());
         replayMocks();
 
@@ -166,7 +166,7 @@
         mMockListener.testStarted(anyObject());
         mMockListener.testEnded(anyObject(), EasyMock.<HashMap<String, Metric>>anyObject());
         mMockListener.testStarted(anyObject());
-        mMockListener.testFailed(anyObject(), anyObject());
+        mMockListener.testFailed(anyObject(), (String) anyObject());
         mMockListener.testEnded(anyObject(), EasyMock.<HashMap<String, Metric>>anyObject());
         mMockListener.testRunEnded(EasyMock.eq(0l), EasyMock.<HashMap<String, Metric>>anyObject());
         replayMocks();
diff --git a/tests/src/com/android/tradefed/testtype/PythonUnitTestResultParserTest.java b/tests/src/com/android/tradefed/testtype/PythonUnitTestResultParserTest.java
index 366f75c..a8f4299 100644
--- a/tests/src/com/android/tradefed/testtype/PythonUnitTestResultParserTest.java
+++ b/tests/src/com/android/tradefed/testtype/PythonUnitTestResultParserTest.java
@@ -497,16 +497,16 @@
 
         mMockListener.testFailed(
                 EasyMock.eq(new TestDescription("__main__.DisconnectionTest", "test_disconnect")),
-                EasyMock.anyObject());
+                (String) EasyMock.anyObject());
         mMockListener.testFailed(
                 EasyMock.eq(new TestDescription("__main__.EmulatorTest", "test_emulator_connect")),
-                EasyMock.anyObject());
+                (String) EasyMock.anyObject());
         mMockListener.testIgnored(
                 EasyMock.eq(new TestDescription("__main__.PowerTest", "test_resume_usb_kick")));
         // Multi-line error
         mMockListener.testFailed(
                 EasyMock.eq(new TestDescription("__main__.ServerTest", "test_handle_inheritance")),
-                EasyMock.anyObject());
+                (String) EasyMock.anyObject());
 
         mMockListener.testRunEnded(10314, new HashMap<String, Metric>());
         replay(mMockListener);
@@ -550,7 +550,7 @@
 
         mMockListener.testFailed(
                 EasyMock.eq(new TestDescription("__main__.ConnectionTest", "test_reconnect")),
-                EasyMock.anyObject());
+                (String) EasyMock.anyObject());
         mMockListener.testIgnored(
                 EasyMock.eq(new TestDescription("__main__.PowerTest", "test_resume_usb_kick")));
 
diff --git a/tests/src/com/android/tradefed/testtype/TfTestLauncherTest.java b/tests/src/com/android/tradefed/testtype/TfTestLauncherTest.java
index 8c1bad4..3c2ccb6 100644
--- a/tests/src/com/android/tradefed/testtype/TfTestLauncherTest.java
+++ b/tests/src/com/android/tradefed/testtype/TfTestLauncherTest.java
@@ -236,6 +236,9 @@
     public void testRunCoverage() throws Exception {
         OptionSetter setter = new OptionSetter(mTfTestLauncher);
         setter.setOptionValue("jacoco-code-coverage", "true");
+        setter.setOptionValue("include-coverage", "com.android.tradefed*");
+        setter.setOptionValue("include-coverage", "com.google.android.tradefed*");
+        setter.setOptionValue("exclude-coverage", "com.test*");
         EasyMock.expect(mMockBuildInfo.getRootDir()).andReturn(new File(""));
         EasyMock.expect(mMockBuildInfo.getTestTag()).andReturn(TEST_TAG);
         EasyMock.expect(mMockBuildInfo.getBuildBranch()).andReturn(BUILD_BRANCH).times(2);
@@ -253,6 +256,13 @@
             mTfTestLauncher.preRun();
             EasyMock.verify(mMockBuildInfo, mMockRunUtil, mMockListener);
             assertTrue(mTfTestLauncher.mCmdArgs.get(2).startsWith("-javaagent:"));
+            assertTrue(
+                    mTfTestLauncher
+                            .mCmdArgs
+                            .get(2)
+                            .contains(
+                                    "includes=com.android.tradefed*:com.google.android.tradefed*,"
+                                            + "excludes=com.test*"));
         } finally {
             FileUtil.recursiveDelete(mTfTestLauncher.mTmpDir);
             mTfTestLauncher.cleanTmpFile();
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
index e973b80..96e99cc 100644
--- a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
@@ -120,8 +120,8 @@
             mExecutableTest.run(mTestInfo, mMockListener);
 
             verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(1));
-            verify(mMockListener, Mockito.times(0)).testRunFailed(any());
-            verify(mMockListener, Mockito.times(0)).testFailed(any(), any());
+            verify(mMockListener, Mockito.times(0)).testRunFailed((String) any());
+            verify(mMockListener, Mockito.times(0)).testFailed(any(), (String) any());
             verify(mMockListener, Mockito.times(1))
                     .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
         } finally {
@@ -154,8 +154,8 @@
             mExecutableTest.run(mTestInfo, mMockListener);
 
             verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(1));
-            verify(mMockListener, Mockito.times(0)).testRunFailed(any());
-            verify(mMockListener, Mockito.times(0)).testFailed(any(), any());
+            verify(mMockListener, Mockito.times(0)).testRunFailed((String) any());
+            verify(mMockListener, Mockito.times(0)).testFailed(any(), (String) any());
             verify(mMockListener, Mockito.times(1))
                     .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
         } finally {
@@ -194,7 +194,7 @@
                                     String.format(
                                             "Device became unavailable after %s.",
                                             tmpBinary.getAbsolutePath())));
-            verify(mMockListener, Mockito.times(0)).testFailed(any(), any());
+            verify(mMockListener, Mockito.times(0)).testFailed(any(), (String) any());
             verify(mMockListener, Mockito.times(1))
                     .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
         } finally {
@@ -226,8 +226,8 @@
             mExecutableTest.run(mTestInfo, mMockListener);
 
             verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(1));
-            verify(mMockListener, Mockito.times(0)).testRunFailed(any());
-            verify(mMockListener, Mockito.times(0)).testFailed(any(), any());
+            verify(mMockListener, Mockito.times(0)).testRunFailed((String) any());
+            verify(mMockListener, Mockito.times(0)).testFailed(any(), (String) any());
             verify(mMockListener, Mockito.times(1))
                     .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
         } finally {
@@ -300,7 +300,7 @@
             mExecutableTest.run(mTestInfo, mMockListener);
 
             verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(1));
-            verify(mMockListener, Mockito.times(0)).testRunFailed(any());
+            verify(mMockListener, Mockito.times(0)).testRunFailed((String) any());
             verify(mMockListener, Mockito.times(1)).testFailed(any(), eq("stdout\nExit Code: 5"));
             verify(mMockListener, Mockito.times(1))
                     .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
diff --git a/tests/src/com/android/tradefed/testtype/junit4/LongevityHostRunnerTest.java b/tests/src/com/android/tradefed/testtype/junit4/LongevityHostRunnerTest.java
index 07c13b0..37b4b74 100644
--- a/tests/src/com/android/tradefed/testtype/junit4/LongevityHostRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/junit4/LongevityHostRunnerTest.java
@@ -226,7 +226,7 @@
         mHostTest.publicSetClassName(PassingLongevitySuite.class.getName());
         mHostTest.run(mTestInfo, mMockListener);
         // Verify nothing failed, but something passed.
-        verify(mMockListener, never()).testFailed(any(), any());
+        verify(mMockListener, never()).testFailed(any(), (String) any());
         verify(mMockListener).testEnded(any(), Mockito.<HashMap<String, Metric>>any());
     }
 
diff --git a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
index b285530..5fd5c5f 100644
--- a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
@@ -188,7 +188,7 @@
                     EasyMock.anyObject());
             // Report a failure if we cannot parse the logs
             mMockListener.testRunStarted(binary.getName(), 0);
-            mMockListener.testRunFailed(EasyMock.anyObject());
+            mMockListener.testRunFailed((String) EasyMock.anyObject());
             mMockListener.testRunEnded(
                     EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
 
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
index 2fbef8e..09831e7 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustBinaryHostTestTest.java
@@ -166,7 +166,7 @@
                     EasyMock.anyObject());
             // Report a failure if we cannot parse the logs and the logs is not empty.
             mMockListener.testRunStarted(binary.getName(), 0);
-            mMockListener.testRunFailed(EasyMock.anyObject());
+            mMockListener.testRunFailed((String) EasyMock.anyObject());
             mMockListener.testRunEnded(
                     EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
 
diff --git a/tests/src/com/android/tradefed/testtype/rust/RustTestResultParserTest.java b/tests/src/com/android/tradefed/testtype/rust/RustTestResultParserTest.java
index 9e06ae9..9a261c0 100644
--- a/tests/src/com/android/tradefed/testtype/rust/RustTestResultParserTest.java
+++ b/tests/src/com/android/tradefed/testtype/rust/RustTestResultParserTest.java
@@ -134,9 +134,10 @@
                     EasyMock.anyObject(), EasyMock.<HashMap<String, Metric>>anyObject());
         }
         mMockListener.testFailed(
-                EasyMock.eq(new TestDescription("test", "idents")), EasyMock.anyObject());
+                EasyMock.eq(new TestDescription("test", "idents")), (String) EasyMock.anyObject());
         mMockListener.testFailed(
-                EasyMock.eq(new TestDescription("test", "literal_string")), EasyMock.anyObject());
+                EasyMock.eq(new TestDescription("test", "literal_string")),
+                (String) EasyMock.anyObject());
         mMockListener.testRunEnded(0, new HashMap<String, Metric>());
         replay(mMockListener);
         mParser.processNewLines(contents);
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
index bc6e8fc..4273924 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
@@ -456,7 +456,7 @@
                     EasyMock.anyLong(),
                     EasyMock.<HashMap<String, Metric>>anyObject());
         }
-        mMockListener.testFailed(EasyMock.anyObject(), EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.anyObject(), (String) EasyMock.anyObject());
         String aggError =
                 "unresponsive\n====Next Error====\n"
                         + "Module fakeName only ran 1 out of 4 expected tests.\n====Next "
@@ -877,8 +877,8 @@
                     EasyMock.anyLong(),
                     EasyMock.<HashMap<String, Metric>>anyObject());
         }
-        mMockListener.testFailed(EasyMock.anyObject(), EasyMock.anyObject());
-        mMockListener.testRunFailed(EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.anyObject(), (String) EasyMock.anyObject());
+        mMockListener.testRunFailed((String) EasyMock.anyObject());
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
         // Recovery is disabled during tearDown
@@ -939,8 +939,8 @@
                     EasyMock.anyLong(),
                     EasyMock.<HashMap<String, Metric>>anyObject());
         }
-        mMockListener.testFailed(EasyMock.anyObject(), EasyMock.anyObject());
-        mMockListener.testRunFailed(EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.anyObject(), (String) EasyMock.anyObject());
+        mMockListener.testRunFailed((String) EasyMock.anyObject());
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
 
@@ -1283,7 +1283,7 @@
                     EasyMock.anyLong(),
                     EasyMock.<HashMap<String, Metric>>anyObject());
         }
-        mMockListener.testFailed(EasyMock.anyObject(), EasyMock.anyObject());
+        mMockListener.testFailed(EasyMock.anyObject(), (String) EasyMock.anyObject());
         mMockListener.testRunFailed(
                 "unresponsive"
                         + TestRunResult.ERROR_DIVIDER
@@ -1531,7 +1531,7 @@
                     EasyMock.<HashMap<String, Metric>>anyObject());
             TestDescription testFail0 = new TestDescription(runName + "0class", "fail0");
             mMockListener.testStarted(EasyMock.eq(testFail0), EasyMock.anyLong());
-            mMockListener.testFailed(EasyMock.eq(testFail0), EasyMock.anyObject());
+            mMockListener.testFailed(EasyMock.eq(testFail0), (String) EasyMock.anyObject());
             mMockListener.testEnded(
                     EasyMock.eq(testFail0),
                     EasyMock.anyLong(),
@@ -1552,7 +1552,7 @@
                     EasyMock.<HashMap<String, Metric>>anyObject());
             TestDescription testFail0_1 = new TestDescription(runName + "1class", "fail0");
             mMockListener.testStarted(EasyMock.eq(testFail0_1), EasyMock.anyLong());
-            mMockListener.testFailed(EasyMock.eq(testFail0_1), EasyMock.anyObject());
+            mMockListener.testFailed(EasyMock.eq(testFail0_1), (String) EasyMock.anyObject());
             mMockListener.testEnded(
                     EasyMock.eq(testFail0_1),
                     EasyMock.anyLong(),
@@ -1637,7 +1637,7 @@
             }
             TestDescription testFail0 = new TestDescription(runName + "0class", "fail0");
             mMockListener.testStarted(EasyMock.eq(testFail0), EasyMock.anyLong());
-            mMockListener.testFailed(EasyMock.eq(testFail0), EasyMock.anyObject());
+            mMockListener.testFailed(EasyMock.eq(testFail0), (String) EasyMock.anyObject());
             mMockListener.testEnded(
                     EasyMock.eq(testFail0),
                     EasyMock.anyLong(),
@@ -1662,7 +1662,7 @@
             }
             TestDescription testFail0_1 = new TestDescription(runName + "1class", "fail0");
             mMockListener.testStarted(EasyMock.eq(testFail0_1), EasyMock.anyLong());
-            mMockListener.testFailed(EasyMock.eq(testFail0_1), EasyMock.anyObject());
+            mMockListener.testFailed(EasyMock.eq(testFail0_1), (String) EasyMock.anyObject());
             mMockListener.testEnded(
                     EasyMock.eq(testFail0_1),
                     EasyMock.anyLong(),
diff --git a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
index 6a75933..6f4fe50 100644
--- a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.targetprep.BaseTargetPreparer;
 import com.android.tradefed.testtype.Abi;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IRemoteTest;
@@ -54,6 +55,7 @@
 
     private static final String TEST_CONFIG =
             "<configuration description=\"Runs a stub tests part of some suite\">\n"
+                    + "    <target_preparer class=\"com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$PreparerInject\" />\n"
                     + "    <test class=\"com.android.tradefed.testtype.suite.SuiteModuleLoaderTest"
                     + "$TestInject\" />\n"
                     + "</configuration>";
@@ -96,6 +98,12 @@
         FileUtil.writeToFile(TEST_INSTANT_CONFIG, module);
     }
 
+    @OptionClass(alias = "preparer-inject")
+    public static class PreparerInject extends BaseTargetPreparer {
+        @Option(name = "preparer-string")
+        public String preparer = null;
+    }
+
     @OptionClass(alias = "test-inject")
     public static class TestInject implements IRemoteTest {
         @Option(name = "simple-string")
@@ -168,6 +176,11 @@
     @Test
     public void testInjectConfigOptions_testArgs() throws Exception {
         List<String> testArgs = new ArrayList<>();
+        // Value for ITargetPreparer
+        testArgs.add(
+                "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$PreparerInject:"
+                        + "preparer-string:preparer");
+        // Values for IRemoteTest
         testArgs.add(
                 "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
                         + "simple-string:value1");
@@ -210,6 +223,9 @@
         assertNotNull(res.get("armeabi-v7a module1"));
         IConfiguration config = res.get("armeabi-v7a module1");
 
+        PreparerInject preparer = (PreparerInject) config.getTargetPreparers().get(0);
+        assertEquals("preparer", preparer.preparer);
+
         TestInject checker = (TestInject) config.getTests().get(0);
         assertEquals("value1", checker.test);
         assertEquals("", checker.testEmpty);
diff --git a/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java b/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java
index 06c8b32..eda01e0 100644
--- a/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.ILeveledLogOutput;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -47,6 +48,7 @@
     private ResultsPlayer mPlayer;
     private ITestInvocationListener mMockListener;
     private IInvocationContext mContext;
+    private TestInformation mTestInfo;
     private ITestDevice mMockDevice;
     private IDevice mMockIDevice;
     private IConfiguration mMockConfig;
@@ -64,9 +66,9 @@
         EasyMock.expect(mMockLogOutput.getLogLevel()).andReturn(LogLevel.VERBOSE);
         mMockLogOutput.setLogLevel(LogLevel.WARN);
         mMockLogOutput.setLogLevel(LogLevel.VERBOSE);
+        mTestInfo = TestInformation.newBuilder().setInvocationContext(mContext).build();
 
         mPlayer = new ResultsPlayer();
-        mPlayer.setInvocationContext(mContext);
         mPlayer.setConfiguration(mMockConfig);
         mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
 
@@ -96,7 +98,7 @@
         mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
 
         EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
-        mPlayer.run(mMockListener);
+        mPlayer.run(mTestInfo, mMockListener);
         EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
     }
 
@@ -142,7 +144,7 @@
         mMockListener.testModuleEnded();
 
         EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
-        mPlayer.run(mMockListener);
+        mPlayer.run(mTestInfo, mMockListener);
         EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
     }
 
@@ -169,7 +171,7 @@
         mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
 
         EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
-        mPlayer.run(mMockListener);
+        mPlayer.run(mTestInfo, mMockListener);
         EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
     }
 
@@ -212,7 +214,7 @@
         mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
 
         EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
-        mPlayer.run(mMockListener);
+        mPlayer.run(mTestInfo, mMockListener);
         EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
     }
 
diff --git a/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java b/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
index a7709aa..245b2ed 100644
--- a/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
@@ -124,7 +124,7 @@
                 mRescheduledConfiguration,
                 mMockCommandOptions,
                 mMockRequirements);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -169,7 +169,7 @@
                 mRescheduledConfiguration,
                 mMockCommandOptions,
                 mMockRequirements);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -206,7 +206,7 @@
                 mMockFactory,
                 mRescheduledConfiguration,
                 mMockCommandOptions);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -245,7 +245,7 @@
                 mMockFactory,
                 mRescheduledConfiguration,
                 mMockCommandOptions);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -284,7 +284,7 @@
                 mMockFactory,
                 mRescheduledConfiguration,
                 mMockCommandOptions);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -329,7 +329,7 @@
                 mMockFactory,
                 mRescheduledConfiguration,
                 mMockCommandOptions);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -368,7 +368,7 @@
                 mMockFactory,
                 mRescheduledConfiguration,
                 mMockCommandOptions);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -411,7 +411,7 @@
                 mMockFactory,
                 mRescheduledConfiguration,
                 mMockCommandOptions);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -452,7 +452,7 @@
                 mMockFactory,
                 mRescheduledConfiguration,
                 mMockCommandOptions);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -490,7 +490,7 @@
                 mMockFactory,
                 mRescheduledConfiguration,
                 mMockCommandOptions);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
@@ -539,7 +539,7 @@
                 mMockFactory,
                 mRescheduledConfiguration,
                 mMockCommandOptions);
-        mTest.run(null);
+        mTest.run(null, null);
         EasyMock.verify(
                 mMockRescheduler,
                 mMockLoader,
diff --git a/tests/src/com/android/tradefed/util/AaptParserTest.java b/tests/src/com/android/tradefed/util/AaptParserTest.java
index b754df1..f25871c 100644
--- a/tests/src/com/android/tradefed/util/AaptParserTest.java
+++ b/tests/src/com/android/tradefed/util/AaptParserTest.java
@@ -139,4 +139,117 @@
         assertEquals("arm64-v8a", p.getNativeCode().get(0));
         assertEquals("armeabi-v7a", p.getNativeCode().get(1));
     }
+
+    public void testParseXmlTree_withRequestLegacyFlagTrue() {
+        AaptParser p = new AaptParser();
+        p.parseXmlTree(
+                "N: android=http://schemas.android.com/apk/res/android\n"
+                        + "  E: manifest (line=2)\n"
+                        + "    A: android:versionCode(0x0101021b)=(type 0x10)0x1d\n"
+                        + "    A: android:versionName(0x0101021c)=\"R\" (Raw: \"R\")\n"
+                        + "    A: android:compileSdkVersion(0x01010572)=(type 0x10)0x1d\n"
+                        + "    A: android:compileSdkVersionCodename(0x01010573)=\"R\" (Raw: "
+                        + "\"R\")\n"
+                        + "    A: package=\"com.android.foo\" (Raw: \"com.android.foo\")\n"
+                        + "    A: platformBuildVersionCode=(type 0x10)0x1d\n"
+                        + "    A: platformBuildVersionName=\"R\" (Raw: \"R\")\n"
+                        + "    E: uses-sdk (line=5)\n"
+                        + "      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x1c\n"
+                        + "      A: android:targetSdkVersion(0x01010270)=\"R\" (Raw: \"R\")\n"
+                        + "    E: application (line=12)\n"
+                        + "      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1e\n"
+                        + "      A: android:supportsRtl(0x010103af)=(type 0x12)0xffffffff\n"
+                        + "      A: android:extractNativeLibs(0x010104ea)=(type 0x12)0xffffffff\n"
+                        + "      A: android:appComponentFactory(0x0101057a)=\"androidx.core.app"
+                        + ".CoreComponentFactory\" (Raw: \"androidx.core.app"
+                        + ".CoreComponentFactory\")\n"
+                        + "      A: android:requestLegacyExternalStorage(0x01010603)=(type 0x12)"
+                        + "0xffffffff\n");
+        assertTrue(p.isRequestingLegacyStorage());
+    }
+
+    public void testParseXmlTree_withRequestLegacyFlagFalse() {
+        AaptParser p = new AaptParser();
+        p.parseXmlTree(
+                "N: android=http://schemas.android.com/apk/res/android\n"
+                        + "  E: manifest (line=2)\n"
+                        + "    A: android:versionCode(0x0101021b)=(type 0x10)0x1d\n"
+                        + "    A: android:versionName(0x0101021c)=\"R\" (Raw: \"R\")\n"
+                        + "    A: android:compileSdkVersion(0x01010572)=(type 0x10)0x1d\n"
+                        + "    A: android:compileSdkVersionCodename(0x01010573)=\"R\" (Raw: "
+                        + "\"R\")\n"
+                        + "    A: package=\"com.android.foo\" (Raw: \"com.android.foo\")\n"
+                        + "    A: platformBuildVersionCode=(type 0x10)0x1d\n"
+                        + "    A: platformBuildVersionName=\"R\" (Raw: \"R\")\n"
+                        + "    E: uses-sdk (line=5)\n"
+                        + "      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x1c\n"
+                        + "      A: android:targetSdkVersion(0x01010270)=\"R\" (Raw: \"R\")\n"
+                        + "    E: application (line=12)\n"
+                        + "      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1e\n"
+                        + "      A: android:supportsRtl(0x010103af)=(type 0x12)0xffffffff\n"
+                        + "      A: android:extractNativeLibs(0x010104ea)=(type 0x12)0xffffffff\n"
+                        + "      A: android:appComponentFactory(0x0101057a)=\"androidx.core.app"
+                        + ".CoreComponentFactory\" (Raw: \"androidx.core.app"
+                        + ".CoreComponentFactory\")\n"
+                        + "      A: android:requestLegacyExternalStorage(0x01010603)=(type 0x12)"
+                        + "0x0\n");
+        assertFalse(p.isRequestingLegacyStorage());
+    }
+
+    public void testParseXmlTree_withoutRequestLegacyFlag() {
+        AaptParser p = new AaptParser();
+        p.parseXmlTree(
+                "N: android=http://schemas.android.com/apk/res/android\n"
+                        + "  E: manifest (line=2)\n"
+                        + "    A: android:versionCode(0x0101021b)=(type 0x10)0x1d\n"
+                        + "    A: android:versionName(0x0101021c)=\"R\" (Raw: \"R\")\n"
+                        + "    A: android:compileSdkVersion(0x01010572)=(type 0x10)0x1d\n"
+                        + "    A: android:compileSdkVersionCodename(0x01010573)=\"R\" (Raw: "
+                        + "\"R\")\n"
+                        + "    A: package=\"com.android.foo\" (Raw: \"com.android.foo\")\n"
+                        + "    A: platformBuildVersionCode=(type 0x10)0x1d\n"
+                        + "    A: platformBuildVersionName=\"R\" (Raw: \"R\")\n"
+                        + "    E: uses-sdk (line=5)\n"
+                        + "      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x1c\n"
+                        + "      A: android:targetSdkVersion(0x01010270)=\"R\" (Raw: \"R\")\n"
+                        + "    E: application (line=12)\n"
+                        + "      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1e\n"
+                        + "      A: android:supportsRtl(0x010103af)=(type 0x12)0xffffffff\n"
+                        + "      A: android:extractNativeLibs(0x010104ea)=(type 0x12)0xffffffff\n"
+                        + "      A: android:appComponentFactory(0x0101057a)=\"androidx.core.app"
+                        + ".CoreComponentFactory\" (Raw: \"androidx.core.app");
+        assertFalse(p.isRequestingLegacyStorage());
+    }
+
+    public void testParseTargetSdkVersion() {
+        AaptParser p = new AaptParser();
+        p.parse(
+                "package: name='com.android.foo' versionCode='13' versionName='2.3'\n"
+                        + "sdkVersion:'5'\n"
+                        + "targetSdkVersion:'29'\n"
+                        + "application-label-fr:'Faa'\n"
+                        + "uses-permission:'android.permission.INTERNET'");
+        assertEquals(29, p.getTargetSdkVersion());
+    }
+
+    public void testParseInvalidTargetSdkVersion() {
+        AaptParser p = new AaptParser();
+        p.parse(
+                "package: name='com.android.foo' versionCode='13' versionName='2.3'\n"
+                        + "sdkVersion:'5'\n"
+                        + "targetSdkVersion:'R'\n"
+                        + "application-label-fr:'Faa'\n"
+                        + "uses-permission:'android.permission.INTERNET'");
+        assertEquals(10000, p.getTargetSdkVersion());
+    }
+
+    public void testParseNoTargetSdkVersion() {
+        AaptParser p = new AaptParser();
+        p.parse(
+                "package: name='com.android.foo' versionCode='13' versionName='2.3'\n"
+                        + "sdkVersion:'5'\n"
+                        + "application-label-fr:'Faa'\n"
+                        + "uses-permission:'android.permission.INTERNET'");
+        assertEquals(10000, p.getTargetSdkVersion());
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/ZipUtilTest.java b/tests/src/com/android/tradefed/util/ZipUtilTest.java
index 51dc090..e06b846 100644
--- a/tests/src/com/android/tradefed/util/ZipUtilTest.java
+++ b/tests/src/com/android/tradefed/util/ZipUtilTest.java
@@ -266,9 +266,6 @@
             try (BufferedReader br = new BufferedReader(new FileReader(targetFile))) {
                 String line = br.readLine();
                 assertTrue(line.endsWith("this is a text file."));
-            } catch (IOException e) {
-                // fail if the file is corrupt in any way
-                fail("failed reading text file");
             }
             // Verify file permissions - read/write - 666 rw-rw-rw-
             permissions = Files.getPosixFilePermissions(targetFile.toPath());