Snap for 6981242 from 556c976ea4afc4c7b2a5b653f677e8ee3dbf7eac to mainline-release

Change-Id: I012d7bbd8fbea9a0dd6dfb6dcb9fa342d43e1352
diff --git a/Android.bp b/Android.bp
index 4d6f01b..fb8090f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -48,6 +48,8 @@
       "-Xep:TryFailThrowable:ERROR",
       "-Xep:UnnecessaryParentheses:ERROR",
       "-Xep:UseCorrectAssertInTests:ERROR",
+      "-XepDisableWarningsInGeneratedCode",  // Disable warnings in gRPC generated code.
+      "-XepExcludedPaths:.*/srcjars/.*"
     ],
   },
 }
@@ -77,6 +79,24 @@
     ],
 }
 
+java_genrule_host {
+    name: "lab-resource-grpc-gen",
+    srcs: [
+        "proto/monitoring/server/lab_resource.proto",
+    ],
+    tools: [
+        "aprotoc",
+        "protoc-gen-grpc-java-plugin",
+        "soong_zip",
+     ],
+     arch: "common",
+     cmd: "$(location aprotoc) -Iexternal/protobuf/src" +
+        " -Itools/tradefederation/core/proto/monitoring/server" +
+        " --plugin=protoc-gen-grpc=$(location protoc-gen-grpc-java-plugin) $(in)" +
+        " --grpc_out=$(genDir) && $(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)",
+     out: ["tradefed-grpc.srcjar"],
+}
+
 python_library_host {
     name: "tradefed-protos-py",
     pkg_path: "atest",
@@ -133,6 +153,7 @@
     srcs: [
         "src/**/*.java",
         "global_configuration/**/*.java",
+        ":lab-resource-grpc-gen",
     ],
     static_libs: [
         "tradefed-common-util",
@@ -159,6 +180,14 @@
         "tradefed-protos",
         "tradefed-isolation-protos",
         "tradefed-lite",
+        "guava",
+        "guava-testlib",
+        "grpc-java",
+        "grpc-java-testing",
+        "grpc-java-netty-shaded",
+        "javax-annotation-api-prebuilt-host-jar",
+        "opencensus-java-api",
+        "opencensus-java-contrib-grpc-metrics",
     ],
     libs: [
         "loganalysis",
@@ -210,7 +239,6 @@
           "-werror " +
           "-package " +
           "-devsite ",
-    create_stubs: false,
 }
 
 sh_binary_host {
diff --git a/atest/Android.bp b/atest/Android.bp
index 53553fd..a403173 100644
--- a/atest/Android.bp
+++ b/atest/Android.bp
@@ -134,7 +134,6 @@
         "atest_proto",
     ],
     test_config: "atest_unittests.xml",
-    test_suites: ["general-tests"],
     defaults: ["atest_py2_default"],
 }
 
diff --git a/atest/TEST_MAPPING b/atest/TEST_MAPPING
index 32e6a6d..09a0ffb 100644
--- a/atest/TEST_MAPPING
+++ b/atest/TEST_MAPPING
@@ -28,11 +28,11 @@
     }
   ],
   "presubmit": [
-    {
-      // Host side ATest unittests.
-      "name": "atest_unittests",
-      "host": true
-    },
+//    {
+//      // Host side ATest unittests.
+//      "name": "atest_unittests",
+//      "host": true
+//    },
     {
       // Host side metrics tests.
       "name": "asuite_metrics_lib_tests",
diff --git a/common_util/com/android/tradefed/config/ConfigurationException.java b/common_util/com/android/tradefed/config/ConfigurationException.java
index 2af9c01..696ee59 100644
--- a/common_util/com/android/tradefed/config/ConfigurationException.java
+++ b/common_util/com/android/tradefed/config/ConfigurationException.java
@@ -15,10 +15,13 @@
  */
 package com.android.tradefed.config;
 
-/**
- * Thrown if configuration could not be loaded.
- */
-public class ConfigurationException extends Exception {
+import com.android.tradefed.error.HarnessException;
+import com.android.tradefed.result.error.ErrorIdentifier;
+
+import java.lang.StackWalker.Option;
+
+/** Thrown if configuration could not be loaded. */
+public class ConfigurationException extends HarnessException {
     private static final long serialVersionUID = 7742154448569011969L;
 
     /**
@@ -27,7 +30,19 @@
      * @param msg a meaningful error message
      */
     public ConfigurationException(String msg) {
-        super(msg);
+        super(msg, null);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
+    }
+
+    /**
+     * Creates a {@link ConfigurationException}.
+     *
+     * @param msg a meaningful error message
+     * @param error The {@link ErrorIdentifier} associated with the exception
+     */
+    public ConfigurationException(String msg, ErrorIdentifier error) {
+        super(msg, error);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 
     /**
@@ -37,7 +52,20 @@
      * @param cause the {@link Throwable} that represents the original cause of the error
      */
     public ConfigurationException(String msg, Throwable cause) {
-        super(msg, cause);
+        super(msg, cause, null);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
+    }
+
+    /**
+     * Creates a {@link ConfigurationException}.
+     *
+     * @param msg a meaningful error message
+     * @param cause the {@link Throwable} that represents the original cause of the error
+     * @param error The {@link ErrorIdentifier} associated with the exception
+     */
+    public ConfigurationException(String msg, Throwable cause, ErrorIdentifier error) {
+        super(msg, cause, error);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 }
 
diff --git a/device_build_interfaces/com/android/tradefed/error/HarnessException.java b/common_util/com/android/tradefed/error/HarnessException.java
similarity index 100%
rename from device_build_interfaces/com/android/tradefed/error/HarnessException.java
rename to common_util/com/android/tradefed/error/HarnessException.java
diff --git a/device_build_interfaces/com/android/tradefed/error/HarnessRuntimeException.java b/common_util/com/android/tradefed/error/HarnessRuntimeException.java
similarity index 94%
rename from device_build_interfaces/com/android/tradefed/error/HarnessRuntimeException.java
rename to common_util/com/android/tradefed/error/HarnessRuntimeException.java
index 1da5df9..9b88de7 100644
--- a/device_build_interfaces/com/android/tradefed/error/HarnessRuntimeException.java
+++ b/common_util/com/android/tradefed/error/HarnessRuntimeException.java
@@ -78,4 +78,10 @@
     public String getOrigin() {
         return mOrigin;
     }
+
+    protected final void setCallerClass(Class<?> clazz) {
+        if (clazz != null) {
+            mOrigin = clazz.getCanonicalName();
+        }
+    }
 }
diff --git a/device_build_interfaces/com/android/tradefed/error/IHarnessException.java b/common_util/com/android/tradefed/error/IHarnessException.java
similarity index 100%
rename from device_build_interfaces/com/android/tradefed/error/IHarnessException.java
rename to common_util/com/android/tradefed/error/IHarnessException.java
diff --git a/common_util/com/android/tradefed/result/LogDataType.java b/common_util/com/android/tradefed/result/LogDataType.java
index da1eb9e..0bcb9a8 100644
--- a/common_util/com/android/tradefed/result/LogDataType.java
+++ b/common_util/com/android/tradefed/result/LogDataType.java
@@ -32,7 +32,7 @@
     JPEG("jpeg", "image/jpeg", true, false),
     TAR_GZ("tar.gz", "application/gzip", true, false),
     GZIP("gz", "application/gzip", true, false),
-    HPROF("hprof", "text/plain", true, false),
+    HPROF("hprof", "application/octet-stream", true, false),
     COVERAGE("ec", "text/plain", false, false), // Emma coverage file
     NATIVE_COVERAGE("zip", "application/zip", true, false), // gcov coverage archive
     CLANG_COVERAGE("profdata", "text/plain", false, false), // LLVM indexed profile data
@@ -60,6 +60,7 @@
     ATRACE("atr", "text/plain", true, false), // atrace -z format
     KERNEL_TRACE("dat", "text/plain", false, false), // raw kernel ftrace buffer
     DIR("", "text/plain", false, false),
+    CFG("cfg", "application/octet-stream", false, true),
     /* Unknown file type */
     UNKNOWN("dat", "text/plain", false, false);
 
diff --git a/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java b/common_util/com/android/tradefed/result/error/DeviceErrorIdentifier.java
similarity index 91%
rename from test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
rename to common_util/com/android/tradefed/result/error/DeviceErrorIdentifier.java
index 72efb6a..483d02b 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/DeviceErrorIdentifier.java
+++ b/common_util/com/android/tradefed/result/error/DeviceErrorIdentifier.java
@@ -30,6 +30,9 @@
 
     SHELL_COMMAND_ERROR(520_100, FailureStatus.DEPENDENCY_ISSUE),
     DEVICE_UNEXPECTED_RESPONSE(30_101, FailureStatus.DEPENDENCY_ISSUE),
+    FAIL_PUSH_FILE(30_102, FailureStatus.DEPENDENCY_ISSUE),
+    FAIL_PULL_FILE(30_103, FailureStatus.DEPENDENCY_ISSUE),
+    DEVICE_FAILED_TO_RESET(30_104, FailureStatus.DEPENDENCY_ISSUE),
 
     INSTRUMENTATION_CRASH(520_200, FailureStatus.SYSTEM_UNDER_TEST_CRASHED),
 
diff --git a/test_result_interfaces/com/android/tradefed/result/error/ErrorIdentifier.java b/common_util/com/android/tradefed/result/error/ErrorIdentifier.java
similarity index 92%
rename from test_result_interfaces/com/android/tradefed/result/error/ErrorIdentifier.java
rename to common_util/com/android/tradefed/result/error/ErrorIdentifier.java
index 4d00bdd..48e0b40 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/ErrorIdentifier.java
+++ b/common_util/com/android/tradefed/result/error/ErrorIdentifier.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.result.error;
 
-import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 
 /**
@@ -33,7 +32,7 @@
 
     /**
      * The failure status associated with the identifier, this status is expected to align with the
-     * {@link FailureDescription} one.
+     * FailureDescription one.
      */
     public FailureStatus status();
 }
diff --git a/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java b/common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java
similarity index 83%
rename from test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
rename to common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java
index a910457..619b4fb 100644
--- a/test_result_interfaces/com/android/tradefed/result/error/InfraErrorIdentifier.java
+++ b/common_util/com/android/tradefed/result/error/InfraErrorIdentifier.java
@@ -30,20 +30,28 @@
     CODE_COVERAGE_ERROR(500_004, FailureStatus.INFRA_FAILURE),
     MODULE_SETUP_RUNTIME_EXCEPTION(500_005, FailureStatus.CUSTOMER_ISSUE),
     CONFIGURED_ARTIFACT_NOT_FOUND(500_006, FailureStatus.CUSTOMER_ISSUE),
+    INVOCATION_TIMEOUT(500_007, FailureStatus.TIMED_OUT),
+    OPTION_CONFIGURATION_ERROR(500_008, FailureStatus.CUSTOMER_ISSUE),
+    RUNNER_ALLOCATION_ERROR(500_009, FailureStatus.INFRA_FAILURE),
+    SCHEDULER_ALLOCATION_ERROR(500_010, FailureStatus.CUSTOMER_ISSUE),
 
     // 500_501 - 501_000: Build, Artifacts download related errors
     ARTIFACT_REMOTE_PATH_NULL(500_501, FailureStatus.INFRA_FAILURE),
     ARTIFACT_UNSUPPORTED_PATH(500_502, FailureStatus.INFRA_FAILURE),
-    ARTIFACT_DOWNLOAD_ERROR(500_503, FailureStatus.INFRA_FAILURE),
+    ARTIFACT_DOWNLOAD_ERROR(500_503, FailureStatus.DEPENDENCY_ISSUE),
     GCS_ERROR(500_504, FailureStatus.DEPENDENCY_ISSUE),
 
     // 501_001 - 501_500: environment issues: For example: lab wifi
     WIFI_FAILED_CONNECT(501_001, FailureStatus.DEPENDENCY_ISSUE),
     GOOGLE_ACCOUNT_SETUP_FAILED(501_002, FailureStatus.DEPENDENCY_ISSUE),
+    NO_WIFI(501_003, FailureStatus.DEPENDENCY_ISSUE),
 
     // 502_000 - 502_100: Test issues detected by infra
     EXPECTED_TESTS_MISMATCH(502_000, FailureStatus.TEST_FAILURE),
 
+    // 505_000 - 505_250: Acloud errors
+    NO_ACLOUD_REPORT(505_000, FailureStatus.DEPENDENCY_ISSUE),
+
     UNDETERMINED(510_000, FailureStatus.UNSET);
 
     private final long code;
diff --git a/common_util/com/android/tradefed/result/error/TestErrorIdentifier.java b/common_util/com/android/tradefed/result/error/TestErrorIdentifier.java
new file mode 100644
index 0000000..5f80ba4
--- /dev/null
+++ b/common_util/com/android/tradefed/result/error/TestErrorIdentifier.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.error;
+
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+
+/** Error identifier from tests and tests runners. */
+public enum TestErrorIdentifier implements ErrorIdentifier {
+    MODULE_DID_NOT_EXECUTE(530_001, FailureStatus.NOT_EXECUTED);
+
+    private final long code;
+    private final FailureStatus status;
+
+    TestErrorIdentifier(int code, FailureStatus status) {
+        this.code = code;
+        this.status = status;
+    }
+
+    @Override
+    public long code() {
+        return code;
+    }
+
+    @Override
+    public FailureStatus status() {
+        return status;
+    }
+}
diff --git a/common_util/com/android/tradefed/util/SparseImageUtil.java b/common_util/com/android/tradefed/util/SparseImageUtil.java
new file mode 100644
index 0000000..ce557a5
--- /dev/null
+++ b/common_util/com/android/tradefed/util/SparseImageUtil.java
@@ -0,0 +1,264 @@
+/*
+ * 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.util;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/**
+ * Utility to unsparse sparse images.
+ *
+ * <p>This piece of code is adopted from:
+ * frameworks/base/packages/DynamicSystemInstallationService/src/com/android/dynsystem/SparseInputStream.java
+ */
+public class SparseImageUtil {
+    private static final int SPARSE_IMAGE_MAGIC = 0xED26FF3A;
+
+    /**
+     * Tests if file is a sparse image.
+     *
+     * @param imgFile a {@link File} that is to be tested.
+     * @return true if imgFile is a sparse image.
+     */
+    public static boolean isSparse(File imgFile) {
+        if (!imgFile.isFile()) {
+            return false;
+        }
+        try (FileInputStream in = new FileInputStream(imgFile)) {
+            // Check magic bytes
+            return readBuffer(in, 4).getInt() == SPARSE_IMAGE_MAGIC;
+        } catch (IOException e) {
+            // Return false if failed to read file
+            return false;
+        }
+    }
+
+    /**
+     * Unsparses a sparse image file.
+     *
+     * @param imgFile a {@link File} that is a sparse image.
+     * @param destFile a {@link File} to write the unsparsed image to.
+     * @throws IOException if imgFile is not a sparse image.
+     */
+    public static void unsparse(File imgFile, File destFile) throws IOException {
+        try (FileInputStream in = new FileInputStream(imgFile)) {
+            SparseInputStream sis = new SparseInputStream(new BufferedInputStream(in));
+            if (!sis.isSparse()) {
+                throw new IOException("Not a sparse image: " + imgFile);
+            }
+            FileUtil.writeToFile(sis, destFile);
+        }
+    }
+
+    /** Reads exact number of bytes. */
+    private static byte[] readFully(InputStream in, int size) throws IOException {
+        byte[] buf = new byte[size];
+        int n = 0;
+        int off = 0;
+        int left = size;
+        while (left > 0) {
+            n = in.read(buf, off, left);
+            if (n < 0) {
+                throw new IOException("Unexpected EOF in readFully()");
+            }
+            off += n;
+            left -= n;
+        }
+        return buf;
+    }
+
+    /** Helper that wraps result of readFully() in a ByteBuffer for easy consumption. */
+    private static ByteBuffer readBuffer(InputStream in, int size) throws IOException {
+        return ByteBuffer.wrap(readFully(in, size)).order(ByteOrder.LITTLE_ENDIAN);
+    }
+
+    /**
+     * SparseInputStream read from upstream and detects the data format. If the upstream is a valid
+     * sparse data, it will unsparse it on the fly. Otherwise, it just passthrough as is.
+     */
+    private static class SparseInputStream extends InputStream {
+        private static final int FILE_HDR_SIZE = 28;
+        private static final int CHUNK_HDR_SIZE = 12;
+
+        /**
+         * This class represents a chunk in the Android sparse image.
+         *
+         * @see system/core/libsparse/sparse_format.h
+         */
+        private static class SparseChunk {
+            public static final short RAW = (short) 0xCAC1;
+            public static final short FILL = (short) 0xCAC2;
+            public static final short DONTCARE = (short) 0xCAC3;
+            public short mChunkType;
+            public int mChunkSize;
+            public int mTotalSize;
+            public byte[] mFill;
+
+            @Override
+            public String toString() {
+                return String.format(
+                        "type: %x, chunk_size: %d, total_size: %d",
+                        mChunkType, mChunkSize, mTotalSize);
+            }
+
+            public static SparseChunk readChunk(InputStream in) throws IOException {
+                SparseChunk chunk = new SparseChunk();
+                ByteBuffer buf = readBuffer(in, CHUNK_HDR_SIZE);
+                chunk.mChunkType = buf.getShort();
+                /* padding = */ buf.getShort();
+                chunk.mChunkSize = buf.getInt();
+                chunk.mTotalSize = buf.getInt();
+                if (chunk.mChunkType == FILL) {
+                    chunk.mFill = readFully(in, 4);
+                }
+                return chunk;
+            }
+        }
+
+        private BufferedInputStream mIn;
+        private boolean mIsSparse;
+        private long mBlockSize;
+        private long mTotalBlocks;
+        private long mTotalChunks;
+        private SparseChunk mCur;
+        private long mLeft;
+        private int mCurChunks;
+
+        public SparseInputStream(BufferedInputStream in) throws IOException {
+            mIn = in;
+            in.mark(FILE_HDR_SIZE * 2);
+            ByteBuffer buf = readBuffer(mIn, FILE_HDR_SIZE);
+            mIsSparse = (buf.getInt() == SPARSE_IMAGE_MAGIC);
+            if (!mIsSparse) {
+                mIn.reset();
+                return;
+            }
+            int major = buf.getShort();
+            int minor = buf.getShort();
+            if (major > 0x1 || minor > 0x0) {
+                throw new IOException("Unsupported sparse version: " + major + "." + minor);
+            }
+            if (buf.getShort() != FILE_HDR_SIZE) {
+                throw new IOException("Illegal file header size");
+            }
+            if (buf.getShort() != CHUNK_HDR_SIZE) {
+                throw new IOException("Illegal chunk header size");
+            }
+            mBlockSize = buf.getInt();
+            if ((mBlockSize & 0x3) != 0) {
+                throw new IOException("Illegal block size, must be a multiple of 4: " + mBlockSize);
+            }
+            mTotalBlocks = buf.getInt();
+            mTotalChunks = buf.getInt();
+            mLeft = 0;
+            mCurChunks = 0;
+        }
+
+        /**
+         * Check if it needs to open a new chunk.
+         *
+         * @return true if it's EOF
+         */
+        private boolean prepareChunk() throws IOException {
+            if (mCur == null || mLeft <= 0) {
+                if (++mCurChunks > mTotalChunks) {
+                    return true;
+                }
+                mCur = SparseChunk.readChunk(mIn);
+                mLeft = mCur.mChunkSize * mBlockSize;
+            }
+            return mLeft == 0;
+        }
+
+        @Override
+        public int read(byte[] buf, int off, int len) throws IOException {
+            if (!mIsSparse) {
+                return mIn.read(buf, off, len);
+            }
+            if (prepareChunk()) {
+                return -1;
+            }
+            int n = -1;
+            switch (mCur.mChunkType) {
+                case SparseChunk.RAW:
+                    n = mIn.read(buf, off, (int) Math.min(mLeft, len));
+                    mLeft -= n;
+                    break;
+                case SparseChunk.DONTCARE:
+                    n = (int) Math.min(mLeft, len);
+                    Arrays.fill(buf, off, off + n, (byte) 0);
+                    mLeft -= n;
+                    break;
+                case SparseChunk.FILL:
+                    // The FILL type is rarely used, so use a simple implementation.
+                    n = super.read(buf, off, len);
+                    break;
+                default:
+                    throw new IOException("Unsupported Chunk:" + mCur);
+            }
+            return n;
+        }
+
+        @Override
+        public int read() throws IOException {
+            if (!mIsSparse) {
+                return mIn.read();
+            }
+            if (prepareChunk()) {
+                return -1;
+            }
+            int ret = -1;
+            switch (mCur.mChunkType) {
+                case SparseChunk.RAW:
+                    ret = mIn.read();
+                    break;
+                case SparseChunk.DONTCARE:
+                    ret = 0;
+                    break;
+                case SparseChunk.FILL:
+                    ret = mCur.mFill[(4 - ((int) mLeft & 0x3)) & 0x3];
+                    break;
+                default:
+                    throw new IOException("Unsupported Chunk:" + mCur);
+            }
+            mLeft--;
+            return ret;
+        }
+
+        /**
+         * Get the unsparse size
+         *
+         * @return -1 if stream doesn't contain sparse image data.
+         */
+        public long getUnsparseSize() {
+            if (!mIsSparse) {
+                return -1;
+            }
+            return mBlockSize * mTotalBlocks;
+        }
+
+        public boolean isSparse() {
+            return mIsSparse;
+        }
+    }
+}
diff --git a/common_util/com/android/tradefed/util/ZipUtil.java b/common_util/com/android/tradefed/util/ZipUtil.java
index 31f3ff8..a6902ec 100644
--- a/common_util/com/android/tradefed/util/ZipUtil.java
+++ b/common_util/com/android/tradefed/util/ZipUtil.java
@@ -526,6 +526,7 @@
                 return;
             } else if (zipEntry.getCompressedSize() == 0) {
                 // The file is empty, just create an empty file.
+                FileUtil.mkdirsRWX(targetFile.getParentFile());
                 targetFile.createNewFile();
                 return;
             }
diff --git a/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
index fc2687c..9638057 100644
--- a/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
+++ b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
@@ -16,7 +16,6 @@
 
 package com.android.tradefed.util.zip;
 
-import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.ByteArrayUtil;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -241,9 +240,6 @@
         if (Long.toHexString(mUncompressedSize).equals("ffffffff") ||
             Long.toHexString(mCompressedSize).equals("ffffffff") ||
             Long.toHexString(mLocalHeaderOffset).equals("ffffffff")) {
-            CLog.i("Values(compressed/uncompressed size, and relative offset of local header)) in "
-                    + "CentralDirectoryInfo for file name: %s reaches the limitation(0xffffffff), "
-                    + "getting the data from extra field.", mFileName);
             byte[] zip64FieldId = Arrays.copyOfRange(
                     data, startOffset + mFileNameLength + 46, startOffset + mFileNameLength + 48);
             // There should be a ZIP64 Field ID(0x0001) existing here.
diff --git a/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java b/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
index 1e853c8..4a996f4 100644
--- a/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
+++ b/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
@@ -15,31 +15,29 @@
  */
 package com.android.tradefed.device;
 
+import com.android.tradefed.error.HarnessRuntimeException;
+import com.android.tradefed.result.error.ErrorIdentifier;
+
+import java.lang.StackWalker.Option;
+
 /**
  * Thrown when a device action did not results in the expected results.
  *
  * <p>For example: 'pm list users' is vastly expected to return the list of users, failure to do so
  * should be raised as a DeviceRuntimeException since something went very wrong.
  */
-public class DeviceRuntimeException extends RuntimeException {
+public class DeviceRuntimeException extends HarnessRuntimeException {
     private static final long serialVersionUID = -7928528651742852301L;
 
     /**
      * Creates a {@link DeviceRuntimeException}.
      *
      * @param msg a descriptive error message of the error.
+     * @param errorId The {@link ErrorIdentifier} categorizing the exception.
      */
-    public DeviceRuntimeException(String msg) {
-        super(msg);
-    }
-
-    /**
-     * Creates a {@link DeviceRuntimeException}.
-     *
-     * @param t {@link Throwable} that should be wrapped in {@link DeviceRuntimeException}.
-     */
-    public DeviceRuntimeException(Throwable t) {
-        super(t);
+    public DeviceRuntimeException(String msg, ErrorIdentifier errorId) {
+        super(msg, errorId);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 
     /**
@@ -47,8 +45,10 @@
      *
      * @param msg a descriptive error message of the error
      * @param t {@link Throwable} that should be wrapped in {@link DeviceRuntimeException}.
+     * @param errorId The {@link ErrorIdentifier} categorizing the exception.
      */
-    public DeviceRuntimeException(String msg, Throwable t) {
-        super(msg, t);
+    public DeviceRuntimeException(String msg, Throwable t, ErrorIdentifier errorId) {
+        super(msg, t, errorId);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 }
diff --git a/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
index 3e37f20..3163b52 100644
--- a/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
+++ b/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
@@ -244,6 +244,29 @@
         return false;
     }
 
+    /**
+     * Determines if the file or non-empty directory exists on the device.
+     *
+     * @param deviceFilePath The absolute file path on device to check for existence.
+     * @return True if file/directory exists, False otherwise. If directory is empty, it will return
+     *     False as well.
+     */
+    public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
+        String contentUri = createEscapedContentUri(deviceFilePath);
+        String queryContentCommand =
+                String.format(
+                        "content query --user %d --uri %s", mDevice.getCurrentUser(), contentUri);
+
+        String listCommandResult = mDevice.executeShellCommand(queryContentCommand);
+
+        if (NO_RESULTS_STRING.equals(listCommandResult.trim())) {
+            // No file found.
+            return false;
+        }
+
+        return true;
+    }
+
     /** Returns true if {@link CommandStatus} is successful and there is no error message. */
     private boolean isSuccessful(CommandResult result) {
         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
diff --git a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
index caa5f1e..50084c4 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -47,7 +47,13 @@
         CF_FETCH_ARTIFACT_TIME("cf_fetch_artifact_time_ms", false),
         CF_GCE_CREATE_TIME("cf_gce_create_time_ms", false),
         CF_LAUNCH_CVD_TIME("cf_launch_cvd_time_ms", false),
-        CF_INSTANCE_COUNT("cf_instance_count", false);
+        CF_INSTANCE_COUNT("cf_instance_count", false),
+        CRASH_FAILURES("crash_failures", true),
+        UNCAUGHT_CRASH_FAILURES("uncaught_crash_failures", true),
+        TEST_CRASH_FAILURES("test_crash_failures", true),
+        UNCAUGHT_TEST_CRASH_FAILURES("uncaught_test_crash_failures", true),
+        DEVICE_RESET_COUNT("device_reset_count", true),
+        DEVICE_RESET_MODULES("device_reset_modules", true);
 
         private final String mKeyName;
         // Whether or not to add the value when the key is added again.
diff --git a/invocation_interfaces/com/android/tradefed/result/TestResult.java b/invocation_interfaces/com/android/tradefed/result/TestResult.java
index daee50c..1b26db6 100644
--- a/invocation_interfaces/com/android/tradefed/result/TestResult.java
+++ b/invocation_interfaces/com/android/tradefed/result/TestResult.java
@@ -208,6 +208,7 @@
         int ignored = 0;
         int incomplete = 0;
 
+        TestStatus lastStatus = null;
         for (TestResult attempt : results) {
             mergedResult.mProtoMetrics.putAll(attempt.getProtoMetrics());
             mergedResult.mMetrics.putAll(attempt.getMetrics());
@@ -238,6 +239,7 @@
                     ignored++;
                     break;
             }
+            lastStatus = attempt.getStatus();
         }
 
         switch (strategy) {
@@ -258,7 +260,13 @@
                         mergedResult.setStatus(TestStatus.INCOMPLETE);
                     }
                 } else {
-                    mergedResult.setStatus(TestStatus.FAILURE);
+                    if (TestStatus.ASSUMPTION_FAILURE.equals(lastStatus)) {
+                        mergedResult.setStatus(TestStatus.ASSUMPTION_FAILURE);
+                    } else if (TestStatus.IGNORED.equals(lastStatus)) {
+                        mergedResult.setStatus(TestStatus.IGNORED);
+                    } else {
+                        mergedResult.setStatus(TestStatus.FAILURE);
+                    }
                 }
                 break;
             default:
diff --git a/invocation_interfaces/com/android/tradefed/result/TestRunResult.java b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
index a3fdc1e..3f35209 100644
--- a/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
+++ b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
@@ -307,6 +307,10 @@
         updateTestResult(test, TestStatus.ASSUMPTION_FAILURE, FailureDescription.create(trace));
     }
 
+    public void testAssumptionFailure(TestDescription test, FailureDescription failure) {
+        updateTestResult(test, TestStatus.ASSUMPTION_FAILURE, failure);
+    }
+
     public void testIgnored(TestDescription test) {
         updateTestResult(test, TestStatus.IGNORED, null);
     }
diff --git a/proto/monitoring/server/lab_resource.proto b/proto/monitoring/server/lab_resource.proto
new file mode 100644
index 0000000..5397abe
--- /dev/null
+++ b/proto/monitoring/server/lab_resource.proto
@@ -0,0 +1,153 @@
+/*
+ * Copyright 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.
+ */
+
+// The protobuf messages for lab host to export metadata and reosurce metrics.
+syntax = "proto3";
+
+package dual_home_lab.monitoring_agent.resource_monitoring;
+
+import "google/protobuf/timestamp.proto";
+
+option java_package = "com.google.dualhomelab.monitoringagent.resourcemonitoring";
+option java_multiple_files = true;
+option java_generic_services = true;
+
+// A tag-value pair message represents the metric value.
+// For example:
+// To represent device disk used percentage.
+// metric {
+//   tag: "/data"
+//   value: 20.5
+// }
+message Metric {
+  // A string tag associates to the value.
+  string tag = 1;
+  // A float value represents the resource value.
+  float value = 2;
+}
+
+// A message that describes the resource and its metrics.
+// For example:
+// To represent disk space usage values at certain moment.
+// resource {
+//   resource_name: "disk_space"
+//   resource_instance: "/data"
+//   timestamp {
+//     seconds: 1589342214
+//   }
+//   metric: {
+//     tag: "avail"
+//     value: 20.5
+//   }
+//   metric: {
+//     tag: "used"
+//     value: 18.7
+//   }
+//   metric: {
+//     tag: "reserved for root"
+//     value: 16.2
+//   }
+// }
+message Resource {
+  // A string resource name. ex. "cpu", "memory", "disk_space".
+  string resource_name = 1;
+  // A string reperesent the sub resource name.
+  string resource_instance = 2;
+  // The Metric describe the host or device resource usages.
+  repeated Metric metric = 3;
+  // The collecting timestamp.
+  google.protobuf.Timestamp timestamp = 4;
+}
+
+// A name-value message to represent the metadata attribute.
+// For example:
+// To represent the run target.
+// attribute {
+//   name: "run_target"
+//   value: "atom-userdebug"
+// }
+// To reperent the pools.
+// attribute {
+//   name: "pool"
+//   value: "asit"
+// }
+// attribute {
+//   name: "pool"
+//   value: "apct"
+// }
+message Attribute {
+  string name = 1;
+  string value = 2;
+}
+
+// A message that describes the device state and resource usages.
+// For example:
+// To represent a monitored host
+// host {
+//   identifier: {
+//     key: "lab_name"
+//     value: "us-mtv43"
+//   }
+//   identifier: {
+//     key: "host_name"
+//     value: "foo.bar.com"
+//   }
+//   identifier: {
+//     key: "test_harness"
+//     value: "tradefed"
+//   }
+//   attribute: {... check the attribute example above ...}
+//   resource: {... check the resource example abobe ...}
+// }
+// To represent a monitored device
+// device {
+//   identifier: {
+//     key: "device_serial"
+//     value: "VVEG-GIDSAN"
+//   }
+//   attribute: {... check the attribute example above ...}
+//   resource: {... check the resource example abobe ...}
+// }
+message MonitoredEntity {
+  // The string map that helps identify the monitored entity
+  map<string, string> identifier = 1;
+  // The attribute messages that describe the device metadata.
+  repeated Attribute attribute = 2;
+  // The resource messages that describe the device state and resource metrics.
+  repeated Resource resource = 3;
+}
+
+// A message that describe the state and resource usages for a lab host and its
+// connected devices.
+message LabResource {
+  MonitoredEntity host = 1;
+  repeated MonitoredEntity device = 2;
+}
+
+// The request message to query the LabResource.
+message LabResourceRequest {}
+
+// The service which is intend to export device metrics and metadata to the host
+// monitoring agent. The host monitoring agent is responsible for
+// collecting host/device metrics and exporting the metrics to user defined
+// cloud PubSub topics.
+service LabResourceService {
+  // Queries lab resource message.
+  rpc GetLabResource(LabResourceRequest) returns (LabResource) {
+    // The http equivalent is curl http://ADDRESS/v1/lab_resource_message
+    // (Assuming your service is hosted at the given 'ADDRESS')
+  }
+}
diff --git a/res/suite/allowed-preparers.txt b/res/suite/allowed-preparers.txt
new file mode 100644
index 0000000..48ed998
--- /dev/null
+++ b/res/suite/allowed-preparers.txt
@@ -0,0 +1,4 @@
+com.android.tradefed.targetprep.CrashCollector
+com.android.tradefed.targetprep.DeviceCleaner
+com.android.tradefed.targetprep.RootTargetPreparer
+com.android.tradefed.targetprep.WifiPreparer
\ No newline at end of file
diff --git a/src/com/android/tradefed/build/BootstrapBuildProvider.java b/src/com/android/tradefed/build/BootstrapBuildProvider.java
index 383a30c..7a22a94 100644
--- a/src/com/android/tradefed/build/BootstrapBuildProvider.java
+++ b/src/com/android/tradefed/build/BootstrapBuildProvider.java
@@ -90,6 +90,8 @@
                             + "Can be repeated. For example --extra-file file_key_1=/path/to/file")
     private Map<String, File> mExtraFiles = new LinkedHashMap<>();
 
+    private boolean mCreatedTestDir = false;
+
     @Override
     public IBuildInfo getBuild() throws BuildRetrievalError {
         throw new UnsupportedOperationException("Call getBuild(ITestDevice)");
@@ -97,6 +99,9 @@
 
     @Override
     public void cleanUp(IBuildInfo info) {
+        if (mCreatedTestDir) {
+            FileUtil.recursiveDelete(mTestsDir);
+        }
     }
 
     @Override
@@ -128,9 +133,9 @@
             info.setFile("testsdir", mTestsDir, info.getBuildId());
         }
         // Avoid tests dir being null, by creating a temporary dir.
-        boolean createdTestDir = false;
+        mCreatedTestDir = false;
         if (mTestsDir == null) {
-            createdTestDir = true;
+            mCreatedTestDir = true;
             try {
                 mTestsDir =
                         FileUtil.createTempDir(
@@ -147,7 +152,7 @@
                     .put(
                             FilesKey.TESTS_DIRECTORY,
                             mTestsDir,
-                            !createdTestDir /* shouldNotDelete */);
+                            !mCreatedTestDir /* shouldNotDelete */);
         }
         return info;
     }
diff --git a/src/com/android/tradefed/build/DependenciesResolver.java b/src/com/android/tradefed/build/DependenciesResolver.java
index f55d098..7825f39 100644
--- a/src/com/android/tradefed/build/DependenciesResolver.java
+++ b/src/com/android/tradefed/build/DependenciesResolver.java
@@ -88,7 +88,7 @@
             try {
                 mTestsDir =
                         FileUtil.createTempDir(
-                                "bootstrap-test-dir",
+                                "bootstrap-dep-test-dir",
                                 CurrentInvocation.getInfo(InvocationInfo.WORK_FOLDER));
             } catch (IOException e) {
                 throw new BuildRetrievalError(
diff --git a/src/com/android/tradefed/cluster/ClusterCommandConfigBuilder.java b/src/com/android/tradefed/cluster/ClusterCommandConfigBuilder.java
index 6e3ffbb..e50699c 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandConfigBuilder.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandConfigBuilder.java
@@ -192,7 +192,9 @@
         envVars.put("TF_WORK_DIR", mWorkDir.getAbsolutePath());
         envVars.putAll(mTestEnvironment.getEnvVars());
         envVars.putAll(mTestContext.getEnvVars());
+
         for (String serial : mCommand.getTargetDeviceSerials()) {
+            serial = ClusterHostUtil.getLocalDeviceSerial(serial);
             IDeviceConfiguration device =
                     new DeviceConfigurationHolder(String.format("TF_DEVICE_%d", index++));
             device.getDeviceRequirements().setSerial(serial);
@@ -203,6 +205,11 @@
         }
         deviceConfigs.get(0).addSpecificConfig(new ClusterBuildProvider());
         config.setDeviceConfigList(deviceConfigs);
+        // Perform target preparation in parallel with an unlimited timeout
+        // TODO(b/166455187): Consider making parallel setup options configurable
+        config.injectOptionValue("parallel-setup", "true");
+        config.injectOptionValue("parallel-setup-timeout", "0");
+
         config.setTest(new ClusterCommandLauncher());
         config.setLogSaver(new ClusterLogSaver());
         // TODO(b/135636270): return log path to TFC instead of relying on a specific filename
diff --git a/src/com/android/tradefed/cluster/ClusterCommandEvent.java b/src/com/android/tradefed/cluster/ClusterCommandEvent.java
index 725e48e..46db15c 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandEvent.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandEvent.java
@@ -38,6 +38,7 @@
     public static final String DATA_KEY_PASSED_TEST_COUNT = "passed_test_count";
     public static final String DATA_KEY_FAILED_TEST_RUN_COUNT = "failed_test_run_count";
     public static final String DATA_KEY_LOST_DEVICE_DETECTED = "device_lost_detected";
+    public static final String DATA_KEY_SUBPROCESS_COMMAND_ERROR = "subprocess_command_error";
 
     // Maximum size of an individual data string value.
     public static final int MAX_DATA_STRING_SIZE = 4095;
diff --git a/src/com/android/tradefed/cluster/ClusterCommandLauncher.java b/src/com/android/tradefed/cluster/ClusterCommandLauncher.java
index a79a605..0992202 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandLauncher.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandLauncher.java
@@ -42,6 +42,7 @@
 import com.android.tradefed.util.StreamUtil;
 import com.android.tradefed.util.StringEscapeUtils;
 import com.android.tradefed.util.StringUtil;
+import com.android.tradefed.util.SubprocessEventHelper.InvocationFailedEventInfo;
 import com.android.tradefed.util.SubprocessTestResultsParser;
 import com.android.tradefed.util.SystemUtil;
 
@@ -192,12 +193,26 @@
                             javaCommandArgs.toArray(new String[javaCommandArgs.size()]));
             if (!result.getStatus().equals(CommandStatus.SUCCESS)) {
                 String error = null;
+                Throwable cause = null;
                 if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
-                    error = "timeout";
+                    error =
+                            String.format(
+                                    "Command timed out after %sms",
+                                    mConfiguration.getCommandOptions().getInvocationTimeout());
                 } else {
-                    error = FileUtil.readStringFromFile(stderrFile);
+                    error =
+                            String.format(
+                                    "Command finished unsuccessfully: status=%s, exit_code=%s",
+                                    result.getStatus(), result.getExitCode());
+                    InvocationFailedEventInfo errorInfo =
+                            subprocessEventParser.getReportedInvocationFailedEventInfo();
+                    if (errorInfo != null) {
+                        cause = errorInfo.mCause;
+                    } else {
+                        cause = new Throwable(FileUtil.readStringFromFile(stderrFile));
+                    }
                 }
-                throw new RuntimeException(String.format("Command failed to run: %s", error));
+                throw new SubprocessCommandException(error, cause);
             }
             CLog.i("Successfully ran a command");
 
diff --git a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
index 2e09168..075b3e8 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
@@ -145,6 +145,7 @@
         private String mSummary;
         private Set<String> processedSummaries = new HashSet<>();
         private String mError;
+        private String mSubprocessCommandError;
         private File mWorkDir;
         private InvocationStatus mInvocationStatus;
 
@@ -261,6 +262,10 @@
             super.invocationFailed(cause);
 
             mError = StreamUtil.getStackTrace(cause);
+            if (cause instanceof SubprocessCommandException && cause.getCause() != null) {
+                // The inner exception holds an exception stack trace from a subprocess.
+                mSubprocessCommandError = cause.getCause().getMessage();
+            }
         }
 
         /** {@inheritDoc} */
@@ -272,6 +277,9 @@
                     createEventBuilder()
                             .setType(ClusterCommandEvent.Type.InvocationEnded)
                             .setData(ClusterCommandEvent.DATA_KEY_ERROR, mError)
+                            .setData(
+                                    ClusterCommandEvent.DATA_KEY_SUBPROCESS_COMMAND_ERROR,
+                                    mSubprocessCommandError)
                             .build();
             getClusterClient().getCommandEventUploader().postEvent(event);
             getClusterClient().getCommandEventUploader().flush();
@@ -318,6 +326,9 @@
                             .setType(ClusterCommandEvent.Type.InvocationCompleted)
                             .setInvocationStatus(mInvocationStatus)
                             .setData(ClusterCommandEvent.DATA_KEY_ERROR, mError)
+                            .setData(
+                                    ClusterCommandEvent.DATA_KEY_SUBPROCESS_COMMAND_ERROR,
+                                    mSubprocessCommandError)
                             .setData(ClusterCommandEvent.DATA_KEY_SUMMARY, mSummary)
                             .setData(
                                     ClusterCommandEvent.DATA_KEY_FETCH_BUILD_TIME_MILLIS,
@@ -497,7 +508,6 @@
             String runTarget =
                     ClusterHostUtil.getRunTarget(
                             device, runTargetFormat, getClusterOptions().getDeviceTag());
-            CLog.d("%s is available", runTarget);
             devices.put(runTarget, device);
         }
         return devices;
@@ -605,6 +615,9 @@
                         ClusterCommandEvent.createEventBuilder(commandTask)
                                 .setHostName(ClusterHostUtil.getHostName())
                                 .setType(ClusterCommandEvent.Type.AllocationFailed)
+                                .setData(
+                                        ClusterCommandEvent.DATA_KEY_ERROR,
+                                        StreamUtil.getStackTrace(e))
                                 .build());
                 eventUploader.flush();
             } catch (ConfigurationException | IOException | JSONException e) {
@@ -640,7 +653,7 @@
         if (commandTask.getTargetDeviceSerials() != null) {
             for (String serial : commandTask.getTargetDeviceSerials()) {
                 cmdLine += " --serial ";
-                cmdLine += serial;
+                cmdLine += ClusterHostUtil.getLocalDeviceSerial(serial);
             }
         }
         CLog.i("executing cluster command: [%s] %s", commandTask.getTaskId(), cmdLine);
diff --git a/src/com/android/tradefed/cluster/ClusterDeviceInfo.java b/src/com/android/tradefed/cluster/ClusterDeviceInfo.java
index 23c39eb..0fcf346 100644
--- a/src/com/android/tradefed/cluster/ClusterDeviceInfo.java
+++ b/src/com/android/tradefed/cluster/ClusterDeviceInfo.java
@@ -82,7 +82,7 @@
      */
     public JSONObject toJSON() throws JSONException {
         final JSONObject json = new JSONObject();
-        json.put("device_serial", mDeviceDescriptor.getSerial());
+        json.put("device_serial", ClusterHostUtil.getUniqueDeviceSerial(mDeviceDescriptor));
         json.put("run_target", mRunTarget);
         json.put("build_id", mDeviceDescriptor.getBuildId());
         json.put("product", mDeviceDescriptor.getProduct());
diff --git a/src/com/android/tradefed/cluster/ClusterDeviceMonitor.java b/src/com/android/tradefed/cluster/ClusterDeviceMonitor.java
index 33175d4..c8f0fa5 100644
--- a/src/com/android/tradefed/cluster/ClusterDeviceMonitor.java
+++ b/src/com/android/tradefed/cluster/ClusterDeviceMonitor.java
@@ -108,7 +108,12 @@
                             .setHostName(ClusterHostUtil.getHostName())
                             .setTfVersion(ClusterHostUtil.getTfVersion())
                             .setData(getAdditionalHostInfo())
-                            .setData("host_ip", ClusterHostUtil.getHostIpAddress())
+                            .setData(
+                                    ClusterHostEvent.HOST_IP_KEY,
+                                    ClusterHostUtil.getHostIpAddress())
+                            .setData(
+                                    ClusterHostEvent.LABEL_KEY,
+                                    String.join(",", getClusterOptions().getLabels()))
                             .setClusterId(getClusterOptions().getClusterId())
                             .setNextClusterIds(getClusterOptions().getNextClusterIds())
                             .setLabName(getClusterOptions().getLabName());
diff --git a/src/com/android/tradefed/cluster/ClusterHostEvent.java b/src/com/android/tradefed/cluster/ClusterHostEvent.java
index 8a56e93..b3bb430 100644
--- a/src/com/android/tradefed/cluster/ClusterHostEvent.java
+++ b/src/com/android/tradefed/cluster/ClusterHostEvent.java
@@ -37,6 +37,8 @@
     private Map<String, String> mData = new HashMap<>();
     private String mLabName;
     public static final String EVENT_QUEUE = "host-event-queue";
+    public static final String LABEL_KEY = "label";
+    public static final String HOST_IP_KEY = "host_ip";
 
     /** Enums of the different types of host events. */
     public enum HostEventType {
diff --git a/src/com/android/tradefed/cluster/ClusterHostUtil.java b/src/com/android/tradefed/cluster/ClusterHostUtil.java
index 0e167b9..5bc6209 100644
--- a/src/com/android/tradefed/cluster/ClusterHostUtil.java
+++ b/src/com/android/tradefed/cluster/ClusterHostUtil.java
@@ -27,13 +27,22 @@
 import com.google.common.net.InetAddresses;
 import com.google.common.primitives.Longs;
 
+import java.net.Inet6Address;
 import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+
 /** Static util functions for TF Cluster to get global config instances, host information, etc. */
 public class ClusterHostUtil {
 
@@ -42,39 +51,113 @@
     private static String sHostIpAddress = null;
 
     static final String DEFAULT_TF_VERSION = "(unknown)";
+    static final String EMULATOR_SERIAL_PREFIX = "emulator-";
+    static final String NULL_DEVICE_SERIAL_PLACEHOLDER = "(no device serial)";
+    static final String UNKNOWN = "UNKNOWN";
 
     private static long sTfStartTime = getCurrentTimeMillis();
 
     /**
      * Gets the hostname.
      *
+     * <p>1. Try to get hostname from InetAddress. 2. If fail, try to get hostname from HOSTNAME
+     * env. 3. If not set, generate a unique hostname.
+     *
      * @return the hostname or null if we were unable to fetch it.
      */
     public static String getHostName() {
-        if (sHostName == null) {
-            try {
-                sHostName = InetAddress.getLocalHost().getHostName();
-            } catch (UnknownHostException e) {
-                CLog.w("failed to get hostname: %s", e);
-            }
+        if (sHostName != null) {
+            return sHostName;
         }
+        try {
+            sHostName = InetAddress.getLocalHost().getHostName();
+            return sHostName;
+        } catch (UnknownHostException e) {
+            CLog.w("Failed to get hostname from InetAddress: %s", e);
+        }
+        CLog.i("Get hostname from HOSTNAME env.");
+        sHostName = System.getenv("HOSTNAME");
+        if (!Strings.isNullOrEmpty(sHostName)) {
+            return sHostName;
+        }
+        sHostName = "unknown-" + UUID.randomUUID().toString();
+        CLog.i("No HOSTNAME env set. Generate hostname: %s.", sHostName);
         return sHostName;
     }
 
     /**
+     * Returns a unique device serial for a device.
+     *
+     * <p>Non-physical devices (e.g. emulator) have pseudo serials which are not unique across
+     * hosts. This method prefixes those with a hostname to make them unique.
+     *
+     * @param device a device descriptor.
+     * @return a unique device serial.
+     */
+    public static String getUniqueDeviceSerial(DeviceDescriptor device) {
+        String serial = device.getSerial();
+        if (Strings.isNullOrEmpty(serial)
+                || device.isStubDevice()
+                || serial.startsWith(EMULATOR_SERIAL_PREFIX)) {
+            if (Strings.isNullOrEmpty(serial)) {
+                serial = NULL_DEVICE_SERIAL_PLACEHOLDER;
+            }
+            serial = String.format("%s:%s", getHostName(), serial);
+        }
+        return serial;
+    }
+
+    /**
+     * Returns a local device serial for a given unique device serial.
+     *
+     * <p>TFC sends down unique device serials for non-physical devices which TF does not
+     * understand. This method converts them back to local device serials.
+     *
+     * @param serial a unique device serial from TFC.
+     * @return a local device serial.
+     */
+    public static String getLocalDeviceSerial(String serial) {
+        String prefix = getHostName() + ":";
+        if (serial.startsWith(prefix)) {
+            return serial.substring(prefix.length());
+        }
+        return serial;
+    }
+    /**
      * Gets the IP address.
      *
-     * @return the IP address or null if we were unable to fetch it.
+     * @return the IPV4 address String or "UNKNOWN" if we were unable to fetch it.
      */
     public static String getHostIpAddress() {
         if (sHostIpAddress == null) {
+            List<InetAddress> addresses = new ArrayList<>();
             try {
-                sHostIpAddress = InetAddress.getLocalHost().getHostAddress();
-            } catch (UnknownHostException e) {
-                CLog.w("failed to get hostname: %s", e);
+                Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+                if (interfaces == null) {
+                    return UNKNOWN;
+                }
+                for (NetworkInterface networkInterface : Collections.list(interfaces)) {
+                    if (!networkInterface.isUp() || networkInterface.isLoopback()) {
+                        continue;
+                    }
+                    for (InetAddress address :
+                            Collections.list(networkInterface.getInetAddresses())) {
+                        if (address.isLinkLocalAddress()
+                                || address.isLoopbackAddress()
+                                || address instanceof Inet6Address) {
+                            continue;
+                        }
+                        addresses.add(address);
+                    }
+                }
+            } catch (SocketException e) {
+                CLog.w(e);
+            }
+            if (!addresses.isEmpty()) {
+                sHostIpAddress = addresses.get(0).getHostAddress();
             }
         }
-        return sHostIpAddress;
+        return sHostIpAddress == null ? UNKNOWN : sHostIpAddress;
     }
 
     /**
@@ -143,7 +226,7 @@
                         txt = device.getDeviceClass();
                         break;
                     case "SERIAL":
-                        txt = device.getSerial();
+                        txt = getUniqueDeviceSerial(device);
                         break;
                     case "TAG":
                         if (deviceTags == null || deviceTags.isEmpty()) {
diff --git a/src/com/android/tradefed/cluster/SubprocessCommandException.java b/src/com/android/tradefed/cluster/SubprocessCommandException.java
new file mode 100644
index 0000000..3be5dc9
--- /dev/null
+++ b/src/com/android/tradefed/cluster/SubprocessCommandException.java
@@ -0,0 +1,24 @@
+/*
+ * 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.cluster;
+
+/** A subprocess command failed to run. */
+public class SubprocessCommandException extends RuntimeException {
+
+    public SubprocessCommandException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/com/android/tradefed/cluster/SubprocessConfigBuilder.java b/src/com/android/tradefed/cluster/SubprocessConfigBuilder.java
index a69dc86..6952542 100644
--- a/src/com/android/tradefed/cluster/SubprocessConfigBuilder.java
+++ b/src/com/android/tradefed/cluster/SubprocessConfigBuilder.java
@@ -119,8 +119,12 @@
                 in = loader.getResourceAsStream(String.format("config/%s", mOriginalConfig));
             }
             if (in == null) {
+                File f = new File(mOriginalConfig);
+                if (!f.isAbsolute()) {
+                    f = new File(mWorkDir, mOriginalConfig);
+                }
                 try {
-                    in = new FileInputStream(mOriginalConfig);
+                    in = new FileInputStream(f);
                 } catch (FileNotFoundException e) {
                     throw new RuntimeException(
                             String.format("Could not find configuration '%s'", mOriginalConfig));
@@ -143,7 +147,14 @@
             root.appendChild(reporter);
         }
 
-        File f = new File(mWorkDir, createConfigName(mOriginalConfig));
+        File f = new File(mWorkDir, mOriginalConfig);
+        if (!f.exists() || !f.isFile()) {
+            // If the original config is an existing file, we need to update it since some old TFs
+            // check the file system first before bundled configs when loading configs.
+            // If the original config is not an existing file, we can use any name since the
+            // original config name will be assigned when creating a injection jar.
+            f = File.createTempFile("subprocess_config_", ".xml", mWorkDir);
+        }
         TransformerFactory transformerFactory = TransformerFactory.newInstance();
         try {
             Transformer transformer = transformerFactory.newTransformer();
diff --git a/src/com/android/tradefed/cluster/SubprocessReportingHelper.java b/src/com/android/tradefed/cluster/SubprocessReportingHelper.java
index e657226..4944859 100644
--- a/src/com/android/tradefed/cluster/SubprocessReportingHelper.java
+++ b/src/com/android/tradefed/cluster/SubprocessReportingHelper.java
@@ -42,7 +42,8 @@
     private static final String REPORTER_JAR_NAME = "subprocess-results-reporter.jar";
     private static final String CLASS_FILTER =
             String.format(
-                    "(^%s|^%s|^%s|^%s|^%s).*class$",
+                    "(^%s|^%s|^%s|^%s|^%s|^%s).*class$",
+                    "ErrorIdentifier",
                     "LegacySubprocessResultsReporter",
                     "SubprocessTestResultsParser",
                     "SubprocessEventHelper",
diff --git a/src/com/android/tradefed/command/CommandOptions.java b/src/com/android/tradefed/command/CommandOptions.java
index 1760e9c..b3c5ed3 100644
--- a/src/com/android/tradefed/command/CommandOptions.java
+++ b/src/com/android/tradefed/command/CommandOptions.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.UniqueMultiMap;
 
+import java.time.Duration;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -108,6 +109,10 @@
             "[0, shard-count)")
     private Integer mShardIndex;
 
+    @Option(name = "optimize-mainline-test", description =
+            "Whether or not to optimize the list of test modules for mainline.")
+    private boolean mOptimizeMainlineTest;
+
     @Option(
         name = "enable-token-sharding",
         description = "Whether or not to allow sharding with the token support enabled."
@@ -161,6 +166,12 @@
                     "For remote sharded invocation, whether or not to attempt the setup in parallel.")
     private boolean mUseParallelRemoteSetup = false;
 
+    @Option(name = "parallel-setup", description = "Whether to attempt the setup in parallel.")
+    private boolean mUseParallelSetup = false;
+
+    @Option(name = "parallel-setup-timeout", description = "Timeout to use during parallel setup.")
+    private Duration mParallelSetupTimeout = Duration.ofMinutes(30L);
+
     @Option(
             name = "replicate-parent-setup",
             description =
@@ -370,6 +381,14 @@
      * {@inheritDoc}
      */
     @Override
+    public boolean getOptimizeMainlineTest() {
+        return mOptimizeMainlineTest;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public Integer getShardCount() {
         return mShardCount;
     }
@@ -514,6 +533,18 @@
 
     /** {@inheritDoc} */
     @Override
+    public boolean shouldUseParallelSetup() {
+        return mUseParallelSetup;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Duration getParallelSetupTimeout() {
+        return mParallelSetupTimeout;
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public boolean shouldUseReplicateSetup() {
         return mReplicateParentSetup;
     }
diff --git a/src/com/android/tradefed/command/CommandRunner.java b/src/com/android/tradefed/command/CommandRunner.java
index fe77903..0cb94fe 100644
--- a/src/com/android/tradefed/command/CommandRunner.java
+++ b/src/com/android/tradefed/command/CommandRunner.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.device.NoDeviceException;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.SerializationUtil;
 
@@ -119,7 +120,10 @@
             // After 1 min we check if the command was executed.
             if (mScheduler.getReadyCommandCount() > 0
                     && mScheduler.getExecutingCommandCount() == 0) {
-                printStackTrace(new NoDeviceException("No device was allocated for the command."));
+                printStackTrace(
+                        new NoDeviceException(
+                                "No device was allocated for the command.",
+                                InfraErrorIdentifier.RUNNER_ALLOCATION_ERROR));
                 mErrorCode = ExitCode.NO_DEVICE_ALLOCATED;
                 mScheduler.removeAllCommands();
                 mScheduler.shutdown();
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 7228840..0ee2ca0 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -69,6 +69,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.ResultForwarder;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.result.suite.SuiteResultReporter;
 import com.android.tradefed.sandbox.ISandbox;
 import com.android.tradefed.testtype.IRemoteTest;
@@ -1075,8 +1076,9 @@
                 IConfiguration config = cmd.getConfiguration();
                 IInvocationContext context = new InvocationContext();
                 context.setConfigurationDescriptor(config.getConfigurationDescription());
-                Map<String, ITestDevice> devices = allocateDevices(config, manager);
-                if (!devices.isEmpty()) {
+                DeviceAllocationResult allocationResults = allocateDevices(config, manager);
+                if (allocationResults.wasAllocationSuccessful()) {
+                    Map<String, ITestDevice> devices = allocationResults.getAllocatedDevices();
                     cmdIter.remove();
                     mExecutingCommands.add(cmd);
                     context.addAllocatedDevice(devices);
@@ -1534,8 +1536,9 @@
 
         ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
         context.setConfigurationDescriptor(config.getConfigurationDescription());
-        Map<String, ITestDevice> devices = allocateDevices(config, manager);
-        if (!devices.isEmpty()) {
+        DeviceAllocationResult allocationResults = allocateDevices(config, manager);
+        if (allocationResults.wasAllocationSuccessful()) {
+            Map<String, ITestDevice> devices = allocationResults.getAllocatedDevices();
             context.addAllocatedDevice(devices);
             synchronized (this) {
                 mExecutingCommands.add(execCmd);
@@ -1543,8 +1546,16 @@
             CLog.d("Executing '%s' on '%s'", cmdTracker.getArgs()[0], devices);
             startInvocation(context, execCmd, listener, new FreeDeviceHandler(manager));
         } else {
+            // Log adb output just to help debug
+            String adbOutput =
+                    ((DeviceManager) GlobalConfiguration.getDeviceManagerInstance())
+                            .executeGlobalAdbCommand("devices");
+            CLog.e("'adb devices' output:\n%s", adbOutput);
             throw new NoDeviceException(
-                    "no devices is available for command: " + Arrays.asList(args));
+                    String.format(
+                            "no devices is available for command: %s\n%s",
+                            Arrays.asList(args), allocationResults.formattedReason()),
+                    InfraErrorIdentifier.SCHEDULER_ALLOCATION_ERROR);
         }
     }
 
@@ -1585,11 +1596,12 @@
 
     /**
      * Allocate devices for a config.
+     *
      * @param config a {@link IConfiguration} has device requirements.
      * @param manager a {@link IDeviceManager}
      * @return allocated devices
      */
-    Map<String, ITestDevice> allocateDevices(IConfiguration config, IDeviceManager manager) {
+    DeviceAllocationResult allocateDevices(IConfiguration config, IDeviceManager manager) {
         Map<String, ITestDevice> devices = new LinkedHashMap<String, ITestDevice>();
         ITestDevice device = null;
         if (config.getDeviceConfig().isEmpty()) {
@@ -1598,6 +1610,7 @@
         // If we need to replicate the setup on all devices
         ParentShardReplicate.replicatedSetup(config, getKeyStoreClient());
         synchronized(this) {
+            DeviceAllocationResult allocationResults = new DeviceAllocationResult();
             for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) {
                 device =
                         manager.allocateDevice(
@@ -1605,6 +1618,9 @@
                 if (device != null) {
                     devices.put(deviceConfig.getDeviceName(), device);
                 } else {
+                    allocationResults.addAllocationFailureReason(
+                            deviceConfig.getDeviceName(),
+                            deviceConfig.getDeviceRequirements().getNoMatchReason());
                     // If one of the several device cannot be allocated, we de-allocate
                     // all the previous one.
                     for (ITestDevice allocatedDevice : devices.values()) {
@@ -1623,7 +1639,8 @@
                     break;
                 }
             }
-            return devices;
+            allocationResults.addAllocatedDevices(devices);
+            return allocationResults;
         }
     }
 
diff --git a/src/com/android/tradefed/command/DeviceAllocationResult.java b/src/com/android/tradefed/command/DeviceAllocationResult.java
new file mode 100644
index 0000000..849b04a
--- /dev/null
+++ b/src/com/android/tradefed/command/DeviceAllocationResult.java
@@ -0,0 +1,67 @@
+/*
+ * 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.command;
+
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Represents the results of an allocation attempt for a command. */
+public class DeviceAllocationResult {
+
+    private Map<String, String> mNotAllocatedReason = new LinkedHashMap<>();
+    private Map<String, ITestDevice> mAllocatedDevices = new LinkedHashMap<>();
+
+    /** returns whether or not the allocation was successful. */
+    public boolean wasAllocationSuccessful() {
+        return !mAllocatedDevices.isEmpty();
+    }
+
+    /** Add devices that have been allocated. */
+    public void addAllocatedDevices(Map<String, ITestDevice> devices) {
+        mAllocatedDevices.putAll(devices);
+    }
+
+    /** Add the reasons for not being allocated for each device config. */
+    public void addAllocationFailureReason(String deviceConfigName, Map<String, String> reasons) {
+        mNotAllocatedReason.put(deviceConfigName, createReasonMessage(reasons));
+    }
+
+    /** Returns the map of allocated devices */
+    public Map<String, ITestDevice> getAllocatedDevices() {
+        return mAllocatedDevices;
+    }
+
+    public String formattedReason() {
+        if (mNotAllocatedReason.size() == 1) {
+            return mNotAllocatedReason.values().iterator().next().toString();
+        }
+        return mNotAllocatedReason.toString();
+    }
+
+    private String createReasonMessage(Map<String, String> reasons) {
+        StringBuilder sb = new StringBuilder();
+        for (String serial : reasons.keySet()) {
+            String reason = reasons.get(serial);
+            if (reason == null) {
+                reason = "No reason provided";
+            }
+            sb.append(String.format("device '%s': %s", serial, reason));
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/com/android/tradefed/command/ICommandOptions.java b/src/com/android/tradefed/command/ICommandOptions.java
index 37dd022..ea2ce99 100644
--- a/src/com/android/tradefed/command/ICommandOptions.java
+++ b/src/com/android/tradefed/command/ICommandOptions.java
@@ -19,6 +19,7 @@
 import com.android.tradefed.device.metric.AutoLogCollector;
 import com.android.tradefed.util.UniqueMultiMap;
 
+import java.time.Duration;
 import java.util.Map;
 import java.util.Set;
 
@@ -120,6 +121,10 @@
      */
     public void setInvocationTimeout(Long mInvocationTimeout);
 
+
+    /** Returns true if we should optimize the list of test modules for mainline test. */
+    public boolean getOptimizeMainlineTest();
+
     /**
      * Return the total shard count for the command.
      */
@@ -185,6 +190,12 @@
     /** Whether or not to attempt parallel setup of the remote devices. */
     public boolean shouldUseParallelRemoteSetup();
 
+    /** Whether or not to attempt parallel setup. */
+    public boolean shouldUseParallelSetup();
+
+    /** Returns the timeout to use during parallel setups. */
+    public Duration getParallelSetupTimeout();
+
     /** Whether or not to use replicated setup for all the remote devices. */
     public boolean shouldUseReplicateSetup();
 
diff --git a/src/com/android/tradefed/config/proxy/TradefedDelegator.java b/src/com/android/tradefed/config/proxy/TradefedDelegator.java
index 19c9ae9..17af019 100644
--- a/src/com/android/tradefed/config/proxy/TradefedDelegator.java
+++ b/src/com/android/tradefed/config/proxy/TradefedDelegator.java
@@ -18,15 +18,17 @@
 import com.android.tradefed.command.CommandOptions;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.UniqueMultiMap;
 
 import com.google.common.base.Joiner;
 
 import java.io.File;
-import java.io.FileFilter;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 
 /** Objects that helps delegating the invocation to another Tradefed binary. */
 public class TradefedDelegator {
@@ -60,16 +62,8 @@
     }
 
     /** Creates the classpath out of the jars in the directory. */
-    public String createClasspath() {
-        List<File> jars =
-                Arrays.asList(
-                        mDelegatedTfRootDir.listFiles(
-                                new FileFilter() {
-                                    @Override
-                                    public boolean accept(File pathname) {
-                                        return pathname.getName().endsWith(".jar");
-                                    }
-                                }));
+    public String createClasspath() throws IOException {
+        Set<File> jars = FileUtil.findFilesObject(mDelegatedTfRootDir, ".*\\.jar");
         return Joiner.on(":").join(jars);
     }
 
@@ -96,10 +90,17 @@
      */
     public static String[] clearCommandline(String[] originalCommand)
             throws ConfigurationException {
+        String[] commandLine = clearCommandlineFromOneArg(originalCommand, DELETEGATED_OPTION_NAME);
+        return commandLine;
+    }
+
+    /** Remove a given option from the command line. */
+    private static String[] clearCommandlineFromOneArg(String[] originalCommand, String optionName)
+            throws ConfigurationException {
         List<String> argsList = new ArrayList<>(Arrays.asList(originalCommand));
         try {
-            while (argsList.contains("--" + DELETEGATED_OPTION_NAME)) {
-                int index = argsList.indexOf("--" + DELETEGATED_OPTION_NAME);
+            while (argsList.contains("--" + optionName)) {
+                int index = argsList.indexOf("--" + optionName);
                 if (index != -1) {
                     argsList.remove(index + 1);
                     argsList.remove(index);
diff --git a/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java b/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
index 1aef60c..7a84771 100644
--- a/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
+++ b/src/com/android/tradefed/config/yaml/ConfigurationYamlParser.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.config.yaml.IDefaultObjectLoader.LoaderConfiguration;
+import com.android.tradefed.config.yaml.YamlClassOptionsParser.ClassAndOptions;
 
 import com.google.common.collect.ImmutableList;
 
@@ -40,6 +41,8 @@
 public final class ConfigurationYamlParser {
 
     private static final String DESCRIPTION_KEY = "description";
+    public static final String PRE_SETUP_ACTION_KEY = "pre_setup_action";
+    public static final String POST_SETUP_ACTION_KEY = "post_setup_action";
     public static final String DEPENDENCIES_KEY = "dependencies";
     public static final String TESTS_KEY = "tests";
 
@@ -89,6 +92,16 @@
             mSeenKeys.add(DESCRIPTION_KEY);
         }
         Set<String> dependencyFiles = new LinkedHashSet<>();
+        if (yamlObjects.containsKey(PRE_SETUP_ACTION_KEY)) {
+            YamlClassOptionsParser classAndOptions =
+                    new YamlClassOptionsParser(
+                            "action",
+                            PRE_SETUP_ACTION_KEY,
+                            (List<Map<String, Object>>) yamlObjects.get(PRE_SETUP_ACTION_KEY));
+            mSeenKeys.add(PRE_SETUP_ACTION_KEY);
+            convertClassAndOptionsToObjects(
+                    configDef, classAndOptions, Configuration.TARGET_PREPARER_TYPE_NAME);
+        }
         if (yamlObjects.containsKey(DEPENDENCIES_KEY)) {
             YamlTestDependencies testDeps =
                     new YamlTestDependencies(
@@ -97,12 +110,24 @@
             mSeenKeys.add(DEPENDENCIES_KEY);
         }
         if (yamlObjects.containsKey(TESTS_KEY)) {
-            YamlTestRunners runnerInfo =
-                    new YamlTestRunners((List<Map<String, Object>>) yamlObjects.get(TESTS_KEY));
+            YamlClassOptionsParser runnerInfo =
+                    new YamlClassOptionsParser(
+                            "test",
+                            TESTS_KEY,
+                            (List<Map<String, Object>>) yamlObjects.get(TESTS_KEY));
             mSeenKeys.add(TESTS_KEY);
-            convertTestsToObjects(configDef, runnerInfo);
+            convertClassAndOptionsToObjects(configDef, runnerInfo, Configuration.TEST_TYPE_NAME);
         }
-
+        if (yamlObjects.containsKey(POST_SETUP_ACTION_KEY)) {
+            YamlClassOptionsParser runnerInfo =
+                    new YamlClassOptionsParser(
+                            "action",
+                            POST_SETUP_ACTION_KEY,
+                            (List<Map<String, Object>>) yamlObjects.get(POST_SETUP_ACTION_KEY));
+            mSeenKeys.add(POST_SETUP_ACTION_KEY);
+            convertClassAndOptionsToObjects(
+                    configDef, runnerInfo, Configuration.TARGET_PREPARER_TYPE_NAME);
+        }
         if (!mSeenKeys.containsAll(REQUIRED_KEYS)) {
             Set<String> missingKeys = new HashSet<>(REQUIRED_KEYS);
             missingKeys.removeAll(mSeenKeys);
@@ -185,27 +210,26 @@
         return dependencies;
     }
 
-    private void convertTestsToObjects(ConfigurationDef def, YamlTestRunners tests) {
-        if (tests.getRunner() == null) {
+    private void convertClassAndOptionsToObjects(
+            ConfigurationDef def, YamlClassOptionsParser tests, String configObjType) {
+        if (tests.getClassesAndOptions().isEmpty()) {
             return;
         }
-        String className = tests.getRunner();
-        int classCount = def.addConfigObjectDef(Configuration.TEST_TYPE_NAME, className);
-        for (Entry<String, String> options : tests.getOptions().entries()) {
-            String optionName =
-                    String.format(
-                            "%s%c%d%c%s",
-                            className,
-                            OptionSetter.NAMESPACE_SEPARATOR,
-                            classCount,
-                            OptionSetter.NAMESPACE_SEPARATOR,
-                            options.getKey());
-            def.addOptionDef(
-                    optionName,
-                    null,
-                    options.getValue(),
-                    def.getName(),
-                    Configuration.TEST_TYPE_NAME);
+        for (ClassAndOptions classOptions : tests.getClassesAndOptions()) {
+            String className = classOptions.mClass;
+            int classCount = def.addConfigObjectDef(configObjType, className);
+            for (Entry<String, String> options : classOptions.mOptions.entries()) {
+                String optionName =
+                        String.format(
+                                "%s%c%d%c%s",
+                                className,
+                                OptionSetter.NAMESPACE_SEPARATOR,
+                                classCount,
+                                OptionSetter.NAMESPACE_SEPARATOR,
+                                options.getKey());
+                def.addOptionDef(
+                        optionName, null, options.getValue(), def.getName(), configObjType);
+            }
         }
     }
 }
diff --git a/src/com/android/tradefed/config/yaml/YamlClassOptionsParser.java b/src/com/android/tradefed/config/yaml/YamlClassOptionsParser.java
new file mode 100644
index 0000000..3dcdae1
--- /dev/null
+++ b/src/com/android/tradefed/config/yaml/YamlClassOptionsParser.java
@@ -0,0 +1,73 @@
+/*
+ * 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.config.yaml;
+
+import com.android.tradefed.config.ConfigurationException;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/** Helper to parse test runner information from the YAML Tradefed Configuration. */
+public class YamlClassOptionsParser {
+
+    private static final String CLASS_NAME_KEY = "name";
+    private static final String OPTIONS_KEY = "options";
+
+    class ClassAndOptions {
+        public String mClass;
+        public Multimap<String, String> mOptions = LinkedListMultimap.create();
+    }
+
+    private List<ClassAndOptions> mListClassAndOptions = new ArrayList<>();
+
+    public YamlClassOptionsParser(String mainkey, String category, List<Map<String, Object>> tests)
+            throws ConfigurationException {
+        for (Map<String, Object> runnerEntry : tests) {
+            if (runnerEntry.containsKey(mainkey)) {
+                ClassAndOptions classOptions = new ClassAndOptions();
+                mListClassAndOptions.add(classOptions);
+                for (Entry<String, Object> entry :
+                        ((Map<String, Object>) runnerEntry.get(mainkey)).entrySet()) {
+                    if (CLASS_NAME_KEY.equals(entry.getKey())) {
+                        classOptions.mClass = (String) entry.getValue();
+                    }
+                    if (OPTIONS_KEY.equals(entry.getKey())) {
+                        for (Map<String, Object> optionMap :
+                                (List<Map<String, Object>>) entry.getValue()) {
+                            for (Entry<String, Object> optionVal : optionMap.entrySet()) {
+                                // TODO: Support map option
+                                classOptions.mOptions.put(
+                                        optionVal.getKey(), optionVal.getValue().toString());
+                            }
+                        }
+                    }
+                }
+            } else {
+                throw new ConfigurationException(
+                        String.format("'%s' key is mandatory in '%s'", mainkey, category));
+            }
+        }
+    }
+
+    public List<ClassAndOptions> getClassesAndOptions() {
+        return mListClassAndOptions;
+    }
+}
diff --git a/src/com/android/tradefed/config/yaml/YamlTestRunners.java b/src/com/android/tradefed/config/yaml/YamlTestRunners.java
deleted file mode 100644
index 1c8952e..0000000
--- a/src/com/android/tradefed/config/yaml/YamlTestRunners.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.config.yaml;
-
-import com.android.tradefed.config.ConfigurationException;
-
-import com.google.common.collect.LinkedListMultimap;
-import com.google.common.collect.Multimap;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-/** Helper to parse test runner information from the YAML Tradefed Configuration. */
-public class YamlTestRunners {
-
-    private static final String TEST_KEY = "test";
-    private static final String TEST_NAME_KEY = "name";
-    private static final String OPTIONS_KEY = "options";
-
-    private String mRunner;
-    private Multimap<String, String> mOptions = LinkedListMultimap.create();
-
-    public YamlTestRunners(List<Map<String, Object>> tests) throws ConfigurationException {
-        if (tests.size() > 1) {
-            throw new ConfigurationException("Currently only support one runner at a time.");
-        }
-        for (Map<String, Object> runnerEntry : tests) {
-            if (runnerEntry.containsKey(TEST_KEY)) {
-                for (Entry<String, Object> entry :
-                        ((Map<String, Object>) runnerEntry.get(TEST_KEY)).entrySet()) {
-                    if (TEST_NAME_KEY.equals(entry.getKey())) {
-                        mRunner = (String) entry.getValue();
-                    }
-                    if (OPTIONS_KEY.equals(entry.getKey())) {
-                        for (Map<String, Object> optionMap :
-                                (List<Map<String, Object>>) entry.getValue()) {
-                            for (Entry<String, Object> optionVal : optionMap.entrySet()) {
-                                // TODO: Support map option
-                                mOptions.put(optionVal.getKey(), optionVal.getValue().toString());
-                            }
-                        }
-                    }
-                }
-            } else {
-                throw new ConfigurationException(
-                        String.format(
-                                "'%s' key is mandatory in '%s'",
-                                TEST_KEY, ConfigurationYamlParser.TESTS_KEY));
-            }
-        }
-    }
-
-    /** Returns the test runner to be used. */
-    public String getRunner() {
-        return mRunner;
-    }
-
-    /** Returns the options for the test runner */
-    public Multimap<String, String> getOptions() {
-        return mOptions;
-    }
-}
diff --git a/src/com/android/tradefed/device/DeviceSelectionOptions.java b/src/com/android/tradefed/device/DeviceSelectionOptions.java
index 6dbae6d..fac18c7 100644
--- a/src/com/android/tradefed/device/DeviceSelectionOptions.java
+++ b/src/com/android/tradefed/device/DeviceSelectionOptions.java
@@ -29,6 +29,7 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
@@ -161,6 +162,8 @@
 
     // If we have tried to fetch the environment variable ANDROID_SERIAL before.
     private boolean mFetchedEnvVariable = false;
+    // Store the reason for which the device was not matched.
+    private Map<String, String> mNoMatchReason = new LinkedHashMap<>();
 
     private static final String VARIANT_SEPARATOR = ":";
 
@@ -448,6 +451,7 @@
      */
     @Override
     public boolean matches(IDevice device) {
+        String deviceSerial = device.getSerialNumber();
         Collection<String> serials = getSerials(device);
         Collection<String> excludeSerials = getExcludeSerials();
         Map<String, Collection<String>> productVariants = splitOnVariant(getProductTypes());
@@ -456,9 +460,17 @@
 
         if (!serials.isEmpty() &&
                 !serials.contains(device.getSerialNumber())) {
+            addNoMatchReason(
+                    deviceSerial,
+                    String.format(
+                            "device serial does not match any requested serial(%s)", serials));
             return false;
         }
         if (excludeSerials.contains(device.getSerialNumber())) {
+            addNoMatchReason(
+                    deviceSerial,
+                    String.format(
+                            "device serial was part of excluded serials(%s)", excludeSerials));
             return false;
         }
         if (!productTypes.isEmpty()) {
@@ -468,15 +480,31 @@
                 String productVariant = getDeviceProductVariant(device);
                 Collection<String> variants = productVariants.get(productType);
                 if (variants != null && !variants.contains(productVariant)) {
+                    addNoMatchReason(
+                            deviceSerial,
+                            String.format(
+                                    "device variant (%s) does not match requested variants(%s)",
+                                    productVariant, variants));
                     return false;
                 }
             } else {
                 // no product type matches; bye-bye
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device product type (%s) does not match requested product types(%s)",
+                                productType, productTypes));
                 return false;
             }
         }
         for (Map.Entry<String, String> propEntry : properties.entrySet()) {
-            if (!propEntry.getValue().equals(device.getProperty(propEntry.getKey()))) {
+            String deviceProperty = device.getProperty(propEntry.getKey());
+            if (!propEntry.getValue().equals(deviceProperty)) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device property (%s) value(%s) does not match requested value(%s)",
+                                propEntry.getKey(), deviceProperty, propEntry.getValue()));
                 return false;
             }
         }
@@ -488,12 +516,25 @@
         if ((mMinSdk != null) || (mMaxSdk != null)) {
             int deviceSdkLevel = getDeviceSdkLevel(device);
             if (deviceSdkLevel < 0) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format("device returned unexpected sdk level (%s)", deviceSdkLevel));
                 return false;
             }
             if (mMinSdk != null && deviceSdkLevel < mMinSdk) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device sdk (%s) is below the requested min sdk (%s)",
+                                deviceSdkLevel, mMinSdk));
                 return false;
             }
             if (mMaxSdk != null && mMaxSdk < deviceSdkLevel) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device sdk (%s) is above the requested max sdk (%s)",
+                                deviceSdkLevel, mMaxSdk));
                 return false;
             }
         }
@@ -505,19 +546,35 @@
                 if (device instanceof StubDevice || device instanceof FastbootDevice) {
                     // Reading battery of fastboot and StubDevice device does not work and could
                     // lead to weird log.
+                    addNoMatchReason(
+                            deviceSerial,
+                            String.format(
+                                    "device type is (%s) which cannot have a battery required.",
+                                    device.getClass()));
                     return false;
                 }
                 Integer deviceBattery = getBatteryLevel(device);
                 if (deviceBattery == null) {
                     // Couldn't determine battery level when that check is required; reject device
+                    addNoMatchReason(deviceSerial, "device failed to return a battery reading.");
                     return false;
                 }
                 if (isLessAndNotNull(deviceBattery, mMinBattery)) {
                     // deviceBattery < mMinBattery
+                    addNoMatchReason(
+                            deviceSerial,
+                            String.format(
+                                    "device battery (%s) is below the requested min battery (%s)",
+                                    deviceBattery, mMinBattery));
                     return false;
                 }
                 if (isLessEqAndNotNull(mMaxBattery, deviceBattery)) {
                     // mMaxBattery <= deviceBattery
+                    addNoMatchReason(
+                            deviceSerial,
+                            String.format(
+                                    "device battery (%s) is above the requested max battery (%s)",
+                                    deviceBattery, mMaxBattery));
                     return false;
                 }
             }
@@ -558,39 +615,55 @@
         if ((emulatorRequested() || stubEmulatorRequested()) && !device.isEmulator()) {
             return false;
         }
+        String deviceSerial = device.getSerialNumber();
         // If physical device is requested but device is emulator or remote ip device, skip
         if (deviceRequested()
                 && (device.isEmulator()
                         || RemoteAndroidDevice.checkSerialFormatValid(device.getSerialNumber()))) {
+            addNoMatchReason(deviceSerial, "device is not a physical device");
             return false;
         }
 
         if (mRequestedType != null) {
             Class<?> classNeeded = mRequestedType.getRequiredClass();
             if (!device.getClass().equals(classNeeded)) {
+                addNoMatchReason(
+                        deviceSerial,
+                        String.format(
+                                "device is type (%s) while requested type was (%s)",
+                                device.getClass(), classNeeded));
                 return false;
             }
         } else {
             if (device.isEmulator() && (device instanceof StubDevice) && !stubEmulatorRequested()) {
                 // only allocate the stub emulator if requested
+                addNoMatchReason(deviceSerial, "device is emulator while requested type was not");
                 return false;
             }
             if (nullDeviceRequested() != (device instanceof NullDevice)) {
+                addNoMatchReason(
+                        deviceSerial, "device is null-device while requested type was not");
                 return false;
             }
             if (tcpDeviceRequested() != TcpDevice.class.equals(device.getClass())) {
                 // We only match an exact TcpDevice here, no child class.
+                addNoMatchReason(deviceSerial, "device is tcp-device while requested type was not");
                 return false;
             }
             if (gceDeviceRequested() != RemoteAvdIDevice.class.equals(device.getClass())) {
                 // We only match an exact RemoteAvdIDevice here, no child class.
+                addNoMatchReason(deviceSerial, "device is gce-device while requested type was not");
                 return false;
             }
             if (remoteDeviceRequested() != VmRemoteDevice.class.equals(device.getClass())) {
+                addNoMatchReason(
+                        deviceSerial, "device is remote-device while requested type was not");
                 return false;
             }
             if (localVirtualDeviceRequested()
                     != StubLocalAndroidVirtualDevice.class.equals(device.getClass())) {
+                addNoMatchReason(
+                        deviceSerial, "device is local-virtual while requested type was not");
                 return false;
             }
         }
@@ -704,6 +777,15 @@
         return apiLevel;
     }
 
+    private void addNoMatchReason(String device, String reason) {
+        mNoMatchReason.put(device, reason);
+    }
+
+    @Override
+    public Map<String, String> getNoMatchReason() {
+        return mNoMatchReason;
+    }
+
     /**
      * Helper factory method to create a {@link IDeviceSelection} that will only match device
      * with given serial
diff --git a/src/com/android/tradefed/device/IDeviceSelection.java b/src/com/android/tradefed/device/IDeviceSelection.java
index c61179e..1302532 100644
--- a/src/com/android/tradefed/device/IDeviceSelection.java
+++ b/src/com/android/tradefed/device/IDeviceSelection.java
@@ -116,4 +116,10 @@
      */
     public void setSerial(String... serialNumber);
 
+    /**
+     * Returns the reason for which the device was not matched.
+     *
+     * @return a Map of serial number to reason for which it wasn't allocated
+     */
+    public Map<String, String> getNoMatchReason();
 }
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 8c96162..3bd9abe 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -109,6 +109,7 @@
 public class NativeDevice implements IManagedTestDevice {
 
     protected static final String SD_CARD = "/sdcard/";
+    protected static final String STORAGE_EMULATED = "/storage/emulated/";
     /**
      * Allow pauses of up to 2 minutes while receiving bugreport.
      * <p/>
@@ -1101,7 +1102,7 @@
     public boolean pullFile(final String remoteFilePath, final File localFile)
             throws DeviceNotAvailableException {
 
-        if (remoteFilePath.startsWith(SD_CARD)) {
+        if (isSdcardOrEmulated(remoteFilePath)) {
             ContentProviderHandler handler = getContentProvider();
             if (handler != null) {
                 return handler.pullFile(remoteFilePath, localFile);
@@ -1209,7 +1210,7 @@
     @Override
     public boolean pushFile(final File localFile, final String remoteFilePath)
             throws DeviceNotAvailableException {
-        if (remoteFilePath.startsWith(SD_CARD)) {
+        if (isSdcardOrEmulated(remoteFilePath)) {
             ContentProviderHandler handler = getContentProvider();
             if (handler != null) {
                 return handler.pushFile(localFile, remoteFilePath);
@@ -1284,6 +1285,15 @@
     /** {@inheritDoc} */
     @Override
     public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
+        if (isSdcardOrEmulated(deviceFilePath)) {
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                CLog.d("Delegating check to ContentProvider doesFileExist(%s)", deviceFilePath);
+
+                return handler.doesFileExist(deviceFilePath);
+            }
+        }
+        CLog.d("Using 'ls' to check doesFileExist(%s)", deviceFilePath);
         String lsGrep = executeShellCommand(String.format("ls \"%s\"", deviceFilePath));
         return !lsGrep.contains("No such file or directory");
     }
@@ -1291,7 +1301,7 @@
     /** {@inheritDoc} */
     @Override
     public void deleteFile(String deviceFilePath) throws DeviceNotAvailableException {
-        if (deviceFilePath.startsWith(SD_CARD)) {
+        if (isSdcardOrEmulated(deviceFilePath)) {
             ContentProviderHandler handler = getContentProvider();
             if (handler != null) {
                 if (handler.deleteFile(deviceFilePath)) {
@@ -1624,7 +1634,7 @@
     @Override
     public boolean pullDir(String deviceFilePath, File localDir)
             throws DeviceNotAvailableException {
-        if (deviceFilePath.startsWith(SD_CARD)) {
+        if (isSdcardOrEmulated(deviceFilePath)) {
             ContentProviderHandler handler = getContentProvider();
             if (handler != null) {
                 return handler.pullDir(deviceFilePath, localDir);
@@ -1677,6 +1687,11 @@
         return true;
     }
 
+    /** Checks whether path is external storage path. */
+    private boolean isSdcardOrEmulated(String path) {
+        return path.startsWith(SD_CARD) || path.startsWith(STORAGE_EMULATED);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -4051,7 +4066,8 @@
             throw new DeviceRuntimeException(
                     String.format(
                             "Failed to query property '%s'. device returned null.",
-                            DeviceProperties.BUILD_CODENAME));
+                            DeviceProperties.BUILD_CODENAME),
+                    DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
         }
         codeName = codeName.trim();
         int apiLevel = getApiLevel() + ("REL".equals(codeName) ? 0 : 1);
diff --git a/src/com/android/tradefed/device/NoDeviceException.java b/src/com/android/tradefed/device/NoDeviceException.java
index 45f4339..a2fae93 100644
--- a/src/com/android/tradefed/device/NoDeviceException.java
+++ b/src/com/android/tradefed/device/NoDeviceException.java
@@ -16,36 +16,23 @@
 package com.android.tradefed.device;
 
 import com.android.tradefed.build.BuildSerializedVersion;
+import com.android.tradefed.error.HarnessRuntimeException;
+import com.android.tradefed.result.error.ErrorIdentifier;
 
-/**
- * Thrown when there's no device to execute a given command.
- */
-public class NoDeviceException extends Exception {
+import java.lang.StackWalker.Option;
+
+/** Thrown when there's no device to execute a given command. */
+public class NoDeviceException extends HarnessRuntimeException {
     private static final long serialVersionUID = BuildSerializedVersion.VERSION;
 
     /**
      * Creates a {@link NoDeviceException}.
-     */
-    public NoDeviceException() {
-        super();
-    }
-
-    /**
-     * Creates a {@link NoDeviceException}.
      *
      * @param msg a descriptive message.
+     * @param errorId The {@link ErrorIdentifier} categorizing the exception.
      */
-    public NoDeviceException(String msg) {
-        super(msg);
-    }
-
-    /**
-     * Creates a {@link NoDeviceException}.
-     *
-     * @param msg a descriptive message.
-     * @param cause the root {@link Throwable} that caused the device to become unavailable.
-     */
-    public NoDeviceException(String msg, Throwable cause) {
-        super(msg, cause);
+    public NoDeviceException(String msg, ErrorIdentifier errorId) {
+        super(msg, errorId);
+        setCallerClass(StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass());
     }
 }
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index 80fb98d..958f21d 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.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.util.AaptParser;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -1187,7 +1188,8 @@
         String[] lines = commandOutput.split("\\r?\\n");
         if (!lines[0].equals("Users:")) {
             throw new DeviceRuntimeException(
-                    String.format("'%s' in not a valid output for 'pm list users'", commandOutput));
+                    String.format("'%s' in not a valid output for 'pm list users'", commandOutput),
+                    DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
         }
         ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1);
         for (int i = 1; i < lines.length; i++) {
@@ -1199,7 +1201,8 @@
                         String.format(
                                 "device output: '%s' \nline: '%s' was not in the expected "
                                         + "format for user info.",
-                                commandOutput, lines[i]));
+                                commandOutput, lines[i]),
+                        DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
             }
             users.add(tokens);
         }
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index 4a323c1..a206884 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -26,7 +26,7 @@
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.targetprep.TargetSetupError;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.CommandResult;
@@ -221,7 +221,7 @@
                     throw new TargetSetupError(
                             String.format("acloud errors: %s", errors),
                             mDeviceDescriptor,
-                            DeviceErrorIdentifier.FAILED_TO_LAUNCH_GCE);
+                            InfraErrorIdentifier.NO_ACLOUD_REPORT);
                 }
             }
             mGceAvdInfo =
@@ -229,7 +229,11 @@
                             reportFile, mDeviceDescriptor, mDeviceOptions.getRemoteAdbPort());
             return mGceAvdInfo;
         } catch (IOException e) {
-            throw new TargetSetupError("failed to create log file", e, mDeviceDescriptor);
+            throw new TargetSetupError(
+                    "failed to create log file",
+                    e,
+                    mDeviceDescriptor,
+                    InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
         } finally {
             FileUtil.deleteFile(reportFile);
         }
diff --git a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
index 4925bb6..878e114 100644
--- a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
@@ -37,6 +37,8 @@
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.StreamUtil;
 
@@ -63,6 +65,7 @@
 
     private GceManager mGceHandler = null;
     private GceSshTunnelMonitor mGceSshMonitor;
+    private DeviceNotAvailableException mTunnelInitFailed = null;
 
     private static final long WAIT_FOR_TUNNEL_ONLINE = 2 * 60 * 1000;
     private static final long WAIT_AFTER_REBOOT = 60 * 1000;
@@ -91,6 +94,7 @@
         try {
             mGceAvd = null;
             mGceSshMonitor = null;
+            mTunnelInitFailed = null;
             // We create a brand new GceManager each time to ensure clean state.
             mGceHandler = new GceManager(getDeviceDescriptor(), getOptions(), info);
             getGceHandler().logStableHostImageInfos(info);
@@ -269,7 +273,10 @@
                         String.format(
                                 "Device failed to boot. Error from Acloud: %s",
                                 mGceAvd.getErrors());
-                throw new TargetSetupError(errorMsg, getDeviceDescriptor());
+                throw new TargetSetupError(
+                        errorMsg,
+                        getDeviceDescriptor(),
+                        DeviceErrorIdentifier.FAILED_TO_LAUNCH_GCE);
             }
         }
         createGceSshMonitor(this, buildInfo, mGceAvd.hostAndPort(), this.getOptions());
@@ -322,14 +329,20 @@
             }
             getRunUtil().sleep(RETRY_INTERVAL_MS);
         }
-        throw new DeviceNotAvailableException(
-                String.format("Tunnel did not come back online after %sms", waitTime),
-                getSerialNumber(),
-                DeviceErrorIdentifier.FAILED_TO_CONNECT_TO_GCE);
+        mTunnelInitFailed =
+                new DeviceNotAvailableException(
+                        String.format("Tunnel did not come back online after %sms", waitTime),
+                        getSerialNumber(),
+                        DeviceErrorIdentifier.FAILED_TO_CONNECT_TO_GCE);
+        throw mTunnelInitFailed;
     }
 
     @Override
     public void recoverDevice() throws DeviceNotAvailableException {
+        if (getGceSshMonitor() == null && mTunnelInitFailed != null) {
+            // We threw before but was not reported, so throw the root cause here.
+            throw mTunnelInitFailed;
+        }
         // Re-init tunnel when attempting recovery
         CLog.i("Attempting recovery on GCE AVD %s", getSerialNumber());
         getGceSshMonitor().closeConnection();
@@ -447,4 +460,38 @@
         }
         return descriptor;
     }
+
+    /**
+     * Attempt to powerwash a GCE instance
+     *
+     * @return returns true if powerwash Gce success.
+     * @throws TargetSetupError
+     * @throws DeviceNotAvailableException
+     */
+    public boolean powerwashGce() throws TargetSetupError, DeviceNotAvailableException {
+        if (mGceAvd == null) {
+            String errorMsg = String.format("Can not get GCE AVD Info. launch GCE first?");
+            throw new TargetSetupError(
+                    errorMsg, getDeviceDescriptor(), DeviceErrorIdentifier.DEVICE_UNAVAILABLE);
+        }
+        String username = this.getOptions().getInstanceUser();
+        String powerwashCommand = String.format("/home/%s/bin/powerwash_cvd", username);
+        CommandResult powerwashRes =
+                GceManager.remoteSshCommandExecution(
+                        mGceAvd,
+                        this.getOptions(),
+                        getRunUtil(),
+                        60000L,
+                        powerwashCommand.split(" "));
+        if (!CommandStatus.SUCCESS.equals(powerwashRes.getStatus())) {
+            CLog.e("%s", powerwashRes.getStderr());
+            // Log 'adb devices' to confirm device is gone
+            CommandResult printAdbDevices = getRunUtil().runTimedCmd(60000L, "adb", "devices");
+            CLog.e("%s\n%s", printAdbDevices.getStdout(), printAdbDevices.getStderr());
+            // Proceed here, device could have been already gone.
+            return false;
+        }
+        getMonitor().waitForDeviceAvailable();
+        return true;
+    }
 }
diff --git a/src/com/android/tradefed/device/metric/FilePullerLogCollector.java b/src/com/android/tradefed/device/metric/FilePullerLogCollector.java
index 5474433..da8c240 100644
--- a/src/com/android/tradefed/device/metric/FilePullerLogCollector.java
+++ b/src/com/android/tradefed/device/metric/FilePullerLogCollector.java
@@ -46,6 +46,8 @@
                     type = LogDataType.PB;
                 } else if (".mp4".equals(ext)) {
                     type = LogDataType.MP4;
+                } else if (".hprof".equals(ext)) {
+                    type = LogDataType.HPROF;
                 }
                 testLog(metricFile.getName(), type, source);
             }
diff --git a/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java b/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
index 1b071ef..2255ee7 100644
--- a/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
+++ b/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
@@ -33,6 +33,8 @@
 import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.JavaCodeCoverageFlusher;
+import com.android.tradefed.util.ProcessInfo;
+import com.android.tradefed.util.PsParser;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
@@ -42,6 +44,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Map;
 import java.util.List;
 
@@ -148,7 +151,6 @@
                 devicePaths.addAll(Splitter.on('\n').omitEmptyStrings().split(fileList));
 
                 collectAndLogCoverageMeasurementsAsRoot(device, devicePaths.build());
-
             } catch (DeviceNotAvailableException | IOException e) {
                 throw new RuntimeException(e);
             }
@@ -191,9 +193,24 @@
 
     private void collectAndLogCoverageMeasurements(ITestDevice device, List<String> devicePaths)
             throws IOException, DeviceNotAvailableException {
+        List<Integer> activePids = getRunningProcessIds(device);
 
         for (String devicePath : devicePaths) {
             File coverageFile = device.pullFile(devicePath);
+
+            if (devicePath.endsWith(".mm.ec")) {
+                // Check if the process was still running. The file will have the format
+                // /data/misc/trace/jacoco-XXXXX.mm.ec where XXXXX is the process id.
+                int start = devicePath.indexOf('-') + 1;
+                int end = devicePath.indexOf('.');
+                int pid = Integer.parseInt(devicePath.substring(start, end));
+                if (!activePids.contains(pid)) {
+                    device.deleteFile(devicePath);
+                }
+            } else {
+                device.deleteFile(devicePath);
+            }
+
             verifyNotNull(
                     coverageFile, "Failed to pull the Java code coverage file from %s", devicePath);
 
@@ -216,6 +233,17 @@
         }
     }
 
+    private List<Integer> getRunningProcessIds(ITestDevice device)
+            throws DeviceNotAvailableException {
+        List<ProcessInfo> processes = PsParser.getProcesses(device.executeShellCommand("ps -e"));
+        List<Integer> pids = new ArrayList<>();
+
+        for (ProcessInfo process : processes) {
+            pids.add(process.getPid());
+        }
+        return pids;
+    }
+
     private FailureDescription createCodeCoverageFailure(String message) {
         return CurrentInvocation.createFailure(message, InfraErrorIdentifier.CODE_COVERAGE_ERROR);
     }
diff --git a/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java b/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
index c4c9697..56292d2 100644
--- a/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/DelegatedInvocationExecution.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.invoker.TestInvocation.Stage;
 import com.android.tradefed.log.ITestLogger;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
@@ -131,6 +132,7 @@
             IRunUtil runUtil = createRunUtil(receiver.getSocketServerPort());
             CommandResult result = null;
             RuntimeException runtimeException = null;
+            CLog.d("Command line: %s", commandLine);
             try {
                 result =
                         runUtil.runTimedCmd(
@@ -159,7 +161,7 @@
             }
             if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
                 throw new HarnessRuntimeException(
-                        "Delegated invocation timed out.", InfraErrorIdentifier.UNDETERMINED);
+                        "Delegated invocation timed out.", InfraErrorIdentifier.INVOCATION_TIMEOUT);
             }
         } finally {
             StreamUtil.close(mStderr);
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index 24d809e..7a4ae8a 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -83,6 +83,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -231,9 +232,10 @@
 
             mTrackTargetPreparers = new ConcurrentHashMap<>();
             int index = 0;
-            if (config.getCommandOptions().shouldUseReplicateSetup()
+            if ((config.getCommandOptions().shouldUseParallelSetup()
+                            || config.getCommandOptions().shouldUseReplicateSetup())
                     && config.getDeviceConfig().size() > 1) {
-                CLog.d("Using parallel setup due to replicated setup enabled.");
+                CLog.d("Using parallel setup.");
                 ParallelDeviceExecutor<Boolean> executor =
                         new ParallelDeviceExecutor<>(testInfo.getContext().getDevices().size());
                 List<Callable<Boolean>> callableTasks = new ArrayList<>();
@@ -252,8 +254,8 @@
                     callableTasks.add(callableTask);
                     index++;
                 }
-                // Run setup with 30 minutes right now.
-                executor.invokeAll(callableTasks, 30, TimeUnit.MINUTES);
+                Duration timeout = config.getCommandOptions().getParallelSetupTimeout();
+                executor.invokeAll(callableTasks, timeout.toMillis(), TimeUnit.MILLISECONDS);
                 if (executor.hasErrors()) {
                     List<Throwable> errors = executor.getErrors();
                     // TODO: Handle throwing multi-exceptions, right now throw the first one.
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 9da66eb..c4ffc43 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -43,6 +43,7 @@
 import com.android.tradefed.device.cloud.NestedRemoteDevice;
 import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.error.HarnessException;
+import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.error.IHarnessException;
 import com.android.tradefed.guice.InvocationScope;
 import com.android.tradefed.invoker.logger.CurrentInvocation;
@@ -164,6 +165,7 @@
     private Long mStopRequestTime = null;
     private boolean mTestStarted = false;
     private boolean mInvocationFailed = false;
+    private boolean mDelegatedInvocation = false;
     private List<IScheduledInvocationListener> mSchedulerListeners = new ArrayList<>();
 
     /**
@@ -214,7 +216,6 @@
             throws Throwable {
         ReportHostLog reportThread = new ReportHostLog(listener, config);
         Runtime.getRuntime().addShutdownHook(reportThread);
-        boolean resumed = false;
         String bugreportName = null;
         long startTime = System.currentTimeMillis();
         long elapsedTime = -1;
@@ -306,37 +307,39 @@
             }
             CurrentInvocation.setActionInProgress(ActionInProgress.TEAR_DOWN);
             getRunUtil().allowInterrupt(false);
-            if (config.getCommandOptions().takeBugreportOnInvocationEnded() ||
-                    config.getCommandOptions().takeBugreportzOnInvocationEnded()) {
-                if (bugreportName != null) {
-                    CLog.i("Bugreport to be taken for failure instead of invocation ended.");
-                } else {
-                    bugreportName = INVOCATION_ENDED_BUGREPORT_NAME;
+            if (!mDelegatedInvocation) {
+                if (config.getCommandOptions().takeBugreportOnInvocationEnded()
+                        || config.getCommandOptions().takeBugreportzOnInvocationEnded()) {
+                    if (bugreportName != null) {
+                        CLog.i("Bugreport to be taken for failure instead of invocation ended.");
+                    } else {
+                        bugreportName = INVOCATION_ENDED_BUGREPORT_NAME;
+                    }
                 }
-            }
-            if (bugreportName != null) {
-                if (context.getDevices().size() == 1 || badDevice != null) {
-                    ITestDevice collectBugreport = badDevice;
-                    if (collectBugreport == null) {
-                        collectBugreport = context.getDevices().get(0);
+                if (bugreportName != null) {
+                    if (context.getDevices().size() == 1 || badDevice != null) {
+                        ITestDevice collectBugreport = badDevice;
+                        if (collectBugreport == null) {
+                            collectBugreport = context.getDevices().get(0);
+                        }
+                        // If we have identified a faulty device only take the bugreport on it.
+                        takeBugreport(collectBugreport, listener, bugreportName);
+                    } else if (context.getDevices().size() > 1) {
+                        ParallelDeviceExecutor<Boolean> executor =
+                                new ParallelDeviceExecutor<>(context.getDevices().size());
+                        List<Callable<Boolean>> callableTasks = new ArrayList<>();
+                        final String reportName = bugreportName;
+                        for (ITestDevice device : context.getDevices()) {
+                            Callable<Boolean> callableTask =
+                                    () -> {
+                                        takeBugreport(device, listener, reportName);
+                                        return true;
+                                    };
+                            callableTasks.add(callableTask);
+                        }
+                        // Capture the bugreports best effort, ignore the results.
+                        executor.invokeAll(callableTasks, 5, TimeUnit.MINUTES);
                     }
-                    // If we have identified a faulty device only take the bugreport on it.
-                    takeBugreport(collectBugreport, listener, bugreportName);
-                } else if (context.getDevices().size() > 1) {
-                    ParallelDeviceExecutor<Boolean> executor =
-                            new ParallelDeviceExecutor<>(context.getDevices().size());
-                    List<Callable<Boolean>> callableTasks = new ArrayList<>();
-                    final String reportName = bugreportName;
-                    for (ITestDevice device : context.getDevices()) {
-                        Callable<Boolean> callableTask =
-                                () -> {
-                                    takeBugreport(device, listener, reportName);
-                                    return true;
-                                };
-                        callableTasks.add(callableTask);
-                    }
-                    // Capture the bugreports best effort, ignore the results.
-                    executor.invokeAll(callableTasks, 5, TimeUnit.MINUTES);
                 }
             }
             // Save the device executeShellCommand logs
@@ -384,6 +387,10 @@
                                     mStopCause);
                     FailureDescription failure =
                             FailureDescription.create(message, FailureStatus.CANCELLED);
+                    failure.setErrorIdentifier(InfraErrorIdentifier.INVOCATION_CANCELLED);
+                    failure.setCause(
+                            new HarnessRuntimeException(
+                                    message, InfraErrorIdentifier.INVOCATION_CANCELLED));
                     reportFailure(failure, listener);
                     PrettyPrintDelimiter.printStageDelimiter(message);
                     if (mStopRequestTime != null) {
@@ -399,22 +406,7 @@
                 Runtime.getRuntime().removeShutdownHook(reportThread);
 
                 elapsedTime = System.currentTimeMillis() - startTime;
-                if (!resumed) {
-                    // Init a log for the end of the host_log.
-                    ILeveledLogOutput endHostLog = config.getLogOutput();
-                    endHostLog.init();
-                    getLogRegistry().registerLogger(endHostLog);
-                    PrettyPrintDelimiter.printStageDelimiter("===== Result Reporters =====");
-                    try {
-                        // Copy the invocation metrics to the context
-                        ((InvocationContext) context).logInvocationMetrics();
-                        listener.invocationEnded(elapsedTime);
-                    } finally {
-                        InvocationMetricLogger.clearInvocationMetrics();
-                        endHostLog.closeLog();
-                        getLogRegistry().unregisterLogger();
-                    }
-                }
+                reportInvocationEnded(config, context, listener, elapsedTime);
             } finally {
                 TfObjectTracker.clearTracking();
                 CurrentInvocation.clearInvocationInfos();
@@ -498,7 +490,7 @@
 
     private void reportHostLog(ITestInvocationListener listener, IConfiguration config) {
         String name = TRADEFED_LOG_NAME;
-        if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
+        if (mDelegatedInvocation) {
             name = TRADEFED_DELEGATED_LOG_NAME;
         }
         reportHostLog(listener, config, name);
@@ -650,7 +642,7 @@
             invocationPath.reportLogs(device, listener, Stage.ERROR);
         }
         reportHostLog(listener, config);
-        listener.invocationEnded(0L);
+        reportInvocationEnded(config, testInfo.getContext(), listener, 0L);
         return false;
     }
 
@@ -703,7 +695,7 @@
                 invocationPath.reportLogs(device, listener, Stage.ERROR);
             }
             reportHostLog(listener, config);
-            listener.invocationEnded(0L);
+            reportInvocationEnded(config, context, listener, 0L);
             return false;
         }
     }
@@ -797,6 +789,7 @@
             mode = RunMode.REMOTE_INVOCATION;
         }
         if (config.getConfigurationObject(TradefedDelegator.DELEGATE_OBJECT) != null) {
+            mDelegatedInvocation = true;
             mode = RunMode.DELEGATED_INVOCATION;
         }
         IInvocationExecution invocationPath = createInvocationExec(mode);
@@ -1156,6 +1149,35 @@
         return devicesStates;
     }
 
+    private void reportInvocationEnded(
+            IConfiguration config,
+            IInvocationContext context,
+            ITestInvocationListener listener,
+            long elapsedTime) {
+        // Init a log for the end of the host_log.
+        ILeveledLogOutput endHostLog = config.getLogOutput();
+        try {
+            endHostLog.init();
+            getLogRegistry().registerLogger(endHostLog);
+        } catch (IOException e) {
+            CLog.e(e);
+            endHostLog = null;
+        }
+
+        PrettyPrintDelimiter.printStageDelimiter("===== Result Reporters =====");
+        try {
+            // Copy the invocation metrics to the context
+            ((InvocationContext) context).logInvocationMetrics();
+            listener.invocationEnded(elapsedTime);
+        } finally {
+            InvocationMetricLogger.clearInvocationMetrics();
+            if (endHostLog != null) {
+                endHostLog.closeLog();
+                getLogRegistry().unregisterLogger();
+            }
+        }
+    }
+
     /** Helper Thread that ensures host_log is reported in case of killed JVM */
     private class ReportHostLog extends Thread {
 
diff --git a/src/com/android/tradefed/invoker/shard/StrictShardHelper.java b/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
index f3aeffc..cf30b05 100644
--- a/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
+++ b/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
@@ -35,7 +35,10 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /** Sharding strategy to create strict shards that do not report together, */
 public class StrictShardHelper extends ShardHelper {
@@ -49,6 +52,7 @@
             ITestLogger logger) {
         Integer shardCount = config.getCommandOptions().getShardCount();
         Integer shardIndex = config.getCommandOptions().getShardIndex();
+        boolean optimizeMainline = config.getCommandOptions().getOptimizeMainlineTest();
 
         if (shardIndex == null) {
             return super.shardConfig(config, testInfo, rescheduler, logger);
@@ -69,11 +73,50 @@
             splitList = splitTests(listAllTests, shardCount).get(shardIndex);
         }
         aggregateSuiteModules(splitList);
+        if (optimizeMainline) {
+            CLog.i("Reordering the test modules list for index: %s", shardIndex);
+            reorderTestModules(splitList);
+        }
         config.setTests(splitList);
         return false;
     }
 
     /**
+     * Helper to re order the list full list of {@link IRemoteTest} for mainline.
+     *
+     * @param tests the {@link IRemoteTest} containing all the tests that need to run.
+     */
+    private void reorderTestModules(List<IRemoteTest> tests) {
+        Collections.sort(tests, new Comparator<IRemoteTest>() {
+            @Override
+            public int compare(IRemoteTest o1, IRemoteTest o2) {
+                String moduleId1 = ((ITestSuite)o1).getDirectModule().getId();
+                String moduleId2 = ((ITestSuite)o2).getDirectModule().getId();
+                return getMainlineId(moduleId1).compareTo(getMainlineId(moduleId2));
+            }
+        });
+    }
+
+    /**
+     * Returns the parameterized mainline modules' name defined in the square brackets.
+     *
+     * @param id The module's name.
+     * @throws RuntimeException if the module name doesn't match the pattern for mainline modules.
+     */
+    private String getMainlineId(String id) {
+        // Pattern used to identify the parameterized mainline modules defined in the square
+        // brackets.
+        Pattern parameterizedMainlineRegex = Pattern.compile("\\[(.*(\\.apk|.apex|.apks))\\]$");
+        Matcher m = parameterizedMainlineRegex.matcher(id);
+        if (m.find()) {
+            return m.group(1);
+        }
+        throw new RuntimeException(
+                String.format("Module: %s doesn't match the pattern for mainline modules. The " +
+                        "pattern should end with apk/apex/apks.", id));
+    }
+
+    /**
      * Helper to return the full list of {@link IRemoteTest} based on {@link IShardableTest} split.
      *
      * @param config the {@link IConfiguration} describing the invocation.
diff --git a/src/com/android/tradefed/monitoring/LabResourceDeviceMonitor.java b/src/com/android/tradefed/monitoring/LabResourceDeviceMonitor.java
new file mode 100644
index 0000000..8366e0e
--- /dev/null
+++ b/src/com/android/tradefed/monitoring/LabResourceDeviceMonitor.java
@@ -0,0 +1,96 @@
+/*
+ * 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.monitoring;
+
+import com.android.loganalysis.util.config.OptionClass;
+import com.android.tradefed.device.DeviceAllocationState;
+import com.android.tradefed.device.IDeviceMonitor;
+import com.android.tradefed.log.LogUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.dualhomelab.monitoringagent.resourcemonitoring.LabResource;
+import com.google.dualhomelab.monitoringagent.resourcemonitoring.LabResourceRequest;
+import com.google.dualhomelab.monitoringagent.resourcemonitoring.LabResourceServiceGrpc;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Optional;
+import java.util.concurrent.Executors;
+
+import io.grpc.Server;
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import io.grpc.stub.StreamObserver;
+
+/** The lab resource monitor which initializes/manages the gRPC server for LabResourceService. */
+@OptionClass(alias = "lab-resource-monitor")
+public class LabResourceDeviceMonitor extends LabResourceServiceGrpc.LabResourceServiceImplBase
+        implements IDeviceMonitor {
+    public static final String SERVER_HOSTNAME = "localhost";
+    public static final int DEFAULT_PORT = 8887;
+    public static final int DEFAULT_THREAD_COUNT = 1;
+    private Optional<Server> mServer = Optional.empty();
+
+    @VisibleForTesting
+    Optional<Server> getServer() {
+        return mServer;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void run() {
+        if (!mServer.isPresent()) {
+            mServer =
+                    Optional.of(
+                            NettyServerBuilder.forAddress(
+                                            new InetSocketAddress(SERVER_HOSTNAME, DEFAULT_PORT))
+                                    .addService(this)
+                                    .executor(Executors.newFixedThreadPool(DEFAULT_THREAD_COUNT))
+                                    .build());
+            try {
+                mServer.get().start();
+            } catch (IOException e) {
+                LogUtil.CLog.e(e);
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void stop() {
+        mServer.ifPresent(Server::shutdown);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setDeviceLister(DeviceLister lister) {
+        // Ignore
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void notifyDeviceStateChange(
+            String serial, DeviceAllocationState oldState, DeviceAllocationState newState) {
+        // Ignore
+    }
+
+    /** The gRPC request handler. */
+    @Override
+    public void getLabResource(
+            LabResourceRequest request, StreamObserver<LabResource> responseObserver) {
+        super.getLabResource(request, responseObserver);
+    }
+}
diff --git a/src/com/android/tradefed/postprocessor/StatsdEventMetricPostProcessor.java b/src/com/android/tradefed/postprocessor/StatsdEventMetricPostProcessor.java
index 8833ed5..4a934d8 100644
--- a/src/com/android/tradefed/postprocessor/StatsdEventMetricPostProcessor.java
+++ b/src/com/android/tradefed/postprocessor/StatsdEventMetricPostProcessor.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.ProtoUtil;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import com.google.protobuf.Descriptors.FieldDescriptor;
@@ -148,48 +149,6 @@
         return metrics;
     }
 
-    /**
-     * Get a nested field reference, i.e. field_1.field_2.field_3, from a proto message as a string.
-     * Returns an empty list when a field cannot be found, either because it's invalid or does not
-     * exist in the message.
-     *
-     * <p>If the field reference contains repeated fields, each instance is expanded, resulting in a
-     * list of strings.
-     */
-    private List<String> getNestedFieldFromMessageAsStrings(
-            Object messageOrObject, List<String> references) {
-        if (references.isEmpty()) {
-            return Arrays.asList(String.valueOf(messageOrObject));
-        }
-        if (!(messageOrObject instanceof Message)) {
-            CLog.e(
-                    "Attempting to read field %s from object of type %s, "
-                            + "which is not a proto message.",
-                    references.get(0), messageOrObject.getClass());
-            return new ArrayList<String>();
-        }
-        Message message = (Message) messageOrObject;
-        String reference = references.get(0);
-        FieldDescriptor fieldDescriptor = message.getDescriptorForType().findFieldByName(reference);
-        if (fieldDescriptor == null) {
-            CLog.e("Could not find field %s in message %s.", reference, message);
-            return new ArrayList<String>();
-        }
-        Object fieldValue = message.getField(fieldDescriptor);
-        if (fieldValue instanceof List) {
-            return ((List<? extends Object>) fieldValue)
-                    .stream()
-                    .flatMap(
-                            v ->
-                                    getNestedFieldFromMessageAsStrings(
-                                                    v, references.subList(1, references.size()))
-                                            .stream())
-                    .collect(Collectors.toList());
-        }
-        return getNestedFieldFromMessageAsStrings(
-                fieldValue, references.subList(1, references.size()));
-    }
-
     /** Fill in the placeholders in the formatter using the proto message as source. */
     private List<String> fillInPlaceholders(
             String formatter, EventMetricData eventMetric, Message atomContent) {
@@ -202,12 +161,12 @@
             List<String> actual = new ArrayList();
             if (fieldReference.startsWith("_")) {
                 actual.addAll(
-                        getNestedFieldFromMessageAsStrings(
+                        ProtoUtil.getNestedFieldFromMessageAsStrings(
                                 eventMetric,
                                 Arrays.asList(fieldReference.substring(1).split("\\."))));
             } else {
                 actual.addAll(
-                        getNestedFieldFromMessageAsStrings(
+                        ProtoUtil.getNestedFieldFromMessageAsStrings(
                                 atomContent, Arrays.asList(fieldReference.split("\\."))));
             }
             // If both the existing expansion results and newly expanded results have multiple
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index d9ec6ec..a51d2ae 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -311,6 +311,12 @@
     }
 
     @Override
+    public void testAssumptionFailure(TestDescription test, FailureDescription failure) {
+        setCountDirty();
+        mCurrentTestRunResult.testAssumptionFailure(test, failure);
+    }
+
+    @Override
     public void testIgnored(TestDescription test) {
         setCountDirty();
         mCurrentTestRunResult.testIgnored(test);
diff --git a/src/com/android/tradefed/result/JsonHttpTestResultReporter.java b/src/com/android/tradefed/result/JsonHttpTestResultReporter.java
index b89e4b0..06c9585 100644
--- a/src/com/android/tradefed/result/JsonHttpTestResultReporter.java
+++ b/src/com/android/tradefed/result/JsonHttpTestResultReporter.java
@@ -36,6 +36,7 @@
 import java.net.URL;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -207,7 +208,8 @@
 
             // Parse run metrics
             if (runResult.getRunMetrics().size() > 0) {
-                JSONObject runResultMetrics = new JSONObject(runResult.getRunMetrics());
+                JSONObject runResultMetrics = new JSONObject(
+                        getValidMetrics(runResult.getRunMetrics()));
                 String reportingUnit = runResult.getName();
                 if (mReportingUnitKeySuffix != null && !mReportingUnitKeySuffix.isEmpty()) {
                     reportingUnit += mReportingUnitKeySuffix;
@@ -233,7 +235,8 @@
                 }
                 resultsName.append(String.format("%s%s", reportingUnit, RESULT_SEPARATOR));
                 if (testResult.getMetrics().size() > 0) {
-                    JSONObject testResultMetrics = new JSONObject(testResult.getMetrics());
+                    JSONObject testResultMetrics = new JSONObject(
+                            getValidMetrics(testResult.getMetrics()));
                     allTestMetrics.put(reportingUnit, testResultMetrics);
                 }
             }
@@ -259,4 +262,23 @@
 
         return result;
     }
-}
+
+    /**
+     * Add only the numeric metrics and skip posting the non-numeric metrics.
+     *
+     * @param collectedMetrics contains all the metrics.
+     * @return only the numeric metrics.
+     */
+    public Map<String, String> getValidMetrics(Map<String, String> collectedMetrics) {
+        Map<String, String> validMetrics = new HashMap<>();
+        for (Map.Entry<String, String> entry : collectedMetrics.entrySet()) {
+            try {
+                Double.parseDouble(entry.getValue());
+                validMetrics.put(entry.getKey(), entry.getValue());
+            } catch (Exception e) {
+                // Skip adding the non numeric metric.
+            }
+        }
+        return validMetrics;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
index 8891069..a14cb8a 100644
--- a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
+++ b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
@@ -19,6 +19,8 @@
 import com.android.loganalysis.item.LogcatItem;
 import com.android.loganalysis.parser.LogcatParser;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
@@ -41,6 +43,11 @@
     /** Special error message from the instrumentation when something goes wrong on device side. */
     public static final String ERROR_MESSAGE = "Process crashed.";
     public static final String SYSTEM_CRASH_MESSAGE = "System has crashed.";
+    public static final String TIMEOUT_MESSAGES[] = {
+        "Failed to receive adb shell test output",
+        "TimeoutException when running tests",
+        "TestTimedOutException: test timed out after",
+    };
 
     public static final int MAX_NUMBER_CRASH = 3;
 
@@ -76,8 +83,16 @@
         if (trace.compareTo(failure.getErrorMessage()) != 0) {
             // Crash stack trace found, consider this a test failure.
             failure.setFailureStatus(FailureStatus.TEST_FAILURE);
+        } else if (isTimeout(failure.getErrorMessage())) {
+            failure.setFailureStatus(FailureStatus.TIMED_OUT);
         }
         failure.setErrorMessage(trace);
+        // Add metrics for assessing uncaught IntrumentationTest crash failures (test level).
+        InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.TEST_CRASH_FAILURES, 1);
+        if (FailureStatus.UNSET.equals(failure.getFailureStatus())) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.UNCAUGHT_TEST_CRASH_FAILURES, 1);
+        }
         super.testFailed(test, failure);
     }
 
@@ -109,6 +124,12 @@
         if (isCrash(errorMessage)) {
             error.setErrorIdentifier(DeviceErrorIdentifier.INSTRUMENTATION_CRASH);
         }
+        // Add metrics for assessing uncaught IntrumentationTest crash failures.
+        InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.CRASH_FAILURES, 1);
+        if (FailureStatus.UNSET.equals(error.getFailureStatus())) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.UNCAUGHT_CRASH_FAILURES, 1);
+        }
         super.testRunFailed(error);
     }
 
@@ -131,6 +152,14 @@
         return errorMessage.contains(ERROR_MESSAGE) || errorMessage.contains(SYSTEM_CRASH_MESSAGE);
     }
 
+    private boolean isTimeout(String errorMessage) {
+        for (String timeoutMessage : TIMEOUT_MESSAGES) {
+            if (errorMessage.contains(timeoutMessage)) {
+                return true;
+            }
+        }
+        return false;
+    }
     /**
      * Extract a formatted object from the logcat snippet.
      *
diff --git a/src/com/android/tradefed/result/ResultForwarder.java b/src/com/android/tradefed/result/ResultForwarder.java
index ba38e54..c651405 100644
--- a/src/com/android/tradefed/result/ResultForwarder.java
+++ b/src/com/android/tradefed/result/ResultForwarder.java
@@ -341,6 +341,20 @@
     }
 
     @Override
+    public void testAssumptionFailure(TestDescription test, FailureDescription failure) {
+        for (ITestInvocationListener listener : mListeners) {
+            try {
+                listener.testAssumptionFailure(test, failure);
+            } catch (RuntimeException e) {
+                CLog.e(
+                        "Exception while invoking %s#testAssumptionFailure",
+                        listener.getClass().getName());
+                CLog.e(e);
+            }
+        }
+    }
+
+    @Override
     public void testIgnored(TestDescription test) {
         for (ITestInvocationListener listener : mListeners) {
             try {
diff --git a/src/com/android/tradefed/result/suite/SuiteResultReporter.java b/src/com/android/tradefed/result/suite/SuiteResultReporter.java
index 424cd9d..82e4685 100644
--- a/src/com/android/tradefed/result/suite/SuiteResultReporter.java
+++ b/src/com/android/tradefed/result/suite/SuiteResultReporter.java
@@ -401,6 +401,9 @@
 
     @Override
     public TestSummary getSummary() {
+        if (mSummary == null || mSummary.toString().isEmpty()) {
+            return null;
+        }
         TestSummary summary = new TestSummary(new TypedString(mSummary.toString(), Type.TEXT));
         summary.setSource(SUITE_REPORTER_SOURCE);
         return summary;
diff --git a/src/com/android/tradefed/retry/BaseRetryDecision.java b/src/com/android/tradefed/retry/BaseRetryDecision.java
index 0228ae9..599aed8 100644
--- a/src/com/android/tradefed/retry/BaseRetryDecision.java
+++ b/src/com/android/tradefed/retry/BaseRetryDecision.java
@@ -20,14 +20,20 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
+import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.targetprep.TargetSetupError;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
 import com.android.tradefed.testtype.retry.IAutoRetriableTest;
+import com.android.tradefed.testtype.suite.ModuleDefinition;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -53,6 +59,13 @@
     private boolean mRebootAtLastRetry = false;
 
     @Option(
+            name = "reset-at-last-retry",
+            description =
+                    "Reset or powerwash the device at the last retry attempt. If this option is "
+                            + "set, option `reboot-at-last-retry` will be ignored.")
+    private boolean mResetAtLastRetry = false;
+
+    @Option(
         name = "max-testcase-run-count",
         description =
                 "If the IRemoteTest can have its testcases run multiple times, "
@@ -113,6 +126,16 @@
     public boolean shouldRetry(
             IRemoteTest test, int attemptJustExecuted, List<TestRunResult> previousResults)
             throws DeviceNotAvailableException {
+        return shouldRetry(test, null, attemptJustExecuted, previousResults);
+    }
+
+    @Override
+    public boolean shouldRetry(
+            IRemoteTest test,
+            ModuleDefinition module,
+            int attemptJustExecuted,
+            List<TestRunResult> previousResults)
+            throws DeviceNotAvailableException {
         // Keep track of some results for the test in progress for statistics purpose.
         if (test != mCurrentlyConsideredTest) {
             mCurrentlyConsideredTest = test;
@@ -143,7 +166,7 @@
             boolean shouldRetry = handleRetryFailures(filterableTest, previousResults);
             if (shouldRetry) {
                 // In case of retry, go through the recovery routine
-                recoverStateOfDevices(getDevices(), attemptJustExecuted);
+                recoverStateOfDevices(getDevices(), attemptJustExecuted, module);
             }
             return shouldRetry;
         } else if (test instanceof IAutoRetriableTest) {
@@ -295,13 +318,74 @@
     }
 
     /** Recovery attempt on the device to get it a better state before next retry. */
-    private void recoverStateOfDevices(List<ITestDevice> devices, int lastAttempt)
+    private void recoverStateOfDevices(
+            List<ITestDevice> devices, int lastAttempt, ModuleDefinition module)
             throws DeviceNotAvailableException {
+        if (lastAttempt == (mMaxRetryAttempts - 2)) {
+            if (mResetAtLastRetry) {
+                resetDevice(module, devices);
+            } else if (mRebootAtLastRetry) {
+                for (ITestDevice device : devices) {
+                    device.reboot();
+                    continue;
+                }
+            }
+        }
+    }
+
+    private void resetDevice(ModuleDefinition module, List<ITestDevice> devices)
+            throws DeviceNotAvailableException {
+        CLog.d("Reset devices...");
+        int deviceResetCount = 0;
         for (ITestDevice device : devices) {
-            if (mRebootAtLastRetry && (lastAttempt == (mMaxRetryAttempts - 2))) {
-                device.reboot();
+            if (!(device instanceof RemoteAndroidVirtualDevice)) {
+                CLog.i(
+                        "Device %s of type %s does not support powerwash.",
+                        device.getSerialNumber(), device.getClass());
                 continue;
             }
+            boolean success = false;
+            try {
+                success = ((RemoteAndroidVirtualDevice) device).powerwashGce();
+                deviceResetCount++;
+            } catch (TargetSetupError e) {
+                CLog.e(e);
+                throw new DeviceNotAvailableException(
+                        String.format(
+                                "Failed to powerwash device: %s\nError: %s",
+                                device.getSerialNumber(), e.toString()),
+                        e,
+                        device.getSerialNumber(),
+                        DeviceErrorIdentifier.DEVICE_FAILED_TO_RESET);
+            }
+
+            if (!success) {
+                throw new DeviceNotAvailableException(
+                        String.format("Failed to powerwash device: %s", device.getSerialNumber()),
+                        device.getSerialNumber(),
+                        DeviceErrorIdentifier.DEVICE_FAILED_TO_RESET);
+            }
+        }
+
+        if (module != null) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.DEVICE_RESET_MODULES, module.getId());
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.DEVICE_RESET_COUNT, deviceResetCount);
+
+            // Run all preparers including suite level ones.
+            Throwable preparationException =
+                    module.runPreparation(true /* includeSuitePreparers */);
+            if (preparationException != null) {
+                CLog.e(preparationException);
+                throw new DeviceNotAvailableException(
+                        String.format(
+                                "Failed to reset devices before retry: %s",
+                                preparationException.toString()),
+                        preparationException,
+                        devices.get(0).getSerialNumber(),
+                        DeviceErrorIdentifier.DEVICE_FAILED_TO_RESET);
+            }
         }
     }
 }
diff --git a/src/com/android/tradefed/retry/IRetryDecision.java b/src/com/android/tradefed/retry/IRetryDecision.java
index 70b3e00..bc0face 100644
--- a/src/com/android/tradefed/retry/IRetryDecision.java
+++ b/src/com/android/tradefed/retry/IRetryDecision.java
@@ -19,6 +19,7 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.suite.ModuleDefinition;
 
 import java.util.List;
 
@@ -58,6 +59,24 @@
             throws DeviceNotAvailableException;
 
     /**
+     * Decide whether or not retry should be attempted. Also make any necessary changes to the
+     * {@link IRemoteTest} to be retried (Applying filters, etc.).
+     *
+     * @param test The {@link IRemoteTest} that just ran.
+     * @param module The {@link ModuleDefinition} object for the test module.
+     * @param attemptJustExecuted The number of the attempt that we just ran.
+     * @param previousResults The list of {@link TestRunResult} of the test that just ran.
+     * @return True if we should retry, False otherwise.
+     * @throws DeviceNotAvailableException Can be thrown during device recovery
+     */
+    public boolean shouldRetry(
+            IRemoteTest test,
+            ModuleDefinition module,
+            int attemptJustExecuted,
+            List<TestRunResult> previousResults)
+            throws DeviceNotAvailableException;
+
+    /**
      * {@link #shouldRetry(IRemoteTest, int, List)} will most likely be called before the last retry
      * attempt, so we might be missing the very last attempt results for statistics purpose. This
      * method allows those results to be provided for proper statistics calculations.
diff --git a/src/com/android/tradefed/retry/ResultAggregator.java b/src/com/android/tradefed/retry/ResultAggregator.java
index 43848fd..b028bdf 100644
--- a/src/com/android/tradefed/retry/ResultAggregator.java
+++ b/src/com/android/tradefed/retry/ResultAggregator.java
@@ -257,6 +257,12 @@
     }
 
     @Override
+    public void testAssumptionFailure(TestDescription test, FailureDescription failure) {
+        super.testAssumptionFailure(test, failure);
+        mDetailedForwarder.testAssumptionFailure(test, failure);
+    }
+
+    @Override
     public void testFailed(TestDescription test, String trace) {
         super.testFailed(test, trace);
         mDetailedForwarder.testFailed(test, trace);
diff --git a/src/com/android/tradefed/sandbox/ISandbox.java b/src/com/android/tradefed/sandbox/ISandbox.java
index c78c1a9..ba0fc67 100644
--- a/src/com/android/tradefed/sandbox/ISandbox.java
+++ b/src/com/android/tradefed/sandbox/ISandbox.java
@@ -65,7 +65,7 @@
      */
     public File getTradefedSandboxEnvironment(
             IInvocationContext context, IConfiguration nonVersionedConfig, String[] args)
-            throws ConfigurationException;
+            throws Exception;
 
     /**
      * Create a classpath based on the environment and the working directory returned by {@link
diff --git a/src/com/android/tradefed/sandbox/TradefedSandbox.java b/src/com/android/tradefed/sandbox/TradefedSandbox.java
index 0659c7c..942955d 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandbox.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandbox.java
@@ -256,7 +256,7 @@
                                     config.getCommandLine(),
                                     /** no logging */
                                     false));
-        } catch (ConfigurationException e) {
+        } catch (Exception e) {
             return e;
         }
 
@@ -293,7 +293,7 @@
     @Override
     public File getTradefedSandboxEnvironment(
             IInvocationContext context, IConfiguration nonVersionedConfig, String[] args)
-            throws ConfigurationException {
+            throws Exception {
         SandboxOptions options = getSandboxOptions(nonVersionedConfig);
         // Check that we have no args conflicts.
         if (options.getSandboxTfDirectory() != null && options.getSandboxBuildId() != null) {
diff --git a/src/com/android/tradefed/sandbox/TradefedSandboxRunner.java b/src/com/android/tradefed/sandbox/TradefedSandboxRunner.java
index 69feed3..c26804b 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandboxRunner.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandboxRunner.java
@@ -125,6 +125,8 @@
             initGlobalConfig(new String[] {});
             mScheduler = getCommandScheduler();
             mScheduler.start();
+            // Wait 2 secs to let device discovery finish
+            RunUtil.getDefault().sleep(2000);
             mScheduler.execCommand(
                     context, new StubScheduledInvocationListener(), argList.toArray(new String[0]));
         } catch (NoDeviceException e) {
diff --git a/src/com/android/tradefed/targetprep/DeviceSetup.java b/src/com/android/tradefed/targetprep/DeviceSetup.java
index 2db82ba..9c8a83f 100644
--- a/src/com/android/tradefed/targetprep/DeviceSetup.java
+++ b/src/com/android/tradefed/targetprep/DeviceSetup.java
@@ -894,7 +894,7 @@
             CLog.d("Skipping connect wifi due to force-skip-run-commands");
             return;
         }
-        if (mWifiSsid == null && mWifiSsidToPsk.isEmpty()) {
+        if ((mWifiSsid == null || mWifiSsid.isEmpty()) && mWifiSsidToPsk.isEmpty()) {
             return;
         }
 
diff --git a/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java
index 1b9299b..d896f00 100644
--- a/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/GkiDeviceFlashPreparer.java
@@ -15,7 +15,7 @@
  */
 package com.android.tradefed.targetprep;
 
-import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
@@ -80,7 +80,7 @@
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         ITestDevice device = testInfo.getDevice();
-        IDeviceBuildInfo buildInfo = (IDeviceBuildInfo) testInfo.getBuildInfo();
+        IBuildInfo buildInfo = testInfo.getBuildInfo();
 
         File tmpDir = null;
         try {
@@ -138,10 +138,10 @@
      * Flash GKI images.
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @throws TargetSetupError, DeviceNotAvailableException, IOException
      */
-    private void flashGki(ITestDevice device, IDeviceBuildInfo buildInfo)
+    private void flashGki(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, DeviceNotAvailableException {
         IDeviceManager deviceManager = getDeviceManager();
         device.waitForDeviceOnline();
@@ -180,13 +180,13 @@
      * Validate GKI boot image is expected. (Obsoleted. Please call with tmpDir provided)
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @throws TargetSetupError if there is no valid gki boot.img
      */
-    public void validateGkiBootImg(ITestDevice device, IDeviceBuildInfo buildInfo)
+    public void validateGkiBootImg(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError {
         throw new TargetSetupError(
-                "Obsoleted. Please use validateGkiBootImg(ITestDevice, IDeviceBuildInfo, File)",
+                "Obsoleted. Please use validateGkiBootImg(ITestDevice, IBuildInfo, File)",
                 device.getDeviceDescriptor());
     }
 
@@ -194,12 +194,12 @@
      * Validate GKI boot image is expected. Throw exception if there is no valid boot.img.
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @param tmpDir the temporary directory {@link File}
      * @throws TargetSetupError if there is no valid gki boot.img
      */
     @VisibleForTesting
-    protected void validateGkiBootImg(ITestDevice device, IDeviceBuildInfo buildInfo, File tmpDir)
+    protected void validateGkiBootImg(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
             throws TargetSetupError {
         if (buildInfo.getFile(GKI_BOOT_IMG) != null && mBootImageFileName != null) {
             mBootImg =
@@ -306,7 +306,10 @@
                 ZipUtil2.extractZip(sourceFile, destDir);
                 requestedFile = FileUtil.findFile(destDir, requestedFileName);
             } catch (IOException e) {
-                throw new TargetSetupError(e.getMessage(), e, device.getDeviceDescriptor());
+                throw new TargetSetupError(
+                        String.format("Fail to get %s from %s", requestedFileName, sourceFile),
+                        e,
+                        device.getDeviceDescriptor());
             }
         } else if (sourceFile.isDirectory()) {
             requestedFile = FileUtil.findFile(sourceFile, requestedFileName);
@@ -349,7 +352,7 @@
             throw new TargetSetupError(
                     String.format(
                             "fastboot command %s failed in device %s. stdout: %s, stderr: %s",
-                            cmdArgs[0],
+                            cmdArgs,
                             device.getSerialNumber(),
                             result.getStdout(),
                             result.getStderr()),
diff --git a/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparer.java
index b971a4a..25bb99f 100644
--- a/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparer.java
@@ -15,7 +15,7 @@
  */
 package com.android.tradefed.targetprep;
 
-import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
@@ -49,9 +49,6 @@
 @OptionClass(alias = "gsi-device-flash-preparer")
 public class GsiDeviceFlashPreparer extends BaseTargetPreparer {
 
-    private static final String GSI_SYSTEM_IMG = "gsi_system.img";
-    private static final String GSI_VBMETA_IMG = "gsi_vbmeta.img";
-    private static final String GKI_BOOT_IMG = "gki_boot.img";
     private static final int DYNAMIC_PARTITION_API_LEVEL = 29;
     // Wait time for device state to stablize in millisecond
     private static final int STATE_STABLIZATION_WAIT_TIME_MLLISECS = 60000;
@@ -63,26 +60,40 @@
     private long mDeviceBootTime = 5 * 60 * 1000;
 
     @Option(
+            name = "system-image-zip-name",
+            description = "The name of the zip file containing the system image in BuildInfo.")
+    private String mSystemImageZipName = "gsi_system.img";
+
+    @Option(
             name = "system-image-file-name",
             description =
-                    "The system image file name to search for if provided gsi_system.img is in "
-                            + "a zip file or directory.")
+                    "The system image file name to search for if provided system image "
+                            + "is in a zip file or directory.")
     private String mSystemImageFileName = "system.img";
 
     @Option(
+            name = "vbmeta-image-zip-name",
+            description = "The name of the zip file containing the system image in BuildInfo.")
+    private String mVbmetaImageZipName = "gsi_vbmeta.img";
+
+    @Option(
             name = "vbmeta-image-file-name",
             description =
-                    "The vbmeta image file name to search for if provided gsi_vbmeta.img is in "
-                            + "a zip file or directory.")
+                    "The vbmeta image file name to search for if provided vbmeta image is "
+                            + "in a zip file or directory.")
     private String mVbmetaImageFileName = "vbmeta.img";
 
     @Option(
+            name = "boot-image-zip-name",
+            description = "The name of the zip file containing the boot image in BuildInfo.")
+    private String mBootImageZipName = "gki_boot.img";
+
+    @Option(
             name = "boot-image-file-name",
             description =
-                    "The boot image file name to search for if gki_boot.img is provided in BuildInfo and the provided "
-                            + "file is a zip file or directory, for example boot-5.4.img. By default when gki_boot.img "
-                            + "is provided in BuildInfo with a zip file or file directory, the target preparer will use"
-                            + " the first found file that matches boot(.*).img as file name.")
+                    "The boot image file name to search for if boot image is is in a zip "
+                            + "file or directory, for example boot-5.4.img. The first file"
+                            + "match the provided name string will be used.")
     private String mBootImageFileName = "boot(.*).img";
 
     @Option(
@@ -99,7 +110,7 @@
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         ITestDevice device = testInfo.getDevice();
-        IDeviceBuildInfo buildInfo = (IDeviceBuildInfo) testInfo.getBuildInfo();
+        IBuildInfo buildInfo = testInfo.getBuildInfo();
 
         File tmpDir = null;
         try {
@@ -157,10 +168,10 @@
      * Flash GSI images.
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @throws TargetSetupError, DeviceNotAvailableException, IOException
      */
-    private void flashGsi(ITestDevice device, IDeviceBuildInfo buildInfo)
+    private void flashGsi(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, DeviceNotAvailableException {
         IDeviceManager deviceManager = getDeviceManager();
         device.waitForDeviceOnline();
@@ -198,8 +209,8 @@
                 if (shouldUseFastbootd) {
                     device.rebootIntoFastbootd();
                     if (mShouldEraseProductPartition) {
-                        executeFastbootCmd(
-                                device, "delete-logical-partition", "product" + currSlot);
+                        device.executeLongFastbootCommand(
+                                "delete-logical-partition", "product" + currSlot);
                     }
                 }
                 executeFastbootCmd(device, "erase", "system" + currSlot);
@@ -224,33 +235,38 @@
      * Validate GSI image is expected. Throw exception if there is no valid GSI image.
      *
      * @param device the {@link ITestDevice}
-     * @param buildInfo the {@link IDeviceBuildInfo} the device build info
+     * @param buildInfo the {@link IBuildInfo} the build info
      * @param tmpDir the temporary directory {@link File}
      * @throws TargetSetupError if there is no valid gki boot.img
      */
-    private void validateGsiImg(ITestDevice device, IDeviceBuildInfo buildInfo, File tmpDir)
+    private void validateGsiImg(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
             throws TargetSetupError {
-        if (buildInfo.getFile(GSI_SYSTEM_IMG) == null) {
+        if (buildInfo.getFile(mSystemImageZipName) == null) {
             throw new TargetSetupError(
-                    String.format("BuildInfo doesn't contain file key %s.", GSI_SYSTEM_IMG),
-                    device.getDeviceDescriptor());
-        }
-        if (buildInfo.getFile(GSI_VBMETA_IMG) == null) {
-            throw new TargetSetupError(
-                    String.format("BuildInfo doesn't contain file key %s.", GSI_VBMETA_IMG),
+                    String.format("BuildInfo doesn't contain file key %s.", mSystemImageZipName),
                     device.getDeviceDescriptor());
         }
         mSystemImg =
                 getRequestedFile(
-                        device, mSystemImageFileName, buildInfo.getFile(GSI_SYSTEM_IMG), tmpDir);
-        mVbmetaImg =
-                getRequestedFile(
-                        device, mVbmetaImageFileName, buildInfo.getFile(GSI_VBMETA_IMG), tmpDir);
-
-        if (buildInfo.getFile(GKI_BOOT_IMG) != null && mBootImageFileName != null) {
+                        device,
+                        mSystemImageFileName,
+                        buildInfo.getFile(mSystemImageZipName),
+                        tmpDir);
+        if (buildInfo.getFile(mVbmetaImageZipName) != null) {
+            mVbmetaImg =
+                    getRequestedFile(
+                            device,
+                            mVbmetaImageFileName,
+                            buildInfo.getFile(mVbmetaImageZipName),
+                            tmpDir);
+        }
+        if (buildInfo.getFile(mBootImageZipName) != null && mBootImageFileName != null) {
             mBootImg =
                     getRequestedFile(
-                            device, mBootImageFileName, buildInfo.getFile(GKI_BOOT_IMG), tmpDir);
+                            device,
+                            mBootImageFileName,
+                            buildInfo.getFile(mBootImageZipName),
+                            tmpDir);
         }
     }
 
@@ -300,7 +316,10 @@
                 ZipUtil2.extractZip(sourceFile, destDir);
                 requestedFile = FileUtil.findFile(destDir, requestedFileName);
             } catch (IOException e) {
-                throw new TargetSetupError(e.getMessage(), e, device.getDeviceDescriptor());
+                throw new TargetSetupError(
+                        String.format("Fail to get %s from %s", requestedFileName, sourceFile),
+                        e,
+                        device.getDeviceDescriptor());
             }
         } else if (sourceFile.isDirectory()) {
             requestedFile = FileUtil.findFile(sourceFile, requestedFileName);
@@ -343,7 +362,7 @@
             throw new TargetSetupError(
                     String.format(
                             "fastboot command %s failed in device %s. stdout: %s, stderr: %s",
-                            cmdArgs[0],
+                            cmdArgs,
                             device.getSerialNumber(),
                             result.getStdout(),
                             result.getStderr()),
diff --git a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
index 3c81407..936df0f 100644
--- a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
@@ -65,15 +65,20 @@
     private static final int R_SDK_INT = 30;
 
     private List<ApexInfo> mTestApexInfoList = new ArrayList<>();
+    private List<ApexInfo> mModulesToUninstall = new ArrayList<>();
     private Set<String> mApkToInstall = new LinkedHashSet<>();
     private List<String> mApkInstalled = new ArrayList<>();
     private List<String> mSplitsInstallArgs = new ArrayList<>();
     private BundletoolUtil mBundletoolUtil;
     private String mDeviceSpecFilePath = "";
+    private boolean mOptimizeMainlineTest = false;
 
     @Option(name = "bundletool-file-name", description = "The file name of the bundletool jar.")
     private String mBundletoolFilename;
 
+    @Option(name = "train-path", description = "The absoulte path of the train folder.")
+    private File mTrainFolderPath;
+
     @Option(
         name = "apex-staging-wait-time",
         description = "The time in ms to wait for apex staged session ready.",
@@ -88,18 +93,36 @@
                             + "preloaded on device. Otherwise an exception will be thrown.")
     private boolean mIgnoreIfNotPreloaded = false;
 
+    @Option(
+            name = "skip-apex-teardown",
+            description = "Skip teardown if all files to be installed are apex files. "
+                    + "Currently, this option is only used for Test Mapping use case.")
+    private boolean mSkipApexTearDown = false;
+
     @Override
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
         setTestInformation(testInfo);
         ITestDevice device = testInfo.getDevice();
 
-        if (getTestsFileName().isEmpty()) {
+        if (mTrainFolderPath != null) {
+            addApksToTestFiles();
+        }
+
+        List<File> moduleFileNames = getTestsFileName();
+        if (moduleFileNames.isEmpty()) {
             CLog.i("No apk/apex module file to install. Skipping.");
             return;
         }
 
-        cleanUpStagedAndActiveSession(device);
+        if (!mSkipApexTearDown || hasApkFilesToInstall(moduleFileNames)) {
+            // Cleanup the device if skip-apex-teardown isn't set or not all files to be installed
+            // are apex files. It will always run with the target preparer.
+            cleanUpStagedAndActiveSession(device);
+        }
+        else {
+            mOptimizeMainlineTest = true;
+        }
 
         Set<ApexInfo> activatedApexes = device.getActiveApexes();
 
@@ -113,9 +136,36 @@
             CLog.i("No modules are preloaded on the device, so no modules will be installed.");
             return;
         }
+
+        if (mOptimizeMainlineTest) {
+            CLog.i("Optimizing install apex module target preparer.");
+            // Get the apex files that are already installed on the device.
+            Set<ApexInfo> apexInData = getApexInData(activatedApexes);
+
+            // Get the apex files that are not used by the current test and will be uninstalled.
+            mModulesToUninstall.addAll(
+                    getModulesToUninstall(apexInData, testAppFiles, device));
+
+            for (ApexInfo m : mModulesToUninstall) {
+                CLog.i("Uninstalling module: %s", m.name);
+                super.uninstallPackage(device, m.name);
+            }
+
+            if (testAppFiles.isEmpty()) {
+                if (!mModulesToUninstall.isEmpty()) {
+                    RunUtil.getDefault().sleep(mApexStagingWaitTime);
+                    device.reboot();
+                }
+                // If both the list of files to be installed and uninstalled are empty, that means
+                // the mainline modules are the same as the previous ones.
+                CLog.i("All required modules are installed");
+                return;
+            }
+        }
+
         if (containsApks(testAppFiles)) {
             installUsingBundleTool(testInfo, testAppFiles);
-            if (mTestApexInfoList.isEmpty()) {
+            if (mTestApexInfoList.isEmpty() && mModulesToUninstall.isEmpty()) {
                 CLog.i("No Apex module in the train. Skipping reboot.");
                 return;
             } else {
@@ -164,8 +214,69 @@
         CLog.i("Train activation succeed.");
     }
 
+    /**
+     * Get a set of modules that will be uninstalled.
+     *
+     * @param apexInData A Set<ApexInfo> of modules that are installed on the /data directory.
+     * @param testFiles A List<File> of modules that will be installed on the device.
+     * @param device the {@link ITestDevice}
+     * @return A Set<ApexInfo> of modules that will be uninstalled on the device.
+     */
+    @VisibleForTesting
+    Set<ApexInfo> getModulesToUninstall(Set<ApexInfo> apexInData,
+            List<File> testFiles, ITestDevice device) throws TargetSetupError {
+        Set<ApexInfo> unInstallModules = new HashSet<>(apexInData);
+        List<File> filesToSkipInstall = new ArrayList<>();
+        for (File testFile : testFiles) {
+            String packageName = parsePackageName(testFile, device.getDeviceDescriptor());
+            for (ApexInfo apexModule : apexInData) {
+                if (apexModule.name.equals(packageName)) {
+                    unInstallModules.remove(apexModule);
+                    filesToSkipInstall.add(testFile);
+                }
+            }
+        }
+        // Update the modules to be installed based on what will not be installed.
+        testFiles.removeAll(filesToSkipInstall);
+        return unInstallModules;
+    }
+
+    /**
+     * Return a set of files that is already installed on the /data directory.
+     */
+    @VisibleForTesting
+    Set<ApexInfo> getApexInData(Set<ApexInfo> activatedApexes) {
+        Set<ApexInfo> apexInData = new HashSet<>();
+        for (ApexInfo apex : activatedApexes) {
+            if (apex.sourceDir.startsWith(ACTIVATED_APEX_SOURCEDIR_PREFIX, 1)) {
+                apexInData.add(apex);
+            }
+        }
+        return apexInData;
+    }
+
+    /**
+     * Check if the files to be installed contain .apk or .apks.
+     *
+     * @param testAppFiles List<File> of the modules that will be installed on the device.
+     * @return true if the files contain .apk or .apks, otherwise false.
+     */
+    private boolean hasApkFilesToInstall(List<File> testAppFiles) {
+        List<String> checkLists = Arrays.asList(".apk", ".apks");
+        for (File testAppFile : testAppFiles) {
+            if (checkLists.stream().anyMatch(entry -> testAppFile.getName().endsWith(entry))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @Override
     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        if (mOptimizeMainlineTest) {
+            CLog.d("Skipping tearDown since the installed modules may be used for the next test.");
+            return;
+        }
         ITestDevice device = testInfo.getDevice();
         if (e instanceof DeviceNotAvailableException) {
             CLog.e("Device %s is not available. Teardown() skipped.", device.getSerialNumber());
@@ -194,7 +305,13 @@
         if (mBundletoolUtil != null) {
             return;
         }
-        File bundletoolJar = getLocalPathForFilename(testInfo, getBundletoolFileName());
+        File bundletoolJar;
+        File f = new File(getBundletoolFileName());
+        if (!f.isAbsolute()) {
+            bundletoolJar = getLocalPathForFilename(testInfo, getBundletoolFileName());
+        } else {
+            bundletoolJar = f;
+        }
         if (bundletoolJar == null) {
             throw new TargetSetupError(
                     String.format("Failed to find bundletool jar %s.", getBundletoolFileName()),
@@ -474,7 +591,12 @@
             throws TargetSetupError, DeviceNotAvailableException {
         ITestDevice device = testInfo.getDevice();
         for (File moduleFileName : testAppFileNames) {
-            File moduleFile = getLocalPathForFilename(testInfo, moduleFileName.getName());
+            File moduleFile;
+            if (!moduleFileName.isAbsolute()) {
+                moduleFile = getLocalPathForFilename(testInfo, moduleFileName.getName());
+            } else {
+                moduleFile = moduleFileName;
+            }
             if (moduleFileName.getName().endsWith(SPLIT_APKS_SUFFIX)) {
                 List<File> splits = getSplitsForApks(testInfo, moduleFile);
                 String splitsArgs = createInstallArgsForSplit(splits, device);
@@ -731,6 +853,16 @@
         return failToActivateApex;
     }
 
+    private void addApksToTestFiles() {
+        File[] filesUnderTrainFolder = mTrainFolderPath.listFiles();
+        Arrays.sort(filesUnderTrainFolder, (a, b) -> a.getName().compareTo(b.getName()));
+        for (File f : filesUnderTrainFolder) {
+            if (f.getName().endsWith(".apks")) {
+                getTestsFileName().add(f);
+            }
+        }
+    }
+
     @VisibleForTesting
     protected String getBundletoolFileName() {
         return mBundletoolFilename;
@@ -745,4 +877,9 @@
     protected List<String> getApkInstalled() {
         return mApkInstalled;
     }
+
+    @VisibleForTesting
+    public void setSkipApexTearDown(boolean skip) {
+        mSkipApexTearDown = skip;
+    }
 }
diff --git a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
index 007c59a..a33a532 100644
--- a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
@@ -33,6 +33,7 @@
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IAbiReceiver;
 import com.android.tradefed.util.AaptParser;
+import com.android.tradefed.util.AaptParser.AaptVersion;
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.BuildTestsZipUtils;
 
@@ -129,6 +130,14 @@
                             + "preparer does not verify if the apks are successfully removed.")
     private boolean mCleanup = true;
 
+    @VisibleForTesting static final String CHECK_MIN_SDK_OPTION = "check-min-sdk";
+
+    @Option(
+            name = CHECK_MIN_SDK_OPTION,
+            description =
+                    "check app's min sdk prior to install and skip if device api level is too low.")
+    private boolean mCheckMinSdk = false;
+
     /** @deprecated use test-file-name instead now that it is a File. */
     @Deprecated
     @Option(
@@ -152,6 +161,9 @@
     @Option(name = "instant-mode", description = "Whether or not to install apk in instant mode.")
     private boolean mInstantMode = false;
 
+    @Option(name = "aapt-version", description = "The version of AAPT for APK parsing.")
+    private AaptVersion mAaptVersion = AaptVersion.AAPT;
+
     @Option(
         name = "force-install-mode",
         description =
@@ -163,7 +175,7 @@
     private Integer mUserId = null;
     private Boolean mGrantPermission = null;
 
-    private Set<String> mPackagesInstalled = null;
+    private Set<String> mPackagesInstalled = new HashSet<>();
     private TestInformation mTestInfo;
 
     protected void setTestInformation(TestInformation testInfo) {
@@ -180,6 +192,12 @@
         addTestFile(new File(fileName));
     }
 
+    /** Helper to parse an apk file with aapt. */
+    @VisibleForTesting
+    AaptParser doAaptParse(File apkFile) {
+        return AaptParser.parse(apkFile);
+    }
+
     @VisibleForTesting
     void clearTestFile() {
         mTestFiles.clear();
@@ -221,6 +239,11 @@
         mGrantPermission = shouldGrant;
     }
 
+    /** Sets the version of AAPT for APK parsing. */
+    public void setAaptVersion(AaptVersion aaptVersion) {
+        mAaptVersion = aaptVersion;
+    }
+
     /** Adds one apk installation arg to be used. */
     public void addInstallArg(String arg) {
         mInstallArgs.add(arg);
@@ -277,10 +300,6 @@
             CLog.i("No test apps to install, skipping");
             return;
         }
-        if (mCleanup) {
-            mPackagesInstalled = new HashSet<>();
-        }
-
         // resolve abi flags
         if (mAbi != null && mForceAbi != null) {
             throw new IllegalStateException("cannot specify both abi flags: --abi and --force-abi");
@@ -291,7 +310,6 @@
         } else if (mForceAbi != null) {
             abiName = AbiFormatter.getDefaultAbi(getDevice(), mForceAbi);
         }
-
         // Set all the extra install args outside the loop to avoid adding them several times.
         if (abiName != null && testInfo.getDevice().getApiLevel() > 20) {
             mInstallArgs.add(String.format("--abi %s", abiName));
@@ -370,7 +388,7 @@
     @Override
     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
         mTestInfo = testInfo;
-        if (mCleanup && mPackagesInstalled != null && !(e instanceof DeviceNotAvailableException)) {
+        if (mCleanup && !(e instanceof DeviceNotAvailableException)) {
             for (String packageName : mPackagesInstalled) {
                 try {
                     uninstallPackage(getDevice(), packageName);
@@ -454,8 +472,9 @@
     }
 
     /** Helper to resolve some apk to their File and Package. */
+    @VisibleForTesting
     protected Map<File, String> resolveApkFiles(TestInformation testInfo, List<File> apkFiles)
-            throws TargetSetupError {
+            throws TargetSetupError, DeviceNotAvailableException {
         Map<File, String> appFiles = new LinkedHashMap<>();
         ITestDevice device = testInfo.getDevice();
         for (File apkFile : apkFiles) {
@@ -488,7 +507,32 @@
                 }
             }
 
-            appFiles.put(testAppFile, parsePackageName(testAppFile, device.getDeviceDescriptor()));
+            if (mCheckMinSdk) {
+                AaptParser aaptParser = doAaptParse(testAppFile);
+                if (aaptParser == null) {
+                    throw new TargetSetupError(
+                            String.format(
+                                    "Failed to extract info from `%s` using aapt",
+                                    testAppFile.getAbsoluteFile().getName()),
+                            device.getDeviceDescriptor());
+                }
+                if (device.getApiLevel() < aaptParser.getSdkVersion()) {
+                    CLog.w(
+                            "Skipping installing apk %s on device %s because "
+                                    + "SDK level require is %d, but device SDK level is %d",
+                            apkFile.toString(),
+                            device.getSerialNumber(),
+                            aaptParser.getSdkVersion(),
+                            device.getApiLevel());
+                } else {
+                    appFiles.put(
+                            testAppFile,
+                            parsePackageName(testAppFile, device.getDeviceDescriptor()));
+                }
+            } else {
+                appFiles.put(
+                        testAppFile, parsePackageName(testAppFile, device.getDeviceDescriptor()));
+            }
         }
         return appFiles;
     }
@@ -517,14 +561,16 @@
                     String.format(
                             "Could not list files of specified directory: %s", fileOrDirectory),
                     e,
-                    deviceDescriptor);
+                    deviceDescriptor,
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
 
         if (mThrowIfNoFile && apkFiles.isEmpty()) {
             throw new TargetSetupError(
                     String.format(
                             "Could not find any files in specified directory: %s", fileOrDirectory),
-                    deviceDescriptor);
+                    deviceDescriptor,
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
         }
 
         return apkFiles;
@@ -588,7 +634,7 @@
     /** Get the package name from the test app. */
     protected String parsePackageName(File testAppFile, DeviceDescriptor deviceDescriptor)
             throws TargetSetupError {
-        AaptParser parser = AaptParser.parse(testAppFile);
+        AaptParser parser = AaptParser.parse(testAppFile, mAaptVersion);
         if (parser == null) {
             throw new TargetSetupError(
                     "apk installed but AaptParser failed",
@@ -598,4 +644,3 @@
         return parser.getPackageName();
     }
 }
-
diff --git a/src/com/android/tradefed/testtype/SubprocessTfLauncher.java b/src/com/android/tradefed/testtype/SubprocessTfLauncher.java
index bbb8ea0..814ecf5 100644
--- a/src/com/android/tradefed/testtype/SubprocessTfLauncher.java
+++ b/src/com/android/tradefed/testtype/SubprocessTfLauncher.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -31,6 +32,7 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.result.proto.StreamProtoReceiver;
 import com.android.tradefed.result.proto.StreamProtoResultReporter;
 import com.android.tradefed.util.CommandResult;
@@ -409,11 +411,12 @@
                 if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
                     errMessage = String.format("Timeout after %s",
                             TimeUtil.formatElapsedTime(mMaxTfRunTime));
-                    throw new RuntimeException(
+                    throw new HarnessRuntimeException(
                             String.format(
                                     "%s Tests subprocess failed due to:\n%s\n",
-                                    mConfigName, errMessage));
-                } else {
+                                    mConfigName, errMessage),
+                            InfraErrorIdentifier.INVOCATION_TIMEOUT);
+                } else if (eventParser != null && !eventParser.reportedInvocationFailed()) {
                     SubprocessExceptionParser.handleStderrException(result);
                 }
             }
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index 5c6348d..9ded188 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -284,6 +284,8 @@
             if (mEnableMainlineParameter) {
                 mModuleRepo.setMainlineParameterizedModules(mEnableMainlineParameter);
                 mModuleRepo.setInvocationContext(getInvocationContext());
+                mModuleRepo.setOptimizeMainlineTest(
+                        getConfiguration().getCommandOptions().getOptimizeMainlineTest());
             }
 
             mModuleRepo.setParameterizedModules(mEnableParameter);
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index a9434ed..5400ddd 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -73,6 +73,7 @@
 
     private IRetryDecision mRetryDecision;
     private IRemoteTest mTest;
+    private ModuleDefinition mModule;
     private List<IMetricCollector> mRunMetricCollectors;
     private TestFailureListener mFailureListener;
     private IInvocationContext mModuleInvocationContext;
@@ -95,7 +96,18 @@
             TestFailureListener failureListener,
             List<ITestInvocationListener> moduleLevelListeners,
             int maxRunLimit) {
+        this(test, null, mainListener, failureListener, moduleLevelListeners, maxRunLimit);
+    }
+
+    public GranularRetriableTestWrapper(
+            IRemoteTest test,
+            ModuleDefinition module,
+            ITestInvocationListener mainListener,
+            TestFailureListener failureListener,
+            List<ITestInvocationListener> moduleLevelListeners,
+            int maxRunLimit) {
         mTest = test;
+        mModule = module;
         mMainGranularRunListener = new ModuleListener(mainListener);
         mFailureListener = failureListener;
         mModuleLevelListeners = moduleLevelListeners;
@@ -230,7 +242,7 @@
 
         // Bail out early if there is no need to retry at all.
         if (!mRetryDecision.shouldRetry(
-                mTest, 0, mMainGranularRunListener.getTestRunForAttempts(0))) {
+                mTest, mModule, 0, mMainGranularRunListener.getTestRunForAttempts(0))) {
             return;
         }
         // Avoid rechecking the shouldRetry below the first time as it could retrigger reboot.
@@ -245,6 +257,7 @@
                     boolean retry =
                             mRetryDecision.shouldRetry(
                                     mTest,
+                                    mModule,
                                     attemptNumber - 1,
                                     mMainGranularRunListener.getTestRunForAttempts(
                                             attemptNumber - 1));
diff --git a/src/com/android/tradefed/testtype/suite/ITestSuite.java b/src/com/android/tradefed/testtype/suite/ITestSuite.java
index 3561868..3183e9c 100644
--- a/src/com/android/tradefed/testtype/suite/ITestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/ITestSuite.java
@@ -69,13 +69,17 @@
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.StreamUtil;
 import com.android.tradefed.util.TimeUtil;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -131,6 +135,9 @@
 
     private static final String PRODUCT_CPU_ABI_KEY = "ro.product.cpu.abi";
 
+    private static final Set<String> ALLOWED_PREPARERS_CONFIGS =
+            ImmutableSet.of("/suite/allowed-preparers.txt", "/suite/google-allowed-preparers.txt");
+
     // Options for test failure case
     @Option(
         name = "bugreport-on-failure",
@@ -509,6 +516,9 @@
             return runModules;
         }
 
+        Map<String, List<ITargetPreparer>> suitePreparersPerDevice =
+                getAllowedPreparerPerDevice(mMainConfiguration);
+
         for (Entry<String, IConfiguration> config : runConfig.entrySet()) {
             // Validate the configuration, it will throw if not valid.
             ValidateSuiteConfigHelper.validateConfig(config.getValue());
@@ -519,6 +529,7 @@
                             config.getKey(),
                             config.getValue().getTests(),
                             preparersPerDevice,
+                            suitePreparersPerDevice,
                             config.getValue().getMultiTargetPreparers(),
                             config.getValue());
             if (mDisableAutoRetryTimeReporting) {
@@ -585,6 +596,40 @@
         return res;
     }
 
+    /** Create the mapping of device to its target_preparer that's allowed to rerun. */
+    private Map<String, List<ITargetPreparer>> getAllowedPreparerPerDevice(IConfiguration config) {
+        // For unittests, mMainConfiguration might not have been set.
+        if (config == null) {
+            return new LinkedHashMap<String, List<ITargetPreparer>>();
+        }
+        // Read the list of allowed suite level target preparers from resource files.
+        Set<String> allowedSuitePreparers = new HashSet<>();
+        for (String resource : ALLOWED_PREPARERS_CONFIGS) {
+            try (InputStream resStream = ITestSuite.class.getResourceAsStream(resource)) {
+                if (resStream == null) {
+                    CLog.d("Resource not found for allowed preparers: %s", resource);
+                    continue;
+                }
+                List<String> preparers =
+                        Arrays.asList(StreamUtil.getStringFromStream(resStream).split("\n"));
+                allowedSuitePreparers.addAll(preparers);
+            } catch (IOException e) {
+                CLog.e(e);
+            }
+        }
+
+        Map<String, List<ITargetPreparer>> res = new LinkedHashMap<>();
+        for (IDeviceConfiguration holder : config.getDeviceConfig()) {
+            List<ITargetPreparer> preparers = new ArrayList<>();
+            for (ITargetPreparer preparer : holder.getTargetPreparers()) {
+                if (allowedSuitePreparers.contains(preparer.getClass().getCanonicalName()))
+                    preparers.add(preparer);
+            }
+            res.put(holder.getDeviceName(), preparers);
+        }
+        return res;
+    }
+
     /**
      * Opportunity to clean up all the things that were needed during the suites setup but are not
      * required to run the tests.
@@ -948,6 +993,7 @@
                     ModuleSplitter.splitConfiguration(
                             testInfo,
                             runConfig,
+                            getAllowedPreparerPerDevice(mMainConfiguration),
                             shardCountHint,
                             mShouldMakeDynamicModule,
                             mIntraModuleSharding);
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index 0d29944..2157877 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -59,6 +59,7 @@
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.result.error.ErrorIdentifier;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.result.error.TestErrorIdentifier;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.retry.IRetryDecision;
 import com.android.tradefed.retry.RetryStatistics;
@@ -125,10 +126,13 @@
     private IConfiguration mInternalTestConfiguration;
     private IConfiguration mInternalTargetPreparerConfiguration;
     private ILogSaver mLogSaver;
+    private TestInformation mModuleInfo;
+    private ITestInvocationListener mInvocationListener;
 
     private final String mId;
     private Collection<IRemoteTest> mTests = null;
     private Map<String, List<ITargetPreparer>> mPreparersPerDevice = null;
+    private Map<String, List<ITargetPreparer>> mSuitePreparersPerDevice = null;
 
     private List<IMultiTargetPreparer> mMultiPreparers = new ArrayList<>();
     private IBuildInfo mBuild;
@@ -174,6 +178,24 @@
             Map<String, List<ITargetPreparer>> preparersPerDevice,
             List<IMultiTargetPreparer> multiPreparers,
             IConfiguration moduleConfig) {
+        this(name, tests, preparersPerDevice, null, multiPreparers, moduleConfig);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param name unique name of the test configuration.
+     * @param tests list of {@link IRemoteTest} that needs to run.
+     * @param preparersPerDevice list of {@link ITargetPreparer} to be used to setup the device.
+     * @param moduleConfig the {@link IConfiguration} of the underlying module config.
+     */
+    public ModuleDefinition(
+            String name,
+            Collection<IRemoteTest> tests,
+            Map<String, List<ITargetPreparer>> preparersPerDevice,
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice,
+            List<IMultiTargetPreparer> multiPreparers,
+            IConfiguration moduleConfig) {
         mId = name;
         mTests = tests;
         mModuleConfiguration = moduleConfig;
@@ -204,6 +226,7 @@
 
         mMultiPreparers.addAll(multiPreparers);
         mPreparersPerDevice = preparersPerDevice;
+        mSuitePreparersPerDevice = suitePreparersPerDevice;
 
         // Get the tokens of the module
         List<String> tokens = configDescriptor.getMetaData(ITestSuite.TOKEN_KEY);
@@ -343,6 +366,9 @@
             TestFailureListener failureListener,
             int maxRunLimit)
             throws DeviceNotAvailableException {
+        mModuleInfo = moduleInfo;
+        mInvocationListener = listener;
+
         mStartModuleRunDate = System.currentTimeMillis();
         // Load extra configuration for the module from module_controller
         // TODO: make module_controller a full TF object
@@ -390,22 +416,10 @@
                             moduleInfo.getDevice(), mInternalTargetPreparerConfiguration);
         }
         // Setup
-        long prepStartTime = getCurrentTime();
         if (preparationException == null) {
-            preparationException = runTargetPreparation(moduleInfo, listener);
+            preparationException = runPreparation(false);
         }
-        // Skip multi-preparation if preparation already failed.
-        if (preparationException == null) {
-            for (IMultiTargetPreparer multiPreparer : mMultiPreparers) {
-                preparationException = runMultiPreparerSetup(multiPreparer, moduleInfo, listener);
-                if (preparationException != null) {
-                    mIsFailedModule = true;
-                    CLog.e("Some preparation step failed. failing the module %s", getId());
-                    break;
-                }
-            }
-        }
-        mElapsedPreparation = getCurrentTime() - prepStartTime;
+
         // Run the tests
         try {
             if (preparationException != null) {
@@ -604,7 +618,7 @@
             int maxRunLimit) {
         GranularRetriableTestWrapper retriableTest =
                 new GranularRetriableTestWrapper(
-                        test, listener, failureListener, moduleLevelListeners, maxRunLimit);
+                        test, this, listener, failureListener, moduleLevelListeners, maxRunLimit);
         retriableTest.setModuleId(getId());
         retriableTest.setMarkTestsSkipped(skipTestCases);
         retriableTest.setMetricCollectors(mRunMetricCollectors);
@@ -781,11 +795,41 @@
         }
     }
 
+    /**
+     * Run preparers of the test, including suite level preparers if specified.
+     *
+     * @param includeSuitePreparers Set to {@code true} to also run suite level preparers.
+     * @return {@link Throwable} of any exception raised when running preparers.
+     */
+    public Throwable runPreparation(boolean includeSuitePreparers) {
+        Throwable preparationException = null;
+        long prepStartTime = getCurrentTime();
+        if (includeSuitePreparers) {
+            // Run suite level preparers.
+            preparationException = runTargetPreparation(mSuitePreparersPerDevice);
+        }
+
+        if (preparationException == null) {
+            preparationException = runTargetPreparation(mPreparersPerDevice);
+        }
+        // Skip multi-preparation if preparation already failed.
+        if (preparationException == null) {
+            for (IMultiTargetPreparer multiPreparer : mMultiPreparers) {
+                preparationException = runMultiPreparerSetup(multiPreparer);
+                if (preparationException != null) {
+                    mIsFailedModule = true;
+                    CLog.e("Some preparation step failed. failing the module %s", getId());
+                    break;
+                }
+            }
+        }
+        mElapsedPreparation = getCurrentTime() - prepStartTime;
+        return preparationException;
+    }
+
     /** Run all the prepare steps. */
     private Throwable runPreparerSetup(
-            TestInformation moduleInfo,
             ITargetPreparer preparer,
-            ITestLogger logger,
             int deviceIndex) {
         if (preparer.isDisabled()) {
             // If disabled skip completely.
@@ -796,14 +840,14 @@
         try {
             // set the logger in case they need it.
             if (preparer instanceof ITestLoggerReceiver) {
-                ((ITestLoggerReceiver) preparer).setTestLogger(logger);
+                ((ITestLoggerReceiver) preparer).setTestLogger(mInvocationListener);
             }
             if (preparer instanceof IInvocationContextReceiver) {
                 ((IInvocationContextReceiver) preparer)
                         .setInvocationContext(mModuleInvocationContext);
             }
-            moduleInfo.setActiveDeviceIndex(deviceIndex);
-            preparer.setUp(moduleInfo);
+            mModuleInfo.setActiveDeviceIndex(deviceIndex);
+            preparer.setUp(mModuleInfo);
             return null;
         } catch (BuildError
                 | TargetSetupError
@@ -817,13 +861,12 @@
             CLog.e(e);
             return e;
         } finally {
-            moduleInfo.setActiveDeviceIndex(0);
+            mModuleInfo.setActiveDeviceIndex(0);
         }
     }
 
     /** Run all multi target preparer step. */
-    private Throwable runMultiPreparerSetup(
-            IMultiTargetPreparer preparer, TestInformation moduleInfo, ITestLogger logger) {
+    private Throwable runMultiPreparerSetup(IMultiTargetPreparer preparer) {
         if (preparer.isDisabled()) {
             // If disabled skip completely.
             return null;
@@ -833,13 +876,13 @@
         try {
             // set the logger in case they need it.
             if (preparer instanceof ITestLoggerReceiver) {
-                ((ITestLoggerReceiver) preparer).setTestLogger(logger);
+                ((ITestLoggerReceiver) preparer).setTestLogger(mInvocationListener);
             }
             if (preparer instanceof IInvocationContextReceiver) {
                 ((IInvocationContextReceiver) preparer)
                         .setInvocationContext(mModuleInvocationContext);
             }
-            preparer.setUp(moduleInfo);
+            preparer.setUp(mModuleInfo);
             return null;
         } catch (BuildError
                 | TargetSetupError
@@ -991,6 +1034,14 @@
     }
 
     /**
+     * Returns the list of suite level {@link ITargetPreparer} associated with the given device name
+     */
+    @VisibleForTesting
+    List<ITargetPreparer> getSuitePreparerForDevice(String deviceName) {
+        return mSuitePreparersPerDevice.get(deviceName);
+    }
+
+    /**
      * When running unit tests for ModuleDefinition we don't want to unnecessarily report some auto
      * retry times.
      */
@@ -1011,7 +1062,9 @@
         }
         listener.testRunStarted(getId(), 0, 0, System.currentTimeMillis());
         FailureDescription description =
-                FailureDescription.create(message).setFailureStatus(FailureStatus.NOT_EXECUTED);
+                FailureDescription.create(message)
+                        .setFailureStatus(FailureStatus.NOT_EXECUTED)
+                        .setErrorIdentifier(TestErrorIdentifier.MODULE_DID_NOT_EXECUTE);
         listener.testRunFailed(description);
         listener.testRunEnded(0, new HashMap<String, Metric>());
         listener.testModuleEnded();
@@ -1059,28 +1112,28 @@
                 InvocationMetricKey.AUTO_RETRY_TIME, retryTimeMs);
     }
 
-    private Throwable runTargetPreparation(TestInformation moduleInfo, ITestLogger logger) {
+    private Throwable runTargetPreparation(Map<String, List<ITargetPreparer>> preparersPerDevice) {
         Throwable preparationException = null;
         for (int i = 0; i < mModuleInvocationContext.getDeviceConfigNames().size(); i++) {
             String deviceName = mModuleInvocationContext.getDeviceConfigNames().get(i);
-            if (i >= mPreparersPerDevice.size()) {
+            if (i >= preparersPerDevice.size()) {
                 CLog.d(
                         "Main configuration has more devices than the module configuration. '%s' "
                                 + "will not run any preparation.",
                         deviceName);
                 continue;
             }
-            List<ITargetPreparer> preparers = mPreparersPerDevice.get(deviceName);
+            List<ITargetPreparer> preparers = preparersPerDevice.get(deviceName);
             if (preparers == null) {
                 CLog.w(
                         "Module configuration devices mismatch the main configuration "
                                 + "(Missing device '%s'), resolving preparers by index.",
                         deviceName);
-                String key = new ArrayList<>(mPreparersPerDevice.keySet()).get(i);
-                preparers = mPreparersPerDevice.get(key);
+                String key = new ArrayList<>(preparersPerDevice.keySet()).get(i);
+                preparers = preparersPerDevice.get(key);
             }
             for (ITargetPreparer preparer : preparers) {
-                preparationException = runPreparerSetup(moduleInfo, preparer, logger, i);
+                preparationException = runPreparerSetup(preparer, i);
                 if (preparationException != null) {
                     mIsFailedModule = true;
                     CLog.e("Some preparation step failed. failing the module %s", getId());
diff --git a/src/com/android/tradefed/testtype/suite/ModuleSplitter.java b/src/com/android/tradefed/testtype/suite/ModuleSplitter.java
index a4fa85f..970522a 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleSplitter.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleSplitter.java
@@ -63,6 +63,7 @@
      *
      * @param testInfo the current {@link TestInformation} to proceed with sharding.
      * @param runConfig {@link LinkedHashMap} loaded from {@link ITestSuite#loadTests()}.
+     * @param suitePreparersPerDevice map of suite level preparers per test device.
      * @param shardCount a shard count hint to help with sharding.
      * @param dynamicModule Whether or not module can be shared in pool or must be independent
      *     (strict sharding).
@@ -72,6 +73,7 @@
     public static List<ModuleDefinition> splitConfiguration(
             TestInformation testInfo,
             LinkedHashMap<String, IConfiguration> runConfig,
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice,
             int shardCount,
             boolean dynamicModule,
             boolean intraModuleSharding) {
@@ -93,7 +95,8 @@
                         configMap.getValue(),
                         shardCount,
                         dynamicModule,
-                        intraModuleSharding);
+                        intraModuleSharding,
+                        suitePreparersPerDevice);
             } catch (RuntimeException e) {
                 CLog.e("Exception while creating module for '%s'", configMap.getKey());
                 throw e;
@@ -109,7 +112,8 @@
             IConfiguration config,
             int shardCount,
             boolean dynamicModule,
-            boolean intraModuleSharding) {
+            boolean intraModuleSharding,
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice) {
         List<IRemoteTest> tests = config.getTests();
         // Get rid of the IRemoteTest reference on the shared configuration. It will not be used
         // to run.
@@ -127,11 +131,13 @@
                                     moduleName,
                                     tests,
                                     clonePreparersMap(config),
+                                    clonePreparersMap(suitePreparersPerDevice),
                                     clonePreparers(config.getMultiTargetPreparers()),
                                     config);
                     currentList.add(module);
                 } else {
-                    addModuleToListFromSingleTest(currentList, tests.get(i), moduleName, config);
+                    addModuleToListFromSingleTest(
+                            currentList, tests.get(i), moduleName, config, suitePreparersPerDevice);
                 }
             }
             clearPreparersFromConfig(config);
@@ -153,6 +159,7 @@
                                             moduleName,
                                             shardedTests,
                                             clonePreparersMap(config),
+                                            clonePreparersMap(suitePreparersPerDevice),
                                             clonePreparers(config.getMultiTargetPreparers()),
                                             config);
                             currentList.add(module);
@@ -161,14 +168,19 @@
                         // We create independent modules with each sharded test.
                         for (IRemoteTest moduleTest : shardedTests) {
                             addModuleToListFromSingleTest(
-                                    currentList, moduleTest, moduleName, config);
+                                    currentList,
+                                    moduleTest,
+                                    moduleName,
+                                    config,
+                                    suitePreparersPerDevice);
                         }
                     }
                     continue;
                 }
             }
             // test is not shardable or did not shard
-            addModuleToListFromSingleTest(currentList, test, moduleName, config);
+            addModuleToListFromSingleTest(
+                    currentList, test, moduleName, config, suitePreparersPerDevice);
         }
         clearPreparersFromConfig(config);
     }
@@ -181,7 +193,8 @@
             List<ModuleDefinition> currentList,
             IRemoteTest test,
             String moduleName,
-            IConfiguration config) {
+            IConfiguration config,
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice) {
         List<IRemoteTest> testList = new ArrayList<>();
         testList.add(test);
         ModuleDefinition module =
@@ -189,6 +202,7 @@
                         moduleName,
                         testList,
                         clonePreparersMap(config),
+                        clonePreparersMap(suitePreparersPerDevice),
                         clonePreparers(config.getMultiTargetPreparers()),
                         config);
         currentList.add(module);
@@ -234,6 +248,18 @@
         return res;
     }
 
+    /** Deep cloning of potentially multi-device preparers. */
+    private static Map<String, List<ITargetPreparer>> clonePreparersMap(
+            Map<String, List<ITargetPreparer>> suitePreparersPerDevice) {
+        Map<String, List<ITargetPreparer>> res = new LinkedHashMap<>();
+        for (String device : suitePreparersPerDevice.keySet()) {
+            List<ITargetPreparer> preparers = new ArrayList<>();
+            res.put(device, preparers);
+            preparers.addAll(clonePreparers(suitePreparersPerDevice.get(device)));
+        }
+        return res;
+    }
+
     private static void clearPreparersFromConfig(IConfiguration config) {
         try {
             for (IDeviceConfiguration holder : config.getDeviceConfig()) {
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index 26a7f4a..2831e8b 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -79,6 +79,7 @@
 
     private boolean mAllowParameterizedModules = false;
     private boolean mAllowMainlineParameterizedModules = false;
+    private boolean mOptimizeMainlineTest = false;
     private boolean mAllowOptionalParameterizedModules = false;
     private ModuleParameters mForcedModuleParameter = null;
     private Set<ModuleParameters> mExcludedModuleParameters = new HashSet<>();
@@ -121,6 +122,11 @@
         mAllowMainlineParameterizedModules = allowed;
     }
 
+    /** Sets whether or not to optimize mainline test. */
+    public final void setOptimizeMainlineTest(boolean allowed) {
+        mOptimizeMainlineTest = allowed;
+    }
+
     /** Sets whether or not to allow optional parameterized modules. */
     public final void setOptionalParameterizedModules(boolean allowed) {
         mAllowOptionalParameterizedModules = allowed;
@@ -378,7 +384,8 @@
                                 new MainlineModuleHandler(
                                         param,
                                         abi,
-                                        mContext
+                                        mContext,
+                                        mOptimizeMainlineTest
                                 );
                         skipCreatingBaseConfig = true;
                         IConfiguration paramConfig =
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index da1741f..6082517 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -219,9 +219,9 @@
         if (configPath == null) {
             throw new RuntimeException(String.format("Configuration path is null."));
         }
-        File configFie = new File(configPath);
-        if (!configFie.exists()) {
-            configFie = null;
+        File configFile = new File(configPath);
+        if (!configFile.exists()) {
+            configFile = null;
         }
         // De-duplicate test infos so that there won't be duplicate test options.
         testInfos = dedupTestInfos(testInfos);
@@ -229,10 +229,10 @@
             // Clean up all the test options injected in SuiteModuleLoader.
             super.cleanUpSuiteSetup();
             super.clearModuleArgs();
-            if (configFie != null) {
+            if (configFile != null) {
                 clearConfigPaths();
                 // Set config path to BaseTestSuite to limit the search.
-                addConfigPaths(configFie);
+                addConfigPaths(configFile);
             }
             // Inject the test options from each test info to SuiteModuleLoader.
             parseOptions(testInfo);
diff --git a/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandler.java b/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandler.java
index eb4f4a4..9f4af97 100644
--- a/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandler.java
+++ b/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandler.java
@@ -38,11 +38,17 @@
     private String mDynamicBaseLink = null;
     private IAbi mAbi = null;
     private String mName = null;
+    private boolean mOptimizeMainlineTest = false;
 
-    public MainlineModuleHandler(String name, IAbi abi, IInvocationContext context) {
+    public MainlineModuleHandler(
+            String name,
+            IAbi abi,
+            IInvocationContext context,
+            boolean optimize) {
         mName = name;
         mAbi = abi;
         buildDynamicBaseLink(context.getBuildInfos().get(0));
+        mOptimizeMainlineTest = optimize;
     }
 
     /** Builds the dynamic base link where the mainline modules would be downloaded. */
@@ -78,6 +84,7 @@
     private InstallApexModuleTargetPreparer createMainlineModuleInstaller() {
         InstallApexModuleTargetPreparer mainlineModuleInstaller =
                 new InstallApexModuleTargetPreparer();
+        mainlineModuleInstaller.setSkipApexTearDown(mOptimizeMainlineTest);
         // Inject the real dynamic link to the target preparer so that it will dynamically download
         // the mainline modules.
         String fullDynamicLink = mDynamicBaseLink;
diff --git a/src/com/android/tradefed/util/AaptParser.java b/src/com/android/tradefed/util/AaptParser.java
index a88fb8f..a61d230 100644
--- a/src/com/android/tradefed/util/AaptParser.java
+++ b/src/com/android/tradefed/util/AaptParser.java
@@ -54,6 +54,48 @@
     private static final int AAPT_TIMEOUT_MS = 60000;
     private static final int INVALID_SDK = -1;
 
+    /**
+     * Enum of options for AAPT version used to parse APK files.
+     */
+    public static enum AaptVersion {
+        AAPT {
+            @Override
+            public String[] dumpBadgingCommand(File apkFile) {
+                return new String[] {"aapt", "dump", "badging", apkFile.getAbsolutePath()};
+            }
+
+            @Override
+            public String[] dumpXmlTreeCommand(File apkFile) {
+                return new String[] {
+                    "aapt", "dump", "xmltree", apkFile.getAbsolutePath(), "AndroidManifest.xml"
+                };
+            }
+        },
+
+        AAPT2 {
+            @Override
+            public String[] dumpBadgingCommand(File apkFile) {
+                return new String[] {"aapt2", "dump", "badging", apkFile.getAbsolutePath()};
+            }
+
+            @Override
+            public String[] dumpXmlTreeCommand(File apkFile) {
+                return new String[] {
+                    "aapt2",
+                    "dump",
+                    "xmltree",
+                    apkFile.getAbsolutePath(),
+                    "--file",
+                    "AndroidManifest.xml"
+                };
+            }
+        };
+
+        public abstract String[] dumpBadgingCommand(File apkFile);
+
+        public abstract String[] dumpXmlTreeCommand(File apkFile);
+    };
+
     private String mPackageName;
     private String mVersionCode;
     private String mVersionName;
@@ -130,16 +172,21 @@
      * @return the {@link AaptParser} or <code>null</code> if failed to extract the information
      */
     public static AaptParser parse(File apkFile) {
+        return parse(apkFile, AaptVersion.AAPT);
+    }
+
+    /**
+     * Parse info from the apk.
+     *
+     * @param apkFile the apk file
+     * @param aaptVersion the aapt version
+     * @return the {@link AaptParser} or <code>null</code> if failed to extract the information
+     */
+    public static AaptParser parse(File apkFile, AaptVersion aaptVersion) {
         CommandResult result =
                 RunUtil.getDefault()
                         .runTimedCmdRetry(
-                                AAPT_TIMEOUT_MS,
-                                0L,
-                                2,
-                                "aapt",
-                                "dump",
-                                "badging",
-                                apkFile.getAbsolutePath());
+                                AAPT_TIMEOUT_MS, 0L, 2, aaptVersion.dumpBadgingCommand(apkFile));
 
         String stderr = result.getStderr();
         if (stderr != null && !stderr.isEmpty()) {
@@ -158,11 +205,7 @@
                                 AAPT_TIMEOUT_MS,
                                 0L,
                                 2,
-                                "aapt",
-                                "dump",
-                                "xmltree",
-                                apkFile.getAbsolutePath(),
-                                "AndroidManifest.xml");
+                                aaptVersion.dumpXmlTreeCommand(apkFile));
 
         stderr = result.getStderr();
         if (stderr != null && !stderr.isEmpty()) {
diff --git a/src/com/android/tradefed/util/ProtoUtil.java b/src/com/android/tradefed/util/ProtoUtil.java
new file mode 100644
index 0000000..bf1699a
--- /dev/null
+++ b/src/com/android/tradefed/util/ProtoUtil.java
@@ -0,0 +1,89 @@
+/*
+ * 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.util;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Utility methods for dealing with protobuf messages type-agnostically. */
+public class ProtoUtil {
+
+    /**
+     * Get values of a nested field reference, i.e. field_1.field_2.field_3, from a proto message as
+     * a list of strings. Returns an empty list when a field cannot be found.
+     *
+     * <p>If the field reference contains repeated fields, each instance is expanded, resulting in a
+     * list of strings.
+     *
+     * @param message The protobuf {@link Message} or object to be parsed.
+     * @param references A list of field references starting at the root of the message. e.g. if we
+     *     want to read {@code field_2} under the value of {@code field_1} in {@code
+     *     messageOrObject} the list would be {@code field1}, {@code field2}.
+     * @return A list of all the fields values referred to by the reference. If {@code references}
+     *     is empty, returns {@code message.toString()} as a list. If {@code references} is invalid,
+     *     returns an empty list.
+     */
+    public static List<String> getNestedFieldFromMessageAsStrings(
+            Message message, List<String> references) {
+        return getNestedFieldFromMessageAsStringsHelper(message, references);
+    }
+
+    /**
+     * A helper method to {@code getNestedFieldFromMessageAsStrings} where the "message" can be an
+     * object in case we reach a primitive value field during recursive parsing.
+     */
+    private static List<String> getNestedFieldFromMessageAsStringsHelper(
+            Object messageOrObject, List<String> references) {
+        if (references.isEmpty()) {
+            return Arrays.asList(String.valueOf(messageOrObject));
+        }
+        if (!(messageOrObject instanceof Message)) {
+            CLog.e(
+                    "Attempting to read field %s from object of type %s, "
+                            + "which is not a proto message.",
+                    references.get(0), messageOrObject.getClass());
+            return new ArrayList<String>();
+        }
+        Message message = (Message) messageOrObject;
+        String reference = references.get(0);
+        FieldDescriptor fieldDescriptor = message.getDescriptorForType().findFieldByName(reference);
+        if (fieldDescriptor == null) {
+            CLog.e("Could not find field %s in message %s.", reference, message);
+            return new ArrayList<String>();
+        }
+        Object fieldValue = message.getField(fieldDescriptor);
+        if (fieldValue instanceof List) {
+            return ((List<? extends Object>) fieldValue)
+                    .stream()
+                    .flatMap(
+                            v ->
+                                    getNestedFieldFromMessageAsStringsHelper(
+                                                    v, references.subList(1, references.size()))
+                                            .stream())
+                    .collect(Collectors.toList());
+        }
+        return getNestedFieldFromMessageAsStringsHelper(
+                fieldValue, references.subList(1, references.size()));
+    }
+}
diff --git a/src/com/android/tradefed/util/StringEscapeUtils.java b/src/com/android/tradefed/util/StringEscapeUtils.java
index 846c0e7..4cc5741 100644
--- a/src/com/android/tradefed/util/StringEscapeUtils.java
+++ b/src/com/android/tradefed/util/StringEscapeUtils.java
@@ -47,6 +47,15 @@
                 case '\\':
                     out.append("\\\\");
                     break;
+                case '>':
+                    out.append("\\>");
+                    break;
+                case '<':
+                    out.append("\\<");
+                    break;
+                case '|':
+                    out.append("\\|");
+                    break;
                 default:
                     out.append(ch);
                     break;
diff --git a/src/com/android/tradefed/util/SubprocessTestResultsParser.java b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
index 2a5d280..c560e6a 100644
--- a/src/com/android/tradefed/util/SubprocessTestResultsParser.java
+++ b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
@@ -80,6 +80,7 @@
 
     private TestDescription mCurrentTest = null;
     private IInvocationContext mCurrentModuleContext = null;
+    private InvocationFailedEventInfo mReportedInvocationFailedEventInfo = null;
 
     private Pattern mPattern = null;
     private Map<String, EventHandler> mHandlerMap = null;
@@ -423,6 +424,7 @@
             } else {
                 mListener.invocationFailed(ifi.mCause);
             }
+            mReportedInvocationFailedEventInfo = ifi;
         }
     }
 
@@ -653,4 +655,14 @@
     public TestDescription getCurrentTest() {
         return mCurrentTest;
     }
+
+    /** Returns whether or not an invocation failed was reported. */
+    public boolean reportedInvocationFailed() {
+        return (mReportedInvocationFailedEventInfo != null);
+    }
+
+    /** Returns reported invocation failure event info. */
+    public InvocationFailedEventInfo getReportedInvocationFailedEventInfo() {
+        return mReportedInvocationFailedEventInfo;
+    }
 }
diff --git a/src/com/android/tradefed/util/executor/ParallelDeviceExecutor.java b/src/com/android/tradefed/util/executor/ParallelDeviceExecutor.java
index ee6cc23..7a94bf5 100644
--- a/src/com/android/tradefed/util/executor/ParallelDeviceExecutor.java
+++ b/src/com/android/tradefed/util/executor/ParallelDeviceExecutor.java
@@ -20,13 +20,13 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 
 /** Wrapper of {@link ExecutorService} to execute a function in parallel. */
 public class ParallelDeviceExecutor<V> {
@@ -43,7 +43,7 @@
      * Invoke all the {@link Callable} with the timeout limit.
      *
      * @param callableTasks The List of tasks.
-     * @param timeout The timeout to apply.
+     * @param timeout The timeout to apply, or zero for unlimited.
      * @param unit The unit of the timeout.
      * @return The list of results for each callable task.
      */
@@ -61,12 +61,15 @@
                         });
         List<V> results = new ArrayList<>();
         try {
-            List<Future<V>> futures = executor.invokeAll(callableTasks);
+            List<Future<V>> futures =
+                    timeout == 0L
+                            ? executor.invokeAll(callableTasks)
+                            : executor.invokeAll(callableTasks, timeout, unit);
             for (Future<V> future : futures) {
                 try {
-                    results.add(future.get(timeout, unit));
-                } catch (TimeoutException timeoutException) {
-                    mErrors.add(timeoutException);
+                    results.add(future.get());
+                } catch (CancellationException cancellationException) {
+                    mErrors.add(cancellationException);
                 } catch (ExecutionException execException) {
                     mErrors.add(execException.getCause());
                 }
diff --git a/test_framework/Android.bp b/test_framework/Android.bp
index 31d31a7..75e34cc 100644
--- a/test_framework/Android.bp
+++ b/test_framework/Android.bp
@@ -19,6 +19,7 @@
         "com/**/*.java",
     ],
     static_libs: [
+        "diffutils-prebuilt-jar",
         "longevity-host-lib",
         "perfetto_metrics-full",
         "test-composers",
diff --git a/test_framework/com/android/tradefed/device/metric/AtraceCollector.java b/test_framework/com/android/tradefed/device/metric/AtraceCollector.java
index c6ea63b..5a32035 100644
--- a/test_framework/com/android/tradefed/device/metric/AtraceCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/AtraceCollector.java
@@ -27,6 +27,7 @@
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
@@ -268,7 +269,9 @@
                     postProcess(trace);
                     trace.delete();
                 } else {
-                    throw new DeviceRuntimeException("failed to pull log: " + fullLogPath());
+                    throw new DeviceRuntimeException(
+                            String.format("failed to pull log: %s", fullLogPath()),
+                            DeviceErrorIdentifier.FAIL_PULL_FILE);
                 }
 
                 if (!mPreserveOndeviceLog) {
diff --git a/test_framework/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java
deleted file mode 100644
index 304985e..0000000
--- a/test_framework/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect fragmentation at regular intervals. */
-public class BuddyInfoMetricCollector extends ScheduledDeviceMetricCollector {
-    public BuddyInfoMetricCollector() {
-        setTag("fragmentation");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running unusable-index collector...");
-            String outputFileName =
-                    String.format("%s/unusable-index-%s", createTempDir(), getFileSuffix());
-            File outputFile =
-                    saveProcessOutput(device, "cat /d/extfrag/unusable_index", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/BugreportzMetricCollector.java b/test_framework/com/android/tradefed/device/metric/BugreportzMetricCollector.java
deleted file mode 100644
index 8bf0dd6..0000000
--- a/test_framework/com/android/tradefed/device/metric/BugreportzMetricCollector.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect zipped bugreport at regular intervals. */
-public class BugreportzMetricCollector extends ScheduledDeviceMetricCollector {
-    public BugreportzMetricCollector() {
-        setTag("bugreportz");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        CLog.i("Running bugreportz...");
-
-        String hostBugreportFilename = String.format("bugreport-%s", getFileSuffix());
-        if (!device.logBugreport(hostBugreportFilename, getInvocationListener())) {
-            CLog.e("Failed to run bugreportz or bugreport.");
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/DumpHeapCollector.java b/test_framework/com/android/tradefed/device/metric/DumpHeapCollector.java
deleted file mode 100644
index 404580a..0000000
--- a/test_framework/com/android/tradefed/device/metric/DumpHeapCollector.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device.metric;
-
-import com.android.annotations.VisibleForTesting;
-import com.android.loganalysis.item.CompactMemInfoItem;
-import com.android.loganalysis.parser.CompactMemInfoParser;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.util.FileUtil;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * A {@link ScheduledDeviceMetricCollector} to collect memory dumps of processes at regular
- * intervals.
- */
-public class DumpHeapCollector extends ScheduledDeviceMetricCollector {
-    private static final String DUMPHEAP_OUTPUT = "/data/local/tmp";
-    private static final String SUFFIX = "trigger";
-
-    @Option(
-        name = "dumpheap-thresholds",
-        description =
-                "Threshold map for taking process dumpheaps. "
-                        + "The key should be the process name and its corresponding value is the "
-                        + "maximum acceptable heap size for that process."
-                        + "Note that to get heap dump for native and managed processes set their "
-                        + "threshold to 0."
-    )
-    protected Map<String, Long> mDumpheapThresholds = new HashMap<String, Long>();
-
-    @Override
-    public void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        CLog.i("Running dumpheap collection...");
-        List<File> dumpFiles = new ArrayList<>();
-        try {
-            for (String process : mDumpheapThresholds.keySet()) {
-                String output =
-                        device.executeShellCommand(
-                                String.format("dumpsys meminfo -c | grep %s", process));
-
-                dumpFiles = takeDumpheap(device, output, process, mDumpheapThresholds.get(process));
-
-                dumpFiles.forEach(dumpheap -> saveDumpheap(dumpheap));
-            }
-        } catch (DeviceNotAvailableException e) {
-            CLog.e(e);
-        } finally {
-            dumpFiles.forEach(dumpFile -> FileUtil.deleteFile(dumpFile));
-        }
-    }
-
-    /**
-     * Collects heap dump for each requested process if the PSS is greater than a threshold.
-     *
-     * @param device
-     * @param output of the meminfo command.
-     * @param process for which we need the heap dump.
-     * @param threshold which is the maximum tolerable PSS.
-     * @return the list of {@link File}s in the host containing the report. Empty list if something
-     *     failed.
-     * @throws DeviceNotAvailableException
-     */
-    @VisibleForTesting
-    List<File> takeDumpheap(ITestDevice device, String output, String process, Long threshold)
-            throws DeviceNotAvailableException {
-        List<File> dumpFiles = new ArrayList<>();
-        if (output.isEmpty()) {
-            CLog.i("Skipping %s -- no process found.", process);
-            return dumpFiles;
-        }
-
-        CompactMemInfoItem item =
-                new CompactMemInfoParser().parse(Arrays.asList(output.split("\n")));
-
-        for (Integer pid : item.getPids()) {
-            if (item.getName(pid).equals(process) && item.getPss(pid) > threshold) {
-                File dump = device.dumpHeap(process, getDevicePath(process));
-                dumpFiles.add(dump);
-            }
-        }
-        return dumpFiles;
-    }
-
-    /**
-     * Returns the path on the device to put the dump.
-     *
-     * @param process for which dump is being requested.
-     * @return a write-able path in device.
-     */
-    private String getDevicePath(String process) {
-        return String.format(
-                "%s/%s_%s_%s.hprof", DUMPHEAP_OUTPUT, process, SUFFIX, getFileSuffix());
-    }
-
-    @VisibleForTesting
-    void saveDumpheap(File dumpheap) {
-        if (dumpheap == null) {
-            CLog.e("Failed to take dumpheap.");
-            return;
-        }
-        try (FileInputStreamSource stream = new FileInputStreamSource(dumpheap)) {
-            getInvocationListener()
-                    .testLog(FileUtil.getBaseName(dumpheap.getName()), LogDataType.HPROF, stream);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java b/test_framework/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java
deleted file mode 100644
index 8e73eda..0000000
--- a/test_framework/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect graphics stats at regular intervals. */
-public class GraphicsStatsMetricCollector extends ScheduledDeviceMetricCollector {
-    GraphicsStatsMetricCollector() {
-        setTag("jank");
-    }
-
-    @Override
-    public void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running graphicsstats...");
-            String outputFileName =
-                    String.format("%s/graphics-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "dumpsys graphicsstats", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.GFX_INFO,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java
deleted file mode 100644
index 3312855..0000000
--- a/test_framework/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/**
- * A {@link ScheduledDeviceMetricCollector} to collect audio and system memory heaps at regular
- * intervals.
- */
-public class IonHeapInfoMetricCollector extends ScheduledDeviceMetricCollector {
-    public IonHeapInfoMetricCollector() {
-        setTag("ion");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        collectIonAudio(device);
-        collectIonSystem(device);
-    }
-
-    private void collectIonAudio(ITestDevice device) {
-        try {
-            CLog.i("Running ionheap audio collector...");
-            String outputFileName =
-                    String.format("%s/ion-audio-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "cat /d/ion/heaps/audio", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-
-    private void collectIonSystem(ITestDevice device) {
-        try {
-            CLog.i("Running ionheap system collector...");
-            String outputFileName =
-                    String.format("%s/ion-system-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "cat /d/ion/heaps/system", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/MemInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/MemInfoMetricCollector.java
deleted file mode 100644
index 0890b74..0000000
--- a/test_framework/com/android/tradefed/device/metric/MemInfoMetricCollector.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect memory dumps at regular intervals. */
-public class MemInfoMetricCollector extends ScheduledDeviceMetricCollector {
-    MemInfoMetricCollector() {
-        setTag("compact-meminfo");
-    }
-
-    @Override
-    public void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running meminfo...");
-            String outputFileName =
-                    String.format("%s/compact-meminfo-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "dumpsys meminfo -c -S", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.COMPACT_MEMINFO,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java
deleted file mode 100644
index 9ab0f33..0000000
--- a/test_framework/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect free page counts at regular intervals. */
-public class PagetypeInfoMetricCollector extends ScheduledDeviceMetricCollector {
-    public PagetypeInfoMetricCollector() {
-        setTag("pagetypeinfo");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running pagetype info collector...");
-            String outputFileName =
-                    String.format("%s/pagetypeinfo-%s", createTempDir(), getFileSuffix());
-            File outputFile = saveProcessOutput(device, "cat /proc/pagetypeinfo", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java b/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
index 9e56626..4d496a4 100644
--- a/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
@@ -144,6 +144,11 @@
             description = "Convert the raw trace file to perfetto metric file.")
     private boolean mConvertToMetricFile = true;
 
+    @Option(name = "collect-perfetto-file-size",
+            description = "Set it to true to collect the perfetto file size as part"
+                    + " of the metrics.")
+    private boolean mCollectPerfettoFileSize = false;
+
     @Option(
             name = "trace-processor-binary",
             description = "Path to the trace processor shell. This will"
@@ -193,7 +198,7 @@
         }
 
         // Update the file size metrics.
-        if (processSrcFile != null) {
+        if (processSrcFile != null && mCollectPerfettoFileSize) {
             double perfettoFileSizeInBytes = processSrcFile.length();
             Metric.Builder metricDurationBuilder = Metric.newBuilder();
             metricDurationBuilder.getMeasurementsBuilder().setSingleDouble(
diff --git a/test_framework/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java b/test_framework/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java
deleted file mode 100644
index 5528177..0000000
--- a/test_framework/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.loganalysis.item.DumpsysProcessMeminfoItem;
-import com.android.loganalysis.parser.DumpsysProcessMeminfoParser;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.metrics.proto.MetricMeasurement.NumericValues;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-/**
- * A {@link ScheduledDeviceMetricCollector} to measure peak memory usage of specified processes.
- * Collects PSS and USS (private dirty) memory usage values from dumpsys meminfo. The result will be
- * reported as a test run metric with key in the form of PSS#ProcName[#DeviceNum], in KB.
- */
-public class ProcessMaxMemoryCollector extends ScheduledDeviceMetricCollector {
-
-    @Option(
-        name = "memory-usage-process-name",
-        description = "Process names (from `dumpsys meminfo`) to measure memory usage for"
-    )
-    private List<String> mProcessNames = new ArrayList<>();
-
-    private class DeviceMemoryData {
-        /** Peak PSS per process */
-        private Map<String, Long> mProcPss = new HashMap<>();
-        /** Peak USS per process */
-        private Map<String, Long> mProcUss = new HashMap<>();
-    }
-
-    // Memory usage data per device
-    private Map<ITestDevice, DeviceMemoryData> mMemoryData;
-    private Map<ITestDevice, Map<String, NumericValues.Builder>> mPssMemoryPerProcess;
-    private Map<ITestDevice, Map<String, NumericValues.Builder>> mUssMemoryPerProcess;
-
-    @Override
-    void onStart(DeviceMetricData runData) {
-        mMemoryData = new HashMap<>();
-        mPssMemoryPerProcess = new HashMap<>();
-        mUssMemoryPerProcess = new HashMap<>();
-
-        for (ITestDevice device : getDevices()) {
-            mMemoryData.put(device, new DeviceMemoryData());
-            mPssMemoryPerProcess.put(device, new HashMap<>());
-            mUssMemoryPerProcess.put(device, new HashMap<>());
-        }
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            Map<String, Long> procPss = mMemoryData.get(device).mProcPss;
-            Map<String, Long> procUss = mMemoryData.get(device).mProcUss;
-            for (String proc : mProcessNames) {
-                String dumpResult = device.executeShellCommand("dumpsys meminfo --checkin " + proc);
-                if (dumpResult.startsWith("No process found")) {
-                    // process not found, skip
-                    continue;
-                }
-                DumpsysProcessMeminfoItem item =
-                        new DumpsysProcessMeminfoParser()
-                                .parse(Arrays.asList(dumpResult.split("\n")));
-                Long pss =
-                        item.get(DumpsysProcessMeminfoItem.TOTAL)
-                                .get(DumpsysProcessMeminfoItem.PSS);
-                Long uss =
-                        item.get(DumpsysProcessMeminfoItem.TOTAL)
-                                .get(DumpsysProcessMeminfoItem.PRIVATE_DIRTY);
-                if (pss == null || uss == null) {
-                    CLog.e("Error parsing meminfo output: " + dumpResult);
-                    continue;
-                }
-
-                // Track PSS values
-                if (mPssMemoryPerProcess.get(device) == null) {
-                    mPssMemoryPerProcess.put(device, new HashMap<>());
-                }
-                if (mPssMemoryPerProcess.get(device).get(proc) == null) {
-                    mPssMemoryPerProcess.get(device).put(proc, NumericValues.newBuilder());
-                }
-                mPssMemoryPerProcess.get(device).get(proc).addNumericValue(pss);
-
-                // Track USS values
-                if (mUssMemoryPerProcess.get(device) == null) {
-                    mUssMemoryPerProcess.put(device, new HashMap<>());
-                }
-                if (mUssMemoryPerProcess.get(device).get(proc) == null) {
-                    mUssMemoryPerProcess.get(device).put(proc, NumericValues.newBuilder());
-                }
-                mUssMemoryPerProcess.get(device).get(proc).addNumericValue(uss);
-
-                if (procPss.getOrDefault(proc, 0L) < pss) {
-                    procPss.put(proc, pss);
-                }
-                if (procUss.getOrDefault(proc, 0L) < uss) {
-                    procUss.put(proc, uss);
-                }
-            }
-        } catch (DeviceNotAvailableException e) {
-            CLog.e(e);
-        }
-    }
-
-    @Override
-    void onEnd(DeviceMetricData runData) {
-        for (ITestDevice device : getDevices()) {
-            // Report all the PSS data for each process
-            for (Entry<String, NumericValues.Builder> values :
-                    mPssMemoryPerProcess.get(device).entrySet()) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                                Measurements.newBuilder()
-                                        .setNumericValues(values.getValue().build()))
-                        .build();
-                metric.setUnit("kB").setType(DataType.RAW);
-                runData.addMetricForDevice(device, "PSS#" + values.getKey(), metric);
-            }
-
-            // Report all the USS data for each process
-            for (Entry<String, NumericValues.Builder> values :
-                    mUssMemoryPerProcess.get(device).entrySet()) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                                Measurements.newBuilder()
-                                        .setNumericValues(values.getValue().build()))
-                        .build();
-                metric.setUnit("kB").setType(DataType.RAW);
-                runData.addMetricForDevice(device, "USS#" + values.getKey(), metric);
-            }
-
-            // Continue reporting the max PSS / USS for compatibility
-            Map<String, Long> procPss = mMemoryData.get(device).mProcPss;
-            Map<String, Long> procUss = mMemoryData.get(device).mProcUss;
-            for (Entry<String, Long> pss : procPss.entrySet()) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                        Measurements.newBuilder().setSingleInt(pss.getValue()).build());
-                metric.setUnit("kB").setType(DataType.PROCESSED);
-                runData.addMetricForDevice(device, "MAX_PSS#" + pss.getKey(), metric);
-            }
-            for (Entry<String, Long> uss : procUss.entrySet()) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                        Measurements.newBuilder().setSingleInt(uss.getValue()).build());
-                metric.setUnit("kB").setType(DataType.PROCESSED);
-                runData.addMetricForDevice(device, "MAX_USS#" + uss.getKey(), metric);
-            }
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java b/test_framework/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java
deleted file mode 100644
index 347a4b9..0000000
--- a/test_framework/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-
-import java.io.File;
-import java.lang.reflect.InvocationTargetException;
-import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-
-/**
- * A {@link IMetricCollector} that makes runs multiple metric collectors periodically. This is a
- * best effort scheduler. It makes the best effort to run the collectors at given intervals while
- * making sure that no two collectors are run at the same time.
- */
-public class ScheduleMultipleDeviceMetricCollector extends BaseDeviceMetricCollector {
-    @Option(
-        name = "metric-collection-intervals",
-        description = "The interval at which the collectors should run."
-    )
-    private Map<String, Long> mIntervalMs = new HashMap<>();
-
-    @Option(
-        name = "metric-storage-path",
-        description =
-                "Absolute path to a directory on host where the collected metrics will be stored."
-    )
-    private File mMetricStoragePath = new File(System.getProperty("java.io.tmpdir"));
-
-    @Option(
-        name = "metric-collector-command-classes",
-        description =
-                "Complete package name of a class which registers the commands to do the actual "
-                        + "job of collection. Can be repeated."
-    )
-    private List<String> mMetricCollectorClasses = new ArrayList<>();
-
-    // List of collectors to run.
-    private List<ScheduledDeviceMetricCollector> mMetricCollectors = new ArrayList<>();
-
-    // Time interval at which the commands should run.
-    private Map<ScheduledDeviceMetricCollector, Long> mMetricCollectorIntervals = new HashMap<>();
-
-    // Time when the commands to collect various metrics were last run.
-    private Map<ScheduledDeviceMetricCollector, Long> mLastUpdate = new HashMap<>();
-
-    private Timer mTimer;
-
-    private long mScheduleRate;
-
-    @Override
-    public ITestInvocationListener init(
-            IInvocationContext context, ITestInvocationListener listener) {
-        super.init(context, listener);
-        initMetricCollectors(context, listener);
-
-        return this;
-    }
-
-    /** Gets an instance of all the requested metric collectors. */
-    private void initMetricCollectors(
-            IInvocationContext context, ITestInvocationListener listener) {
-        for (String metricCollectorClass : mMetricCollectorClasses) {
-            try {
-                Class<?> klass = Class.forName(metricCollectorClass);
-
-                ScheduledDeviceMetricCollector singleMetricCollector =
-                        klass.asSubclass(ScheduledDeviceMetricCollector.class)
-                                .getDeclaredConstructor()
-                                .newInstance();
-
-                singleMetricCollector.init(context, listener);
-
-                mMetricCollectors.add(singleMetricCollector);
-            } catch (ClassNotFoundException
-                    | InstantiationException
-                    | IllegalAccessException
-                    | InvocationTargetException
-                    | NoSuchMethodException e) {
-                CLog.e("Class %s not found, skipping.", metricCollectorClass);
-                CLog.e(e);
-            }
-        }
-    }
-
-    @Override
-    public final void onTestRunStart(DeviceMetricData runData) {
-        if (mMetricCollectorClasses.isEmpty()) {
-            CLog.w("No single metric class provided. Skipping collection.");
-            return;
-        }
-
-        setupCollection();
-
-        if (mScheduleRate == 0) {
-            CLog.e(
-                    "Failed to get a valid interval for even one metric collector. "
-                            + "Please make sure that the collectors have non-zero intervals "
-                            + "specified as an argument to this class.");
-            return;
-        }
-
-        // TODO(b/70394486): Investigate if ScheduledThreadPool is better suited here so that we can
-        // schedule all the metrics in their own thread and create a common object which allows
-        // running of only one collector at a time.
-        mTimer = new Timer();
-
-        TimerTask timerTask =
-                new TimerTask() {
-                    @Override
-                    public void run() {
-                        collect(runData);
-                    }
-                };
-
-        mTimer.scheduleAtFixedRate(timerTask, 0, mScheduleRate);
-    }
-
-    /**
-     * Sets up the collection process by parsing all the args, retrieving the intervals from the
-     * args and initializing the last update value of each of the collectors to current time.
-     */
-    private void setupCollection() {
-        parseAllArgs();
-        for (ScheduledDeviceMetricCollector singleMetricCollector :
-                mMetricCollectorIntervals.keySet()) {
-            mLastUpdate.put(singleMetricCollector, System.currentTimeMillis());
-        }
-
-        mScheduleRate = gcdOfIntervals();
-    }
-
-    /**
-     * Runs all the requested collectors sequentially. Dumps the output in {@code
-     * mmResultsDirectory/outputDirFormat} of the collector prefixed.
-     *
-     * @param runData holds the filename of the metrics collected for each collector.
-     */
-    private void collect(DeviceMetricData runData) {
-        for (ScheduledDeviceMetricCollector singleMetricCollector :
-                mMetricCollectorIntervals.keySet()) {
-
-            Long elapsedTime = System.currentTimeMillis() - mLastUpdate.get(singleMetricCollector);
-
-            Long taskInterval = mMetricCollectorIntervals.get(singleMetricCollector);
-
-            if (elapsedTime >= taskInterval) {
-                try {
-                    for (ITestDevice device : getDevices()) {
-                        singleMetricCollector.collect(device, runData);
-                    }
-                    mLastUpdate.put(singleMetricCollector, System.currentTimeMillis());
-                } catch (InterruptedException e) {
-                    CLog.e("Exception during %s", singleMetricCollector.getClass());
-                    CLog.e(e);
-                }
-            }
-        }
-    }
-
-    /** Parse all the intervals provided in the command line. */
-    private void parseAllArgs() {
-        for (ScheduledDeviceMetricCollector metricCollector : mMetricCollectors) {
-            Long value = mIntervalMs.getOrDefault(metricCollector.getTag(), 0L);
-
-            if (value > 0) {
-                mMetricCollectorIntervals.put(metricCollector, value);
-            } else if (value < 0) {
-                throw new IllegalArgumentException(
-                        metricCollector.getClass() + " expects a non negative interval.");
-            }
-        }
-    }
-
-    /** Get the {@code scheduleRate} common to all tasks which is the gcd of all the intervals. */
-    private Long gcdOfIntervals() {
-        Collection<Long> intervals = mMetricCollectorIntervals.values();
-        if (intervals.isEmpty()) {
-            return 0L;
-        }
-        BigInteger gcdSoFar = new BigInteger(intervals.iterator().next().toString());
-
-        for (Long interval : intervals) {
-            gcdSoFar = gcdSoFar.gcd(new BigInteger(interval.toString()));
-        }
-
-        return gcdSoFar.longValue();
-    }
-
-    @Override
-    public final void onTestRunEnd(
-            DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
-        if (mTimer != null) {
-            mTimer.cancel();
-            mTimer.purge();
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java b/test_framework/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java
deleted file mode 100644
index 409ac2b..0000000
--- a/test_framework/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.annotations.VisibleForTesting;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.util.FileUtil;
-
-import java.io.File;
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-
-/**
- * A {@link IMetricCollector} that allows to run a collection task periodically at a set interval.
- */
-public abstract class ScheduledDeviceMetricCollector extends BaseDeviceMetricCollector {
-
-    @Option(
-        name = "fixed-schedule-rate",
-        description = "Schedule the timetask as a fixed schedule rate"
-    )
-    private boolean mFixedScheduleRate = false;
-
-    @Option(
-        name = "interval",
-        description = "the interval between two tasks being scheduled",
-        isTimeVal = true
-    )
-    private long mIntervalMs = 60 * 1000l;
-
-    private Timer timer;
-
-    @Override
-    public final void onTestRunStart(DeviceMetricData runData) {
-        CLog.d("starting with interval = %s", mIntervalMs);
-        onStart(runData);
-        timer = new Timer();
-        TimerTask timerTask =
-                new TimerTask() {
-                    @Override
-                    public void run() {
-                        try {
-                            for (ITestDevice device : getDevices()) {
-                                collect(device, runData);
-                            }
-                        } catch (InterruptedException e) {
-                            timer.cancel();
-                            Thread.currentThread().interrupt();
-                            CLog.e("Interrupted exception thrown from task:");
-                            CLog.e(e);
-                        }
-                    }
-                };
-
-        if (mFixedScheduleRate) {
-            timer.scheduleAtFixedRate(timerTask, 0, mIntervalMs);
-        } else {
-            timer.schedule(timerTask, 0, mIntervalMs);
-        }
-    }
-
-    @Override
-    public final void onTestRunEnd(
-            DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
-        if (timer != null) {
-            timer.cancel();
-            timer.purge();
-        }
-        onEnd(runData);
-        CLog.d("finished");
-    }
-
-    /**
-     * Task periodically & asynchronously run during the test running on a specific device.
-     *
-     * @param device the {@link ITestDevice} the metric is associated to.
-     * @param runData the {@link DeviceMetricData} where to put metrics.
-     * @throws InterruptedException
-     */
-    abstract void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException;
-
-    /**
-     * Executed when entering this collector.
-     *
-     * @param runData the {@link DeviceMetricData} where to put metrics.
-     */
-    void onStart(DeviceMetricData runData) {
-        // Does nothing.
-    }
-
-    /**
-     * Executed when finishing this collector.
-     *
-     * @param runData the {@link DeviceMetricData} where to put metrics.
-     */
-    void onEnd(DeviceMetricData runData) {
-        // Does nothing.
-    }
-
-    /**
-     * Send all the output of a process from all the devices to a file.
-     *
-     * <p>Please note, metric collections should not overlap.
-     *
-     * @throws DeviceNotAvailableException
-     * @throws IOException
-     */
-    File saveProcessOutput(ITestDevice device, String command, String outputFileName)
-            throws DeviceNotAvailableException, IOException {
-        String output = device.executeShellCommand(command);
-
-        // Create the output file and dump the output of the command to this file.
-        File outputFile = new File(outputFileName);
-
-        FileUtil.writeToFile(output, outputFile);
-
-        return outputFile;
-    }
-
-    /**
-     * Create a suffix string to be appended at the end of each metric file to keep the name unique
-     * at each run.
-     *
-     * @return suffix string in the format year-month-date-hour-minute-seconds-milliseconds.
-     */
-    @VisibleForTesting
-    String getFileSuffix() {
-        return new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US).format(new Date());
-    }
-
-    /**
-     * Creates temporary directory to store the metric files.
-     *
-     * @return {@link File} directory with 'tmp' prefixed to its name to signify that its temporary.
-     * @throws IOException
-     */
-    @VisibleForTesting
-    File createTempDir() throws IOException {
-        return FileUtil.createTempDir(String.format("tmp_%s", getTag()));
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/TemperatureCollector.java b/test_framework/com/android/tradefed/device/metric/TemperatureCollector.java
deleted file mode 100644
index 2c38cd5..0000000
--- a/test_framework/com/android/tradefed/device/metric/TemperatureCollector.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static com.android.tradefed.targetprep.TemperatureThrottlingWaiter.DEVICE_TEMPERATURE_FILE_PATH_NAME;
-
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
-import com.android.tradefed.metrics.proto.MetricMeasurement.DoubleValues;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * A {@link ScheduledDeviceMetricCollector} to measure min and max device temperature. Useful for
- * long duration performance tests to monitor if the device overheats.
- */
-public class TemperatureCollector extends ScheduledDeviceMetricCollector {
-
-    private static final String CELCIUS_UNIT = "celcius";
-
-    // Option name intentionally shared with TemperatureThrottlingWaiter
-    @Option(
-        name = DEVICE_TEMPERATURE_FILE_PATH_NAME,
-        description =
-                "Name of file that contains device temperature. "
-                        + "Example: /sys/class/hwmon/hwmon1/device/msm_therm"
-    )
-    private String mDeviceTemperatureFilePath = null;
-
-    @Option(
-        name = "device-temperature-file-regex",
-        description =
-                "Regex to parse temperature file. First group must be the temperature parsable"
-                        + "to Double. Default: Result:(\\d+) Raw:.*"
-    )
-    private String mDeviceTemperatureFileRegex = "Result:(\\d+) Raw:.*";
-
-    /**
-     * Stores the highest recorded temperature per device. Device will not be present in the map if
-     * no valid temperature was recorded.
-     */
-    private Map<ITestDevice, Double> mMaxDeviceTemps;
-
-    /**
-     * Stores the lowest recorded temperature per device. Device will not be present in the map if
-     * no valid temperature was recorded.
-     */
-    private Map<ITestDevice, Double> mMinDeviceTemps;
-
-    // Example: Result:32 Raw:7e51
-    private static Pattern mTemperatureRegex;
-
-    private Map<ITestDevice, DoubleValues.Builder> mValues;
-
-    @Override
-    void onStart(DeviceMetricData runData) {
-        mTemperatureRegex = Pattern.compile(mDeviceTemperatureFileRegex);
-        mMaxDeviceTemps = new HashMap<>();
-        mMinDeviceTemps = new HashMap<>();
-        mValues = new HashMap<>();
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        if (mDeviceTemperatureFilePath == null) {
-            return;
-        }
-        try {
-            if (!device.isAdbRoot()) {
-                return;
-            }
-            Double temp = getTemperature(device);
-            if (temp == null) {
-                return;
-            }
-            if (mValues.get(device) == null) {
-                mValues.put(device, DoubleValues.newBuilder());
-            }
-            mValues.get(device).addDoubleValue(temp);
-            mMaxDeviceTemps.putIfAbsent(device, temp);
-            mMinDeviceTemps.putIfAbsent(device, temp);
-            if (mMaxDeviceTemps.get(device) < temp) {
-                mMaxDeviceTemps.put(device, temp);
-            }
-            if (mMinDeviceTemps.get(device) > temp) {
-                mMinDeviceTemps.put(device, temp);
-            }
-        } catch (DeviceNotAvailableException e) {
-            CLog.e(e);
-        }
-    }
-
-    private Double getTemperature(ITestDevice device) throws DeviceNotAvailableException {
-        String cmd = "cat " + mDeviceTemperatureFilePath;
-        String result = device.executeShellCommand(cmd).trim();
-        Matcher m = mTemperatureRegex.matcher(result);
-        if (m.matches()) {
-            return Double.parseDouble(m.group(1));
-        }
-        CLog.e("Error parsing temperature file output: " + result);
-        return null;
-    }
-
-    @Override
-    void onEnd(DeviceMetricData runData) {
-        for (ITestDevice device : getDevices()) {
-            DoubleValues.Builder values = mValues.get(device);
-            if (values != null) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(
-                        Measurements.newBuilder().setDoubleValues(values.build()).build());
-                metric.setUnit(CELCIUS_UNIT).setType(DataType.RAW);
-                runData.addMetricForDevice(device, "temperature", metric);
-            }
-            // Report the max and min for compatibility
-            Double maxTemp = mMaxDeviceTemps.get(device);
-            if (maxTemp != null) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(Measurements.newBuilder().setSingleDouble(maxTemp).build());
-                // Since we report some processed value report it as PROCESSED.
-                metric.setUnit(CELCIUS_UNIT).setType(DataType.PROCESSED);
-                runData.addMetricForDevice(device, "max_temperature", metric);
-            }
-            Double minTemp = mMinDeviceTemps.get(device);
-            if (minTemp != null) {
-                Metric.Builder metric = Metric.newBuilder();
-                metric.setMeasurements(Measurements.newBuilder().setSingleDouble(minTemp).build());
-                // Since we report some processed value report it as PROCESSED.
-                metric.setUnit(CELCIUS_UNIT).setType(DataType.PROCESSED);
-                runData.addMetricForDevice(device, "min_temperature", metric);
-            }
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/device/metric/TraceMetricCollector.java b/test_framework/com/android/tradefed/device/metric/TraceMetricCollector.java
deleted file mode 100644
index f1cc72f..0000000
--- a/test_framework/com/android/tradefed/device/metric/TraceMetricCollector.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.google.common.io.Files;
-import java.io.File;
-import java.io.IOException;
-
-/** A {@link ScheduledDeviceMetricCollector} to collect kernel debug trace at regular intervals. */
-public class TraceMetricCollector extends ScheduledDeviceMetricCollector {
-    TraceMetricCollector() {
-        setTag("trace");
-    }
-
-    @Override
-    void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-        try {
-            CLog.i("Running trace collector...");
-            String outputFileName = String.format("%s/trace-%s", createTempDir(), getFileSuffix());
-            File outputFile =
-                    saveProcessOutput(
-                            device, "cat /sys/kernel/debug/tracing/trace", outputFileName);
-            try (InputStreamSource source = new FileInputStreamSource(outputFile, true)) {
-                getInvocationListener()
-                        .testLog(
-                                Files.getNameWithoutExtension(outputFile.getName()),
-                                LogDataType.TEXT,
-                                source);
-            }
-        } catch (DeviceNotAvailableException | IOException e) {
-            CLog.e(e);
-        }
-    }
-}
diff --git a/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
index 618b6bc..cd80332 100644
--- a/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
+++ b/test_framework/com/android/tradefed/postprocessor/PerfettoGenericPostProcessor.java
@@ -344,7 +344,14 @@
         for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
             if (!(entry.getValue() instanceof Message) && !(entry.getValue() instanceof List)) {
                 if (isNumeric(entry.getValue().toString())) {
-                    // Construct the metric if it is numeric value.
+                    // Check if the current field has to be used as prefix for other fields
+                    // and add it to the list of prefixes.
+                    if (mPerfettoPrefixKeyFields.contains(entry.getKey().toString())) {
+                        keyPrefixOtherFields.add(String.format("%s-%s",
+                                entry.getKey().getName().toString(), entry.getValue().toString()));
+                        continue;
+                    }
+                    // Otherwise treat this numeric field as metric.
                     if (mNumberPattern.matcher(entry.getValue().toString()).matches()) {
                         convertedMetrics.put(
                                 entry.getKey().getName(),
@@ -355,9 +362,9 @@
                         convertedMetrics.put(
                                 entry.getKey().getName(),
                                 TfMetricProtoUtil.stringToMetric(
-                                                Long.toString(
-                                                        Double.valueOf(entry.getValue().toString())
-                                                                .longValue()))
+                                        Long.toString(
+                                                Double.valueOf(entry.getValue().toString())
+                                                        .longValue()))
                                         .toBuilder());
                     }
                 } else {
@@ -375,20 +382,6 @@
             }
         }
 
-        // Add prefix key to all the keys in current proto message which has numeric values.
-        Map<String, Metric.Builder> additionalConvertedMetrics =
-                new HashMap<String, Metric.Builder>();
-        for (String prefix : keyPrefixOtherFields) {
-            for (Map.Entry<String, Metric.Builder> currentMetric : convertedMetrics.entrySet()) {
-                additionalConvertedMetrics.put(String.format("%s-%s", prefix,
-                        currentMetric.getKey()), currentMetric.getValue());
-            }
-        }
-
-        // Not cleaning up the other metrics without prefix fields.
-        convertedMetrics.putAll(additionalConvertedMetrics);
-
-
         // Recursively expand the proto messages and repeated fields(i.e list).
         // Recursion when there are no messages or list with in the current message.
         for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
@@ -458,6 +451,20 @@
                 }
             }
         }
+
+        // Add prefix key to all the keys in current proto message which has numeric values.
+        Map<String, Metric.Builder> additionalConvertedMetrics =
+                new HashMap<String, Metric.Builder>();
+        for (String prefix : keyPrefixOtherFields) {
+            for (Map.Entry<String, Metric.Builder> currentMetric : convertedMetrics.entrySet()) {
+                additionalConvertedMetrics.put(String.format("%s-%s", prefix,
+                        currentMetric.getKey()), currentMetric.getValue());
+            }
+        }
+
+        // Not cleaning up the other metrics without prefix fields.
+        convertedMetrics.putAll(additionalConvertedMetrics);
+
         return convertedMetrics;
     }
 
@@ -511,3 +518,4 @@
         return mProcessedMetric ? DataType.PROCESSED : DataType.RAW;
     }
 }
+
diff --git a/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java b/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java
new file mode 100644
index 0000000..0d32890
--- /dev/null
+++ b/test_framework/com/android/tradefed/targetprep/ArtChrootPreparer.java
@@ -0,0 +1,157 @@
+/*
+ * 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.targetprep;
+
+import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.command.remote.DeviceDescriptor;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
+/** Create chroot directory for ART tests. */
+@OptionClass(alias = "art-chroot-preparer")
+public class ArtChrootPreparer extends BaseTargetPreparer {
+
+    // Predefined location of the chroot root directory.
+    public static final String CHROOT_PATH = "/data/local/tmp/art-test-chroot";
+
+    // Directories to create in the chroot.
+    private static final String[] MKDIRS = {
+        "/", "/apex", "/data", "/data/dalvik-cache", "/data/local/tmp", "/tmp",
+    };
+
+    // System mount points to replicate in the chroot.
+    private static final String[] MOUNTS = {
+        "/dev", "/linkerconfig", "/proc", "/sys", "/system", "/apex/com.android.os.statsd",
+    };
+
+    @Override
+    public void setUp(TestInformation testInfo)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        ITestDevice device = testInfo.getDevice();
+
+        // Ensure there are no files left from previous runs.
+        cleanup(device);
+
+        // Create directories required for ART testing in chroot.
+        for (String dir : MKDIRS) {
+            adbShell(device, "mkdir -p " + CHROOT_PATH + dir);
+        }
+
+        // Replicate system mount point in the chroot.
+        for (String dir : MOUNTS) {
+            adbShell(device, "mkdir -p " + CHROOT_PATH + dir);
+            adbShell(device, "mount --bind " + dir + " " + CHROOT_PATH + dir);
+        }
+
+        // Activate APEXes in the chroot.
+        IBuildInfo buildInfo = testInfo.getBuildInfo();
+        IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) buildInfo;
+        DeviceDescriptor deviceDesc = device.getDeviceDescriptor();
+        File tests_dir = deviceBuild.getFile(BuildInfoFileKey.TARGET_LINKED_DIR);
+        // The art_chroot is a shared module containing comment ART test data.
+        File apexes_dir = FileUtil.getFileForPath(tests_dir, "art_chroot", "system", "apex");
+        if (apexes_dir.listFiles() == null) {
+            throw new TargetSetupError(
+                    "No apex files found in " + apexes_dir.getPath(), deviceDesc);
+        }
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("art-test-apex");
+            for (File apex : apexes_dir.listFiles()) {
+                activateApex(device, tempDir, apex);
+            }
+        } catch (IOException e) {
+            throw new TargetSetupError("Error when activating apex", e, deviceDesc);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    private void activateApex(ITestDevice device, File tempDir, File apex)
+            throws TargetSetupError, IOException, DeviceNotAvailableException {
+        CLog.i("Activate apex in ART chroot: " + apex.getName());
+        ZipFile apex_zip = new ZipFile(apex);
+        ZipArchiveEntry apex_payload = apex_zip.getEntry("apex_payload.img");
+        File temp = FileUtil.createTempFile("payload-", ".img", tempDir);
+        FileUtil.writeToFile(apex_zip.getInputStream(apex_payload), temp);
+        String deviceApexDir = CHROOT_PATH + "/apex/" + apex.getName();
+        // Rename "com.android.art.testing.apex" to just "com.android.art.apex".
+        deviceApexDir = deviceApexDir.replace(".testing.apex", "").replace(".apex", "");
+        String deviceApexImg = deviceApexDir + ".img";
+        if (!device.pushFile(temp, deviceApexImg)) {
+            throw new TargetSetupError(
+                    "adb push failed for " + apex.getName(), device.getDeviceDescriptor());
+        }
+        // TODO(b/168048638): Work-around for cuttlefish: first losetup call always fails.
+        device.executeShellV2Command("losetup -f");
+        // Mount the apex file via a loopback device.
+        String loopbackDevice = adbShell(device, "losetup -f -s " + deviceApexImg);
+        adbShell(device, "mkdir -p " + deviceApexDir);
+        adbShell(device, "mount -o loop,ro " + loopbackDevice + " " + deviceApexDir);
+    }
+
+    @Override
+    public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        try {
+            cleanup(testInfo.getDevice());
+        } catch (TargetSetupError ex) {
+            CLog.e("Tear-down failed: " + ex.toString());
+        }
+    }
+
+    // Wrapper for executeShellV2Command that checks that the command succeeds.
+    private String adbShell(ITestDevice device, String cmd)
+            throws TargetSetupError, DeviceNotAvailableException {
+        CommandResult result = device.executeShellV2Command(cmd);
+        if (result.getStatus() != CommandStatus.SUCCESS) {
+            throw new TargetSetupError(
+                    String.format(
+                            "adb shell command failed: '%s': %s".format(cmd, result.getStderr())));
+        }
+        return result.getStdout();
+    }
+
+    private void cleanup(ITestDevice device) throws TargetSetupError, DeviceNotAvailableException {
+        String mounts = adbShell(device, "mount");
+        Pattern pattern = Pattern.compile("^([^ ]+) on ([^ ]+) type ([^ ]+) .*$");
+        for (String mount : mounts.split("\n")) {
+            Matcher matcher = pattern.matcher(mount);
+            if (!matcher.matches()) {
+                throw new TargetSetupError("Failed to parse mount command output: " + mount);
+            }
+            if (matcher.group(2).startsWith(CHROOT_PATH)) {
+                adbShell(device, "umount " + matcher.group(2));
+            }
+        }
+        adbShell(device, "rm -rf " + CHROOT_PATH);
+    }
+}
diff --git a/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java b/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java
index 1c837da..d54cb49 100644
--- a/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/DynamicSystemPreparer.java
@@ -26,11 +26,14 @@
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.SparseImageUtil;
 import com.android.tradefed.util.ZipUtil;
 import com.android.tradefed.util.ZipUtil2;
+
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
 import java.io.File;
 import java.io.IOException;
-import org.apache.commons.compress.archivers.zip.ZipFile;
 
 /**
  * An {@link ITargetPreparer} that sets up a system image on top of a device build with the Dynamic
@@ -43,11 +46,15 @@
     private static final String DEST_PATH = "/sdcard/system.raw.gz";
 
     @Option(
-        name = "system-image-zip-name",
-        description = "The name of the zip file containing system.img."
-    )
+            name = "system-image-zip-name",
+            description = "The name of the zip file containing system.img.")
     private String mSystemImageZipName = "system-img.zip";
 
+    @Option(
+            name = "user-data-size-in-gb",
+            description = "Number of GB to be allocated for DSU user-data.")
+    private long mUserDataSizeInGb = 16L; // 16GB
+
     private boolean isDSURunning(ITestDevice device) throws DeviceNotAvailableException {
         CollectingOutputReceiver receiver = new CollectingOutputReceiver();
         device.executeShellCommand("gsi_tool status", receiver);
@@ -67,15 +74,21 @@
 
         ZipFile zipFile = null;
         File systemImage = null;
+        File rawSystemImage = null;
         File systemImageGZ = null;
         try {
             zipFile = new ZipFile(systemImageZipFile);
             systemImage = ZipUtil2.extractFileFromZip(zipFile, "system.img");
-            //     The prequest here is the system.img must be an unsparsed image.
-            //     Is there any way to detect the actual format and convert it accordingly.
+            if (SparseImageUtil.isSparse(systemImage)) {
+                rawSystemImage = FileUtil.createTempFile("system", ".raw");
+                SparseImageUtil.unsparse(systemImage, rawSystemImage);
+            } else {
+                // system.img is already non-sparse
+                rawSystemImage = systemImage;
+            }
             systemImageGZ = FileUtil.createTempFile("system", ".raw.gz");
-            long rawSize = systemImage.length();
-            ZipUtil.gzipFile(systemImage, systemImageGZ);
+            long rawSize = rawSystemImage.length();
+            ZipUtil.gzipFile(rawSystemImage, systemImageGZ);
             CLog.i("Pushing %s to %s", systemImageGZ.getAbsolutePath(), DEST_PATH);
             if (!device.pushFile(systemImageGZ, DEST_PATH)) {
                 throw new TargetSetupError(
@@ -95,17 +108,26 @@
                             + "--el KEY_SYSTEM_SIZE "
                             + rawSize
                             + " "
-                            + "--el KEY_USERDATA_SIZE 8589934592 "
-                            + "--ez KEY_ENABLE_WHEN_COMPLETED true";
+                            + "--el KEY_USERDATA_SIZE "
+                            + mUserDataSizeInGb * 1024 * 1024 * 1024
+                            + " --ez KEY_ENABLE_WHEN_COMPLETED true";
             device.executeShellCommand(command);
             // Check if device shows as unavailable (as expected after the activity finished).
-            device.waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000);
-            device.waitForDeviceOnline();
-            // the waitForDeviceOnline may block and we need to correct the 'i'
-            // which is used to measure timeout accordingly
-            if (!isDSURunning(device)) {
+            if (!device.waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000)) {
                 throw new TargetSetupError(
-                        "Timeout to boot into DSU", device.getDeviceDescriptor());
+                        "Timed out waiting for DSU installation to complete and reboot",
+                        device.getDeviceDescriptor());
+            }
+            try {
+                // waitForDeviceOnline() throws DeviceNotAvailableException if device does not
+                // become online within timeout.
+                device.waitForDeviceOnline();
+            } catch (DeviceNotAvailableException e) {
+                throw new TargetSetupError(
+                        "Timed out booting into DSU", e, device.getDeviceDescriptor());
+            }
+            if (!isDSURunning(device)) {
+                throw new TargetSetupError("Failed to boot into DSU", device.getDeviceDescriptor());
             }
             CommandResult result = device.executeShellV2Command("gsi_tool enable");
             if (CommandStatus.SUCCESS.equals(result.getStatus())) {
@@ -120,6 +142,7 @@
                     "fail to install the DynamicSystemUpdate", e, device.getDeviceDescriptor());
         } finally {
             FileUtil.deleteFile(systemImage);
+            FileUtil.deleteFile(rawSystemImage);
             FileUtil.deleteFile(systemImageGZ);
             ZipUtil2.closeZip(zipFile);
         }
diff --git a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
index f3de9b4..0e61a42 100644
--- a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -29,6 +29,9 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.result.error.ErrorIdentifier;
+import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IAbiReceiver;
 import com.android.tradefed.testtype.IInvocationContextReceiver;
@@ -137,9 +140,10 @@
      * Helper method to only throw if mAbortOnFailure is enabled. Callers should behave as if this
      * method may return.
      */
-    private void fail(String message, DeviceDescriptor descriptor) throws TargetSetupError {
+    private void fail(String message, DeviceDescriptor descriptor, ErrorIdentifier identifier)
+            throws TargetSetupError {
         if (shouldAbortOnFailure()) {
-            throw new TargetSetupError(message, descriptor);
+            throw new TargetSetupError(message, descriptor, identifier);
         } else {
             // Log the error and return
             Log.w(LOG_TAG, message);
@@ -153,7 +157,10 @@
         for (String pushspec : mPushSpecs) {
             String[] pair = pushspec.split("->");
             if (pair.length != 2) {
-                fail(String.format("Invalid pushspec: '%s'", Arrays.asList(pair)), descriptor);
+                fail(
+                        String.format("Invalid pushspec: '%s'", Arrays.asList(pair)),
+                        descriptor,
+                        InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
                 continue;
             }
             remoteToLocalMapping.put(pair[1], new File(pair[0]));
@@ -311,6 +318,22 @@
                 // approach to do individual download from remote artifact.
                 // Try to stage the files from remote zip files.
                 src = buildInfo.stageRemoteFile(fileName, testDir);
+                if (src != null) {
+                    try {
+                        // Search again with filtering on ABI
+                        File srcWithAbi = FileUtil.findFile(fileName, mAbi, testDir);
+                        if (srcWithAbi != null
+                                && !srcWithAbi
+                                        .getAbsolutePath()
+                                        .startsWith(src.getAbsolutePath())) {
+                            // When multiple matches are found, return the one with matching
+                            // ABI unless src is its parent directory.
+                            return srcWithAbi;
+                        }
+                    } catch (IOException e) {
+                        CLog.w("Failed to find test files with matching ABI from directory.");
+                    }
+                }
             }
         }
         return src;
@@ -388,7 +411,8 @@
         if (src == null || !src.exists()) {
             fail(
                     String.format("Local source file '%s' does not exist", localPath),
-                    device.getDeviceDescriptor());
+                    device.getDeviceDescriptor(),
+                    InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
             return;
         }
         if (src.isDirectory()) {
@@ -402,7 +426,8 @@
                         String.format(
                                 "Attempting to push dir '%s' to an existing device file '%s'",
                                 src.getAbsolutePath(), remotePath),
-                        device.getDeviceDescriptor());
+                        device.getDeviceDescriptor(),
+                        DeviceErrorIdentifier.FAIL_PUSH_FILE);
             }
             Set<String> filter = new HashSet<>();
             if (mAbi != null) {
@@ -415,7 +440,8 @@
                 fail(
                         String.format(
                                 "Failed to push local '%s' to remote '%s'", localPath, remotePath),
-                        device.getDeviceDescriptor());
+                        device.getDeviceDescriptor(),
+                        DeviceErrorIdentifier.FAIL_PUSH_FILE);
                 return;
             } else {
                 if (deleteContentOnly) {
@@ -428,7 +454,8 @@
                 fail(
                         String.format(
                                 "Failed to push local '%s' to remote '%s'", localPath, remotePath),
-                        device.getDeviceDescriptor());
+                        device.getDeviceDescriptor(),
+                        DeviceErrorIdentifier.FAIL_PUSH_FILE);
                 return;
             } else {
                 mFilesPushed.add(remotePath);
diff --git a/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java b/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
index 7034330..90928da 100644
--- a/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.PythonVirtualenvHelper;
 import com.android.tradefed.util.RunUtil;
 
 import java.io.File;
@@ -40,8 +41,7 @@
 @OptionClass(alias = "python-venv")
 public class PythonVirtualenvPreparer extends BaseTargetPreparer {
 
-    private static final String PIP = "pip";
-    private static final String PATH = "PATH";
+    private static final String PIP = "pip3";
     protected static final String PYTHONPATH = "PYTHONPATH";
     private static final int BASE_TIMEOUT = 1000 * 60;
 
@@ -70,6 +70,7 @@
 
     protected void installDeps(IBuildInfo buildInfo, ITestDevice device) throws TargetSetupError {
         boolean hasDependencies = false;
+        mPip = getPipPath();
         if (mRequirementsFile != null) {
             CommandResult c = mRunUtil.runTimedCmd(BASE_TIMEOUT * 5, mPip,
                     "install", "-r", mRequirementsFile.getAbsolutePath());
@@ -90,6 +91,9 @@
                     CLog.e("Installing %s failed", dep);
                     throw new TargetSetupError("Failed to install dependencies with pip",
                             device.getDeviceDescriptor());
+                } else {
+                    CLog.d("Successfullly installed %s.", dep);
+                    CLog.d("Stdout: %s", c.getStdout());
                 }
                 hasDependencies = true;
             }
@@ -99,9 +103,12 @@
         } else {
             // make the install directory of new packages available to other classes that
             // receive the build
-            buildInfo.setFile(PYTHONPATH, new File(mVenvDir,
-                    "local/lib/python2.7/site-packages"),
+            // TODO(b/166688272): Get install location from pip rather than hard code it.
+            buildInfo.setFile(
+                    PYTHONPATH,
+                    new File(mVenvDir, "local/lib/python3.8/site-packages"),
                     buildInfo.getBuildId());
+            buildInfo.setFile("VIRTUAL_ENV", mVenvDir, buildInfo.getBuildId());
         }
     }
 
@@ -109,13 +116,26 @@
             throws TargetSetupError {
         if (mVenvDir != null) {
             CLog.i("Using existing virtualenv based at %s", mVenvDir.getAbsolutePath());
-            activate();
+            PythonVirtualenvHelper.activate(mRunUtil, mVenvDir);
             return;
         }
+        checkVirtualenvVersion(device);
         try {
             mVenvDir = FileUtil.createNamedTempDir(buildInfo.getTestTag() + "-virtualenv");
-            mRunUtil.runTimedCmd(BASE_TIMEOUT, "virtualenv", mVenvDir.getAbsolutePath());
-            activate();
+            CommandResult c =
+                    mRunUtil.runTimedCmd(BASE_TIMEOUT, "virtualenv", mVenvDir.getAbsolutePath());
+            if (c.getStatus() != CommandStatus.SUCCESS) {
+                CLog.e("Creating virtual environment at %s failed.", mVenvDir.getAbsoluteFile());
+                CLog.e(
+                        "Status: %s\nStdout: %s\nStderr: %s",
+                        c.getStatus(), c.getStdout(), c.getStderr());
+                throw new TargetSetupError(
+                        String.format(
+                                "Failed to create virtual environment. Error:\n%s", c.getStderr()),
+                        device.getDeviceDescriptor());
+            }
+            CLog.i("Created a virtualenv based at %s", mVenvDir.getAbsolutePath());
+            PythonVirtualenvHelper.activate(mRunUtil, mVenvDir);
         } catch (IOException e) {
             CLog.e("Failed to create temp directory for virtualenv");
             throw new TargetSetupError("Error creating virtualenv", e,
@@ -131,13 +151,35 @@
         mRequirementsFile = f;
     }
 
-    private void activate() {
-        File binDir = new File(mVenvDir, "bin");
-        mRunUtil.setWorkingDir(binDir);
-        String path = System.getenv(PATH);
-        mRunUtil.setEnvVariable(PATH, binDir + ":" + path);
-        File pipFile = new File(binDir, PIP);
+    private String getPipPath() {
+        if (mVenvDir == null || !mVenvDir.exists()) {
+            return null;
+        }
+        String virtualenvPath = mVenvDir.getAbsolutePath();
+        File pipFile = new File(PythonVirtualenvHelper.getPythonBinDir(virtualenvPath), PIP);
         pipFile.setExecutable(true);
-        mPip = pipFile.getAbsolutePath();
+        return pipFile.getAbsolutePath();
+    }
+
+    /** Check if the virtualenv on the host is too old. */
+    private void checkVirtualenvVersion(ITestDevice device) throws TargetSetupError {
+        CommandResult result = mRunUtil.runTimedCmd(BASE_TIMEOUT, "virtualenv", "--version");
+        if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
+            throw new TargetSetupError(
+                    "Failed to run `virtualenv --version`. Reason:\n" + result.getStderr(),
+                    device.getDeviceDescriptor());
+        }
+        String stdout = result.getStdout(); // should start with 'virtualenv <version> from'
+        if (stdout.contains("command not found")) {
+            throw new TargetSetupError(
+                    "virtualenv is not installed.", device.getDeviceDescriptor());
+        }
+        String version = stdout.split(" ")[1];
+        int majorVersion = Integer.parseInt(version.split("\\.")[0]);
+        if (majorVersion < 20) {
+            throw new TargetSetupError(
+                    "virtualenv is too old. Required: >=20.0.1, yours: " + version,
+                    device.getDeviceDescriptor());
+        }
     }
 }
\ No newline at end of file
diff --git a/test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java
index e3c485c..fd4c694 100644
--- a/test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java
@@ -16,9 +16,11 @@
 
 package com.android.tradefed.targetprep;
 
+import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IDeviceManager;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.ITestLogger;
@@ -86,6 +88,11 @@
     @Option(name = "host-cmd-timeout", description = "Timeout for each command specified.")
     private Duration mTimeout = Duration.ofMinutes(1L);
 
+    @Option(
+            name = "use-flashing-permit",
+            description = "Acquire a flashing permit before running commands.")
+    private boolean mUseFlashingPermit = false;
+
     private List<Process> mBgProcesses = new ArrayList<>();
     private List<BgCommandLog> mBgCommandLogs = new ArrayList<>();
     private ITestLogger mLogger;
@@ -147,7 +154,16 @@
         }
         ITestDevice device = testInfo.getDevice();
         replaceSerialNumber(mSetUpCommands, device);
-        runCommandList(mSetUpCommands, device);
+        try {
+            if (mUseFlashingPermit) {
+                getDeviceManager().takeFlashingPermit();
+            }
+            runCommandList(mSetUpCommands, device);
+        } finally {
+            if (mUseFlashingPermit) {
+                getDeviceManager().returnFlashingPermit();
+            }
+        }
 
         try {
             mBgCommandLogs = createBgCommandLogs();
@@ -164,9 +180,16 @@
         ITestDevice device = testInfo.getDevice();
         replaceSerialNumber(mTearDownCommands, device);
         try {
+            if (mUseFlashingPermit) {
+                getDeviceManager().takeFlashingPermit();
+            }
             runCommandList(mTearDownCommands, device);
         } catch (TargetSetupError tse) {
             CLog.e(tse);
+        } finally {
+            if (mUseFlashingPermit) {
+                getDeviceManager().returnFlashingPermit();
+            }
         }
 
         // Terminate background commands after test finished
@@ -273,6 +296,12 @@
         return mRunUtil;
     }
 
+    /** @return {@link IDeviceManager} instance used for flashing permits */
+    @VisibleForTesting
+    IDeviceManager getDeviceManager() {
+        return GlobalConfiguration.getDeviceManagerInstance();
+    }
+
     /**
      * Create a BgCommandLog object that is based on a temporary file for each background command
      *
diff --git a/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
index def9252..6dd2b56 100644
--- a/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
@@ -56,6 +56,11 @@
     @Option(name = "script-timeout", description = "Script execution timeout.")
     private Duration mTimeout = Duration.ofMinutes(1L);
 
+    @Option(
+            name = "use-flashing-permit",
+            description = "Acquire a flashing permit before executing the script.")
+    private boolean mUseFlashingPermit = false;
+
     private IRunUtil mRunUtil;
 
     @Override
@@ -82,28 +87,15 @@
         getRunUtil().setEnvVariable("ANDROID_SERIAL", device.getSerialNumber());
         setPathVariable(testInfo);
 
-        // Execute script and handle result
-        CommandResult result =
-                getRunUtil().runTimedCmd(mTimeout.toMillis(), scriptFile.getAbsolutePath());
-        switch (result.getStatus()) {
-            case SUCCESS:
-                CLog.i("Script executed successfully, stdout = [%s].", result.getStdout());
-                break;
-            case FAILED:
-                throw new TargetSetupError(
-                        String.format(
-                                "Script execution failed, stdout = [%s], stderr = [%s].",
-                                result.getStdout(), result.getStderr()),
-                        device.getDeviceDescriptor());
-            case TIMED_OUT:
-                throw new TargetSetupError(
-                        "Script execution timed out.", device.getDeviceDescriptor());
-            case EXCEPTION:
-                throw new TargetSetupError(
-                        String.format(
-                                "Exception during script execution, stdout = [%s], stderr = [%s].",
-                                result.getStdout(), result.getStderr()),
-                        device.getDeviceDescriptor());
+        try {
+            if (mUseFlashingPermit) {
+                getDeviceManager().takeFlashingPermit();
+            }
+            executeScript(scriptFile, device);
+        } finally {
+            if (mUseFlashingPermit) {
+                getDeviceManager().returnFlashingPermit();
+            }
         }
     }
 
@@ -116,7 +108,7 @@
         return mRunUtil;
     }
 
-    /** @return {@link IDeviceManager} instance used to fetch the configured adb/fastboot paths */
+    /** @return {@link IDeviceManager} instance used for adb/fastboot paths and flashing permits */
     @VisibleForTesting
     IDeviceManager getDeviceManager() {
         return GlobalConfiguration.getDeviceManagerInstance();
@@ -175,4 +167,35 @@
             getRunUtil().setEnvVariable("PATH", path);
         }
     }
+
+    /**
+     * Execute script and handle result.
+     *
+     * @param scriptFile script file to execute
+     * @param device device being prepared
+     */
+    private void executeScript(File scriptFile, ITestDevice device) throws TargetSetupError {
+        CommandResult result =
+                getRunUtil().runTimedCmd(mTimeout.toMillis(), scriptFile.getAbsolutePath());
+        switch (result.getStatus()) {
+            case SUCCESS:
+                CLog.i("Script executed successfully, stdout = [%s].", result.getStdout());
+                break;
+            case FAILED:
+                throw new TargetSetupError(
+                        String.format(
+                                "Script execution failed, stdout = [%s], stderr = [%s].",
+                                result.getStdout(), result.getStderr()),
+                        device.getDeviceDescriptor());
+            case TIMED_OUT:
+                throw new TargetSetupError(
+                        "Script execution timed out.", device.getDeviceDescriptor());
+            case EXCEPTION:
+                throw new TargetSetupError(
+                        String.format(
+                                "Exception during script execution, stdout = [%s], stderr = [%s].",
+                                result.getStdout(), result.getStderr()),
+                        device.getDeviceDescriptor());
+        }
+    }
 }
diff --git a/test_framework/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
new file mode 100644
index 0000000..7f2b28d
--- /dev/null
+++ b/test_framework/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
@@ -0,0 +1,98 @@
+/*
+ * 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.targetprep;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
+import com.android.tradefed.invoker.TestInformation;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An {@link ITargetPreparer} that creates a secondary user in setup, and marks that tests should be
+ * run in that user.
+ *
+ * <p>In teardown, the secondary user is removed.
+ */
+@OptionClass(alias = "run-on-secondary-user")
+public class RunOnSecondaryUserTargetPreparer extends BaseTargetPreparer {
+
+    @VisibleForTesting static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
+
+    @VisibleForTesting static final String TEST_PACKAGE_NAME_OPTION = "test-package-name";
+
+    @Option(
+            name = TEST_PACKAGE_NAME_OPTION,
+            description =
+                    "the name of a package to be installed on the secondary user. "
+                            + "This must already be installed on the device.",
+            importance = Option.Importance.IF_UNSET)
+    private List<String> mTestPackages = new ArrayList<>();
+
+    @Override
+    public void setUp(TestInformation testInfo)
+            throws TargetSetupError, DeviceNotAvailableException {
+        int secondaryUserId = getSecondaryUserId(testInfo.getDevice());
+
+        if (secondaryUserId != -1) {
+            // There is already a secondary user - so we don't want to remove it
+            setDisableTearDown(true);
+        } else {
+            secondaryUserId = createSecondaryUser(testInfo.getDevice());
+        }
+
+        for (String pkg : mTestPackages) {
+            testInfo.getDevice()
+                    .executeShellCommand(
+                            "pm install-existing --user " + secondaryUserId + " " + pkg);
+        }
+
+        testInfo.properties().put(RUN_TESTS_AS_USER_KEY, Integer.toString(secondaryUserId));
+    }
+
+    /** Get the id of a secondary user currently on the device. -1 if there is none */
+    private static int getSecondaryUserId(ITestDevice device) throws DeviceNotAvailableException {
+        for (Map.Entry<Integer, UserInfo> userInfo : device.getUserInfos().entrySet()) {
+            if (userInfo.getValue().isSecondary()) {
+                return userInfo.getKey();
+            }
+        }
+        return -1;
+    }
+
+    /** Creates a secondary user and returns the new user ID. */
+    private static int createSecondaryUser(ITestDevice device) throws DeviceNotAvailableException {
+        final String createUserOutput = device.executeShellCommand("pm create-user secondary");
+        final int userId = Integer.parseInt(createUserOutput.split(" id ")[1].trim());
+        device.executeShellCommand("am start-user -w " + userId);
+        return userId;
+    }
+
+    @Override
+    public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        int userId = Integer.parseInt(testInfo.properties().get(RUN_TESTS_AS_USER_KEY));
+
+        testInfo.getDevice().removeUser(userId);
+    }
+}
diff --git a/test_framework/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
new file mode 100644
index 0000000..274a096
--- /dev/null
+++ b/test_framework/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
@@ -0,0 +1,100 @@
+/*
+ * 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.targetprep;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
+import com.android.tradefed.invoker.TestInformation;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An {@link ITargetPreparer} that creates a work profile in setup, and marks that tests should be
+ * run in that user.
+ *
+ * <p>In teardown, the work profile is removed.
+ */
+@OptionClass(alias = "run-on-work-profile")
+public class RunOnWorkProfileTargetPreparer extends BaseTargetPreparer {
+
+    @VisibleForTesting static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
+
+    @VisibleForTesting static final String TEST_PACKAGE_NAME_OPTION = "test-package-name";
+
+    @Option(
+            name = TEST_PACKAGE_NAME_OPTION,
+            description =
+                    "the name of a package to be installed on the work profile. "
+                            + "This must already be installed on the device.",
+            importance = Option.Importance.IF_UNSET)
+    private List<String> mTestPackages = new ArrayList<>();
+
+    @Override
+    public void setUp(TestInformation testInfo)
+            throws TargetSetupError, DeviceNotAvailableException {
+        int workProfileId = getWorkProfileId(testInfo.getDevice());
+
+        if (workProfileId != -1) {
+            // There is already a work profile - so we don't want to remove it
+            setDisableTearDown(true);
+        } else {
+            workProfileId = createWorkProfile(testInfo.getDevice());
+        }
+
+        for (String pkg : mTestPackages) {
+            testInfo.getDevice()
+                    .executeShellCommand("pm install-existing --user " + workProfileId + " " + pkg);
+        }
+
+        testInfo.properties().put(RUN_TESTS_AS_USER_KEY, Integer.toString(workProfileId));
+    }
+
+    /** Get the id of a work profile currently on the device. -1 if there is none */
+    private static int getWorkProfileId(ITestDevice device) throws DeviceNotAvailableException {
+        for (Map.Entry<Integer, UserInfo> userInfo : device.getUserInfos().entrySet()) {
+            if (userInfo.getValue().isManagedProfile()) {
+                return userInfo.getKey();
+            }
+        }
+        return -1;
+    }
+
+    /** Creates a work profile and returns the new user ID. */
+    private static int createWorkProfile(ITestDevice device) throws DeviceNotAvailableException {
+        int parentProfile = device.getCurrentUser();
+        final String createUserOutput =
+                device.executeShellCommand(
+                        "pm create-user --profileOf " + parentProfile + " --managed work");
+        final int profileId = Integer.parseInt(createUserOutput.split(" id ")[1].trim());
+        device.executeShellCommand("am start-user -w " + profileId);
+        return profileId;
+    }
+
+    @Override
+    public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        int workProfileId = Integer.parseInt(testInfo.properties().get(RUN_TESTS_AS_USER_KEY));
+
+        testInfo.getDevice().removeUser(workProfileId);
+    }
+}
diff --git a/test_framework/com/android/tradefed/targetprep/WifiPreparer.java b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
index 1bfc635..b1f14e0 100644
--- a/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
@@ -64,10 +64,14 @@
         if (mVerifyOnly) {
             if (!device.isWifiEnabled()) {
                 throw new TargetSetupError(
-                        "The device does not have wifi enabled.", device.getDeviceDescriptor());
+                        "The device does not have wifi enabled.",
+                        device.getDeviceDescriptor(),
+                        InfraErrorIdentifier.NO_WIFI);
             } else if (!device.checkConnectivity()) {
                 throw new TargetSetupError(
-                        "The device has no wifi connection.", device.getDeviceDescriptor());
+                        "The device has no wifi connection.",
+                        device.getDeviceDescriptor(),
+                        InfraErrorIdentifier.NO_WIFI);
             }
             return;
         }
diff --git a/test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java b/test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
index d436e13..cec9d30 100644
--- a/test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
@@ -95,10 +95,6 @@
     )
     private Set<String> mSystemFileNames = new TreeSet<>();
 
-    @Deprecated
-    @Option(name = "dummy-file-name", description = "use stub-file-name instead.")
-    private Set<String> mDummyFileNames = new TreeSet<>();
-
     @Option(
             name = "stub-file-name",
             description =
@@ -221,7 +217,6 @@
             systemFiles = replaceExistingEntries(systemFiles, files);
             filesNotInDeviceBuild.putAll(systemFiles);
 
-            mStubFileNames.addAll(mDummyFileNames);
             // Generate specified stub files and replace those in device build.
             Map<String, InputStreamFactory> stubFiles =
                     createStubInputStreamFactories(mStubFileNames);
diff --git a/test_framework/com/android/tradefed/testtype/ArtGTest.java b/test_framework/com/android/tradefed/testtype/ArtGTest.java
new file mode 100644
index 0000000..cf6a333
--- /dev/null
+++ b/test_framework/com/android/tradefed/testtype/ArtGTest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.testtype;
+
+import com.android.tradefed.targetprep.ArtChrootPreparer;
+
+public class ArtGTest extends GTest {
+    @Override
+    protected String getGTestCmdLineWrapper(String fullPath, String flags) {
+        String chroot = ArtChrootPreparer.CHROOT_PATH;
+        if (fullPath.startsWith(chroot)) {
+            fullPath = fullPath.substring(chroot.length());
+        }
+        return String.format("chroot %s %s %s", chroot, fullPath, flags);
+    }
+}
diff --git a/test_framework/com/android/tradefed/testtype/ArtRunTest.java b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
index a982104..dbeaae5 100644
--- a/test_framework/com/android/tradefed/testtype/ArtRunTest.java
+++ b/test_framework/com/android/tradefed/testtype/ArtRunTest.java
@@ -17,35 +17,51 @@
 package com.android.tradefed.testtype;
 
 import com.android.ddmlib.CollectingOutputReceiver;
-
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
+import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.FileUtil;
 
+import difflib.DiffUtils;
+import difflib.Patch;
+
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
 
 /** A test runner to run ART run-tests. */
-public class ArtRunTest implements IDeviceTest, IRemoteTest, IAbiReceiver {
+public class ArtRunTest implements IDeviceTest, IRemoteTest, IAbiReceiver, ITestFilterReceiver {
 
     private static final String RUNTEST_TAG = "ArtRunTest";
 
     private static final String DALVIKVM_CMD =
             "dalvikvm|#BITNESS#| -classpath |#CLASSPATH#| |#MAINCLASS#|";
+    public static final String CHECKER_EXECUTABLE = "art/tools/checker/checker.py";
 
     @Option(
             name = "test-timeout",
@@ -63,6 +79,8 @@
 
     private ITestDevice mDevice = null;
     private IAbi mAbi = null;
+    private Set<String> mIncludeFilters = new LinkedHashSet<>();
+    private Set<String> mExcludeFilters = new LinkedHashSet<>();
 
     /** {@inheritDoc} */
     @Override
@@ -89,6 +107,54 @@
 
     /** {@inheritDoc} */
     @Override
+    public void addIncludeFilter(String filter) {
+        mIncludeFilters.add(filter);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addAllIncludeFilters(Set<String> filters) {
+        mIncludeFilters.addAll(filters);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addExcludeFilter(String filter) {
+        mExcludeFilters.add(filter);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addAllExcludeFilters(Set<String> filters) {
+        mExcludeFilters.addAll(filters);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<String> getIncludeFilters() {
+        return mIncludeFilters;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<String> getExcludeFilters() {
+        return mExcludeFilters;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearIncludeFilters() {
+        mIncludeFilters.clear();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearExcludeFilters() {
+        mExcludeFilters.clear();
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public void run(TestInformation testInfo, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         if (mDevice == null) {
@@ -111,24 +177,29 @@
      * Run a single ART run-test (on device).
      *
      * @param listener {@link ITestInvocationListener} listener for test
-     * @throws DeviceNotAvailableException
+     * @throws DeviceNotAvailableException If there was a problem communicating with
+     *      the test device.
      */
     void runArtTest(TestInformation testInfo, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
+        String abi = mAbi.getName();
+        String runName = String.format("%s_%s", RUNTEST_TAG, abi);
+        TestDescription testId = new TestDescription(runName, mRunTestName);
+        if (shouldSkipCurrentTest(testId)) {
+            return;
+        }
+
         CLog.i("Running ArtRunTest %s on %s", mRunTestName, mDevice.getSerialNumber());
 
         String cmd = DALVIKVM_CMD;
-        String abi = mAbi.getName();
         cmd = cmd.replace("|#BITNESS#|", AbiUtils.getBitness(abi));
         cmd = cmd.replace("|#CLASSPATH#|", ArrayUtil.join(File.pathSeparator, mClasspath));
         // TODO: Turn this into an an option of the `ArtRunTest` class?
         cmd = cmd.replace("|#MAINCLASS#|", "Main");
 
         CLog.d("About to run run-test command: %s", cmd);
-        String runName = String.format("%s_%s", RUNTEST_TAG, abi);
         // Note: We only run one test at the moment.
         int testCount = 1;
-        TestDescription testId = new TestDescription(runName, mRunTestName);
         listener.testRunStarted(runName, testCount);
         listener.testStarted(testId);
 
@@ -143,57 +214,171 @@
             // Check the output producted by the test.
             if (output != null) {
                 try {
-                    File expectedFile = getDependencyFileFromRunTestDir(testInfo, "expected.txt");
+                    String expectedFileName = String.format("%s-expected.txt", mRunTestName);
+                    File expectedFile =
+                            testInfo.getDependencyFile(expectedFileName, /* targetFirst */ true);
                     CLog.i("Found expected output for run-test %s: %s", mRunTestName, expectedFile);
                     String expected = FileUtil.readStringFromFile(expectedFile);
+
+                    // TODO: The "check" step should be configurable, as is the case in current ART
+                    // `run-test` scripts).
                     if (!output.equals(expected)) {
-                        String error = String.format("'%s' instead of '%s'", output, expected);
-                        // TODO: Implement better reporting, e.g. using a diff output.
-                        // Also, the "check" step should be configurable, as this is the case in
-                        // current ART run-test scripts).
-                        CLog.i("%s FAILED: %s", mRunTestName, error);
-                        listener.testFailed(testId, error);
+                        // Compute the difference between the expected and actual outputs.
+                        List<String> expectedLines = Arrays.asList(expected.split("\\r?\\n"));
+                        List<String> outputLines = Arrays.asList(output.split("\\r?\\n"));
+                        Patch<String> diff = DiffUtils.diff(expectedLines, outputLines);
+                        List<String> unifiedDiff =
+                                DiffUtils.generateUnifiedDiff(
+                                        "expected.txt", "stdout", expectedLines, diff, 3);
+                        // Produce a unified diff output for the error message.
+                        StringBuilder errorMessage =
+                                new StringBuilder(
+                                        "The test's standard output does not match the expected "
+                                                + "output:\n");
+                        for (String delta : unifiedDiff) {
+                            errorMessage.append(delta).append('\n');
+                        }
+                        CLog.i("%s FAILED: %s", mRunTestName, errorMessage.toString());
+                        listener.testFailed(testId, errorMessage.toString());
+                        return;
                     }
                 } catch (IOException ioe) {
                     CLog.e(
                             "I/O error while accessing expected output file for test %s: %s",
                             mRunTestName, ioe);
                     listener.testFailed(testId, "I/O error while accessing expected output file.");
+                    return;
                 }
             } else {
                 listener.testFailed(testId, "No output received to compare to.");
+                return;
+            }
+
+            if (mRunTestName.contains("-checker-")) {
+                // not particularly reliable way of constructing a temporary dir
+                String cfgPathDir =
+                        String.format("/data/local/tmp/%s", mRunTestName.replaceAll("/", "-"));
+                mDevice.executeShellCommand(String.format("mkdir -p \"%s\"", cfgPathDir));
+
+                String cfgPath = cfgPathDir + "/graph.cfg";
+                mDevice.executeShellCommand(
+                        String.format(
+                                "dex2oat --dex-file=%s --oat-file=/dev/null --dump-cfg=%s -j1",
+                                mClasspath.get(0), cfgPath));
+
+                File runTestDir;
+                try {
+                    runTestDir = getRunTestDir(testInfo);
+                } catch (FileNotFoundException e) {
+                    listener.testFailed(testId, "I/O error while accessing test dir.");
+                    return;
+                }
+
+                File localCfgPath = new File(runTestDir, "graph.cfg");
+                if (localCfgPath.isFile()) {
+                    localCfgPath.delete();
+                }
+
+                mDevice.pullFile(cfgPath, localCfgPath);
+
+                File tempJar = new File(runTestDir, "temp.jar");
+                mDevice.pullFile(mClasspath.get(0), tempJar);
+
+                try (ZipFile archive = new ZipFile(tempJar)) {
+                    File srcFile = new File(runTestDir, "src");
+                    if (srcFile.exists()) {
+                        Files.walk(srcFile.toPath())
+                                .map(Path::toFile)
+                                .sorted(Comparator.reverseOrder())
+                                .forEach(File::delete);
+                    }
+
+                    List<? extends ZipEntry> entries = archive.stream()
+                            .sorted(Comparator.comparing(ZipEntry::getName))
+                            .collect(Collectors.toList());
+
+                    for (ZipEntry entry : entries) {
+                        if (entry.getName().startsWith("src")) {
+                            Path entryDest = runTestDir.toPath().resolve(entry.getName());
+                            if (entry.isDirectory()) {
+                                Files.createDirectory(entryDest);
+                            } else {
+                                Files.copy(archive.getInputStream(entry), entryDest);
+                            }
+                        }
+                    }
+                } catch (IOException e) {
+                    listener.testFailed(testId, "Error unpacking test jar");
+                    CLog.e("Jar unpacking failed with exception %s", e);
+                    CLog.e(e);
+                    return;
+                }
+
+                String checkerArch = AbiUtils.getArchForAbi(abi).toUpperCase();
+
+                ProcessBuilder processBuilder =
+                        new ProcessBuilder(
+                                CHECKER_EXECUTABLE,
+                                "-q",
+                                "--arch=" + checkerArch,
+                                localCfgPath.getAbsolutePath(),
+                                runTestDir.getAbsolutePath());
+
+                try {
+                    Process process = processBuilder.start();
+                    if (process.waitFor() != 0) {
+                        String checkerOutput = new BufferedReader(
+                                new InputStreamReader(process.getErrorStream())).lines().collect(
+                                Collectors.joining("\n"));
+                        listener.testFailed(testId, "Checker failed\n" + checkerOutput);
+                        listener.testLog("graph.cfg", LogDataType.CFG,
+                                new FileInputStreamSource(localCfgPath));
+                    }
+                } catch (IOException | InterruptedException e) {
+                    listener.testFailed(testId, "I/O error while starting Checker process");
+                }
             }
         } finally {
-            HashMap<String, Metric> emptyTestMetrics = new HashMap();
+            HashMap<String, Metric> emptyTestMetrics = new HashMap<>();
             listener.testEnded(testId, emptyTestMetrics);
-            HashMap<String, Metric> emptyTestRunMetrics = new HashMap();
+            HashMap<String, Metric> emptyTestRunMetrics = new HashMap<>();
             // TODO: Pass an actual value as `elapsedTimeMillis` argument.
             listener.testRunEnded(/* elapsedTimeMillis*/ 0, emptyTestRunMetrics);
         }
     }
 
+    /**
+     * Check if current test should be skipped.
+     *
+     * @param description The test in progress.
+     * @return true if the test should be skipped.
+     */
+    private boolean shouldSkipCurrentTest(TestDescription description) {
+        // Force to skip any test not listed in include filters, or listed in exclude filters.
+        // exclude filters have highest priority.
+        String testName = description.getTestName();
+        String descString = description.toString();
+        if (mExcludeFilters.contains(testName) || mExcludeFilters.contains(descString)) {
+            return true;
+        }
+        if (!mIncludeFilters.isEmpty()) {
+            return !mIncludeFilters.contains(testName) && !mIncludeFilters.contains(descString);
+        }
+        return false;
+    }
+
     /** Create an output receiver for the test command executed on the device. */
     protected CollectingOutputReceiver createTestOutputReceiver() {
         return new CollectingOutputReceiver();
     }
 
-    /** Search for a dependency/artifact file in the run-test's directory. */
-    protected File getDependencyFileFromRunTestDir(TestInformation testInfo, String fileName)
-            throws FileNotFoundException {
+    private File getRunTestDir(TestInformation testInfo) throws FileNotFoundException {
         File testsDir = testInfo.executionFiles().get(FilesKey.TARGET_TESTS_DIRECTORY);
         if (testsDir == null || !testsDir.exists()) {
             throw new FileNotFoundException(
                     String.format(
                             "Could not find target tests directory for test %s.", mRunTestName));
         }
-        File runTestDir = new File(testsDir, mRunTestName);
-        File file = FileUtil.findFile(runTestDir, fileName);
-        if (file == null) {
-            throw new FileNotFoundException(
-                    String.format(
-                            "Could not find an artifact file associated with %s in directory %s.",
-                            fileName, runTestDir));
-        }
-        return file;
+        return new File(testsDir, mRunTestName);
     }
 }
diff --git a/test_framework/com/android/tradefed/testtype/GTest.java b/test_framework/com/android/tradefed/testtype/GTest.java
index 70beba9..cd971f4 100644
--- a/test_framework/com/android/tradefed/testtype/GTest.java
+++ b/test_framework/com/android/tradefed/testtype/GTest.java
@@ -158,6 +158,10 @@
         return testPath.toString();
     }
 
+    public void setNativeTestDevicePath(String path) {
+        mNativeTestDevicePath = path;
+    }
+
     /**
      * Executes all native tests in a folder as well as in all subfolders recursively.
      *
diff --git a/test_framework/com/android/tradefed/testtype/GTestBase.java b/test_framework/com/android/tradefed/testtype/GTestBase.java
index 316d2f9..a7eaea7 100644
--- a/test_framework/com/android/tradefed/testtype/GTestBase.java
+++ b/test_framework/com/android/tradefed/testtype/GTestBase.java
@@ -547,6 +547,14 @@
     }
 
     /**
+     * Helper which allows derived classes to wrap the gtest command under some other tool (chroot,
+     * strace, gdb, and similar).
+     */
+    protected String getGTestCmdLineWrapper(String fullPath, String flags) {
+        return String.format("%s %s", fullPath, flags);
+    }
+
+    /**
      * Helper method to build the gtest command to run.
      *
      * @param fullPath absolute file system path to gtest binary on device
@@ -568,7 +576,7 @@
             gTestCmdLine.append(String.format("su %s ", mRunTestAs));
         }
 
-        gTestCmdLine.append(String.format("%s %s", fullPath, flags));
+        gTestCmdLine.append(getGTestCmdLineWrapper(fullPath, flags));
         return gTestCmdLine.toString();
     }
 
diff --git a/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java b/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
index 373844a..837ba81 100644
--- a/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
+++ b/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
+import com.android.tradefed.util.StringEscapeUtils;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -406,7 +407,7 @@
         if (iterator.hasNext()) {
             filterFlag.append(String.format(" %s=%s", GBENCHMARK_FILTER_OPTION, iterator.next()));
             while (iterator.hasNext()) {
-                filterFlag.append(String.format("\\|%s", iterator.next()));
+                filterFlag.append(String.format("|%s", iterator.next()));
             }
         }
         return filterFlag.toString();
@@ -421,7 +422,7 @@
             // Format benchmark as "^benchmark$" to avoid unintended regex partial matching.
             filterFlag.append(String.format(" %s=^%s$", GBENCHMARK_FILTER_OPTION, iterator.next()));
             while (iterator.hasNext()) {
-                filterFlag.append(String.format("\\|^%s$", iterator.next()));
+                filterFlag.append(String.format("|^%s$", iterator.next()));
             }
         }
         return filterFlag.toString();
@@ -441,14 +442,14 @@
             final String cmd,
             final IShellOutputReceiver outputReceiver)
             throws DeviceNotAvailableException {
+        String shellCmd = StringEscapeUtils.escapeShell(cmd);
         // Ensure that command is not too long for adb
-        if (cmd.length() < ADB_CMD_CHAR_LIMIT) {
+        if (shellCmd.length() < ADB_CMD_CHAR_LIMIT) {
             if (outputReceiver == null) {
-                return testDevice.executeShellCommand(cmd);
+                return testDevice.executeShellCommand(shellCmd);
             }
-
             testDevice.executeShellCommand(
-                    cmd,
+                    shellCmd,
                     outputReceiver,
                     mMaxRunTime /* maxTimeToShellOutputResponse */,
                     TimeUnit.MILLISECONDS,
@@ -457,7 +458,7 @@
         }
 
         // Wrap adb shell command in script if command is too long for direct execution
-        return executeCommandByScript(testDevice, cmd, outputReceiver);
+        return executeCommandByScript(testDevice, shellCmd, outputReceiver);
     }
 
     /** Runs a command from a temporary script. */
diff --git a/test_framework/com/android/tradefed/testtype/HostGTest.java b/test_framework/com/android/tradefed/testtype/HostGTest.java
index 43d599e..3da63e4 100644
--- a/test_framework/com/android/tradefed/testtype/HostGTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostGTest.java
@@ -41,21 +41,10 @@
 
 /** A Test that runs a native test package. */
 @OptionClass(alias = "hostgtest")
-public class HostGTest extends GTestBase implements IAbiReceiver, IBuildReceiver {
+public class HostGTest extends GTestBase implements IBuildReceiver {
     private static final long DEFAULT_HOST_COMMAND_TIMEOUT_MS = 2 * 60 * 1000;
 
     private IBuildInfo mBuildInfo = null;
-    private IAbi mAbi = null;
-
-    @Override
-    public void setAbi(IAbi abi) {
-        this.mAbi = abi;
-    }
-
-    @Override
-    public IAbi getAbi() {
-        return this.mAbi;
-    }
 
     @Override
     public void setBuild(IBuildInfo buildInfo) {
@@ -216,7 +205,7 @@
         String moduleName = getTestModule();
         File gTestFile = null;
         try {
-            gTestFile = FileUtil.findFile(moduleName, mAbi, scanDirs.toArray(new File[] {}));
+            gTestFile = FileUtil.findFile(moduleName, getAbi(), scanDirs.toArray(new File[] {}));
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
@@ -226,7 +215,8 @@
             // search for it with a potential suffix (which is allowed).
             try {
                 File byBaseName =
-                        FileUtil.findFile(moduleName + ".*", mAbi, scanDirs.toArray(new File[] {}));
+                        FileUtil.findFile(
+                                moduleName + ".*", getAbi(), scanDirs.toArray(new File[] {}));
                 if (byBaseName != null && byBaseName.isFile()) {
                     gTestFile = byBaseName;
                 }
diff --git a/test_framework/com/android/tradefed/testtype/InstrumentationTest.java b/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
index bba5fa3..b1f7f1a 100644
--- a/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
+++ b/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
@@ -47,6 +47,7 @@
 import com.android.tradefed.result.BugreportCollector;
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.ITestLifeCycleReceiver;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
@@ -100,6 +101,8 @@
     /** default timeout for tests collection */
     static final long TEST_COLLECTION_TIMEOUT_MS = 2 * 60 * 1000;
 
+    static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
+
     /** test run name for merging coverage measurements */
     static final String MERGE_COVERAGE_MEASUREMENTS_TEST_NAME = "mergeCoverageMeasurements";
 
@@ -886,7 +889,8 @@
                     "Tests to run should not be set explicitly when --collect-tests-only is set.");
 
             // Use the actual listener to collect the tests, and print a error if this fails
-            Collection<TestDescription> collectedTests = collectTestsToRun(mRunner, listener);
+            Collection<TestDescription> collectedTests =
+                    collectTestsToRun(testInfo, mRunner, listener);
             if (collectedTests == null) {
                 CLog.e("Failed to collect tests for %s", mPackageName);
             } else {
@@ -899,7 +903,7 @@
         Collection<TestDescription> testsToRun = mTestsToRun;
         if (testsToRun == null) {
             // Don't notify the listener since it's not a real run.
-            testsToRun = collectTestsToRun(mRunner, null);
+            testsToRun = collectTestsToRun(testInfo, mRunner, null);
         }
 
         // Only set the debug flag after collecting tests.
@@ -962,7 +966,7 @@
 
         if (testsToRun == null) {
             // Failed to collect the tests or collection is off. Just try to run them all.
-            mDevice.runInstrumentationTests(mRunner, listener);
+            runInstrumentationTests(testInfo, mRunner, listener);
         } else if (!testsToRun.isEmpty()) {
             runWithRerun(testInfo, listener, testsToRun);
         } else {
@@ -977,6 +981,20 @@
         }
     }
 
+    private boolean runInstrumentationTests(
+            TestInformation testInfo,
+            IRemoteAndroidTestRunner runner,
+            ITestLifeCycleReceiver... receivers)
+            throws DeviceNotAvailableException {
+        if (testInfo != null && testInfo.properties().containsKey(RUN_TESTS_AS_USER_KEY)) {
+            return mDevice.runInstrumentationTestsAsUser(
+                    runner,
+                    Integer.parseInt(testInfo.properties().get(RUN_TESTS_AS_USER_KEY)),
+                    receivers);
+        }
+        return mDevice.runInstrumentationTests(runner, receivers);
+    }
+
     /**
      * Returns a listener that will collect bugreports, or the original {@code listener} if this
      * feature is disabled.
@@ -1071,7 +1089,7 @@
                     getDevice().getProcessByName("system_server"));
         }
         instrumentationListener.setReportUnexecutedTests(mReportUnexecuted);
-        mDevice.runInstrumentationTests(mRunner, instrumentationListener);
+        runInstrumentationTests(testInfo, mRunner, instrumentationListener);
         TestRunResult testRun = testTracker.getCurrentRunResults();
         if (testRun.isRunFailure() || !testRun.getCompletedTests().containsAll(expectedTests)) {
             // Don't re-run any completed tests, unless this is a coverage run.
@@ -1172,7 +1190,9 @@
      * @throws DeviceNotAvailableException
      */
     private Collection<TestDescription> collectTestsToRun(
-            final IRemoteAndroidTestRunner runner, final ITestInvocationListener listener)
+            final TestInformation testInfo,
+            final IRemoteAndroidTestRunner runner,
+            final ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         if (isRerunMode()) {
             Log.d(LOG_TAG, String.format("Collecting test info for %s on device %s",
@@ -1182,7 +1202,7 @@
             runner.setDebug(false);
             // try to collect tests multiple times, in case device is temporarily not available
             // on first attempt
-            Collection<TestDescription> tests = collectTestsAndRetry(runner, listener);
+            Collection<TestDescription> tests = collectTestsAndRetry(testInfo, runner, listener);
             // done with "logOnly" mode, restore proper test timeout before real test execution
             addTimeoutsToRunner(runner);
             runner.setTestCollection(false);
@@ -1202,7 +1222,9 @@
      */
     @VisibleForTesting
     Collection<TestDescription> collectTestsAndRetry(
-            final IRemoteAndroidTestRunner runner, final ITestInvocationListener listener)
+            final TestInformation testInfo,
+            final IRemoteAndroidTestRunner runner,
+            final ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         boolean communicationFailure = false;
         for (int i=0; i < COLLECT_TESTS_ATTEMPTS; i++) {
@@ -1211,9 +1233,9 @@
             // We allow to override the ddmlib default timeout for collection of tests.
             runner.setMaxTimeToOutputResponse(mCollectTestTimeout, TimeUnit.MILLISECONDS);
             if (listener == null) {
-                instrResult = mDevice.runInstrumentationTests(runner, collector);
+                instrResult = runInstrumentationTests(testInfo, runner, collector);
             } else {
-                instrResult = mDevice.runInstrumentationTests(runner, collector, listener);
+                instrResult = runInstrumentationTests(testInfo, runner, collector, listener);
             }
             TestRunResult runResults = collector.getCurrentRunResults();
             if (!instrResult || !runResults.isRunComplete()) {
diff --git a/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java
index 67cd44c..950f8df 100644
--- a/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java
@@ -24,10 +24,13 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FailureDescription;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
 import com.android.tradefed.targetprep.adb.AdbStopServerPreparer;
 import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IDeviceTest;
@@ -37,6 +40,7 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.PythonVirtualenvHelper;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
 
@@ -48,6 +52,7 @@
 import java.io.InputStream;
 import java.io.Writer;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -136,7 +141,11 @@
 
     @Override
     public final void run(ITestInvocationListener listener) {
-        List<File> parFilesList = findParFiles();
+        List<File> parFilesList = findParFiles(listener);
+        File venvDir = mBuildInfo.getFile("VIRTUAL_ENV");
+        if (venvDir != null) {
+            PythonVirtualenvHelper.activate(getRunUtil(), venvDir);
+        }
         for (File parFile : parFilesList) {
             // TODO(b/159365341): add a failure reporting for nonexistent binary.
             if (!parFile.exists()) {
@@ -153,9 +162,13 @@
                 reportLogs(getLogDir(), listener);
             }
         }
+        if (venvDir != null
+                && venvDir.getAbsolutePath().startsWith(System.getProperty("java.io.tmpdir"))) {
+            FileUtil.recursiveDelete(venvDir);
+        }
     }
 
-    private List<File> findParFiles() {
+    private List<File> findParFiles(ITestInvocationListener listener) {
         File testsDir = null;
         if (mBuildInfo instanceof IDeviceBuildInfo) {
             testsDir = ((IDeviceBuildInfo) mBuildInfo).getTestsDir();
@@ -169,6 +182,8 @@
                 res = FileUtil.findFile(testsDir, binaryName);
             }
             if (res == null) {
+                reportFailure(
+                        listener, binaryName, "Couldn't find Mobly test binary " + binaryName);
                 throw new RuntimeException(
                         String.format("Couldn't find a par file %s", binaryName));
             }
@@ -251,31 +266,43 @@
         InputStream inputStream = null;
         try {
             inputStream = new FileInputStream(yamlSummaryFile);
-            processYamlTestResults(inputStream, parser);
+            processYamlTestResults(inputStream, parser, listener, runName);
         } catch (FileNotFoundException ex) {
-            // TODO(b/159367088): report a test failure.
-            CLog.e("Fail processing test results: ", ex);
+            reportFailure(
+                    listener,
+                    runName,
+                    "Fail processing test results, result file not found.\n" + ex);
         } finally {
             StreamUtil.close(inputStream);
         }
     }
 
+    /**
+     * Parses Mobly test results and does result reporting.
+     *
+     * @param inputStream An InputStream object reading in Mobly test result file.
+     * @param parser An MoblyYamlResultParser object that processes Mobly test results.
+     * @param listener An ITestInvocationListener instance that does various reporting.
+     * @param runName str, the name of the Mobly test binary run.
+     */
     @VisibleForTesting
-    protected void processYamlTestResults(InputStream inputStream, MoblyYamlResultParser parser) {
+    protected void processYamlTestResults(
+            InputStream inputStream,
+            MoblyYamlResultParser parser,
+            ITestInvocationListener listener,
+            String runName) {
         try {
             parser.parse(inputStream);
         } catch (MoblyYamlResultHandlerFactory.InvalidResultTypeException
                 | IllegalAccessException
                 | InstantiationException ex) {
-            // TODO(b/159367088): report a test failure.
-            CLog.e("Failed to parse result file: %s", ex);
+            reportFailure(listener, runName, "Failed to parse the result file.\n" + ex);
         }
     }
 
     private void updateConfigFile() {
         InputStream inputStream = null;
         FileWriter fileWriter = null;
-        // TODO(b/159369745): clean up the tmp files created.
         File localConfigFile = new File(getLogDir(), "local_config.yaml");
         try {
             inputStream = new FileInputStream(mConfigFile);
@@ -334,6 +361,15 @@
         return mLogDir;
     }
 
+    private void reportFailure(
+            ITestInvocationListener listener, String runName, String errorMessage) {
+        listener.testRunStarted(runName, 0);
+        FailureDescription description =
+                FailureDescription.create(errorMessage, FailureStatus.TEST_FAILURE);
+        listener.testRunFailed(description);
+        listener.testRunEnded(0L, new HashMap<String, Metric>());
+    }
+
     @VisibleForTesting
     String getLogDirAbsolutePath() {
         return getLogDir().getAbsolutePath();
@@ -348,6 +384,8 @@
     protected String[] buildCommandLineArray(String filePath) {
         List<String> commandLine = new ArrayList<>();
         commandLine.add(filePath);
+        // TODO(b/166468397): some test binaries are actually a wrapper of Mobly runner and need --
+        //  to separate Python options.
         commandLine.add("--");
         if (getConfigPath() != null) {
             commandLine.add("--config=" + getConfigPath());
diff --git a/test_framework/com/android/tradefed/testtype/mobly/MoblyYamlResultHandlerFactory.java b/test_framework/com/android/tradefed/testtype/mobly/MoblyYamlResultHandlerFactory.java
index 4d4a7ae..e093851 100644
--- a/test_framework/com/android/tradefed/testtype/mobly/MoblyYamlResultHandlerFactory.java
+++ b/test_framework/com/android/tradefed/testtype/mobly/MoblyYamlResultHandlerFactory.java
@@ -45,6 +45,8 @@
         return resultHandler;
     }
 
+    // MoblyYamlResultHandlerFactory won't be serialized so suppress serial warning.
+    @SuppressWarnings("serial")
     public class InvalidResultTypeException extends Exception {
         public InvalidResultTypeException(String errorMsg) {
             super(errorMsg);
@@ -59,9 +61,9 @@
         SUMMARY("Summary", MoblyYamlResultSummaryHandler.class);
 
         private String tag;
-        private Class handlerClass;
+        private Class<?> handlerClass;
 
-        Type(String tag, Class handlerClass) {
+        Type(String tag, Class<?> handlerClass) {
             this.tag = tag;
             this.handlerClass = handlerClass;
         }
diff --git a/test_framework/com/android/tradefed/testtype/mobly/MoblyYamlResultParser.java b/test_framework/com/android/tradefed/testtype/mobly/MoblyYamlResultParser.java
index d3a692c..e3dda00 100644
--- a/test_framework/com/android/tradefed/testtype/mobly/MoblyYamlResultParser.java
+++ b/test_framework/com/android/tradefed/testtype/mobly/MoblyYamlResultParser.java
@@ -40,9 +40,9 @@
 public class MoblyYamlResultParser {
     private static final String TYPE = "Type";
     private ImmutableList.Builder<ITestInvocationListener> mListenersBuilder =
-            new ImmutableList.Builder();
+            new ImmutableList.Builder<>();
     private final String mRunName;
-    private ImmutableList.Builder<ITestResult> mResultCacheBuilder = new ImmutableList.Builder();
+    private ImmutableList.Builder<ITestResult> mResultCacheBuilder = new ImmutableList.Builder<>();
     private int mTestCount;
     private long mRunStartTime;
     private long mRunEndTime;
diff --git a/test_framework/com/android/tradefed/util/PythonVirtualenvHelper.java b/test_framework/com/android/tradefed/util/PythonVirtualenvHelper.java
new file mode 100644
index 0000000..35c4190
--- /dev/null
+++ b/test_framework/com/android/tradefed/util/PythonVirtualenvHelper.java
@@ -0,0 +1,128 @@
+/*
+ * 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.util;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+import java.util.stream.Stream;
+
+/** A helper class for activating Python 3 virtual environment. */
+public class PythonVirtualenvHelper {
+
+    private static final String PATH = "PATH";
+    private static final String PYTHONHOME = "PYTHONHOME";
+    private static final String PYTHONPATH = "PYTHONPATH";
+    public static final String VIRTUAL_ENV = "VIRTUAL_ENV";
+
+    /**
+     * Gets python bin directory path.
+     *
+     * <p>This method will check the directory existence.
+     *
+     * @return str, the path to the python bin directory in venv.
+     * @throws NullPointerException if arg virtualenvPath is null.
+     * @throws RuntimeException if /path/to/venv/bin does not exist.
+     */
+    public static String getPythonBinDir(String virtualenvPath) {
+        if (virtualenvPath == null) {
+            throw new NullPointerException(
+                    "Path to the Python virtual environment should not be null");
+        }
+        File res = new File(virtualenvPath, "bin");
+        if (!res.exists()) {
+            throw new RuntimeException("Invalid python virtualenv path " + res.getAbsolutePath());
+        }
+        return res.getAbsolutePath();
+    }
+
+    /**
+     * Activate virtualenv for a RunUtil.
+     *
+     * @param runUtil an utility object for running virtualenv activation commands.
+     * @param virtualenvDir a File object representing the created virtualenv directory.
+     */
+    public static void activate(IRunUtil runUtil, File virtualenvDir) {
+        activate(runUtil, virtualenvDir.getAbsolutePath());
+    }
+
+    /**
+     * Activate virtualenv for a RunUtil.
+     *
+     * <p>This method will check for python bin directory existence
+     *
+     * @param runUtil an utility object for running virtualenv activation commands.
+     * @param virtualenvPath the path to the created virtualenv directory.
+     */
+    public static void activate(IRunUtil runUtil, String virtualenvPath) {
+        String pythonBinDir = getPythonBinDir(virtualenvPath);
+        String separater = ":";
+        String pythonPath =
+                getPackageInstallLocation(runUtil, virtualenvPath)
+                        + separater
+                        + System.getenv(PYTHONPATH);
+        runUtil.setEnvVariable(PATH, pythonBinDir + separater + System.getenv().get(PATH));
+        runUtil.setEnvVariable(VIRTUAL_ENV, virtualenvPath);
+        runUtil.setEnvVariable(PYTHONPATH, pythonPath);
+        runUtil.unsetEnvVariable(PYTHONHOME);
+        CLog.d("Activating virtual environment:");
+        CLog.d("%s: %s", PATH, pythonBinDir + separater + System.getenv().get(PATH));
+        CLog.d("%s: %s", VIRTUAL_ENV, virtualenvPath);
+        CLog.d("%s: %s", PYTHONPATH, pythonPath);
+    }
+
+    /**
+     * Gets the absolute path to the pip3 binary in the given venv directory.
+     *
+     * @param virtualenvPath the path to the venv directory.
+     * @return a string representing the absolute path to the pip3 binary.
+     */
+    private static String getPipPath(String virtualenvPath) {
+        File pipFile = new File(PythonVirtualenvHelper.getPythonBinDir(virtualenvPath), "pip3");
+        return pipFile.getAbsolutePath();
+    }
+
+    /**
+     * Gets python package install location.
+     *
+     * <p>This method will call /path/to/venv/bin/pip3 show pip and parse out package location from
+     * stdout output.
+     *
+     * @param runUtil an utility object for running for running commands.
+     * @param virtualenvPath the path to the created virtualenv directory.
+     * @return a string representing the absolute path to the location where Python packages are
+     *     installed.
+     */
+    private static String getPackageInstallLocation(IRunUtil runUtil, String virtualenvPath) {
+        CommandResult result =
+                runUtil.runTimedCmd(60000, getPipPath(virtualenvPath), "show", "pip");
+        if (result.getStatus() != CommandStatus.SUCCESS) {
+            throw new RuntimeException(
+                    String.format(
+                            "Fail to run command: %s show pip.\nStatus:%s\nStdout:%s\nStderr:%s",
+                            getPipPath(virtualenvPath),
+                            result.getStatus(),
+                            result.getStdout(),
+                            result.getStderr()));
+        }
+        String stdout = result.getStdout();
+        String[] lines = stdout.split("\n");
+        String locationLine =
+                Stream.of(lines).filter(x -> x.startsWith("Location")).findFirst().orElse("");
+        return locationLine.split(" ")[1];
+    }
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index 7d1334a..caf95bb 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -12,6 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+java_library_host {
+    name: "tradefed-test-protos",
+    srcs: ["res/**/*.proto"],
+    libs: [
+        "libprotobuf-java-full",
+    ],
+    proto: {
+        include_dirs: ["external/protobuf/src"],
+        type: "full",
+    }
+}
+
 tradefed_java_library_host {
     name: "tradefed-tests",
     defaults: ["tradefed_errorprone_defaults"],
@@ -30,6 +42,7 @@
         "easymock",
         "objenesis",
         "mockito",
+        "tradefed-test-protos",
     ],
     libs: [
         "tradefed",
diff --git a/tests/res/proto/proto_util_test.proto b/tests/res/proto/proto_util_test.proto
new file mode 100644
index 0000000..a593326
--- /dev/null
+++ b/tests/res/proto/proto_util_test.proto
@@ -0,0 +1,38 @@
+// 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.
+
+syntax = "proto3";
+
+option java_package = "com.android.tradefed.util.test";
+option java_outer_classname = "ProtoUtilTestProto";
+
+// A proto for com.android.tradefed.util.ProtoUtilTest.
+message TestMessage {
+    string string_field = 1;
+    int64 int_field = 2;
+    repeated string repeated_string_field = 3;
+
+    message SubMessage {
+        int64 int_field = 1;
+        repeated string repeated_string_field = 2;
+    }
+
+    SubMessage message_field = 4;
+    repeated SubMessage repeated_message_field = 5;
+
+    oneof oneof_field {
+        string oneof_string_field = 6;
+        SubMessage oneof_message_field = 7;
+    }
+}
diff --git a/tests/res/testconfigs/yaml/not-target-preparer.tf_yaml b/tests/res/testconfigs/yaml/not-target-preparer.tf_yaml
new file mode 100644
index 0000000..610b637
--- /dev/null
+++ b/tests/res/testconfigs/yaml/not-target-preparer.tf_yaml
@@ -0,0 +1,16 @@
+description: "Human friendly description of the test"
+
+# pre_setup_action will run before the dependencies installation
+pre_setup_action:
+   - action:
+        name: "com.android.tradefed.testtype.AndroidJUnitTest"
+        options:
+          - package: "android.package"
+
+dependencies:
+
+tests:
+   - test:
+       name: "com.android.tradefed.testtype.AndroidJUnitTest"
+       options:
+         - package: "android.package"
diff --git a/tests/res/testconfigs/yaml/test-config.tf_yaml b/tests/res/testconfigs/yaml/test-config.tf_yaml
index eb89137..2df8a06 100644
--- a/tests/res/testconfigs/yaml/test-config.tf_yaml
+++ b/tests/res/testconfigs/yaml/test-config.tf_yaml
@@ -1,11 +1,29 @@
 description: "Human friendly description of the test"
 
+# pre_setup_action will run before the dependencies installation
+pre_setup_action:
+   - action:
+        name: "com.android.tradefed.targetprep.RunCommandTargetPreparer"
+        options:
+          - run-command: "dumpsys value"
+   - action:
+        name: "com.android.tradefed.targetprep.RunCommandTargetPreparer"
+        options:
+          - run-command: "another one"
+
 dependencies:
    - apks: ["test.apk", "test2.apk"]
    - apks: ["test1.apk"]
    - files: ["file1.txt", "file2.txt"]
    - device_files: {"tobepushed.txt": "/sdcard", "tobepushed2.txt": "/sdcard/"}
 
+# post_setup_action will run after the dependencies installation
+post_setup_action:
+   - action:
+        name: "com.android.tradefed.targetprep.RunCommandTargetPreparer"
+        options:
+          - run-command: "dumpsys value2"
+
 tests:
    - test:
        name: "com.android.tradefed.testtype.AndroidJUnitTest"
diff --git a/tests/res/util/partial_zip.zip b/tests/res/util/partial_zip.zip
index 1f50fef..4fb046e 100644
--- a/tests/res/util/partial_zip.zip
+++ b/tests/res/util/partial_zip.zip
Binary files differ
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 897aace..74717a5 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -89,6 +89,7 @@
 import com.android.tradefed.device.cloud.GceSshTunnelMonitorTest;
 import com.android.tradefed.device.cloud.ManagedRemoteDeviceTest;
 import com.android.tradefed.device.cloud.NestedRemoteDeviceTest;
+import com.android.tradefed.device.cloud.RemoteAndroidVirtualDeviceTest;
 import com.android.tradefed.device.cloud.RemoteFileUtilTest;
 import com.android.tradefed.device.contentprovider.ContentProviderHandlerTest;
 import com.android.tradefed.device.helper.TelephonyHelperTest;
@@ -96,33 +97,21 @@
 import com.android.tradefed.device.metric.AtraceRunMetricCollectorTest;
 import com.android.tradefed.device.metric.AutoLogCollectorTest;
 import com.android.tradefed.device.metric.BaseDeviceMetricCollectorTest;
-import com.android.tradefed.device.metric.BuddyInfoMetricCollectorTest;
-import com.android.tradefed.device.metric.BugreportzMetricCollectorTest;
 import com.android.tradefed.device.metric.BugreportzOnFailureCollectorTest;
 import com.android.tradefed.device.metric.DebugHostLogOnFailureCollectorTest;
 import com.android.tradefed.device.metric.DeviceMetricDataTest;
-import com.android.tradefed.device.metric.DumpHeapCollectorTest;
 import com.android.tradefed.device.metric.FilePullerDeviceMetricCollectorTest;
 import com.android.tradefed.device.metric.FilePullerLogCollectorTest;
-import com.android.tradefed.device.metric.GraphicsStatsMetricCollectorTest;
 import com.android.tradefed.device.metric.GcovCodeCoverageCollectorTest;
 import com.android.tradefed.device.metric.HostStatsdMetricCollectorTest;
 import com.android.tradefed.device.metric.IncidentReportCollectorTest;
-import com.android.tradefed.device.metric.IonHeapInfoMetricCollectorTest;
 import com.android.tradefed.device.metric.JavaCodeCoverageCollectorTest;
 import com.android.tradefed.device.metric.LogcatOnFailureCollectorTest;
 import com.android.tradefed.device.metric.LogcatTimingMetricCollectorTest;
-import com.android.tradefed.device.metric.MemInfoMetricCollectorTest;
-import com.android.tradefed.device.metric.PagetypeInfoMetricCollectorTest;
 import com.android.tradefed.device.metric.PerfettoPullerMetricCollectorTest;
-import com.android.tradefed.device.metric.ProcessMaxMemoryCollectorTest;
 import com.android.tradefed.device.metric.RebootReasonCollectorTest;
 import com.android.tradefed.device.metric.RuntimeRestartCollectorTest;
-import com.android.tradefed.device.metric.ScheduleMultipleDeviceMetricCollectorTest;
-import com.android.tradefed.device.metric.ScheduledDeviceMetricCollectorTest;
 import com.android.tradefed.device.metric.ScreenshotOnFailureCollectorTest;
-import com.android.tradefed.device.metric.TemperatureCollectorTest;
-import com.android.tradefed.device.metric.TraceMetricCollectorTest;
 import com.android.tradefed.device.recovery.BatteryUnavailableDeviceRecoveryTest;
 import com.android.tradefed.device.recovery.RunConfigDeviceRecoveryTest;
 import com.android.tradefed.device.recovery.UsbResetMultiDeviceRecoveryTest;
@@ -157,6 +146,7 @@
 import com.android.tradefed.log.LogRegistryTest;
 import com.android.tradefed.log.SimpleFileLoggerTest;
 import com.android.tradefed.log.TerribleFailureEmailHandlerTest;
+import com.android.tradefed.monitoring.LabResourceDeviceMonitorTest;
 import com.android.tradefed.postprocessor.AggregatePostProcessorTest;
 import com.android.tradefed.postprocessor.AveragePostProcessorTest;
 import com.android.tradefed.postprocessor.BasePostProcessorTest;
@@ -245,6 +235,8 @@
 import com.android.tradefed.targetprep.RestartSystemServerTargetPreparerTest;
 import com.android.tradefed.targetprep.RootTargetPreparerTest;
 import com.android.tradefed.targetprep.RunCommandTargetPreparerTest;
+import com.android.tradefed.targetprep.RunOnSecondaryUserTargetPreparerTest;
+import com.android.tradefed.targetprep.RunOnWorkProfileTargetPreparerTest;
 import com.android.tradefed.targetprep.RunHostCommandTargetPreparerTest;
 import com.android.tradefed.targetprep.RunHostScriptTargetPreparerTest;
 import com.android.tradefed.targetprep.StopServicesSetupTest;
@@ -260,6 +252,7 @@
 import com.android.tradefed.targetprep.multi.MixImageZipPreparerTest;
 import com.android.tradefed.targetprep.suite.SuiteApkInstallerTest;
 import com.android.tradefed.testtype.AndroidJUnitTestTest;
+import com.android.tradefed.testtype.ArtGTestTest;
 import com.android.tradefed.testtype.ArtRunTestTest;
 import com.android.tradefed.testtype.ClangCodeCoverageListenerTest;
 import com.android.tradefed.testtype.DeviceBatteryLevelCheckerTest;
@@ -365,7 +358,9 @@
 import com.android.tradefed.util.NativeCodeCoverageFlusherTest;
 import com.android.tradefed.util.PairTest;
 import com.android.tradefed.util.PropertyChangerTest;
+import com.android.tradefed.util.ProtoUtilTest;
 import com.android.tradefed.util.PsParserTest;
+import com.android.tradefed.util.PythonVirtualenvHelperTest;
 import com.android.tradefed.util.QuotationAwareTokenizerTest;
 import com.android.tradefed.util.RegexTrieTest;
 import com.android.tradefed.util.RemoteZipTest;
@@ -379,6 +374,7 @@
 import com.android.tradefed.util.SimpleStatsTest;
 import com.android.tradefed.util.SizeLimitedOutputStreamTest;
 import com.android.tradefed.util.Sl4aBluetoothUtilTest;
+import com.android.tradefed.util.SparseImageUtilTest;
 import com.android.tradefed.util.StreamUtilTest;
 import com.android.tradefed.util.StringEscapeUtilsTest;
 import com.android.tradefed.util.StringUtilTest;
@@ -519,7 +515,7 @@
     GceSshTunnelMonitorTest.class,
     ManagedRemoteDeviceTest.class,
     NestedRemoteDeviceTest.class,
-    RemoteAndroidDeviceTest.class,
+    RemoteAndroidVirtualDeviceTest.class,
     RemoteFileUtilTest.class,
 
     // device.contentprovider
@@ -533,33 +529,21 @@
     AtraceRunMetricCollectorTest.class,
     AutoLogCollectorTest.class,
     BaseDeviceMetricCollectorTest.class,
-    BuddyInfoMetricCollectorTest.class,
-    BugreportzMetricCollectorTest.class,
     BugreportzOnFailureCollectorTest.class,
     DebugHostLogOnFailureCollectorTest.class,
     DeviceMetricDataTest.class,
-    DumpHeapCollectorTest.class,
     FilePullerDeviceMetricCollectorTest.class,
     FilePullerLogCollectorTest.class,
-    GraphicsStatsMetricCollectorTest.class,
     GcovCodeCoverageCollectorTest.class,
     IncidentReportCollectorTest.class,
-    IonHeapInfoMetricCollectorTest.class,
     JavaCodeCoverageCollectorTest.class,
     LogcatOnFailureCollectorTest.class,
     LogcatTimingMetricCollectorTest.class,
-    MemInfoMetricCollectorTest.class,
-    PagetypeInfoMetricCollectorTest.class,
     PerfettoPullerMetricCollectorTest.class,
-    ProcessMaxMemoryCollectorTest.class,
     RebootReasonCollectorTest.class,
     RuntimeRestartCollectorTest.class,
-    ScheduledDeviceMetricCollectorTest.class,
-    ScheduleMultipleDeviceMetricCollectorTest.class,
     ScreenshotOnFailureCollectorTest.class,
     HostStatsdMetricCollectorTest.class,
-    TemperatureCollectorTest.class,
-    TraceMetricCollectorTest.class,
 
     // device.recovery
     BatteryUnavailableDeviceRecoveryTest.class,
@@ -583,7 +567,6 @@
     InvocationContextTest.class,
     InvocationExecutionTest.class,
     RemoteInvocationExecutionTest.class,
-    SandboxedInvocationExecutionTest.class,
     ShardListenerTest.class,
     ShardMainResultForwarderTest.class,
     TestInvocationMultiTest.class,
@@ -607,7 +590,6 @@
 
     // invoker.sandbox
     ParentSandboxInvocationExecutionTest.class,
-    SandboxedInvocationExecutionTest.class,
 
     // lite
     DryRunnerTest.class,
@@ -651,7 +633,6 @@
     MultiFailureDescriptionTest.class,
     SnapshotInputStreamSourceTest.class,
     SubprocessResultsReporterTest.class,
-    TestDescriptionTest.class,
     TestFailureEmailResultReporterTest.class,
     PassingTestFileReporterTest.class,
     TestDescriptionTest.class,
@@ -711,6 +692,8 @@
     RunCommandTargetPreparerTest.class,
     RunHostCommandTargetPreparerTest.class,
     RunHostScriptTargetPreparerTest.class,
+    RunOnSecondaryUserTargetPreparerTest.class,
+    RunOnWorkProfileTargetPreparerTest.class,
     StopServicesSetupTest.class,
     SystemUpdaterDeviceFlasherTest.class,
     TargetSetupErrorTest.class,
@@ -754,6 +737,7 @@
 
     // testtype
     AndroidJUnitTestTest.class,
+    ArtGTestTest.class,
     ArtRunTestTest.class,
     ClangCodeCoverageListenerTest.class,
     CoverageMeasurementForwarderTest.class,
@@ -879,7 +863,9 @@
     MergedZipEntryCollectionTest.class,
     NativeCodeCoverageFlusherTest.class,
     PairTest.class,
+    ProtoUtilTest.class,
     PsParserTest.class,
+    PythonVirtualenvHelperTest.class,
     QuotationAwareTokenizerTest.class,
     RegexTrieTest.class,
     RemoteZipTest.class,
@@ -893,6 +879,7 @@
     SimpleStatsTest.class,
     SizeLimitedOutputStreamTest.class,
     Sl4aBluetoothUtilTest.class,
+    SparseImageUtilTest.class,
     StreamUtilTest.class,
     StringEscapeUtilsTest.class,
     StringUtilTest.class,
@@ -937,6 +924,9 @@
     // util/testmapping
     TestInfoTest.class,
     TestMappingTest.class,
+
+    // monitoring
+    LabResourceDeviceMonitorTest.class,
 })
 public class UnitTests {
     // empty of purpose
diff --git a/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherFuncTest.java b/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherFuncTest.java
index 3bf18e0..86248bd 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherFuncTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterCommandLauncherFuncTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.cluster;
 
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -35,11 +36,15 @@
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.util.FileUtil;
 
+import org.hamcrest.CoreMatchers;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InOrder;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -52,8 +57,9 @@
 public class ClusterCommandLauncherFuncTest {
 
     private static final String LEGACY_TRADEFED_JAR = "/testdata/tradefed-prebuilt-cts-8.0_r21.jar";
-    private static final String LEGACY_TRADEFED_COMMAND =
-            "host --null-device --class com.android.tradefed.device.DeviceDiagTest";
+    private static final String LEGACY_TRADEFED_COMMAND = "fake.xml --null-device --run testRun PF";
+    private static final String LEGACY_TRADEFED_COMMAND_FOR_INVOCATION_FAILURE =
+            "fake.xml --null-device --fail-invocation-with-cause cause";
 
     private File mRootDir;
     private IConfiguration mConfiguration;
@@ -82,22 +88,51 @@
         FileUtil.recursiveDelete(mRootDir);
     }
 
-    // @Ignore
     @Test
     public void testRun_withLegacyTradefed()
             throws IOException, ConfigurationException, DeviceNotAvailableException {
         File tfJar = new File(mRootDir, "tradefed.jar");
         FileUtil.writeToFile(getClass().getResourceAsStream(LEGACY_TRADEFED_JAR), tfJar);
+        FileUtil.writeToFile(
+                getClass().getResourceAsStream("/config/tf/fake.xml"),
+                new File(mRootDir, "fake.xml"));
         mOptionSetter.setOptionValue("cluster:env-var", "TF_PATH", mRootDir.getAbsolutePath());
         mOptionSetter.setOptionValue("cluster:use-subprocess-reporting", "true");
         mOptionSetter.setOptionValue("cluster:command-line", LEGACY_TRADEFED_COMMAND);
 
         mLauncher.run(mTestInformation, mListener);
 
+        InOrder inOrder = Mockito.inOrder(mListener);
         HashMap<String, MetricMeasurement.Metric> emptyMap = new HashMap<>();
-        verify(mListener).testRunStarted(anyString(), anyInt(), anyInt(), anyLong());
-        verify(mListener).testStarted(any(TestDescription.class), anyLong());
-        verify(mListener).testEnded(any(TestDescription.class), anyLong(), eq(emptyMap));
-        verify(mListener).testRunEnded(anyLong(), eq(emptyMap));
+        inOrder.verify(mListener).testRunStarted(eq("testRun"), anyInt(), anyInt(), anyLong());
+        inOrder.verify(mListener).testStarted(any(TestDescription.class), anyLong());
+        inOrder.verify(mListener).testEnded(any(TestDescription.class), anyLong(), eq(emptyMap));
+        inOrder.verify(mListener).testStarted(any(TestDescription.class), anyLong());
+        inOrder.verify(mListener).testFailed(any(TestDescription.class), anyString());
+        inOrder.verify(mListener).testEnded(any(TestDescription.class), anyLong(), eq(emptyMap));
+        inOrder.verify(mListener).testRunEnded(anyLong(), eq(emptyMap));
+    }
+
+    @Test
+    public void testRun_withLegacyTradefed_invocationFailed()
+            throws IOException, ConfigurationException, DeviceNotAvailableException {
+        File tfJar = new File(mRootDir, "tradefed.jar");
+        FileUtil.writeToFile(getClass().getResourceAsStream(LEGACY_TRADEFED_JAR), tfJar);
+        FileUtil.writeToFile(
+                getClass().getResourceAsStream("/config/tf/fake.xml"),
+                new File(mRootDir, "fake.xml"));
+        mOptionSetter.setOptionValue("cluster:env-var", "TF_PATH", mRootDir.getAbsolutePath());
+        mOptionSetter.setOptionValue("cluster:use-subprocess-reporting", "true");
+        mOptionSetter.setOptionValue(
+                "cluster:command-line", LEGACY_TRADEFED_COMMAND_FOR_INVOCATION_FAILURE);
+
+        try {
+            mLauncher.run(mTestInformation, mListener);
+            fail("SubprocessCommandException should be thrown");
+        } catch (SubprocessCommandException e) {
+            Assert.assertThat(e.getCause().getMessage(), CoreMatchers.containsString("cause"));
+        }
+
+        verify(mListener).invocationFailed(any(Throwable.class));
     }
 }
diff --git a/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java b/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
index d4befb9..adfc487 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterCommandSchedulerTest.java
@@ -521,6 +521,23 @@
                 new String[] {CMD_LINE, "--serial", "deviceSerial"}, getExecCommandArgs());
     }
 
+    /**
+     * If a unique device serial (one with a hostname prefix) is specified for a command task,
+     * convert it to a local device serial before appending it.
+     */
+    @Test
+    public void testExecCommandWithVirtualDeviceSerial() {
+        List<ClusterCommand> cmds = new ArrayList<>();
+        ClusterCommand cmd = new ClusterCommand(COMMAND_ID, TASK_ID, CMD_LINE);
+        cmd.setTargetDeviceSerials(
+                ArrayUtil.list(ClusterHostUtil.getHostName() + ":emulator-5554"));
+        cmds.add(cmd);
+        mScheduler.execCommands(cmds);
+        assertEquals(CMD_LINE, cmds.get(0).getCommandLine());
+        assertArrayEquals(
+                new String[] {CMD_LINE, "--serial", "emulator-5554"}, getExecCommandArgs());
+    }
+
     /** Multiple serials specified for a command task. */
     @Test
     public void testExecCommandWithMultipleSerials() {
@@ -828,6 +845,70 @@
         handler.invocationInitiated(context);
     }
 
+    @Test
+    public void testInvocationEventHandler_withSubprocessCommandException() {
+        ClusterCommand mockCommand = new ClusterCommand(COMMAND_ID, TASK_ID, CMD_LINE);
+        IInvocationContext context = new InvocationContext();
+        ITestDevice mockTestDevice = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(mockTestDevice.getSerialNumber()).andReturn(DEVICE_SERIAL);
+        EasyMock.expect(mockTestDevice.getIDevice()).andReturn(new StubDevice(DEVICE_SERIAL));
+        context.addAllocatedDevice("", mockTestDevice);
+        IBuildInfo mockBuildInfo = EasyMock.createMock(IBuildInfo.class);
+        context.addDeviceBuildInfo("", mockBuildInfo);
+        ClusterCommandScheduler.InvocationEventHandler handler =
+                mScheduler.new InvocationEventHandler(mockCommand);
+        mMockClusterOptions.setCollectEarlyTestSummary(true);
+
+        mMockEventUploader.postEvent(
+                checkClusterCommandEvent(ClusterCommandEvent.Type.InvocationInitiated));
+        mMockEventUploader.flush();
+        mMockEventUploader.postEvent(
+                checkClusterCommandEvent(ClusterCommandEvent.Type.InvocationStarted));
+        mMockEventUploader.flush();
+        mMockEventUploader.postEvent(
+                checkClusterCommandEvent(ClusterCommandEvent.Type.TestRunInProgress));
+        EasyMock.expectLastCall().anyTimes();
+        mMockEventUploader.postEvent(
+                checkClusterCommandEvent(ClusterCommandEvent.Type.InvocationEnded));
+        mMockEventUploader.flush();
+        Capture<ClusterCommandEvent> capture = new Capture<>();
+        mMockEventUploader.postEvent(EasyMock.capture(capture));
+        mMockEventUploader.flush();
+
+        EasyMock.replay(mMockEventUploader, mockBuildInfo, mockTestDevice);
+        handler.invocationInitiated(context);
+        List<TestSummary> summaries = new ArrayList<>();
+        summaries.add(
+                new TestSummary(new TestSummary.TypedString("http://uri", TestSummary.Type.URI)));
+        handler.putEarlySummary(summaries);
+        handler.putSummary(summaries);
+        handler.invocationStarted(context);
+        handler.invocationFailed(
+                new SubprocessCommandException(
+                        "error_message", new Throwable("subprocess_command_error_message")));
+        handler.invocationEnded(100L);
+        context.addAllocatedDevice(DEVICE_SERIAL, mockTestDevice);
+        Map<ITestDevice, FreeDeviceState> releaseMap = new HashMap<>();
+        releaseMap.put(mockTestDevice, FreeDeviceState.AVAILABLE);
+        handler.invocationComplete(context, releaseMap);
+        EasyMock.verify(mMockEventUploader, mockBuildInfo, mockTestDevice);
+        ClusterCommandEvent capturedEvent = capture.getValue();
+        assertTrue(capturedEvent.getType().equals(ClusterCommandEvent.Type.InvocationCompleted));
+        assertTrue(
+                ((String) capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_ERROR))
+                        .contains("SubprocessCommandException"));
+        assertEquals(
+                "subprocess_command_error_message",
+                capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_SUBPROCESS_COMMAND_ERROR));
+        assertEquals(
+                "0", capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_FAILED_TEST_COUNT));
+        assertEquals(
+                "0", capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_PASSED_TEST_COUNT));
+        assertEquals(
+                "URI: http://uri\n",
+                capturedEvent.getData().get(ClusterCommandEvent.DATA_KEY_SUMMARY));
+    }
+
     /**
      * Test that when dry-run is used we validate the config and no ConfigurationException gets
      * thrown.
@@ -1058,7 +1139,8 @@
         List<IDeviceConfiguration> deviceConfigs = config.getDeviceConfig();
         assertEquals(cmd.getTargetDeviceSerials().size(), deviceConfigs.size());
         for (int i = 0; i < cmd.getTargetDeviceSerials().size(); i++) {
-            String serial = cmd.getTargetDeviceSerials().get(i);
+            String serial =
+                    ClusterHostUtil.getLocalDeviceSerial(cmd.getTargetDeviceSerials().get(i));
             Collection<String> serials =
                     deviceConfigs.get(i).getDeviceRequirements().getSerials(null);
             assertTrue(serials.size() == 1 && serials.contains(serial));
@@ -1165,6 +1247,44 @@
         }
     }
 
+    /** Tests an execution of a managed cluster command. */
+    @Test
+    public void testExecManagedClusterCommand_virtualDeviceTest() throws Exception {
+        File workDir = null;
+        try {
+            ClusterCommand cmd = createMockManagedCommand(1);
+            cmd.setTargetDeviceSerials(
+                    ArrayUtil.list(ClusterHostUtil.getHostName() + ":emulator-5554"));
+            workDir = new File(System.getProperty("java.io.tmpdir"), cmd.getAttemptId());
+            TestEnvironment testEnvironment = createMockTestEnvironment();
+            List<TestResource> testResources = createMockTestResources();
+            TestContext testContext = new TestContext();
+            mMockClusterClient = Mockito.spy(mMockClusterClient);
+            Mockito.doReturn(testEnvironment)
+                    .when(mMockClusterClient)
+                    .getTestEnvironment(REQUEST_ID);
+            Mockito.doReturn(testResources).when(mMockClusterClient).getTestResources(REQUEST_ID);
+            Mockito.doReturn(testContext)
+                    .when(mMockClusterClient)
+                    .getTestContext(REQUEST_ID, COMMAND_ID);
+            InvocationEventHandler invocationEventHandler =
+                    mScheduler.new InvocationEventHandler(cmd);
+
+            mScheduler.execManagedClusterCommand(cmd, invocationEventHandler);
+
+            String[] args = getExecCommandArgs();
+            assertTrue(args.length > 0);
+            IConfiguration config =
+                    ConfigurationFactory.getInstance().createConfigurationFromArgs(args);
+            verifyConfig(config, cmd, testEnvironment, testResources, workDir);
+        } finally {
+            if (workDir != null) {
+                // Clean up work directory
+                FileUtil.recursiveDelete(workDir);
+            }
+        }
+    }
+
     /** Tests an execution of a managed cluster command for multiple devices. */
     @Test
     public void testExecManagedClusterCommand_multiDeviceTest() throws Exception {
diff --git a/tests/src/com/android/tradefed/cluster/ClusterDeviceMonitorTest.java b/tests/src/com/android/tradefed/cluster/ClusterDeviceMonitorTest.java
index fc98304..d0ba372 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterDeviceMonitorTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterDeviceMonitorTest.java
@@ -100,6 +100,24 @@
         Assert.assertEquals("cluster1", hostEvent.getClusterId());
         Assert.assertEquals(Arrays.asList("cluster2", "cluster3"), hostEvent.getNextClusterIds());
         Assert.assertEquals("lab1", hostEvent.getLabName());
+        Assert.assertEquals("", hostEvent.getData().get("label"));
+    }
+
+    @Test
+    public void testLabel() throws Exception {
+        mClusterOptionSetter.setOptionValue("cluster:label", "label1");
+        mClusterOptionSetter.setOptionValue("cluster:label", "label2");
+        Capture<ClusterHostEvent> capture = new Capture<>();
+        mHostEventUploader.postEvent(EasyMock.capture(capture));
+        mHostEventUploader.flush();
+        EasyMock.replay(mHostEventUploader);
+        mEventDispatcher.dispatch();
+        EasyMock.verify(mHostEventUploader);
+        ClusterHostEvent hostEvent = capture.getValue();
+        Assert.assertEquals("cluster1", hostEvent.getClusterId());
+        Assert.assertEquals(Arrays.asList("cluster2", "cluster3"), hostEvent.getNextClusterIds());
+        Assert.assertEquals("lab1", hostEvent.getLabName());
+        Assert.assertEquals("label1,label2", hostEvent.getData().get("label"));
     }
 
     void setOptions() throws Exception {
diff --git a/tests/src/com/android/tradefed/cluster/ClusterHostUtilTest.java b/tests/src/com/android/tradefed/cluster/ClusterHostUtilTest.java
index e2c10bd..a7ff29c 100644
--- a/tests/src/com/android/tradefed/cluster/ClusterHostUtilTest.java
+++ b/tests/src/com/android/tradefed/cluster/ClusterHostUtilTest.java
@@ -25,6 +25,8 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.easymock.EasyMock;
 import org.junit.Assert;
@@ -37,6 +39,7 @@
 public class ClusterHostUtilTest {
 
     private static final String DEVICE_SERIAL = "serial";
+    private static final String EMULATOR_SERIAL = "emulator-5554";
 
     @Test
     public void testIsIpPort() {
@@ -287,4 +290,71 @@
                         "simOperator");
         Assert.assertEquals("product", ClusterHostUtil.getRunTarget(device, format, null));
     }
+
+    @Test
+    public void testGetRunTarget_withStubDevice() {
+        final String hostname = ClusterHostUtil.getHostName();
+        // with a stub device.
+        DeviceDescriptor device =
+                new DeviceDescriptor(
+                        DEVICE_SERIAL,
+                        true,
+                        DeviceAllocationState.Available,
+                        "product",
+                        "productVariant",
+                        "sdkVersion",
+                        "buildId",
+                        "batteryLevel");
+        Assert.assertEquals(
+                hostname + ":" + DEVICE_SERIAL,
+                ClusterHostUtil.getRunTarget(device, "{SERIAL}", null));
+    }
+
+    @Test
+    public void testGetRunTarget_withEmulator() {
+        final String hostname = ClusterHostUtil.getHostName();
+        // with a stub device.
+        DeviceDescriptor device =
+                new DeviceDescriptor(
+                        EMULATOR_SERIAL,
+                        false,
+                        DeviceAllocationState.Available,
+                        "product",
+                        "productVariant",
+                        "sdkVersion",
+                        "buildId",
+                        "batteryLevel");
+        Assert.assertEquals(
+                hostname + ":" + EMULATOR_SERIAL,
+                ClusterHostUtil.getRunTarget(device, "{SERIAL}", null));
+    }
+
+    @Test
+    public void testGetRunTarget_withEmptyDeviceSerial() {
+        final String hostname = ClusterHostUtil.getHostName();
+        // with a stub device.
+        DeviceDescriptor device =
+                new DeviceDescriptor(
+                        "",
+                        false,
+                        DeviceAllocationState.Available,
+                        "product",
+                        "productVariant",
+                        "sdkVersion",
+                        "buildId",
+                        "batteryLevel");
+        Assert.assertEquals(
+                hostname + ":" + ClusterHostUtil.NULL_DEVICE_SERIAL_PLACEHOLDER,
+                ClusterHostUtil.getRunTarget(device, "{SERIAL}", null));
+    }
+
+    @Test
+    public void testGetHostIpAddress() {
+        final String hostIp = ClusterHostUtil.getHostIpAddress();
+        Assert.assertNotEquals(hostIp, "127.0.0.1");
+        Pattern pattern =
+                Pattern.compile("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}" + "|UNKNOWN");
+        Matcher matcher = pattern.matcher(hostIp);
+        Assert.assertTrue("host ip format not match: " + hostIp, matcher.matches());
+    }
 }
diff --git a/tests/src/com/android/tradefed/cluster/SubprocessConfigBuilderTest.java b/tests/src/com/android/tradefed/cluster/SubprocessConfigBuilderTest.java
index f83ba2b..67622f3 100644
--- a/tests/src/com/android/tradefed/cluster/SubprocessConfigBuilderTest.java
+++ b/tests/src/com/android/tradefed/cluster/SubprocessConfigBuilderTest.java
@@ -76,6 +76,23 @@
         verifyWrapperXml(doc, reporterPort);
     }
 
+    @Test
+    public void testCreateWrapperConfig_forCommandWithSlashes() throws Exception {
+        String oriConfigName = "util/timewaster";
+        String reporterPort = "1024";
+        mConfigBuilder
+                .setClasspath(mClasspath)
+                .setWorkingDir(mWorkDir)
+                .setOriginalConfig(oriConfigName)
+                .setPort(reporterPort);
+        File config = mConfigBuilder.build();
+        assertNotNull(config);
+        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
+        Document doc = dBuilder.parse(config);
+        verifyWrapperXml(doc, reporterPort);
+    }
+
     private void verifyWrapperXml(Document doc, String reporterPort) {
         NodeList reporters = doc.getElementsByTagName("result_reporter");
         assertTrue(0 < reporters.getLength());
diff --git a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
index 793ee11..7373e33 100644
--- a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
@@ -365,12 +365,14 @@
         mScheduler =
                 new TestableCommandScheduler() {
                     @Override
-                    Map<String, ITestDevice> allocateDevices(
+                    DeviceAllocationResult allocateDevices(
                             IConfiguration config, IDeviceManager manager) {
+                        DeviceAllocationResult results = new DeviceAllocationResult();
                         Map<String, ITestDevice> allocated = new HashMap<>();
                         ((MockDeviceManager) manager).addDevice(mockDevice);
                         allocated.put("device", ((MockDeviceManager) manager).allocateDevice());
-                        return allocated;
+                        results.addAllocatedDevices(allocated);
+                        return results;
                     }
                 };
         replayMocks(mockDevice, mockListener);
@@ -1094,8 +1096,10 @@
         mMockConfiguration.validateOptions();
         replayMocks();
         mScheduler.start();
-        Map<String, ITestDevice> devices = mScheduler.allocateDevices(
-                mMockConfiguration, mMockManager);
+        DeviceAllocationResult results =
+                mScheduler.allocateDevices(mMockConfiguration, mMockManager);
+        assertTrue(results.wasAllocationSuccessful());
+        Map<String, ITestDevice> devices = results.getAllocatedDevices();
         assertEquals(1, devices.size());
         mScheduler.shutdown();
     }
@@ -1120,8 +1124,10 @@
         mMockConfiguration.setDeviceConfigList(EasyMock.anyObject());
         replayMocks();
         mScheduler.start();
-        Map<String, ITestDevice> devices =
+        DeviceAllocationResult results =
                 mScheduler.allocateDevices(mMockConfiguration, mMockManager);
+        assertTrue(results.wasAllocationSuccessful());
+        Map<String, ITestDevice> devices = results.getAllocatedDevices();
         // With replicated setup, all devices get allocated.
         assertEquals(3, devices.size());
         mScheduler.shutdown();
@@ -1147,8 +1153,10 @@
         mMockConfiguration.validateOptions();
         replayMocks();
         mScheduler.start();
-        Map<String, ITestDevice> devices = mScheduler.allocateDevices(
-                mMockConfiguration, mMockManager);
+        DeviceAllocationResult results =
+                mScheduler.allocateDevices(mMockConfiguration, mMockManager);
+        assertTrue(results.wasAllocationSuccessful());
+        Map<String, ITestDevice> devices = results.getAllocatedDevices();
         assertEquals(2, devices.size());
         assertEquals(0, mMockManager.getQueueOfAvailableDeviceSize());
         mScheduler.shutdown();
@@ -1166,8 +1174,10 @@
         mMockConfiguration.validateOptions();
         replayMocks();
         mScheduler.start();
-        Map<String, ITestDevice> devices = mScheduler.allocateDevices(
-                mMockConfiguration, mMockManager);
+        DeviceAllocationResult results =
+                mScheduler.allocateDevices(mMockConfiguration, mMockManager);
+        assertFalse(results.wasAllocationSuccessful());
+        Map<String, ITestDevice> devices = results.getAllocatedDevices();
         assertEquals(0, devices.size());
         assertEquals(2, mMockManager.getQueueOfAvailableDeviceSize());
         mScheduler.shutdown();
@@ -1279,12 +1289,14 @@
         mScheduler =
                 new TestableCommandScheduler() {
                     @Override
-                    Map<String, ITestDevice> allocateDevices(
+                    DeviceAllocationResult allocateDevices(
                             IConfiguration config, IDeviceManager manager) {
+                        DeviceAllocationResult results = new DeviceAllocationResult();
                         Map<String, ITestDevice> allocated = new HashMap<>();
                         ((MockDeviceManager) manager).addDevice(mockDevice);
                         allocated.put("device", ((MockDeviceManager) manager).allocateDevice());
-                        return allocated;
+                        results.addAllocatedDevices(allocated);
+                        return results;
                     }
                 };
 
diff --git a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
index 87e60b7..c13780f 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationFactoryTest.java
@@ -1819,9 +1819,13 @@
         IBuildProvider provider = config.getBuildProvider();
         assertTrue(provider instanceof IDeviceBuildProvider);
         IBuildInfo info = ((IDeviceBuildProvider) provider).getBuild(null);
-        assertEquals("5", info.getBuildId());
-        assertEquals("test", info.getBuildFlavor());
-        assertEquals("main", info.getBuildBranch());
+        try {
+            assertEquals("5", info.getBuildId());
+            assertEquals("test", info.getBuildFlavor());
+            assertEquals("main", info.getBuildBranch());
+        } finally {
+            info.cleanUp();
+        }
     }
 
     private static String getClassName(String name) {
diff --git a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
index 69dd56d..97730b4 100644
--- a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
+++ b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
@@ -604,7 +604,7 @@
 
         IRemoteFileResolver actual = loader.load(NullFileResolver.PROTOCOL, ImmutableMap.of());
 
-        assertThat(actual).isSameAs(expected);
+        assertThat(actual).isSameInstanceAs(expected);
     }
 
     @Test
@@ -616,7 +616,7 @@
         IRemoteFileResolver resolver1 = loader1.load(NullFileResolver.PROTOCOL, ImmutableMap.of());
         IRemoteFileResolver resolver2 = loader2.load(NullFileResolver.PROTOCOL, ImmutableMap.of());
 
-        assertThat(resolver1).isNotSameAs(resolver2);
+        assertThat(resolver1).isNotSameInstanceAs(resolver2);
     }
 
     @Test
@@ -968,15 +968,9 @@
     public static final class DuplicateNullFileResolver extends NullFileResolver {}
 
     private static final Correspondence<File, File> FILE_PATH_EQUIVALENCE =
-            new Correspondence<File, File>() {
-                @Override
-                public boolean compare(File actual, File expected) {
-                    return expected.getAbsolutePath().equals(actual.getAbsolutePath());
-                }
-
-                @Override
-                public String toString() {
-                    return "is equivalent to";
-                }
-            };
+            Correspondence.from(
+                    (File actual, File expected) -> {
+                        return expected.getAbsolutePath().equals(actual.getAbsolutePath());
+                    },
+                    "is equivalent to");
 }
diff --git a/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java b/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
index 6a1c081..4784443 100644
--- a/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
+++ b/tests/src/com/android/tradefed/config/yaml/ConfigurationYamlParserTest.java
@@ -19,15 +19,18 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tradefed.build.DependenciesResolver;
 import com.android.tradefed.build.StubBuildProvider;
 import com.android.tradefed.config.ConfigurationDef;
+import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.suite.SuiteResultReporter;
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.targetprep.PushFilePreparer;
+import com.android.tradefed.targetprep.RunCommandTargetPreparer;
 import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
 import com.android.tradefed.testtype.AndroidJUnitTest;
 
@@ -45,6 +48,8 @@
 public class ConfigurationYamlParserTest {
 
     private static final String YAML_TEST_CONFIG_1 = "/testconfigs/yaml/test-config.tf_yaml";
+    private static final String YAML_TEST_CONFIG_2 =
+            "/testconfigs/yaml/not-target-preparer.tf_yaml";
 
     private ConfigurationYamlParser mParser;
     private ConfigurationDef mConfigDef;
@@ -84,14 +89,19 @@
 
             // Dependencies
             // apk dependencies
-            assertEquals(2, config.getTargetPreparers().size());
-            ITargetPreparer installApk = config.getTargetPreparers().get(0);
+            assertEquals(5, config.getTargetPreparers().size());
+            ITargetPreparer preCommandRunner = config.getTargetPreparers().get(0);
+            assertTrue(preCommandRunner instanceof RunCommandTargetPreparer);
+            ITargetPreparer preCommandRunner2 = config.getTargetPreparers().get(1);
+            assertTrue(preCommandRunner2 instanceof RunCommandTargetPreparer);
+
+            ITargetPreparer installApk = config.getTargetPreparers().get(2);
             assertTrue(installApk instanceof SuiteApkInstaller);
             assertThat(((SuiteApkInstaller) installApk).getTestsFileName())
                     .containsExactly(
                             new File("test.apk"), new File("test2.apk"), new File("test1.apk"));
             // device file dependencies
-            ITargetPreparer pushFile = config.getTargetPreparers().get(1);
+            ITargetPreparer pushFile = config.getTargetPreparers().get(3);
             assertTrue(pushFile instanceof PushFilePreparer);
             assertThat(((PushFilePreparer) pushFile).getPushSpecs(null))
                     .containsExactly(
@@ -99,6 +109,8 @@
                             new File("tobepushed2.txt"),
                             "/sdcard",
                             new File("tobepushed.txt"));
+            ITargetPreparer postCommandRunner = config.getTargetPreparers().get(4);
+            assertTrue(postCommandRunner instanceof RunCommandTargetPreparer);
             // Result reporters
             List<ITestInvocationListener> listeners = config.getTestInvocationListeners();
             assertTrue(listeners.get(0) instanceof SuiteResultReporter);
@@ -123,14 +135,14 @@
 
             // Dependencies
             // apk dependencies
-            assertEquals(2, config.getTargetPreparers().size());
-            ITargetPreparer installApk = config.getTargetPreparers().get(0);
+            assertEquals(5, config.getTargetPreparers().size());
+            ITargetPreparer installApk = config.getTargetPreparers().get(2);
             assertTrue(installApk instanceof SuiteApkInstaller);
             assertThat(((SuiteApkInstaller) installApk).getTestsFileName())
                     .containsExactly(
                             new File("test.apk"), new File("test2.apk"), new File("test1.apk"));
             // device file dependencies
-            ITargetPreparer pushFile = config.getTargetPreparers().get(1);
+            ITargetPreparer pushFile = config.getTargetPreparers().get(3);
             assertTrue(pushFile instanceof PushFilePreparer);
             assertThat(((PushFilePreparer) pushFile).getPushSpecs(null))
                     .containsExactly(
@@ -145,6 +157,19 @@
         }
     }
 
+    @Test
+    public void testParseConfig_notTargetPreparer() throws Exception {
+        try (InputStream res = readFromRes(YAML_TEST_CONFIG_2)) {
+            mParser.parse(mConfigDef, "source", res, false);
+            try {
+                mConfigDef.createConfiguration();
+                fail("Should have thrown an exception");
+            } catch (ConfigurationException expected) {
+
+            }
+        }
+    }
+
     private InputStream readFromRes(String resourceFile) {
         return getClass().getResourceAsStream(resourceFile);
     }
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index e6007fd..f4e8aee 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -4747,64 +4747,43 @@
     }
 
     /**
-     * Test {@link TestDevice#doesFileExist(String)} when the file exists on an sdcard from another
-     * user.
+     * Test {@link TestDevice#doesFileExist(String)} using content provider when the file is in
+     * external storage path.
      */
     public void testDoesFileExists_sdcard() throws Exception {
-        mTestDevice =
-                new TestableTestDevice() {
-                    @Override
-                    public int getCurrentUser()
-                            throws DeviceNotAvailableException, DeviceRuntimeException {
-                        return 10;
-                    }
-                };
-        injectShellResponse("ls \"/storage/emulated/10/file\"", "file");
+        mTestDevice = createTestDevice();
+
+        TestableTestDevice spy = (TestableTestDevice) Mockito.spy(mTestDevice);
+        ContentProviderHandler cp = Mockito.mock(ContentProviderHandler.class);
+        doReturn(cp).when(spy).getContentProvider();
+
+        final String fakeFile = "/sdcard/file";
+        final String targetFilePath = "/storage/emulated/10/file";
+
+        doReturn("").when(spy).executeShellCommand(Mockito.contains("content query --user 10"));
+
         EasyMock.replay(mMockIDevice);
-        assertTrue(mTestDevice.doesFileExist("/sdcard/file"));
+        spy.doesFileExist(fakeFile);
         EasyMock.verify(mMockIDevice);
+
+        verify(spy, times(1)).getContentProvider();
+        verify(cp, times(1)).doesFileExist(targetFilePath);
     }
 
     /** Push a file using the content provider. */
     public void testPushFile_contentProvider() throws Exception {
-        mTestDevice =
-                new TestableTestDevice() {
-                    @Override
-                    public int getApiLevel() throws DeviceNotAvailableException {
-                        return 29;
-                    }
-
-                    @Override
-                    public int getCurrentUser()
-                            throws DeviceNotAvailableException, DeviceRuntimeException {
-                        return 10;
-                    }
-
-                    @Override
-                    public boolean isPackageInstalled(String packageName, String userId)
-                            throws DeviceNotAvailableException {
-                        return false;
-                    }
-                };
+        mTestDevice = createTestDevice();
         TestableTestDevice spy = (TestableTestDevice) Mockito.spy(mTestDevice);
+        setupContentProvider(spy);
+
         final String fakeRemotePath = "/sdcard/";
         File tmpFile = FileUtil.createTempFile("push", ".test");
-        doReturn(null)
-                .when(spy)
-                .installPackage(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean());
-        CommandResult setLegacy = new CommandResult(CommandStatus.SUCCESS);
-        doReturn(setLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops set"));
-
-        CommandResult getLegacy = new CommandResult(CommandStatus.SUCCESS);
-        getLegacy.setStdout("LEGACY_STORAGE: allow");
-        doReturn(getLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops get"));
 
         CommandResult writeContent = new CommandResult(CommandStatus.SUCCESS);
         writeContent.setStdout("");
         doReturn(writeContent)
                 .when(spy)
                 .executeShellV2Command(Mockito.contains("content write"), (File) Mockito.any());
-        doReturn(null).when(spy).uninstallPackage(Mockito.eq("android.tradefed.contentprovider"));
         EasyMock.replay(mMockIDevice);
         try {
             boolean res = spy.pushFile(tmpFile, fakeRemotePath);
@@ -4825,37 +4804,12 @@
 
     /** Push a file using the content provider. */
     public void testPushFile_contentProvider_notFound() throws Exception {
-        mTestDevice =
-                new TestableTestDevice() {
-                    @Override
-                    public int getApiLevel() throws DeviceNotAvailableException {
-                        return 29;
-                    }
-
-                    @Override
-                    public int getCurrentUser()
-                            throws DeviceNotAvailableException, DeviceRuntimeException {
-                        return 10;
-                    }
-
-                    @Override
-                    public boolean isPackageInstalled(String packageName, String userId)
-                            throws DeviceNotAvailableException {
-                        return false;
-                    }
-                };
+        mTestDevice = createTestDevice();
         TestableTestDevice spy = (TestableTestDevice) Mockito.spy(mTestDevice);
+        setupContentProvider(spy);
+
         final String fakeRemotePath = "/sdcard/";
         File tmpFile = FileUtil.createTempFile("push", ".test");
-        doReturn(null)
-                .when(spy)
-                .installPackage(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean());
-        CommandResult setLegacy = new CommandResult(CommandStatus.SUCCESS);
-        doReturn(setLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops set"));
-
-        CommandResult getLegacy = new CommandResult(CommandStatus.SUCCESS);
-        getLegacy.setStdout("LEGACY_STORAGE: allow");
-        doReturn(getLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops get"));
 
         CommandResult writeContent = new CommandResult(CommandStatus.SUCCESS);
         writeContent.setStdout("");
@@ -4901,4 +4855,38 @@
                                 EasyMock.eq(property)))
                 .andReturn(stubResult);
     }
+
+    private void setupContentProvider(TestableTestDevice spy) throws Exception {
+        doReturn(null)
+                .when(spy)
+                .installPackage(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean());
+        CommandResult setLegacy = new CommandResult(CommandStatus.SUCCESS);
+        doReturn(setLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops set"));
+
+        CommandResult getLegacy = new CommandResult(CommandStatus.SUCCESS);
+        getLegacy.setStdout("LEGACY_STORAGE: allow");
+        doReturn(getLegacy).when(spy).executeShellV2Command(Mockito.contains("cmd appops get"));
+
+        doReturn(null).when(spy).uninstallPackage(Mockito.eq("android.tradefed.contentprovider"));
+    }
+
+    private TestableTestDevice createTestDevice() {
+        return new TestableTestDevice() {
+            @Override
+            public int getApiLevel() throws DeviceNotAvailableException {
+                return 29;
+            }
+
+            @Override
+            public int getCurrentUser() throws DeviceNotAvailableException, DeviceRuntimeException {
+                return 10;
+            }
+
+            @Override
+            public boolean isPackageInstalled(String packageName, String userId)
+                    throws DeviceNotAvailableException {
+                return false;
+            }
+        };
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
index e8d0219..18109d1 100644
--- a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
@@ -58,6 +58,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -323,6 +324,32 @@
     /** Test {@link RemoteAndroidVirtualDevice#postInvocationTearDown(Throwable)}. */
     @Test
     public void testPostInvocationTearDown() throws Exception {
+        mTestDevice =
+                new TestableRemoteAndroidVirtualDevice() {
+                    @Override
+                    protected IRunUtil getRunUtil() {
+                        return mMockRunUtil;
+                    }
+
+                    @Override
+                    void createGceSshMonitor(
+                            ITestDevice device,
+                            IBuildInfo buildInfo,
+                            HostAndPort hostAndPort,
+                            TestDeviceOptions deviceOptions) {
+                        // ignore
+                    }
+
+                    @Override
+                    GceManager getGceHandler() {
+                        return mGceHandler;
+                    }
+
+                    @Override
+                    public DeviceDescriptor getDeviceDescriptor() {
+                        return null;
+                    }
+                };
         mTestDevice.setTestLogger(mTestLogger);
         EasyMock.expect(mMockStateMonitor.waitForDeviceNotAvailable(EasyMock.anyLong()))
                 .andReturn(true);
@@ -803,4 +830,83 @@
         }
         verifyMocks();
     }
+
+    /**
+     * Run powerwash() but GceAvdInfo = null, RemoteAndroidVirtualDevice choose to throw exception.
+     */
+    @Test
+    public void testPowerwashNoAvdInfo() throws Exception {
+        final String expectedException = "Can not get GCE AVD Info. launch GCE first? [ : ]";
+        EasyMock.replay(mMockRunUtil, mMockIDevice);
+        try {
+            mTestDevice.powerwashGce();
+            fail("Should have thrown an exception");
+        } catch (TargetSetupError expected) {
+            assertEquals(expectedException, expected.getMessage());
+        }
+        EasyMock.verify(mMockRunUtil, mMockIDevice);
+    }
+
+    /** Test powerwash GCE command */
+    @Test
+    public void testPowerwashGce() throws Exception {
+        mTestDevice =
+                new TestableRemoteAndroidVirtualDevice() {
+                    @Override
+                    public IDevice getIDevice() {
+                        return mMockIDevice;
+                    }
+
+                    @Override
+                    GceManager getGceHandler() {
+                        return mGceHandler;
+                    }
+
+                    @Override
+                    void createGceSshMonitor(
+                            ITestDevice device,
+                            IBuildInfo buildInfo,
+                            HostAndPort hostAndPort,
+                            TestDeviceOptions deviceOptions) {
+                        // ignore
+                    }
+                };
+        String instanceUser = "user1";
+        IBuildInfo mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
+        OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
+        setter.setOptionValue("instance-user", instanceUser);
+        String powerwashCommand = String.format("/home/%s/bin/powerwash_cvd", instanceUser);
+        String avdConnectHost = String.format("%s@127.0.0.1", instanceUser);
+        GceAvdInfo gceAvd =
+                new GceAvdInfo(
+                        instanceUser, HostAndPort.fromHost("127.0.0.1"), null, GceStatus.SUCCESS);
+        doReturn(gceAvd).when(mGceHandler).startGce(null);
+        OutputStream stdout = null;
+        OutputStream stderr = null;
+        CommandResult powerwashCmdResult = new CommandResult(CommandStatus.SUCCESS);
+        EasyMock.expect(
+                        mMockRunUtil.runTimedCmd(
+                                EasyMock.anyLong(),
+                                EasyMock.eq(stdout),
+                                EasyMock.eq(stderr),
+                                EasyMock.eq("ssh"),
+                                EasyMock.eq("-o"),
+                                EasyMock.eq("UserKnownHostsFile=/dev/null"),
+                                EasyMock.eq("-o"),
+                                EasyMock.eq("StrictHostKeyChecking=no"),
+                                EasyMock.eq("-o"),
+                                EasyMock.eq("ServerAliveInterval=10"),
+                                EasyMock.eq("-i"),
+                                EasyMock.anyObject(),
+                                EasyMock.eq(avdConnectHost),
+                                EasyMock.eq(powerwashCommand)))
+                .andReturn(powerwashCmdResult);
+        EasyMock.expect(mMockStateMonitor.waitForDeviceAvailable(EasyMock.anyLong()))
+                .andReturn(mMockIDevice);
+        EasyMock.replay(mMockRunUtil, mMockIDevice);
+        // Launch GCE before powerwash.
+        mTestDevice.launchGce(mMockBuildInfo);
+        mTestDevice.powerwashGce();
+        EasyMock.verify(mMockRunUtil, mMockIDevice);
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
index b0da7d4..0570a59 100644
--- a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
+++ b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.util.CommandResult;
@@ -369,6 +370,36 @@
                 espacedUrl);
     }
 
+    /** Test {@link ContentProviderHandler#doesFileExist(String)}. */
+    @Test
+    public void testDoesFileExist() throws Exception {
+        String devicePath = "path/somewhere/file.txt";
+
+        when(mMockDevice.getCurrentUser()).thenReturn(99);
+        when(mMockDevice.executeShellCommand(
+                        "content query --user 99 --uri "
+                                + ContentProviderHandler.createEscapedContentUri(devicePath)))
+                .thenReturn("");
+
+        assertTrue(mProvider.doesFileExist(devicePath));
+    }
+
+    /**
+     * Test {@link ContentProviderHandler#doesFileExist(String)} returns false when 'adb shell
+     * content query' returns no results.
+     */
+    @Test
+    public void testDoesFileExist_NotExists() throws Exception {
+        String devicePath = "path/somewhere/";
+
+        when(mMockDevice.getCurrentUser()).thenReturn(99);
+        when(mMockDevice.executeShellCommand(
+                        "content query --user 99 --uri "
+                                + ContentProviderHandler.createEscapedContentUri(devicePath)))
+                .thenReturn("No result found.\n");
+        assertFalse(mProvider.doesFileExist(devicePath));
+    }
+
     @Test
     public void testParseQueryResultRow() {
         String row =
diff --git a/tests/src/com/android/tradefed/device/metric/BuddyInfoMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/BuddyInfoMetricCollectorTest.java
deleted file mode 100644
index 27bf498..0000000
--- a/tests/src/com/android/tradefed/device/metric/BuddyInfoMetricCollectorTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link BuddyInfoMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class BuddyInfoMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice device;
-
-    @Spy BuddyInfoMetricCollector mBuddyInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mBuddyInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.TEXT), any(InputStreamSource.class));
-
-        doReturn(new File("unusable-index-1"))
-                .when(mBuddyInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mBuddyInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        mBuddyInfoMetricCollector.collect(device, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("unusable-index-1"), eq(LogDataType.TEXT), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/BugreportzMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/BugreportzMetricCollectorTest.java
deleted file mode 100644
index 6b19eb4..0000000
--- a/tests/src/com/android/tradefed/device/metric/BugreportzMetricCollectorTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import java.util.Arrays;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-/** Unit tests for {@link BugreportzMetricCollector}. */
-@RunWith(JUnit4.class)
-public class BugreportzMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestDevice mTestDevice;
-
-    @Mock ITestInvocationListener mForwarder;
-
-    @Spy BugreportzMetricCollector mBugreportzMetricCollector;
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        doReturn(Arrays.asList(mTestDevice)).when(mContext).getDevices();
-
-        when(mTestDevice.logBugreport(anyString(), any(ITestInvocationListener.class)))
-                .thenReturn(true);
-
-        mBugreportzMetricCollector.init(mContext, mForwarder);
-
-        when(mBugreportzMetricCollector.getFileSuffix()).thenReturn("1");
-    }
-
-    /** Tests successful collection of bugreport. */
-    @Test
-    public void testCollect_success() throws Exception {
-        when(mTestDevice.logBugreport("bugreportz-1", mForwarder)).thenReturn(true);
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        mBugreportzMetricCollector.collect(mTestDevice, runData);
-
-        verify(mTestDevice).logBugreport(eq("bugreport-1"), eq(mForwarder));
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/DumpHeapCollectorTest.java b/tests/src/com/android/tradefed/device/metric/DumpHeapCollectorTest.java
deleted file mode 100644
index 6a9cb56..0000000
--- a/tests/src/com/android/tradefed/device/metric/DumpHeapCollectorTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.util.FileUtil;
-import com.google.common.truth.Truth;
-import java.io.File;
-import java.util.Arrays;
-import java.util.List;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-/** Unit tests for {@link DumpHeapCollector}. */
-@RunWith(JUnit4.class)
-public class DumpHeapCollectorTest {
-
-    @Mock private IInvocationContext mContext;
-
-    @Mock private ITestInvocationListener mListener;
-
-    @Mock private ITestDevice mDevice;
-
-    @Rule public TemporaryFolder folder = new TemporaryFolder();
-
-    @Spy private DumpHeapCollector mDumpheapCollector;
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        when(mDevice.executeShellCommand("dumpsys meminfo -c | grep camera"))
-                .thenReturn("proc,native,camera,21348,800,N/A,e\n");
-
-        when(mDevice.executeShellCommand("dumpsys meminfo -c | grep maps"))
-                .thenReturn("proc,native,maps,21349,900,N/A,e\n");
-
-        when(mDevice.executeShellCommand("am dumpheap camera /data/local/tmp/camera_trigger.hprof"))
-                .thenReturn("");
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), any(LogDataType.class), any(InputStreamSource.class));
-    }
-
-    @Test
-    public void testTakeDumpheap_success() throws Exception {
-        File mapsDumpheap1 = folder.newFile("maps1");
-        File mapsDumpheap2 = folder.newFile("maps2");
-
-        doReturn("1").when(mDumpheapCollector).getFileSuffix();
-
-        when(mDevice.dumpHeap("maps", "/data/local/tmp/maps_trigger_1.hprof"))
-                .thenReturn(mapsDumpheap1)
-                .thenReturn(mapsDumpheap2);
-
-        String fakeDumpheapOutput =
-                "proc,native,maps,21349,900,N/A,e\nproc,native,camera,21350,800,N/A,e\n";
-
-        List<File> files =
-                mDumpheapCollector.takeDumpheap(mDevice, fakeDumpheapOutput, "maps", 850L);
-
-        Truth.assertThat(files).containsExactly(mapsDumpheap1);
-    }
-
-    @Test
-    public void testCollect_success() throws Exception {
-        File tempFile1 = folder.newFile();
-        File tempFile2 = folder.newFile();
-        when(mDevice.dumpHeap(anyString(), anyString()))
-                .thenReturn(tempFile1)
-                .thenReturn(tempFile2);
-
-        OptionSetter options = new OptionSetter(mDumpheapCollector);
-
-        options.setOptionValue("dumpheap-thresholds", "camera", "700");
-        options.setOptionValue("dumpheap-thresholds", "maps", "800");
-
-        mDumpheapCollector.init(mContext, mListener);
-
-        mDumpheapCollector.collect(mDevice, null);
-
-        ArgumentCaptor<String> dataNameCaptor = ArgumentCaptor.forClass(String.class);
-        ArgumentCaptor<LogDataType> dataTypeCaptor = ArgumentCaptor.forClass(LogDataType.class);
-        ArgumentCaptor<InputStreamSource> inputCaptor =
-                ArgumentCaptor.forClass(InputStreamSource.class);
-
-        verify(mListener, times(2))
-                .testLog(dataNameCaptor.capture(), dataTypeCaptor.capture(), inputCaptor.capture());
-
-        // Assert that the correct filename was sent to testLog.
-        Truth.assertThat(dataNameCaptor.getAllValues())
-                .containsExactlyElementsIn(
-                        Arrays.asList(
-                                FileUtil.getBaseName(tempFile1.getName()),
-                                FileUtil.getBaseName(tempFile2.getName())));
-
-        // Assert that the correct data type was sent to testLog.
-        Truth.assertThat(dataTypeCaptor.getAllValues())
-                .containsExactlyElementsIn(Arrays.asList(LogDataType.HPROF, LogDataType.HPROF));
-    }
-
-    @Test
-    public void testCollectSuccess_thresholdTooHigh() throws Exception {
-        File tempFile1 = folder.newFile();
-        File tempFile2 = folder.newFile();
-        when(mDevice.pullFile(anyString())).thenReturn(tempFile1).thenReturn(tempFile2);
-
-        OptionSetter options = new OptionSetter(mDumpheapCollector);
-
-        options.setOptionValue("dumpheap-thresholds", "camera", "7000");
-        options.setOptionValue("dumpheap-thresholds", "maps", "8000");
-
-        mDumpheapCollector.init(mContext, mListener);
-
-        mDumpheapCollector.collect(mDevice, null);
-
-        ArgumentCaptor<String> dataNameCaptor = ArgumentCaptor.forClass(String.class);
-        ArgumentCaptor<LogDataType> dataTypeCaptor = ArgumentCaptor.forClass(LogDataType.class);
-        ArgumentCaptor<InputStreamSource> inputCaptor =
-                ArgumentCaptor.forClass(InputStreamSource.class);
-
-        verify(mListener, times(0))
-                .testLog(dataNameCaptor.capture(), dataTypeCaptor.capture(), inputCaptor.capture());
-    }
-
-    @Test
-    public void testCollectNoError_processNotFound() throws Exception {
-        // Make the meminfo dump not contain the heap info of fake_process.
-        when(mDevice.executeShellCommand("dumpsys meminfo -c | grep fake_process")).thenReturn("");
-
-        OptionSetter options = new OptionSetter(mDumpheapCollector);
-        options.setOptionValue("dumpheap-thresholds", "fake_process", "7000");
-
-        mDumpheapCollector.init(mContext, mListener);
-
-        mDumpheapCollector.collect(mDevice, null);
-
-        ArgumentCaptor<String> dataNameCaptor = ArgumentCaptor.forClass(String.class);
-        ArgumentCaptor<LogDataType> dataTypeCaptor = ArgumentCaptor.forClass(LogDataType.class);
-        ArgumentCaptor<InputStreamSource> inputCaptor =
-                ArgumentCaptor.forClass(InputStreamSource.class);
-
-        // Verify that no testLog calls were made.
-        verify(mListener, times(0))
-                .testLog(dataNameCaptor.capture(), dataTypeCaptor.capture(), inputCaptor.capture());
-    }
-}
-
diff --git a/tests/src/com/android/tradefed/device/metric/GraphicsStatsMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/GraphicsStatsMetricCollectorTest.java
deleted file mode 100644
index cb051a6..0000000
--- a/tests/src/com/android/tradefed/device/metric/GraphicsStatsMetricCollectorTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link GraphicsStatsMetricCollector}. */
-//TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class GraphicsStatsMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy GraphicsStatsMetricCollector mGfxInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mGfxInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.GFX_INFO), any(InputStreamSource.class));
-
-        doReturn(new File("graphics-1"))
-                .when(mGfxInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mGfxInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        mGfxInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("graphics-1"), eq(LogDataType.GFX_INFO), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/IonHeapInfoMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/IonHeapInfoMetricCollectorTest.java
deleted file mode 100644
index 2547672..0000000
--- a/tests/src/com/android/tradefed/device/metric/IonHeapInfoMetricCollectorTest.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link IonHeapInfoMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class IonHeapInfoMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy IonHeapInfoMetricCollector mIonHeapInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mIonHeapInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.TEXT), any(InputStreamSource.class));
-
-        doReturn(new File("ion-system-2"))
-                .when(mIonHeapInfoMetricCollector)
-                .saveProcessOutput(
-                        any(ITestDevice.class), eq("cat /d/ion/heaps/system"), anyString());
-        doReturn(new File("ion-audio-1"))
-                .when(mIonHeapInfoMetricCollector)
-                .saveProcessOutput(
-                        any(ITestDevice.class), eq("cat /d/ion/heaps/audio"), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mIonHeapInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        mIonHeapInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("ion-system-2"), eq(LogDataType.TEXT), any());
-        verify(mListener).testLog(eq("ion-audio-1"), eq(LogDataType.TEXT), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java b/tests/src/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
index 164e536..db9f1e6 100644
--- a/tests/src/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
@@ -347,6 +347,49 @@
         mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
     }
 
+    @Test
+    public void testRunningProcess_coverageFileNotDeleted() throws Exception {
+        enableJavaCoverage();
+
+        List<String> coverageFileList =
+                ImmutableList.of(
+                        "/data/misc/trace/coverage1.ec",
+                        "/data/misc/trace/coverage2.ec",
+                        "/data/misc/trace/jacoco-123.mm.ec",
+                        "/data/misc/trace/jacoco-456.mm.ec");
+        String psOutput =
+                "USER       PID   PPID  VSZ   RSS   WCHAN       PC  S NAME\n"
+                        + "bluetooth   123  1366  123    456   SyS_epoll+   0  S com.android.bluetooth\n"
+                        + "radio       890     1 7890   123   binder_io+   0  S com.android.phone\n"
+                        + "root         11  1234  567   890   binder_io+   0  S not.a.java.package\n";
+
+        // Setup mocks.
+        mockCoverageFileOnDevice(DEVICE_PATH);
+
+        for (String additionalFile : coverageFileList) {
+            mockCoverageFileOnDevice(additionalFile);
+        }
+
+        doReturn("").when(mMockDevice).executeShellCommand("pm list packages -a");
+        doReturn(psOutput).when(mMockDevice).executeShellCommand("ps -e");
+        doReturn(String.join("\n", coverageFileList))
+                .when(mMockDevice)
+                .executeShellCommand("find /data/misc/trace -name '*.ec'");
+
+        // Simulate a test run.
+        mCodeCoverageCollector.init(mMockContext, mFakeListener);
+        mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
+        Map<String, String> metric = new HashMap<>();
+        metric.put("coverageFilePath", DEVICE_PATH);
+        mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+
+        // Verify the correct files were deleted and some files were not deleted.
+        verify(mMockDevice).deleteFile(coverageFileList.get(0));
+        verify(mMockDevice).deleteFile(coverageFileList.get(1));
+        verify(mMockDevice, never()).deleteFile(coverageFileList.get(2));
+        verify(mMockDevice).deleteFile(coverageFileList.get(3));
+    }
+
     private void mockCoverageFileOnDevice(String devicePath)
             throws IOException, DeviceNotAvailableException {
         File coverageFile = folder.newFile(new File(devicePath).getName());
diff --git a/tests/src/com/android/tradefed/device/metric/MemInfoMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/MemInfoMetricCollectorTest.java
deleted file mode 100644
index c56a081..0000000
--- a/tests/src/com/android/tradefed/device/metric/MemInfoMetricCollectorTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link MemInfoMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class MemInfoMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy MemInfoMetricCollector mMemInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mMemInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(
-                        anyString(), eq(LogDataType.COMPACT_MEMINFO), any(InputStreamSource.class));
-
-        doReturn(new File("compact-meminfo-1"))
-                .when(mMemInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mMemInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-        when(mMemInfoMetricCollector.getFileSuffix()).thenReturn("1");
-
-        mMemInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("compact-meminfo-1"), eq(LogDataType.COMPACT_MEMINFO), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/PagetypeInfoMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/PagetypeInfoMetricCollectorTest.java
deleted file mode 100644
index 9348743..0000000
--- a/tests/src/com/android/tradefed/device/metric/PagetypeInfoMetricCollectorTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link PagetypeInfoMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class PagetypeInfoMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy PagetypeInfoMetricCollector mPagetypeInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mPagetypeInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.TEXT), any(InputStreamSource.class));
-
-        doReturn(new File("pagetypeinfo-1"))
-                .when(mPagetypeInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mPagetypeInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-        when(mPagetypeInfoMetricCollector.getFileSuffix()).thenReturn("1");
-
-        mPagetypeInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("pagetypeinfo-1"), eq(LogDataType.TEXT), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
index a9807b0..9d131c5 100644
--- a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.device.metric;
 
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.times;
 
@@ -120,7 +121,45 @@
         assertTrue("Trace duration metrics not available but expected.",
                 currentMetrics.get("perfetto_trace_extractor_runtime").getMeasurements()
                         .getSingleDouble() >= 0);
-        assertTrue("Trace file size metric is not available.",
+        assertNull("Trace duration metrics not available but expected.",
+                currentMetrics.get("perfetto_trace_file_size_bytes"));
+    }
+
+    @Test
+    public void testProcessingFlowWithFileSizeMetric() throws Exception {
+
+        OptionSetter setter = new OptionSetter(mPerfettoMetricCollector);
+        setter.setOptionValue("pull-pattern-keys", "perfettofile");
+        setter.setOptionValue("perfetto-binary-path", "trx");
+        setter.setOptionValue("convert-metric-file", "false");
+        setter.setOptionValue("collect-perfetto-file-size", "true");
+        HashMap<String, Metric> currentMetrics = new HashMap<>();
+        currentMetrics.put("perfettofile", TfMetricProtoUtil.stringToMetric("/data/trace.pb"));
+        Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/trace.pb")))
+                .thenReturn(new File("trace"));
+
+        TestDescription testDesc = new TestDescription("xyz", "abc");
+        CommandResult cr = new CommandResult();
+        cr.setStatus(CommandStatus.SUCCESS);
+        cr.setStdout("abc:efg");
+
+        Mockito.doReturn(cr).when(mPerfettoMetricCollector).runHostCommand(Mockito.anyLong(),
+                Mockito.any(), Mockito.any(), Mockito.any());
+
+        mPerfettoMetricCollector.testStarted(testDesc);
+        mPerfettoMetricCollector.testEnded(testDesc, currentMetrics);
+
+        Mockito.verify(mPerfettoMetricCollector).runHostCommand(Mockito.anyLong(),
+                Mockito.any(), Mockito.any(), Mockito.any());
+        Mockito.verify(mMockListener)
+                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PERFETTO), Mockito.any());
+        assertTrue("Expected two metrics that includes success status",
+                currentMetrics.get("perfetto_trace_extractor_status").getMeasurements()
+                        .getSingleString().equals("1"));
+        assertTrue("Trace duration metrics not available but expected.",
+                currentMetrics.get("perfetto_trace_extractor_runtime").getMeasurements()
+                        .getSingleDouble() >= 0);
+        assertTrue("Trace file size metric is not available in the final metrics.",
                 currentMetrics.get("perfetto_trace_file_size_bytes").getMeasurements()
                         .getSingleDouble() >= 0);
     }
@@ -331,3 +370,4 @@
     }
 
 }
+
diff --git a/tests/src/com/android/tradefed/device/metric/ProcessMaxMemoryCollectorTest.java b/tests/src/com/android/tradefed/device/metric/ProcessMaxMemoryCollectorTest.java
deleted file mode 100644
index b66138f..0000000
--- a/tests/src/com/android/tradefed/device/metric/ProcessMaxMemoryCollectorTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
-
-import java.util.Collections;
-import java.util.HashMap;
-
-/** Unit tests for {@link ProcessMaxMemoryCollector}. */
-@RunWith(JUnit4.class)
-public class ProcessMaxMemoryCollectorTest {
-
-    private ProcessMaxMemoryCollector mCollector;
-    private IInvocationContext mContext;
-    private ITestDevice mDevice;
-    private ITestInvocationListener mListener;
-
-    private static final String TEST_INPUT =
-            "time,28506638,177086152\n"
-                    + "4,938,system_server,11,22,N/A,44,0,0,N/A,0,0,0,N/A,0,27613,14013,176602,"
-                    + "218228,0,0,122860,122860,1512,1412,5740,8664,0,0,154924,154924,27568,"
-                    + "13972,11916,53456,0,0,123008,123008,0,0,0,0,0,0,0,0,Dalvik Other,3662,0,"
-                    + "104,0,3660,0,0,0,Stack,1576,0,8,0,1576,0,0,0,Cursor,0,0,0,0,0,0,0,0,"
-                    + "Ashmem,156,0,20,0,148,0,0,0,Gfx dev,100,0,48,0,76,0,0,0,Other dev,116,0,"
-                    + "164,0,0,96,0,0,.so mmap,7500,2680,3984,21864,904,2680,0,0,.jar mmap,0,0,0,"
-                    + "0,0,0,0,0,.apk mmap,72398,71448,0,11736,0,71448,0,0,.ttf mmap,0,0,0,0,0,0,"
-                    + "0,0,.dex mmap,76874,46000,0,83644,40,46000,0,0,.oat mmap,8127,2684,64,"
-                    + "26652,0,2684,0,0,.art mmap,1991,48,972,10004,1544,48,0,0,Other mmap,137,0,"
-                    + "44,1024,4,52,0,0,EGL mtrack,0,0,0,0,0,0,0,0,GL mtrack,111,222,333,444,555,"
-                    + "666,777,888,";
-
-    @Before
-    public void setup() throws Exception {
-        mCollector = new ProcessMaxMemoryCollector();
-        mContext = mock(IInvocationContext.class);
-        mDevice = mock(ITestDevice.class);
-        when(mContext.getDevices()).thenReturn(Collections.singletonList(mDevice));
-        mListener = mock(ITestInvocationListener.class);
-        mCollector.init(mContext, mListener);
-        OptionSetter setter = new OptionSetter(mCollector);
-        setter.setOptionValue("memory-usage-process-name", "system_server");
-    }
-
-    @Test
-    public void testCollector() throws Exception {
-        when(mDevice.executeShellCommand(Mockito.eq("dumpsys meminfo --checkin system_server")))
-                .thenReturn(TEST_INPUT);
-
-        DeviceMetricData data = new DeviceMetricData(mContext);
-        mCollector.onStart(data);
-        mCollector.collect(mDevice, data);
-        mCollector.onEnd(data);
-
-        verify(mDevice).executeShellCommand(Mockito.eq("dumpsys meminfo --checkin system_server"));
-
-        HashMap<String, Metric> results = new HashMap<>();
-        data.addToMetrics(results);
-        assertEquals(218228, results.get("MAX_PSS#system_server").getMeasurements().getSingleInt());
-        assertEquals(53456, results.get("MAX_USS#system_server").getMeasurements().getSingleInt());
-    }
-
-    @Test
-    public void testCollectorNoProcess() throws Exception {
-        when(mDevice.executeShellCommand(Mockito.eq("dumpsys meminfo --checkin system_server")))
-                .thenReturn("No process found for: system_server");
-
-        DeviceMetricData data = new DeviceMetricData(mContext);
-        mCollector.onStart(data);
-        mCollector.collect(mDevice, data);
-        mCollector.onEnd(data);
-
-        verify(mDevice).executeShellCommand(Mockito.eq("dumpsys meminfo --checkin system_server"));
-
-        HashMap<String, Metric> results = new HashMap<>();
-        data.addToMetrics(results);
-        assertTrue(results.isEmpty());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollectorTest.java
deleted file mode 100644
index 13605f3..0000000
--- a/tests/src/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollectorTest.java
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/** Unit tests for {@link ScheduleMultipleDeviceMetricCollector}. */
-@RunWith(JUnit4.class)
-public class ScheduleMultipleDeviceMetricCollectorTest {
-    @Rule public final TemporaryFolder folder = new TemporaryFolder();
-    @Mock private ITestDevice mTestDevice;
-    @Mock private ITestInvocationListener mMockListener;
-    @Spy private ScheduleMultipleDeviceMetricCollector mMultipleMetricCollector;
-
-    private IInvocationContext mContext;
-
-    static class TestMeminfoCollector extends ScheduledDeviceMetricCollector {
-        private int mInternalCounter = 0;
-        private String key = "meminfo";
-
-        TestMeminfoCollector() {
-            setTag("meminfoInterval");
-        }
-
-        @Override
-        public void collect(ITestDevice device, DeviceMetricData runData)
-                throws InterruptedException {
-            mInternalCounter++;
-            runData.addMetricForDevice(
-                    device,
-                    key + mInternalCounter,
-                    Metric.newBuilder()
-                            .setMeasurements(
-                                    Measurements.newBuilder()
-                                            .setSingleString("value" + mInternalCounter)));
-        }
-    }
-
-    static class TestJankinfoCollector extends ScheduledDeviceMetricCollector {
-        private int mInternalCounter = 0;
-        private String key = "jankinfo";
-
-        TestJankinfoCollector() {
-            setTag("jankInterval");
-        }
-
-        @Override
-        public void collect(ITestDevice device, DeviceMetricData runData)
-                throws InterruptedException {
-            mInternalCounter++;
-            runData.addMetricForDevice(
-                    device,
-                    key + mInternalCounter,
-                    Metric.newBuilder()
-                            .setMeasurements(
-                                    Measurements.newBuilder()
-                                            .setSingleString("value" + mInternalCounter)));
-        }
-    }
-
-    static class TestFragmentationCollector extends ScheduledDeviceMetricCollector {
-        private int mInternalCounter = 0;
-        private String key = "fragmentation";
-
-        TestFragmentationCollector() {
-            setTag("fragmentationInterval");
-        }
-
-        @Override
-        public void collect(ITestDevice device, DeviceMetricData runData)
-                throws InterruptedException {
-            mInternalCounter++;
-            runData.addMetricForDevice(
-                    device,
-                    key + mInternalCounter,
-                    Metric.newBuilder()
-                            .setMeasurements(
-                                    Measurements.newBuilder()
-                                            .setSingleString("value" + mInternalCounter)));
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        mContext = new InvocationContext();
-        mContext.addAllocatedDevice("test device", mTestDevice);
-    }
-
-    @Test
-    public void testMultipleMetricCollector_success() throws Exception {
-        OptionSetter setter = new OptionSetter(mMultipleMetricCollector);
-
-        // Set up the metric collection storage path.
-        File metricStoragePath = folder.newFolder();
-        setter.setOptionValue("metric-storage-path", metricStoragePath.toString());
-
-        // Set up the intervals.
-        Map<String, Long> intervals = new HashMap<>();
-        intervals.put("meminfoInterval", 100L);
-        intervals.put("fragmentationInterval", 100L);
-        intervals.put("jankInterval", 100L);
-        for (String key : intervals.keySet()) {
-            setter.setOptionValue(
-                    "metric-collection-intervals", key, intervals.get(key).toString());
-        }
-
-        // Request the collectors.
-        List<String> classnames = new ArrayList<>();
-        classnames.add(TestMeminfoCollector.class.getName());
-        classnames.add(TestJankinfoCollector.class.getName());
-        classnames.add(TestFragmentationCollector.class.getName());
-        for (String key : classnames) {
-            setter.setOptionValue("metric-collector-command-classes", key);
-        }
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        // Start the tests.
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mMultipleMetricCollector.init(mContext, mMockListener);
-        try {
-            mMultipleMetricCollector.onTestRunStart(runData);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mMultipleMetricCollector.onTestRunEnd(runData, metrics);
-        }
-
-        // We give it 500msec to run and 100msec interval we should easily have at least run all the
-        // metrics once.
-        // assert that the metrics contains filenames of all the collected metrics.
-        HashMap<String, Metric> metricsCollected = new HashMap<>();
-        runData.addToMetrics(metricsCollected);
-
-        assertTrue(metricsCollected.containsKey("jankinfo1"));
-        assertTrue(metricsCollected.containsKey("meminfo1"));
-        assertTrue(metricsCollected.containsKey("fragmentation1"));
-    }
-
-    @Test
-    public void testMultipleMetricCollector_noFailureEvenIfNoCollectorRequested() throws Exception {
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mMultipleMetricCollector.init(mContext, mMockListener);
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        try {
-            mMultipleMetricCollector.onTestRunStart(runData);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mMultipleMetricCollector.onTestRunEnd(runData, metrics);
-        }
-
-        // No metrics should have been collected.
-        HashMap<String, Metric> metricsCollected = new HashMap<>();
-        runData.addToMetrics(metricsCollected);
-
-        assertEquals(0, metricsCollected.size());
-    }
-
-    /** Test that if a specified collector does not exists, we ignore it and proceed. */
-    @Test
-    public void testMultipleMetricCollector_collectorNotFound() throws Exception {
-        OptionSetter setter = new OptionSetter(mMultipleMetricCollector);
-
-        // Set up the metric collection storage path.
-        File metricStoragePath = folder.newFolder();
-        setter.setOptionValue("metric-storage-path", metricStoragePath.toString());
-
-        // Set up the intervals.
-        Map<String, Long> intervals = new HashMap<>();
-        intervals.put("meminfoInterval", 100L);
-        for (String key : intervals.keySet()) {
-            setter.setOptionValue(
-                    "metric-collection-intervals", key, intervals.get(key).toString());
-        }
-
-        // Request the collectors.
-        List<String> classnames = new ArrayList<>();
-        classnames.add(TestMeminfoCollector.class.getName());
-        classnames.add("this.does.not.exists.collector");
-        for (String key : classnames) {
-            setter.setOptionValue("metric-collector-command-classes", key);
-        }
-
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mMultipleMetricCollector.init(mContext, mMockListener);
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        try {
-            mMultipleMetricCollector.onTestRunStart(runData);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mMultipleMetricCollector.onTestRunEnd(runData, metrics);
-        }
-
-        // No metrics should have been collected.
-        HashMap<String, Metric> metricsCollected = new HashMap<>();
-        runData.addToMetrics(metricsCollected);
-
-        assertTrue(metricsCollected.containsKey("meminfo1"));
-    }
-
-    @Test
-    public void testMultipleMetricCollector_failsForNonNegativeInterval() throws Exception {
-        String expectedStderr =
-                "class com.android.tradefed.device.metric."
-                        + "ScheduleMultipleDeviceMetricCollectorTest$TestJankinfoCollector expects "
-                        + "a non negative interval.";
-
-        OptionSetter setter = new OptionSetter(mMultipleMetricCollector);
-
-        // Set up the metric collection storage path.
-        setter.setOptionValue("metric-storage-path", folder.newFolder().toString());
-
-        // Set up the interval.
-        Map<String, Long> intervals = new HashMap<>();
-        intervals.put("jankInterval", -100L);
-        for (String key : intervals.keySet()) {
-            setter.setOptionValue(
-                    "metric-collection-intervals", key, intervals.get(key).toString());
-        }
-
-        // Set up the classname.
-        List<String> classnames = new ArrayList<>();
-        classnames.add(TestJankinfoCollector.class.getName());
-        for (String key : classnames) {
-            setter.setOptionValue("metric-collector-command-classes", key);
-        }
-
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-
-        // Start the tests, which should fail with the expected error message.
-        mMultipleMetricCollector.init(mContext, mMockListener);
-
-        try {
-            mMultipleMetricCollector.onTestRunStart(runData);
-            fail("Should throw illegal argument exception in case of negative intervals.");
-        } catch (IllegalArgumentException e) {
-            assertEquals(expectedStderr, e.getMessage());
-        }
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/ScheduledDeviceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/ScheduledDeviceMetricCollectorTest.java
deleted file mode 100644
index 1f23a43..0000000
--- a/tests/src/com/android/tradefed/device/metric/ScheduledDeviceMetricCollectorTest.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/** Unit tests for {@link ScheduledDeviceMetricCollector}. */
-@RunWith(JUnit4.class)
-public class ScheduledDeviceMetricCollectorTest {
-    private Map<String, ITestDevice> mDevicesWithNames = new HashMap<>();
-
-    public static class TestableAsyncTimer extends ScheduledDeviceMetricCollector {
-        private int mInternalCounter = 0;
-
-        @Override
-        void collect(ITestDevice device, DeviceMetricData runData) throws InterruptedException {
-            mInternalCounter++;
-            runData.addMetricForDevice(
-                    device,
-                    "key" + mInternalCounter,
-                    Metric.newBuilder()
-                            .setMeasurements(
-                                    Measurements.newBuilder()
-                                            .setSingleString("value" + mInternalCounter)));
-        }
-    }
-
-    private TestableAsyncTimer mBase;
-    private IInvocationContext mContext;
-    private ITestInvocationListener mMockListener;
-
-    @Before
-    public void setUp() {
-        mBase = new TestableAsyncTimer();
-        mContext = new InvocationContext();
-        mMockListener = Mockito.mock(ITestInvocationListener.class);
-    }
-
-    /** Test the periodic run of the collector once testRunStarted has been called. */
-    @Test
-    public void testSetupAndPeriodicRunSingleDevice() throws Exception {
-        // Setup the context with the devices.
-        mDevicesWithNames.put("test device 1", mock(ITestDevice.class));
-        mContext.addAllocatedDevice(mDevicesWithNames);
-
-        OptionSetter setter = new OptionSetter(mBase);
-        // 100 ms interval
-        setter.setOptionValue("interval", "100");
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mBase.init(mContext, mMockListener);
-        try {
-            mBase.testRunStarted("testRun", 1);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mBase.testRunEnded(0l, metrics);
-        }
-        // We give it 500msec to run and 100msec interval we should easily have at least three
-        // iterations
-        assertTrue(metrics.containsKey("key1"));
-        assertTrue(metrics.containsKey("key2"));
-        assertTrue(metrics.containsKey("key3"));
-    }
-
-    /**
-     * Test the periodic run of the collector on multiple devices once testRunStarted has been
-     * called.
-     */
-    @Test
-    public void testSetupAndPeriodicRunMultipleDevices() throws Exception {
-        // Setup the context with the devices.
-        mDevicesWithNames.put("test device 1", mock(ITestDevice.class));
-        mDevicesWithNames.put("test device 2", mock(ITestDevice.class));
-        mContext.addAllocatedDevice(mDevicesWithNames);
-
-        OptionSetter setter = new OptionSetter(mBase);
-        // 100 ms interval
-        setter.setOptionValue("interval", "100");
-        HashMap<String, Metric> metrics = new HashMap<>();
-        mBase.init(mContext, mMockListener);
-        try {
-            mBase.testRunStarted("testRun", 1);
-            RunUtil.getDefault().sleep(500);
-        } finally {
-            mBase.testRunEnded(0l, metrics);
-        }
-        // We give it 500msec to run and 100msec interval we should easily have at least two
-        // iterations one for each device. The order of execution is arbitrary so check for prefix
-        // only.
-        assertTrue(metrics.keySet().stream().anyMatch(key -> key.startsWith("{test device 1}")));
-        assertTrue(metrics.keySet().stream().anyMatch(key -> key.startsWith("{test device 2}")));
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/TemperatureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/TemperatureCollectorTest.java
deleted file mode 100644
index a4c8550..0000000
--- a/tests/src/com/android/tradefed/device/metric/TemperatureCollectorTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Collections;
-import java.util.HashMap;
-
-/** Unit tests for {@link TemperatureCollector}. */
-@RunWith(JUnit4.class)
-public class TemperatureCollectorTest {
-
-    private TemperatureCollector mCollector;
-    private IInvocationContext mContext;
-    private ITestDevice mDevice;
-    private ITestInvocationListener mListener;
-
-    @Before
-    public void setup() throws Exception {
-        mCollector = new TemperatureCollector();
-        mContext = mock(IInvocationContext.class);
-        mDevice = mock(ITestDevice.class);
-        when(mDevice.isAdbRoot()).thenReturn(true);
-        when(mContext.getDevices()).thenReturn(Collections.singletonList(mDevice));
-        mListener = mock(ITestInvocationListener.class);
-        mCollector.init(mContext, mListener);
-        OptionSetter setter = new OptionSetter(mCollector);
-        setter.setOptionValue(
-                "device-temperature-file-path", "/sys/class/hwmon/hwmon1/device/msm_therm");
-    }
-
-    @Test
-    public void testCollector() throws Exception {
-        when(mDevice.executeShellCommand(eq("cat /sys/class/hwmon/hwmon1/device/msm_therm")))
-                .thenReturn("Result:32 Raw:7e51", "Result:22 Raw:7b51");
-
-        DeviceMetricData data = new DeviceMetricData(mContext);
-        mCollector.onStart(data);
-        mCollector.collect(mDevice, data);
-        mCollector.collect(mDevice, data);
-        mCollector.onEnd(data);
-
-        verify(mDevice, times(2))
-                .executeShellCommand(eq("cat /sys/class/hwmon/hwmon1/device/msm_therm"));
-
-        HashMap<String, Metric> results = new HashMap<>();
-        data.addToMetrics(results);
-        assertEquals(32D, results.get("max_temperature").getMeasurements().getSingleDouble(), 0);
-        assertEquals(22D, results.get("min_temperature").getMeasurements().getSingleDouble(), 0);
-    }
-
-    @Test
-    public void testCollectorNoData() throws Exception {
-        when(mDevice.executeShellCommand(eq("cat /sys/class/hwmon/hwmon1/device/msm_therm")))
-                .thenReturn(
-                        "cat: /sys/class/hwmon/hwmon1/device/msm_therm: No such file or directory");
-
-        DeviceMetricData data = new DeviceMetricData(mContext);
-        mCollector.onStart(data);
-        mCollector.collect(mDevice, data);
-        mCollector.onEnd(data);
-
-        verify(mDevice).executeShellCommand(eq("cat /sys/class/hwmon/hwmon1/device/msm_therm"));
-
-        HashMap<String, Metric> results = new HashMap<>();
-        data.addToMetrics(results);
-        assertTrue(results.isEmpty());
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/metric/TraceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/TraceMetricCollectorTest.java
deleted file mode 100644
index ffb3727..0000000
--- a/tests/src/com/android/tradefed/device/metric/TraceMetricCollectorTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device.metric;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.File;
-
-/** Unit tests for {@link TraceMetricCollector}. */
-// TODO(b/71868090): Consolidate all the individual metric collector tests into one common tests.
-@RunWith(JUnit4.class)
-public class TraceMetricCollectorTest {
-    @Mock IInvocationContext mContext;
-
-    @Mock ITestInvocationListener mListener;
-
-    @Mock ITestDevice mDevice;
-
-    @Spy TraceMetricCollector mTraceInfoMetricCollector;
-
-    @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
-
-    @Before
-    public void setup() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mTraceInfoMetricCollector.init(mContext, mListener);
-
-        doNothing()
-                .when(mListener)
-                .testLog(anyString(), eq(LogDataType.TEXT), any(InputStreamSource.class));
-
-        doReturn(new File("trace-1"))
-                .when(mTraceInfoMetricCollector)
-                .saveProcessOutput(any(ITestDevice.class), anyString(), anyString());
-
-        doReturn(tempFolder.newFolder()).when(mTraceInfoMetricCollector).createTempDir();
-    }
-
-    @Test
-    public void testCollect() throws Exception {
-        DeviceMetricData runData = new DeviceMetricData(mContext);
-        when(mTraceInfoMetricCollector.getFileSuffix()).thenReturn("1");
-
-        mTraceInfoMetricCollector.collect(mDevice, runData);
-
-        // Verify that we logged the metric file.
-        verify(mListener).testLog(eq("trace-1"), eq(LogDataType.TEXT), any());
-    }
-}
diff --git a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
index dfb8b95..65e7c06 100644
--- a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
@@ -16,6 +16,8 @@
 package com.android.tradefed.invoker;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -425,4 +427,26 @@
             FileUtil.deleteFile(buildFile);
         }
     }
+
+    @Test
+    public void testBuildInfo_testTag() throws Exception {
+        IBuildInfo info = new BuildInfo();
+        assertEquals("stub", info.getTestTag());
+        File testsDir = FileUtil.createTempDir("doesnt_matter_testsdir");
+        try {
+            info.setFile(BuildInfoFileKey.TESTDIR_IMAGE, testsDir, "tests");
+            mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, info);
+            mConfig.getCommandOptions().setTestTag("test");
+            TestInformation testInfo =
+                    TestInformation.newBuilder().setInvocationContext(mContext).build();
+            assertNull(testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY));
+            mExecution.fetchBuild(testInfo, mConfig, null, null);
+            // Build test tag was updated
+            assertEquals("test", info.getTestTag());
+            // Execution file was back filled
+            assertNotNull(testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY));
+        } finally {
+            FileUtil.recursiveDelete(testsDir);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index b66ba56..768ae2c 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -174,15 +174,17 @@
         EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogger);
         EasyMock.expect(mMockConfig.getConfigurationDescription()).andReturn(mConfigDesc);
         mMockLogger.init();
+        EasyMock.expectLastCall().times(2);
         EasyMock.expect(mMockLogger.getLog())
                 .andReturn(new ByteArrayInputStreamSource("fake".getBytes()));
         mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         mMockLogRegistry.registerLogger(mMockLogger);
+        EasyMock.expectLastCall().times(2);
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
@@ -270,15 +272,17 @@
         EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogger);
         EasyMock.expect(mMockConfig.getConfigurationDescription()).andReturn(mConfigDesc);
         mMockLogger.init();
+        EasyMock.expectLastCall().times(2);
         EasyMock.expect(mMockLogger.getLog())
                 .andReturn(new ByteArrayInputStreamSource("fake".getBytes()));
         mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         mMockLogRegistry.registerLogger(mMockLogger);
+        EasyMock.expectLastCall().times(2);
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
@@ -352,15 +356,17 @@
         EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogger);
         EasyMock.expect(mMockConfig.getConfigurationDescription()).andReturn(mConfigDesc);
         mMockLogger.init();
+        EasyMock.expectLastCall().times(2);
         EasyMock.expect(mMockLogger.getLog())
                 .andReturn(new ByteArrayInputStreamSource("fake".getBytes()));
         mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         mMockLogRegistry.registerLogger(mMockLogger);
+        EasyMock.expectLastCall().times(2);
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
@@ -442,15 +448,17 @@
         EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogger);
         EasyMock.expect(mMockConfig.getConfigurationDescription()).andReturn(mConfigDesc);
         mMockLogger.init();
+        EasyMock.expectLastCall().times(2);
         EasyMock.expect(mMockLogger.getLog())
                 .andReturn(new ByteArrayInputStreamSource("fake".getBytes()));
         mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         mMockLogRegistry.registerLogger(mMockLogger);
+        EasyMock.expectLastCall().times(2);
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expectLastCall().times(3);
 
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index f8c4eb2..dc53f52 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -412,11 +412,6 @@
 
         setupMockFailureListeners(exception);
         setupInvoke();
-        EasyMock.reset(mMockLogger, mMockLogRegistry);
-        mMockLogRegistry.registerLogger(mMockLogger);
-        mMockLogger.init();
-        mMockLogger.closeLog();
-        mMockLogRegistry.unregisterLogger();
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         CommandOptions cmdOptions = new CommandOptions();
         final String expectedTestTag = "TEST_TAG";
@@ -449,13 +444,6 @@
         setupMockFailureListenersAny(
                 new BuildRetrievalError("fake", InfraErrorIdentifier.ARTIFACT_DOWNLOAD_ERROR),
                 true);
-
-        EasyMock.reset(mMockLogger, mMockLogRegistry);
-        mMockLogRegistry.registerLogger(mMockLogger);
-        mMockLogger.init();
-        mMockLogger.closeLog();
-        mMockLogRegistry.unregisterLogger();
-
         EasyMock.expect(mMockLogger.getLog()).andReturn(EMPTY_STREAM_SOURCE);
         EasyMock.expect(mMockDevice.getLogcat()).andReturn(EMPTY_STREAM_SOURCE).times(2);
         mMockDevice.clearLogcat();
@@ -484,12 +472,6 @@
                         "No build found to test.", InfraErrorIdentifier.ARTIFACT_NOT_FOUND),
                 true);
 
-        EasyMock.reset(mMockLogger, mMockLogRegistry);
-        mMockLogRegistry.registerLogger(mMockLogger);
-        mMockLogger.init();
-        mMockLogger.closeLog();
-        mMockLogRegistry.unregisterLogger();
-
         EasyMock.expect(mMockLogger.getLog()).andReturn(EMPTY_STREAM_SOURCE);
         EasyMock.expect(mMockDevice.getLogcat()).andReturn(EMPTY_STREAM_SOURCE).times(2);
         mMockDevice.clearLogcat();
@@ -520,13 +502,6 @@
                         "No build found to test.", InfraErrorIdentifier.ARTIFACT_NOT_FOUND),
                 true, /* don't expect host log */
                 false);
-
-        EasyMock.reset(mMockLogger, mMockLogRegistry);
-        mMockLogRegistry.registerLogger(mMockLogger);
-        mMockLogger.init();
-        mMockLogger.closeLog();
-        EasyMock.expectLastCall().times(2);
-
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         mStubConfiguration.setTest(test);
         // Host log fails to report
@@ -537,7 +512,7 @@
         Capture<IBuildInfo> captured = new Capture<>();
         mMockBuildProvider.cleanUp(EasyMock.capture(captured));
         mMockLogRegistry.unregisterLogger();
-        EasyMock.expectLastCall().times(2);
+        mMockLogger.closeLog();
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         replayMocks(test, mockRescheduler);
         mTestInvocation.invoke(mStubInvocationMetadata, mStubConfiguration, mockRescheduler);
@@ -1149,9 +1124,12 @@
                 mMockTestListener.invocationFailed(EasyMock.<FailureDescription>anyObject());
                 mMockSummaryListener.invocationFailed(EasyMock.<FailureDescription>anyObject());
             } else {
+                FailureStatus failureStatus = FailureStatus.INFRA_FAILURE;
+                if (throwable instanceof BuildError) {
+                    failureStatus = FailureStatus.DEPENDENCY_ISSUE;
+                }
                 FailureDescription failure =
-                        FailureDescription.create(
-                                        throwable.getMessage(), FailureStatus.INFRA_FAILURE)
+                        FailureDescription.create(throwable.getMessage(), failureStatus)
                                 .setCause(throwable);
                 if (throwable instanceof BuildRetrievalError) {
                     failure.setActionInProgress(ActionInProgress.FETCHING_ARTIFACTS);
diff --git a/tests/src/com/android/tradefed/invoker/logger/InvocationLocalTest.java b/tests/src/com/android/tradefed/invoker/logger/InvocationLocalTest.java
index 7ce1e0d..d7a9b68 100644
--- a/tests/src/com/android/tradefed/invoker/logger/InvocationLocalTest.java
+++ b/tests/src/com/android/tradefed/invoker/logger/InvocationLocalTest.java
@@ -89,7 +89,7 @@
         Object value0 = invocation(() -> local.get());
         Object value1 = invocation(() -> local.get());
 
-        assertThat(value0).isNotSameAs(value1);
+        assertThat(value0).isNotSameInstanceAs(value1);
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java b/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
index 91b8e71..47a791f 100644
--- a/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
+++ b/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.StubBuildProvider;
@@ -47,6 +48,7 @@
 import com.android.tradefed.testtype.suite.ITestSuite;
 import com.android.tradefed.util.FileUtil;
 
+import java.util.Arrays;
 import org.easymock.EasyMock;
 import org.junit.Assert;
 import org.junit.Before;
@@ -256,11 +258,36 @@
         }
     }
 
+    public class FakeStrictShardHelper extends StrictShardHelper {
+        List<IRemoteTest> fakeModules = new ArrayList<>();
+
+        public FakeStrictShardHelper(List<IRemoteTest> modules) {
+            fakeModules.addAll(modules);
+        }
+
+        @Override
+        protected List<List<IRemoteTest>> splitTests(List<IRemoteTest> fullList, int shardCount) {
+            List<List<IRemoteTest>> shards = new ArrayList<>();
+            shards.add(new ArrayList<>(fakeModules));
+            shards.add(new ArrayList<>(fakeModules));
+            return shards;
+        }
+    }
+
     private ITestSuite createFakeSuite(String name) throws Exception {
         ITestSuite suite = new SplitITestSuite(name);
         return suite;
     }
 
+    private ITestSuite createFakeSuite(String name, boolean intraModuleSharding) throws Exception {
+        ITestSuite suite = new SplitITestSuite(name);
+        if (!intraModuleSharding) {
+            OptionSetter setter = new OptionSetter(suite);
+            setter.setOptionValue("intra-module-sharding", "false");
+        }
+        return suite;
+    }
+
     private List<IRemoteTest> testShard(int shardIndex) throws Exception {
         mContext.addAllocatedDevice("default", EasyMock.createMock(ITestDevice.class));
         List<IRemoteTest> test = new ArrayList<>();
@@ -282,6 +309,26 @@
         return mConfig.getTests();
     }
 
+    private List<IRemoteTest> createITestSuiteList(List<String> modules) throws Exception {
+        List<IRemoteTest> tests = new ArrayList<>();
+        for (String name : modules) {
+            tests.add(createFakeSuite(name, false).split(2, mTestInfo).iterator().next());
+        }
+
+        CommandOptions options = new CommandOptions();
+        OptionSetter setter = new OptionSetter(options);
+        setter.setOptionValue("shard-count", "2");
+        setter.setOptionValue("shard-index", Integer.toString(1));
+        setter.setOptionValue("optimize-mainline-test", "true");
+        mConfig.setCommandOptions(options);
+        mConfig.setCommandLine(new String[] {"empty"});
+        mConfig.setTests(tests);
+
+        FakeStrictShardHelper fakeHelper = new FakeStrictShardHelper(tests);
+        fakeHelper.shardConfig(mConfig, mTestInfo, mRescheduler, null);
+        return mConfig.getTests();
+    }
+
     /**
      * Total for all the _shardX test should be 14 tests (2 per modules). 6 for module1: 3 module1
      * shard * 2 4 for module2: 2 module2 shard * 2 4 for module3: 2 module3 shard * 2
@@ -304,6 +351,61 @@
         assertEquals(1, ((ITestSuite) res.get(2)).getDirectModule().numTests());
     }
 
+    /**
+     * Test that the unsorted test modules are re-ordered.
+     */
+    @Test
+    public void testReorderTestModules() throws Exception {
+        List<String> unSortedModules =
+            Arrays.asList(
+                "module1[com.android.mod1.apex]",
+                "module1[com.android.mod1.apex+com.android.mod2.apex]",
+                "module2[com.android.mod1.apex]",
+                "module1[com.android.mod3.apk]",
+                "module2[com.android.mod1.apex+com.android.mod2.apex]",
+                "module2[com.android.mod3.apk]",
+                "module3[com.android.mod1.apex+com.android.mod2.apex]",
+                "module3[com.android.mod3.apk]",
+                "module4[com.android.mod3.apk]",
+                "module5[com.android.mod3.apk]"
+            );
+        List<IRemoteTest> res = createITestSuiteList(unSortedModules);
+
+        List<String> sortedModules =
+            Arrays.asList(
+                "module1[com.android.mod1.apex]",
+                "module2[com.android.mod1.apex]",
+                "module1[com.android.mod1.apex+com.android.mod2.apex]",
+                "module2[com.android.mod1.apex+com.android.mod2.apex]",
+                "module3[com.android.mod1.apex+com.android.mod2.apex]",
+                "module1[com.android.mod3.apk]",
+                "module2[com.android.mod3.apk]",
+                "module3[com.android.mod3.apk]",
+                "module4[com.android.mod3.apk]",
+                "module5[com.android.mod3.apk]"
+            );
+        for (int i = 0 ; i < sortedModules.size() ; i++) {
+            assertEquals(sortedModules.get(i), ((ITestSuite)res.get(i)).getDirectModule().getId());
+        }
+    }
+
+    /**
+     * Test that the there exist a module with invalid parameterized modules defined.
+     */
+    @Test
+    public void testReorderTestModulesWithUnexpectedMainlineModules() throws Exception {
+        List<String> modules = Arrays.asList("module1[com.mod1.apex]", "module1[com.mod1]");
+        try {
+            List<IRemoteTest> res = createITestSuiteList(modules);
+            fail("Should have thrown an exception.");
+        } catch (RuntimeException expected) {
+            // expected
+            assertTrue(expected.getMessage().contains(
+                    "Module: module1[com.mod1] doesn't match the pattern for mainline " +
+                        "modules. The pattern should end with apk/apex/apks."));
+        }
+    }
+
     @Test
     public void testMergeSuite_shard1() throws Exception {
         List<IRemoteTest> res = testShard(1);
diff --git a/tests/src/com/android/tradefed/monitoring/LabResourceDeviceMonitorTest.java b/tests/src/com/android/tradefed/monitoring/LabResourceDeviceMonitorTest.java
new file mode 100644
index 0000000..ee51dfd
--- /dev/null
+++ b/tests/src/com/android/tradefed/monitoring/LabResourceDeviceMonitorTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.monitoring;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class LabResourceDeviceMonitorTest {
+
+    private LabResourceDeviceMonitor mLabResourceDeviceMonitor;
+
+    @Before
+    public void setUp() {
+        mLabResourceDeviceMonitor = new LabResourceDeviceMonitor();
+    }
+
+    @Test
+    public void testServerStartAndShutdown() {
+        Assert.assertFalse(
+                "server should be empty before monitor run",
+                mLabResourceDeviceMonitor.getServer().isPresent());
+        mLabResourceDeviceMonitor.run();
+        Assert.assertTrue(
+                "server should present after monitor run",
+                mLabResourceDeviceMonitor.getServer().isPresent());
+        Assert.assertEquals(
+                LabResourceDeviceMonitor.DEFAULT_PORT,
+                mLabResourceDeviceMonitor.getServer().get().getPort());
+        mLabResourceDeviceMonitor.stop();
+        Assert.assertTrue(
+                "server should be shutdown after monitor stop",
+                mLabResourceDeviceMonitor.getServer().get().isShutdown());
+    }
+}
diff --git a/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java b/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
index d83c343..82fc304 100644
--- a/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
+++ b/tests/src/com/android/tradefed/postprocessor/PerfettoGenericPostProcessorTest.java
@@ -306,10 +306,11 @@
     }
 
     /**
-     * Test metrics enabled with key prefixing.
+     * Test metrics enabled with key and string value prefixing.
      */
     @Test
-    public void testParsingWithKeyPrefixing() throws ConfigurationException, IOException {
+    public void testParsingWithKeyAndStringValuePrefixing()
+            throws ConfigurationException, IOException {
         setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
         mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
         mOptionSetter.setOptionValue(KEY_PREFIX_OPTION,
@@ -320,8 +321,8 @@
                 PREFIX_OPTION_VALUE,
                 new LogFile(
                         perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
-        Map<String, Metric.Builder> parsedMetrics =
-                mProcessor.processRunMetricsAndLogs(new HashMap<>(), testLogs);
+        Map<String, Metric.Builder> parsedMetrics = mProcessor
+                .processRunMetricsAndLogs(new HashMap<>(), testLogs);
 
         assertMetricsContain(parsedMetrics,
                 "perfetto_android_hwui_metric-process_info-process_name-com.android.systemui-all_mem_min",
@@ -329,6 +330,28 @@
 
     }
 
+    /**
+     * Test metrics enabled with key and integer value prefixing.
+     */
+    @Test
+    public void testParsingWithKeyAndIntegerValuePrefixing()
+            throws ConfigurationException, IOException {
+        setupPerfettoMetricFile(METRIC_FILE_FORMAT.text, true);
+        mOptionSetter.setOptionValue(PREFIX_OPTION, PREFIX_OPTION_VALUE);
+        mOptionSetter.setOptionValue(KEY_PREFIX_OPTION,
+                "perfetto.protos.AndroidCpuMetric.CoreData.id");
+        mOptionSetter.setOptionValue(ALL_METRICS_OPTION, "true");
+        Map<String, LogFile> testLogs = new HashMap<>();
+        testLogs.put(
+                PREFIX_OPTION_VALUE,
+                new LogFile(
+                        perfettoMetricProtoFile.getAbsolutePath(), "some.url", LogDataType.TEXTPB));
+        Map<String, Metric.Builder> parsedMetrics = mProcessor
+                .processRunMetricsAndLogs(new HashMap<>(), testLogs);
+        assertMetricsContain(parsedMetrics, "perfetto_android_cpu-process_info-name-com.google."
+                + "android.apps.messaging-threads-name-BG Thread #1-core-id-1-metrics-runtime_ns",
+                14376405);
+    }
 
     /** Test the post processor can parse binary perfetto metric proto format. */
     @Test
@@ -515,6 +538,54 @@
                         "    all_mem_min: 15120269\n" +
                         "    all_mem_avg: 24468104.289592762\n" +
                         "  }\n" +
+                        "}"
+                        + "android_cpu {\n" +
+                        "  process_info {\n" +
+                        "    name: \"com.google.android.apps.messaging\"\n" +
+                        "    metrics {\n" +
+                        "      mcycles: 139\n" +
+                        "      runtime_ns: 639064902\n" +
+                        "      min_freq_khz: 576000\n" +
+                        "      max_freq_khz: 2016000\n" +
+                        "      avg_freq_khz: 324000\n" +
+                        "    }\n" +
+                        "    threads {\n" +
+                        "      name: \"BG Thread #1\"\n" +
+                        "      core {\n" +
+                        "        id: 0\n" +
+                        "        metrics {\n" +
+                        "          runtime_ns: 8371202\n" +
+                        "        }\n" +
+                        "      }\n" +
+                        "      core {\n" +
+                        "        id: 1\n" +
+                        "        metrics {\n" +
+                        "          mcycles: 0\n" +
+                        "          runtime_ns: 14376405\n" +
+                        "          min_freq_khz: 1785600\n" +
+                        "          max_freq_khz: 1785600\n" +
+                        "          avg_freq_khz: 57977\n" +
+                        "        }\n" +
+                        "      }\n" +
+                        "      metrics {\n" +
+                        "        mcycles: 0\n" +
+                        "        runtime_ns: 22747607\n" +
+                        "        min_freq_khz: 1785600\n" +
+                        "        max_freq_khz: 1785600\n" +
+                        "        avg_freq_khz: 36000\n" +
+                        "      }\n" +
+                        "      core_type {\n" +
+                        "        type: \"little\"\n" +
+                        "        metrics {\n" +
+                        "          mcycles: 0\n" +
+                        "          runtime_ns: 22747607\n" +
+                        "          min_freq_khz: 1785600\n" +
+                        "          max_freq_khz: 1785600\n" +
+                        "          avg_freq_khz: 36000\n" +
+                        "        }\n" +
+                        "      }\n" +
+                        "    }\n" +
+                        " }\n" +
                         "}";
         FileWriter fileWriter = null;
         try {
@@ -593,3 +664,4 @@
                                                                         .getSingleString())));
     }
 }
+
diff --git a/tests/src/com/android/tradefed/presubmit/GeneralTestsConfigValidation.java b/tests/src/com/android/tradefed/presubmit/GeneralTestsConfigValidation.java
index ff0692e..852a4df 100644
--- a/tests/src/com/android/tradefed/presubmit/GeneralTestsConfigValidation.java
+++ b/tests/src/com/android/tradefed/presubmit/GeneralTestsConfigValidation.java
@@ -80,6 +80,8 @@
                             "com.android.tradefed.testtype.rust.RustBinaryTest",
                             "com.android.tradefed.testtype.StubTest",
                             "com.android.tradefed.testtype.ArtRunTest",
+                            "com.android.tradefed.testtype.ArtGTest",
+                            "com.android.tradefed.testtype.mobly.MoblyBinaryHostTest",
                             // Others
                             "com.google.android.deviceconfig.RebootTest"));
 
diff --git a/tests/src/com/android/tradefed/result/JsonHttpTestResultReporterTest.java b/tests/src/com/android/tradefed/result/JsonHttpTestResultReporterTest.java
index d9b8eb6..26e020b 100644
--- a/tests/src/com/android/tradefed/result/JsonHttpTestResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/JsonHttpTestResultReporterTest.java
@@ -25,6 +25,7 @@
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import org.json.JSONException;
@@ -65,14 +66,18 @@
     @Test
     public void testSkipFailedRuns_notSet() throws JSONException {
         mReporter.invocationStarted(mContext);
-        injectTestRun(mReporter, "run1", "test", "metric1", 0, true);
-        injectTestRun(mReporter, "run2", "test", "metric2", 1, false);
+        injectTestRun(mReporter, "run1", "test", "123", 0, true);
+        injectTestRun(mReporter, "run2", "test", "456", 1, false);
         mReporter.invocationEnded(0);
         ArgumentCaptor<JSONObject> jsonCaptor = ArgumentCaptor.forClass(JSONObject.class);
         verify(mReporter).postResults(jsonCaptor.capture());
         // Both runs should be in the posted metrics.
         Assert.assertTrue(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY).has("run1"));
+        Assert.assertTrue(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY).getJSONObject("run1")
+                .has("run_metric"));
         Assert.assertTrue(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY).has("run2"));
+        Assert.assertTrue(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY).getJSONObject("run2")
+                .has("run_metric"));
     }
 
     /** Test that failed runs are skipped when skip-failed-runs is set. */
@@ -81,16 +86,59 @@
         OptionSetter optionSetter = new OptionSetter(mReporter);
         optionSetter.setOptionValue(SKIP_FAILED_RUNS_OPTION, String.valueOf(true));
         mReporter.invocationStarted(mContext);
-        injectTestRun(mReporter, "run1", "test", "metric1", 0, true);
-        injectTestRun(mReporter, "run2", "test", "metric2", 1, false);
+        injectTestRun(mReporter, "run1", "test", "123", 0, true);
+        injectTestRun(mReporter, "run2", "test", "456", 1, false);
         mReporter.invocationEnded(0);
         ArgumentCaptor<JSONObject> jsonCaptor = ArgumentCaptor.forClass(JSONObject.class);
         verify(mReporter).postResults(jsonCaptor.capture());
         // Only the first run should be in the posted metrics.
         Assert.assertTrue(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY).has("run1"));
+        Assert.assertTrue(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY).getJSONObject("run1")
+                .has("run_metric"));
         Assert.assertFalse(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY).has("run2"));
     }
 
+    /** Test non-numeric metrics are not posted in the final JSONObject. */
+    @Test
+    public void testInvalidMetricsNotSet() throws ConfigurationException, JSONException {
+        OptionSetter optionSetter = new OptionSetter(mReporter);
+        optionSetter.setOptionValue(SKIP_FAILED_RUNS_OPTION, String.valueOf(true));
+        mReporter.invocationStarted(mContext);
+        // Inject invalid metric "1.23invalid".
+        injectTestRun(mReporter, "run1", "test", "1.23invalid", 0, false);
+        mReporter.invocationEnded(0);
+        ArgumentCaptor<JSONObject> jsonCaptor = ArgumentCaptor.forClass(JSONObject.class);
+        verify(mReporter).postResults(jsonCaptor.capture());
+        // Only the first run should be in the posted metrics.
+        CLog.i(jsonCaptor.getValue().toString());
+        // Check the metric is not added in the JSONObject.
+        Assert.assertFalse(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY)
+                .getJSONObject("run1").has("run_metric"));
+    }
+
+    /** Test valid and invalid metrics in JSONObject. */
+    @Test
+    public void testInvalidAndInvalidMetricsNotSet() throws ConfigurationException, JSONException {
+        OptionSetter optionSetter = new OptionSetter(mReporter);
+        optionSetter.setOptionValue(SKIP_FAILED_RUNS_OPTION, String.valueOf(true));
+        mReporter.invocationStarted(mContext);
+        // Inject invalid metric "1.23invalid".
+        injectTestRun(mReporter, "run1", "test1", "1.23invalid", 0, false);
+        // Inject valid metric "5.99".
+        injectTestRun(mReporter, "run2", "test1", "5.99", 0, false);
+        mReporter.invocationEnded(0);
+        ArgumentCaptor<JSONObject> jsonCaptor = ArgumentCaptor.forClass(JSONObject.class);
+        verify(mReporter).postResults(jsonCaptor.capture());
+        CLog.i(jsonCaptor.getValue().toString());
+        // Check the invalid metric is not added in the JSONObject.
+        Assert.assertFalse(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY)
+                .getJSONObject("run1").has("run_metric"));
+        // Check the valid metric is added in the JSONObject.
+        Assert.assertTrue(jsonCaptor.getValue().getJSONObject(JSON_METRIC_KEY)
+                .getJSONObject("run2").has("run_metric"));
+    }
+
+
     /** Test for parsing additional device details when collect device details is enabled. */
     @Test
     public void testIncludeAdditionalTestDetails() throws ConfigurationException {
@@ -132,4 +180,4 @@
         target.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(runMetrics));
         return test;
     }
-}
+}
\ No newline at end of file
diff --git a/tests/src/com/android/tradefed/result/LogcatCrashResultForwarderTest.java b/tests/src/com/android/tradefed/result/LogcatCrashResultForwarderTest.java
index 1bf8eff..583a4c0 100644
--- a/tests/src/com/android/tradefed/result/LogcatCrashResultForwarderTest.java
+++ b/tests/src/com/android/tradefed/result/LogcatCrashResultForwarderTest.java
@@ -216,4 +216,56 @@
                                         + "\tat class.method1(Class.java:1)\n"
                                         + "\tat class.method2(Class.java:2)\n"));
     }
+
+    /** Test that test-timeout tests have failure status TIMED_OUT. */
+    @Test
+    @SuppressWarnings("MustBeClosedChecker")
+    public void testTestTimedOutTests() {
+        String trace =
+                "org.junit.runners.model.TestTimedOutException: "
+                        + "test timed out after 1000 milliseconds";
+        mReporter = new LogcatCrashResultForwarder(mMockDevice, mMockListener);
+        TestDescription test = new TestDescription("com.class", "test");
+
+        mMockListener.testStarted(test, 0L);
+
+        Capture<FailureDescription> captured = new Capture<>();
+        mMockListener.testFailed(EasyMock.eq(test), EasyMock.capture(captured));
+        mMockListener.testEnded(test, 5L, new HashMap<String, Metric>());
+
+        EasyMock.replay(mMockListener, mMockDevice);
+        mReporter.testStarted(test, 0L);
+        mReporter.testFailed(test, trace);
+        mReporter.testEnded(test, 5L, new HashMap<String, Metric>());
+        EasyMock.verify(mMockListener, mMockDevice);
+        assertTrue(captured.getValue().getErrorMessage().contains(trace));
+        assertTrue(FailureStatus.TIMED_OUT.equals(captured.getValue().getFailureStatus()));
+    }
+
+    /** Test that shell-timeout tests have failure status TIMED_OUT. */
+    @Test
+    @SuppressWarnings("MustBeClosedChecker")
+    public void testShellTimedOutTests() {
+        String trace =
+                "Test failed to run to completion. "
+                        + " Reason: 'Failed to receive adb shell test output within 3000 ms. "
+                        + "Test may have timed out, or adb connection to device became "
+                        + "unresponsive'. Check device logcat for details";
+        mReporter = new LogcatCrashResultForwarder(mMockDevice, mMockListener);
+        TestDescription test = new TestDescription("com.class", "test");
+
+        mMockListener.testStarted(test, 0L);
+
+        Capture<FailureDescription> captured = new Capture<>();
+        mMockListener.testFailed(EasyMock.eq(test), EasyMock.capture(captured));
+        mMockListener.testEnded(test, 5L, new HashMap<String, Metric>());
+
+        EasyMock.replay(mMockListener, mMockDevice);
+        mReporter.testStarted(test, 0L);
+        mReporter.testFailed(test, trace);
+        mReporter.testEnded(test, 5L, new HashMap<String, Metric>());
+        EasyMock.verify(mMockListener, mMockDevice);
+        assertTrue(captured.getValue().getErrorMessage().contains(trace));
+        assertTrue(FailureStatus.TIMED_OUT.equals(captured.getValue().getFailureStatus()));
+    }
 }
diff --git a/tests/src/com/android/tradefed/result/error/ErrorIdentifierTest.java b/tests/src/com/android/tradefed/result/error/ErrorIdentifierTest.java
index 63556ab..b7cc8eb 100644
--- a/tests/src/com/android/tradefed/result/error/ErrorIdentifierTest.java
+++ b/tests/src/com/android/tradefed/result/error/ErrorIdentifierTest.java
@@ -37,6 +37,7 @@
         List<ErrorIdentifier> errors = new ArrayList<>();
         errors.addAll(Arrays.asList(InfraErrorIdentifier.values()));
         errors.addAll(Arrays.asList(DeviceErrorIdentifier.values()));
+        errors.addAll(Arrays.asList(TestErrorIdentifier.values()));
 
         List<String> names = errors.stream().map(e -> e.name()).collect(Collectors.toList());
         Set<String> uniques = new HashSet<>();
diff --git a/tests/src/com/android/tradefed/retry/ResultAggregatorTest.java b/tests/src/com/android/tradefed/retry/ResultAggregatorTest.java
index 43aaa7a..bd17c87 100644
--- a/tests/src/com/android/tradefed/retry/ResultAggregatorTest.java
+++ b/tests/src/com/android/tradefed/retry/ResultAggregatorTest.java
@@ -211,6 +211,135 @@
     }
 
     @Test
+    public void testForwarding_assumptionFailure() {
+        mDetailedListener = EasyMock.createStrictMock(ITestDetailedReceiver.class);
+        LogFile test1Log = new LogFile("test1", "url", LogDataType.TEXT);
+        LogFile test2LogBefore = new LogFile("test2-before", "url", LogDataType.TEXT);
+        LogFile test2LogAfter = new LogFile("test2-after", "url", LogDataType.TEXT);
+        LogFile testRun1LogBefore = new LogFile("test-run1-before", "url", LogDataType.TEXT);
+        LogFile testRun1LogAfter = new LogFile("test-run1-after", "url", LogDataType.TEXT);
+        LogFile beforeEnd = new LogFile("path", "url", LogDataType.TEXT);
+        LogFile betweenAttemptsLog = new LogFile("between-attempts", "url", LogDataType.TEXT);
+        LogFile moduleLog = new LogFile("module-log", "url", LogDataType.TEXT);
+        TestDescription test1 = new TestDescription("classname", "test1");
+        TestDescription test2 = new TestDescription("classname", "test2");
+        ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+        EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+        // Invocation level
+        mAggListener.setLogSaver(logger);
+        mAggListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+        mDetailedListener.setLogSaver(logger);
+        mDetailedListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+        mAggListener.testModuleStarted(mModuleContext);
+        mDetailedListener.testModuleStarted(mModuleContext);
+
+        // Detailed receives the breakdown
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.logAssociation("test1-log", test1Log);
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.logAssociation("test2-before-log", test2LogBefore);
+        mDetailedListener.testFailed(test2, FailureDescription.create("I failed. retry me."));
+        mDetailedListener.logAssociation("test2-after-log", test2LogAfter);
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.logAssociation("test-run1-before-log", testRun1LogBefore);
+        mDetailedListener.logAssociation("test-run1-after-log", testRun1LogAfter);
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testAssumptionFailure(
+                EasyMock.eq(test2), EasyMock.eq(FailureDescription.create("Assump failure")));
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.logAssociation("between-attempts", betweenAttemptsLog);
+        mDetailedListener.logAssociation("module-log", moduleLog);
+
+        // Aggregated listeners receives the aggregated results
+        mAggListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.logAssociation("test1-log", test1Log);
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mAggListener.testAssumptionFailure(
+                EasyMock.eq(test2), (FailureDescription) EasyMock.anyObject());
+        mAggListener.logAssociation("test2-before-log", test2LogBefore);
+        mAggListener.logAssociation("test2-after-log", test2LogAfter);
+        mAggListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.logAssociation("test-run1-before-log", testRun1LogBefore);
+        mAggListener.logAssociation("test-run1-after-log", testRun1LogAfter);
+        mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mAggListener.logAssociation("between-attempts", betweenAttemptsLog);
+        mAggListener.logAssociation("module-log", moduleLog);
+        mAggListener.testModuleEnded();
+        mDetailedListener.testModuleEnded();
+        mAggListener.logAssociation("before-end", beforeEnd);
+        mAggListener.invocationEnded(500L);
+        mDetailedListener.logAssociation("before-end", beforeEnd);
+        mDetailedListener.invocationEnded(500L);
+
+        EasyMock.replay(mAggListener, mDetailedListener);
+        mAggregator =
+                new TestableResultAggregator(
+                        Arrays.asList(mAggListener, mDetailedListener),
+                        RetryStrategy.RETRY_ANY_FAILURE);
+        mAggregator.setLogSaver(logger);
+        mAggregator.invocationStarted(mInvocationContext);
+        mAggregator.testModuleStarted(mModuleContext);
+        // Attempt 1
+        mAggregator.testRunStarted("run1", 2, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.logAssociation("test1-log", test1Log);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testStarted(test2);
+        mAggregator.logAssociation("test2-before-log", test2LogBefore);
+        mAggregator.testFailed(test2, FailureDescription.create("I failed. retry me."));
+        mAggregator.logAssociation("test2-after-log", test2LogAfter);
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.logAssociation("test-run1-before-log", testRun1LogBefore);
+        mAggregator.testRunFailed("run fail");
+        mAggregator.logAssociation("test-run1-after-log", testRun1LogAfter);
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        mAggregator.logAssociation("between-attempts", betweenAttemptsLog);
+        // Attempt 2
+        mAggregator.testRunStarted("run1", 2, 1);
+        mAggregator.testStarted(test2);
+        mAggregator.testAssumptionFailure(test2, FailureDescription.create("Assump failure"));
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.logAssociation("module-log", moduleLog);
+        mAggregator.testModuleEnded();
+        mAggregator.logAssociation("before-end", beforeEnd);
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+        assertEquals("run fail", mAggregator.getInvocationMetricRunError());
+    }
+
+    @Test
     public void testForwarding_runFailure() {
         mDetailedListener = EasyMock.createStrictMock(ITestDetailedReceiver.class);
         TestDescription test1 = new TestDescription("classname", "test1");
diff --git a/tests/src/com/android/tradefed/sandbox/SandboxedInvocationExecutionTest.java b/tests/src/com/android/tradefed/sandbox/SandboxedInvocationExecutionTest.java
deleted file mode 100644
index 7594ce4..0000000
--- a/tests/src/com/android/tradefed/sandbox/SandboxedInvocationExecutionTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2019 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.sandbox;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.Configuration;
-import com.android.tradefed.config.ConfigurationDef;
-import com.android.tradefed.config.IConfiguration;
-import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.File;
-
-/** Unit tests for {@link SandboxedInvocationExecution}. */
-@RunWith(JUnit4.class)
-public class SandboxedInvocationExecutionTest {
-
-    private SandboxedInvocationExecution mExecution;
-    private IInvocationContext mContext;
-    private IConfiguration mConfig;
-
-    @Before
-    public void setUp() {
-        mExecution = new SandboxedInvocationExecution();
-        mContext = new InvocationContext();
-        mConfig = new Configuration("name", "desc");
-        mConfig.getConfigurationDescription().setSandboxed(true);
-    }
-
-    @Test
-    public void testBuildInfo_testTag() throws Exception {
-        IBuildInfo info = new BuildInfo();
-        assertEquals("stub", info.getTestTag());
-        info.setFile(BuildInfoFileKey.TESTDIR_IMAGE, new File("doesnt_matter_testsdir"), "tests");
-        mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, info);
-        mConfig.getCommandOptions().setTestTag("test");
-        TestInformation testInfo =
-                TestInformation.newBuilder().setInvocationContext(mContext).build();
-        assertNull(testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY));
-        mExecution.fetchBuild(testInfo, mConfig, null, null);
-        // Build test tag was updated
-        assertEquals("test", info.getTestTag());
-        // Execution file was back filled
-        assertNotNull(testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY));
-    }
-}
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
index a32f9b6..244934c 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
@@ -266,6 +266,22 @@
         EasyMock.verify(mMockDevice);
     }
 
+    public void testSetup_wifi_network_empty() throws Exception {
+        doSetupExpectations();
+        doCheckExternalStoreSpaceExpectations();
+        doSettingExpectations("global", "wifi_on", "1");
+        doCommandsExpectations("svc wifi enable");
+        EasyMock.replay(mMockDevice);
+
+        mDeviceSetup.setWifiNetwork("");
+        mDeviceSetup.setWifiPsk("psk");
+
+        mDeviceSetup.setWifi(BinaryState.ON);
+        mDeviceSetup.setUp(mTestInfo);
+
+        EasyMock.verify(mMockDevice);
+    }
+
     public void testSetup_wifi_multiple_network_names() throws Exception {
         doSetupExpectations();
         doCheckExternalStoreSpaceExpectations();
diff --git a/tests/src/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java b/tests/src/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java
index 9b38ce6..e889441 100644
--- a/tests/src/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DynamicSystemPreparerTest.java
@@ -28,10 +28,9 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.ZipUtil;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
+
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -40,6 +39,10 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
 /** Unit tests for {@link DynamicSystemPreparer}. */
 @RunWith(JUnit4.class)
 public class DynamicSystemPreparerTest {
@@ -95,15 +98,12 @@
         }
     }
 
-    @Test
-    public void testSetUp() throws TargetSetupError, BuildError, DeviceNotAvailableException {
-        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
-                .thenReturn(Boolean.TRUE);
+    private void mockGsiToolStatus(String status) throws DeviceNotAvailableException {
         doAnswer(
                         new Answer<Object>() {
                             @Override
                             public Object answer(InvocationOnMock invocation) {
-                                byte[] outputBytes = "running".getBytes();
+                                byte[] outputBytes = status.getBytes();
                                 ((CollectingOutputReceiver) invocation.getArguments()[1])
                                         .addOutput(outputBytes, 0, outputBytes.length);
                                 return null;
@@ -112,10 +112,62 @@
                 .when(mMockDevice)
                 .executeShellCommand(
                         matches("gsi_tool status"), any(CollectingOutputReceiver.class));
+    }
+
+    @Test
+    public void testSetUp() throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
+                .thenReturn(Boolean.TRUE);
+        Mockito.when(mMockDevice.waitForDeviceNotAvailable(Mockito.anyLong())).thenReturn(true);
+        mockGsiToolStatus("running");
         CommandResult res = new CommandResult();
         res.setStdout("");
         res.setStatus(CommandStatus.SUCCESS);
         Mockito.when(mMockDevice.executeShellV2Command("gsi_tool enable")).thenReturn(res);
         mPreparer.setUp(mMockDevice, mBuildInfo);
     }
+
+    @Test
+    public void testSetUp_installationFail() throws BuildError, DeviceNotAvailableException {
+        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
+                .thenReturn(Boolean.TRUE);
+        Mockito.when(mMockDevice.waitForDeviceNotAvailable(Mockito.anyLong())).thenReturn(false);
+        try {
+            mPreparer.setUp(mMockDevice, mBuildInfo);
+            Assert.fail("setUp() should have thrown.");
+        } catch (TargetSetupError e) {
+            Assert.assertEquals(
+                    "Timed out waiting for DSU installation to complete and reboot",
+                    e.getMessage());
+        }
+    }
+
+    @Test
+    public void testSetUp_rebootFail() throws BuildError, DeviceNotAvailableException {
+        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
+                .thenReturn(Boolean.TRUE);
+        Mockito.when(mMockDevice.waitForDeviceNotAvailable(Mockito.anyLong())).thenReturn(true);
+        Mockito.doThrow(new DeviceNotAvailableException()).when(mMockDevice).waitForDeviceOnline();
+        try {
+            mPreparer.setUp(mMockDevice, mBuildInfo);
+            Assert.fail("setUp() should have thrown.");
+        } catch (TargetSetupError e) {
+            Assert.assertEquals("Timed out booting into DSU", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testSetUp_noDsuRunningAfterRebootFail()
+            throws BuildError, DeviceNotAvailableException {
+        Mockito.when(mMockDevice.pushFile(Mockito.any(), Mockito.eq("/sdcard/system.raw.gz")))
+                .thenReturn(Boolean.TRUE);
+        Mockito.when(mMockDevice.waitForDeviceNotAvailable(Mockito.anyLong())).thenReturn(true);
+        mockGsiToolStatus("normal");
+        try {
+            mPreparer.setUp(mMockDevice, mBuildInfo);
+            Assert.fail("setUp() should have thrown.");
+        } catch (TargetSetupError e) {
+            Assert.assertEquals("Failed to boot into DSU", e.getMessage());
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparerTest.java b/tests/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparerTest.java
index f91253e..bccf68d 100644
--- a/tests/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/GsiDeviceFlashPreparerTest.java
@@ -178,21 +178,36 @@
         EasyMock.verify(mMockDevice, mMockRunUtil);
     }
 
-    /* Verifies that setUp will throw exception when there is no vbmeta.img in the zip file*/
+    /* Verifies that setUp can pass when there is no vbmeta.img is provided*/
     @Test
-    public void testSetUp_NoVbmetaImageInGsiZip() throws Exception {
+    public void testSetUp_Success_NoVbmetaImage() throws Exception {
         File gsiDir = FileUtil.createTempDir("gsi_folder", mTmpDir);
         File systemImg = new File(gsiDir, "system.img");
-        File gsiZip = FileUtil.createTempFile("gsi_image", ".zip", mTmpDir);
-        ZipUtil.createZip(List.of(systemImg), gsiZip);
-        mBuildInfo.setFile("gsi_system.img", gsiZip, "0");
+        FileUtil.writeToFile("ddd", systemImg);
+        mBuildInfo.setFile("gsi_system.img", systemImg, "0");
+        mMockDevice.waitForDeviceOnline();
+        EasyMock.expect(mMockDevice.getApiLevel()).andReturn(29);
+        mMockDevice.rebootIntoBootloader();
+        mMockRunUtil.allowInterrupt(false);
+        mMockDevice.rebootIntoFastbootd();
+        doGetSlotExpectation();
+        EasyMock.expect(
+                        mMockDevice.executeLongFastbootCommand(
+                                "delete-logical-partition", "product_a"))
+                .andReturn(mSuccessResult);
+        EasyMock.expect(mMockDevice.executeLongFastbootCommand("erase", "system_a"))
+                .andReturn(mSuccessResult);
+        EasyMock.expect(
+                        mMockDevice.executeLongFastbootCommand(
+                                "flash",
+                                "system",
+                                mBuildInfo.getFile("gsi_system.img").getAbsolutePath()))
+                .andReturn(mSuccessResult);
+        EasyMock.expect(mMockDevice.executeLongFastbootCommand("-w")).andReturn(mSuccessResult);
+        mMockRunUtil.allowInterrupt(true);
+        doSetupExpectations();
         EasyMock.replay(mMockDevice, mMockRunUtil);
-        try {
-            mPreparer.setUp(mTestInfo);
-            fail("TargetSetupError is expected");
-        } catch (TargetSetupError e) {
-            // expected
-        }
+        mPreparer.setUp(mTestInfo);
         EasyMock.verify(mMockDevice, mMockRunUtil);
     }
 
diff --git a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
index 2541183..0652a21 100644
--- a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.ITestDevice.ApexInfo;
 import com.android.tradefed.invoker.IInvocationContext;
@@ -35,6 +36,7 @@
 
 import com.google.common.collect.ImmutableSet;
 
+import java.util.Arrays;
 import org.easymock.EasyMock;
 import org.junit.After;
 import org.junit.Before;
@@ -60,6 +62,8 @@
     private TestInformation mTestInfo;
     private BundletoolUtil mMockBundletoolUtil;
     private File mFakeApex;
+    private File mFakeApex2;
+    private File mFakeApex3;
     private File mFakeApk;
     private File mFakeApk2;
     private File mFakePersistentApk;
@@ -68,6 +72,8 @@
     private File mBundletoolJar;
     private OptionSetter mSetter;
     private static final String APEX_PACKAGE_NAME = "com.android.FAKE_APEX_PACKAGE_NAME";
+    private static final String APEX2_PACKAGE_NAME = "com.android.FAKE_APEX2_PACKAGE_NAME";
+    private static final String APEX3_PACKAGE_NAME = "com.android.FAKE_APEX3_PACKAGE_NAME";
     private static final String APK_PACKAGE_NAME = "com.android.FAKE_APK_PACKAGE_NAME";
     private static final String APK2_PACKAGE_NAME = "com.android.FAKE_APK2_PACKAGE_NAME";
     private static final String PERSISTENT_APK_PACKAGE_NAME = "com.android.PERSISTENT_PACKAGE_NAME";
@@ -78,6 +84,7 @@
     private static final String APEX_PACKAGE_KEYWORD = "FAKE_APEX_PACKAGE_NAME";
     private static final long APEX_VERSION = 1;
     private static final String APEX_NAME = "fakeApex.apex";
+    private static final String APEX2_NAME = "fakeApex_2.apex";
     private static final String APK_NAME = "fakeApk.apk";
     private static final String APK2_NAME = "fakeSecondApk.apk";
     private static final String PERSISTENT_APK_NAME = "fakePersistentApk.apk";
@@ -92,6 +99,8 @@
     @Before
     public void setUp() throws Exception {
         mFakeApex = FileUtil.createTempFile("fakeApex", ".apex");
+        mFakeApex2 = FileUtil.createTempFile("fakeApex_2", ".apex");
+        mFakeApex3 = FileUtil.createTempFile("fakeApex_3", ".apex");
         mFakeApk = FileUtil.createTempFile("fakeApk", ".apk");
         mFakeApk2 = FileUtil.createTempFile("fakeSecondApk", ".apk");
         mFakePersistentApk = FileUtil.createTempFile("fakePersistentApk", ".apk");
@@ -126,7 +135,13 @@
                     @Override
                     protected File getLocalPathForFilename(
                             TestInformation testInfo, String appFileName) throws TargetSetupError {
-                        if (APEX_NAME.equals(appFileName)) {
+                        if (appFileName.endsWith(".apex")) {
+                            if (appFileName.contains("fakeApex_2")) {
+                                return mFakeApex2;
+                            }
+                            else if (appFileName.contains("fakeApex_3")) {
+                                return mFakeApex3;
+                            }
                             return mFakeApex;
                         }
                         if (appFileName.endsWith(".apk")) {
@@ -138,13 +153,11 @@
                                 return mFakeApk;
                             }
                         }
-                        if (appFileName.endsWith(".apks")) {
-                            if (appFileName.contains("Apex")) {
-                                return mFakeApexApks;
-                            }
-                            if (appFileName.contains("Apk")) {
-                                return mFakeApkApks;
-                            }
+                        if (SPLIT_APEX_APKS_NAME.equals(appFileName)) {
+                            return mFakeApexApks;
+                        }
+                        if (SPLIT_APK__APKS_NAME.equals(appFileName)) {
+                            return mFakeApkApks;
                         }
                         if (appFileName.endsWith(".jar")) {
                             return mBundletoolJar;
@@ -156,6 +169,12 @@
                     protected String parsePackageName(
                             File testAppFile, DeviceDescriptor deviceDescriptor) {
                         if (testAppFile.getName().endsWith(".apex")) {
+                            if (testAppFile.getName().contains("fakeApex_2")) {
+                                return APEX2_PACKAGE_NAME;
+                            }
+                            else if (testAppFile.getName().contains("fakeApex_3")) {
+                                return APEX3_PACKAGE_NAME;
+                            }
                             return APEX_PACKAGE_NAME;
                         }
                         if (testAppFile.getName().endsWith(".apk")
@@ -185,6 +204,8 @@
                         ApexInfo apexInfo;
                         if (apex.getName().contains("Split")) {
                             apexInfo = new ApexInfo(SPLIT_APEX_PACKAGE_NAME, APEX_VERSION);
+                        } else if (apex.getName().contains("fakeApex_2")) {
+                            apexInfo = new ApexInfo(APEX2_PACKAGE_NAME, APEX_VERSION);
                         } else {
                             apexInfo = new ApexInfo(APEX_PACKAGE_NAME, APEX_VERSION);
                         }
@@ -209,11 +230,343 @@
     @After
     public void tearDown() throws Exception {
         FileUtil.deleteFile(mFakeApex);
+        FileUtil.deleteFile(mFakeApex2);
+        FileUtil.deleteFile(mFakeApex3);
         FileUtil.deleteFile(mFakeApk);
         FileUtil.deleteFile(mFakeApk2);
         FileUtil.deleteFile(mFakePersistentApk);
     }
 
+    /**
+     * Test that it gets the correct apex files that are already installed on the /data directory.
+     */
+    @Test
+    public void testGetApexInData() throws Exception {
+        Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
+        Set<ApexInfo> expectedApex = new HashSet<ApexInfo>();
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexSystem =
+                new ApexInfo(
+                        "com.android.FAKE_APEX3_PACKAGE_NAME",
+                        1,
+                        "/system/apex/com.android.FAKE_APEX3_PACKAGE_NAME@1.apex");
+
+        activatedApex = new HashSet<>(Arrays.asList(fakeApexData, fakeApexData2, fakeApexSystem));
+        expectedApex = new HashSet<>(Arrays.asList(fakeApexData, fakeApexData2));
+        assertEquals(2, mInstallApexModuleTargetPreparer.getApexInData(activatedApex).size());
+        assertEquals(expectedApex, mInstallApexModuleTargetPreparer.getApexInData(activatedApex));
+
+        activatedApex = new HashSet<>(Arrays.asList(fakeApexSystem));
+        assertEquals(0, mInstallApexModuleTargetPreparer.getApexInData(activatedApex).size());
+    }
+
+    /**
+     * Test that it returns the correct files to be installed and uninstalled.
+     */
+    @Test
+    public void testGetModulesToUninstall_NoneUninstallAndInstallFiles() throws Exception {
+        Set<ApexInfo> apexInData = new HashSet<>();
+        List<File> testFiles = new ArrayList<>();
+        testFiles.add(mFakeApex);
+        testFiles.add(mFakeApex2);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+
+        EasyMock.replay(mMockBuildInfo, mMockDevice);
+        Set<ApexInfo> results = mInstallApexModuleTargetPreparer.getModulesToUninstall(
+                apexInData, testFiles, mMockDevice);
+
+        assertEquals(0, testFiles.size());
+        assertEquals(0, results.size());
+        EasyMock.verify(mMockBuildInfo, mMockDevice);
+    }
+
+    /**
+     * Test that it returns the correct files to be installed and uninstalled.
+     */
+    @Test
+    public void testGetModulesToUninstall_UninstallAndInstallFiles() throws Exception {
+        Set<ApexInfo> apexInData = new HashSet<>();
+        List<File> testFiles = new ArrayList<>();
+        testFiles.add(mFakeApex3);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+
+        EasyMock.replay(mMockBuildInfo, mMockDevice);
+        Set<ApexInfo> results = mInstallApexModuleTargetPreparer.getModulesToUninstall(
+                apexInData, testFiles, mMockDevice);
+        assertEquals(1, testFiles.size());
+        assertEquals(mFakeApex3, testFiles.get(0));
+        assertEquals(2, results.size());
+        results.containsAll(apexInData);
+        EasyMock.verify(mMockBuildInfo, mMockDevice);
+    }
+
+    /**
+     * Test that it returns the correct files to be installed and uninstalled.
+     */
+    @Test
+    public void testGetModulesToUninstall_UninstallAndInstallFiles2() throws Exception {
+        Set<ApexInfo> apexInData = new HashSet<>();
+        List<File> testFiles = new ArrayList<>();
+        testFiles.add(mFakeApex2);
+        testFiles.add(mFakeApex3);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+
+        EasyMock.replay(mMockDevice);
+        Set<ApexInfo> results =
+                mInstallApexModuleTargetPreparer.getModulesToUninstall(
+                        apexInData, testFiles, mMockDevice);
+        assertEquals(1, testFiles.size());
+        assertEquals(mFakeApex3, testFiles.get(0));
+        assertEquals(1, results.size());
+        results.contains(fakeApexData);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method behaves the same process when the files to be installed contain apk or apks.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_APEXANDAPK_Reboot() throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
+
+        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        CommandResult res = new CommandResult();
+        res.setStdout("test.apex");
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
+        mMockDevice.reboot();
+        EasyMock.expectLastCall();
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<>()).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData))).atLeastOnce();
+        mockSuccessfulInstallMultiPackageAndReboot();
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APK_PACKAGE_NAME);
+        installableModules.add(APEX_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method will optimize the process and it will not reboot because the files to be
+     * installed are already installed on the device.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_MultipleAPEX_NoReboot() throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX2_NAME);
+
+        Set<ApexInfo> apexInData = new HashSet<>();
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(apexInData).times(2);
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APEX_PACKAGE_NAME);
+        installableModules.add(APEX2_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method will uninstall the unused files and install the required files for the
+     * current test, and finally reboot the device.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_MultipleAPEX_UninstallThenInstallAndReboot()
+            throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX2_NAME);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData))).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData2))).atLeastOnce();
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APEX2_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        EasyMock.expect(mMockDevice.uninstallPackage(EasyMock.anyObject()))
+                .andReturn(null)
+                .once();
+        mockSuccessfulInstallPackageAndReboot(mFakeApex2);
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method will uninstall the unused files for the current test, and finally reboot the
+     * device.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_MultipleAPEX_UninstallAndReboot() throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX2_NAME);
+
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData, fakeApexData2))).times(2);
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APEX2_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        EasyMock.expect(mMockDevice.uninstallPackage(EasyMock.anyObject()))
+                .andReturn(null)
+                .once();
+        mMockDevice.reboot();
+        EasyMock.expectLastCall().once();
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+        EasyMock.verify(mMockDevice);
+    }
+
+    /**
+     * Test the method will install the required files for the current test, and finally reboot the
+     * device.
+     */
+    @Test
+    public void testSetupAndTearDown_Optimize_MultipleAPEX_Reboot() throws Exception {
+        mSetter.setOptionValue("skip-apex-teardown", "true");
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(APEX2_NAME);
+
+        Set<ApexInfo> apexInData = new HashSet<>();
+        ApexInfo fakeApexData =
+                new ApexInfo(
+                        APEX_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex");
+
+        ApexInfo fakeApexData2 =
+                new ApexInfo(
+                        APEX2_PACKAGE_NAME,
+                        1,
+                        "/data/apex/active/com.android.FAKE_APEX2_PACKAGE_NAME@1.apex");
+
+        apexInData.add(fakeApexData);
+        apexInData.add(fakeApexData2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(
+                new HashSet<>(Arrays.asList(fakeApexData))).times(2);
+        EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(apexInData).atLeastOnce();
+        Set<String> installableModules = new HashSet<>();
+        installableModules.add(APEX_PACKAGE_NAME);
+        installableModules.add(APEX2_PACKAGE_NAME);
+        EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+        mockSuccessfulInstallPackageAndReboot(mFakeApex2);
+        EasyMock.replay(mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+        mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+        EasyMock.verify(mMockDevice);
+    }
+
     @Test
     public void testSetupSuccess_removeExistingStagedApexSuccess() throws Exception {
         mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
@@ -781,19 +1134,7 @@
     @Test
     public void testSetupAndTearDown() throws Exception {
         mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
-        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        CommandResult res = new CommandResult();
-        res.setStdout("test.apex");
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
-        mMockDevice.reboot();
-        EasyMock.expectLastCall();
+        mockCleanInstalledApexPackagesAndReboot();
         mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(
@@ -834,19 +1175,7 @@
     public void testSetupAndTearDown_MultiInstall() throws Exception {
         mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
         mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
-        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        CommandResult res = new CommandResult();
-        res.setStdout("test.apex");
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
-        mMockDevice.reboot();
-        EasyMock.expectLastCall();
+        mockCleanInstalledApexPackagesAndReboot();
         mockSuccessfulInstallMultiPackageAndReboot();
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(
@@ -891,22 +1220,7 @@
         mBundletoolJar = File.createTempFile("bundletool", ".jar");
         File splitApk2 = File.createTempFile("fakeSplitApk2", ".apk", fakeSplitApkApks);
         try {
-            mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-            EasyMock.expectLastCall().times(2);
-            mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-            EasyMock.expectLastCall().times(2);
-            mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-            EasyMock.expectLastCall().times(2);
-            CommandResult res = new CommandResult();
-            res.setStdout("test.apex");
-            EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR))
-                    .andReturn(res);
-            EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR))
-                    .andReturn(res);
-            EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR))
-                    .andReturn(res);
-            mMockDevice.reboot();
-            EasyMock.expectLastCall();
+            mockCleanInstalledApexPackagesAndReboot();
             when(mMockBundletoolUtil.generateDeviceSpecFile(Mockito.any(ITestDevice.class)))
                     .thenReturn("serial.json");
 
@@ -998,6 +1312,340 @@
     }
 
     @Test
+    public void testInstallUsingBundletool_AbsolutePath() throws Exception {
+        mInstallApexModuleTargetPreparer.addTestFileName(SPLIT_APEX_APKS_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(SPLIT_APK__APKS_NAME);
+        mFakeApexApks = File.createTempFile("fakeApex", ".apks");
+        mFakeApkApks = File.createTempFile("fakeApk", ".apks");
+
+        File fakeSplitApexApks = File.createTempFile("ApexSplits", "");
+        fakeSplitApexApks.delete();
+        fakeSplitApexApks.mkdir();
+        File splitApex = File.createTempFile("fakeSplitApex", ".apex", fakeSplitApexApks);
+
+        File fakeSplitApkApks = File.createTempFile("ApkSplits", "");
+        fakeSplitApkApks.delete();
+        fakeSplitApkApks.mkdir();
+        File splitApk1 = File.createTempFile("fakeSplitApk1", ".apk", fakeSplitApkApks);
+        mBundletoolJar = File.createTempFile("/fake/absolute/path/bundletool", ".jar");
+        File splitApk2 = File.createTempFile("fakeSplitApk2", ".apk", fakeSplitApkApks);
+        try {
+            mockCleanInstalledApexPackagesAndReboot();
+            when(mMockBundletoolUtil.generateDeviceSpecFile(Mockito.any(ITestDevice.class)))
+                    .thenReturn("serial.json");
+
+            assertTrue(fakeSplitApexApks != null);
+            assertTrue(fakeSplitApkApks != null);
+            assertTrue(mFakeApexApks != null);
+            assertTrue(mFakeApkApks != null);
+            assertEquals(1, fakeSplitApexApks.listFiles().length);
+            assertEquals(2, fakeSplitApkApks.listFiles().length);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApexApks);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApkApks);
+
+            mMockDevice.waitForDeviceAvailable();
+
+            List<String> trainInstallCmd = new ArrayList<>();
+            trainInstallCmd.add("install-multi-package");
+            trainInstallCmd.add(splitApex.getAbsolutePath());
+            String cmd = "";
+            for (File f : fakeSplitApkApks.listFiles()) {
+                if (!cmd.isEmpty()) {
+                    cmd += ":" + f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                } else {
+                    cmd += f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                }
+            }
+            trainInstallCmd.add(cmd);
+            EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                    .andReturn("Success")
+                    .once();
+            mMockDevice.reboot();
+            Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
+            activatedApex.add(
+                    new ApexInfo(
+                            SPLIT_APEX_PACKAGE_NAME,
+                            1,
+                            "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex"));
+            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
+            EasyMock.expect(mMockDevice.uninstallPackage(SPLIT_APK_PACKAGE_NAME))
+                    .andReturn(null)
+                    .once();
+            mMockDevice.reboot();
+            EasyMock.expectLastCall();
+            Set<String> installableModules = new HashSet<>();
+            installableModules.add(APEX_PACKAGE_NAME);
+            installableModules.add(SPLIT_APK_PACKAGE_NAME);
+            EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+
+            EasyMock.replay(mMockBuildInfo, mMockDevice);
+            mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+            mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+            Mockito.verify(mMockBundletoolUtil, times(1))
+                    .generateDeviceSpecFile(Mockito.any(ITestDevice.class));
+            // Extract splits 1 time to get the package name for the module, and again during
+            // installation.
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            EasyMock.verify(mMockBuildInfo, mMockDevice);
+        } finally {
+            FileUtil.deleteFile(mFakeApexApks);
+            FileUtil.deleteFile(mFakeApkApks);
+            FileUtil.recursiveDelete(fakeSplitApexApks);
+            FileUtil.deleteFile(fakeSplitApexApks);
+            FileUtil.recursiveDelete(fakeSplitApkApks);
+            FileUtil.deleteFile(fakeSplitApkApks);
+            FileUtil.deleteFile(mBundletoolJar);
+        }
+    }
+
+    @Test
+    public void testInstallUsingBundletool_TrainFolder() throws Exception {
+        File trainFolder = File.createTempFile("tmpTrain", "");
+        trainFolder.delete();
+        trainFolder.mkdir();
+        mSetter.setOptionValue("train-path", trainFolder.getAbsolutePath());
+        mFakeApexApks = File.createTempFile("fakeApex", ".apks", trainFolder);
+        mFakeApkApks = File.createTempFile("fakeApk", ".apks", trainFolder);
+
+        File fakeSplitApexApks = File.createTempFile("ApexSplits", "");
+        fakeSplitApexApks.delete();
+        fakeSplitApexApks.mkdir();
+        File splitApex = File.createTempFile("fakeSplitApex", ".apex", fakeSplitApexApks);
+
+        File fakeSplitApkApks = File.createTempFile("ApkSplits", "");
+        fakeSplitApkApks.delete();
+        fakeSplitApkApks.mkdir();
+        File splitApk1 = File.createTempFile("fakeSplitApk1", ".apk", fakeSplitApkApks);
+        mBundletoolJar = File.createTempFile("bundletool", ".jar");
+        File splitApk2 = File.createTempFile("fakeSplitApk2", ".apk", fakeSplitApkApks);
+        try {
+            mockCleanInstalledApexPackagesAndReboot();
+            when(mMockBundletoolUtil.generateDeviceSpecFile(Mockito.any(ITestDevice.class)))
+                    .thenReturn("serial.json");
+
+            assertTrue(fakeSplitApexApks != null);
+            assertTrue(fakeSplitApkApks != null);
+            assertTrue(mFakeApexApks != null);
+            assertTrue(mFakeApkApks != null);
+            assertEquals(1, fakeSplitApexApks.listFiles().length);
+            assertEquals(2, fakeSplitApkApks.listFiles().length);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApexApks);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApkApks);
+
+            mMockDevice.waitForDeviceAvailable();
+
+            List<String> trainInstallCmd = new ArrayList<>();
+            trainInstallCmd.add("install-multi-package");
+            trainInstallCmd.add(splitApex.getAbsolutePath());
+            String cmd = "";
+            for (File f : fakeSplitApkApks.listFiles()) {
+                if (!cmd.isEmpty()) {
+                    cmd += ":" + f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                } else {
+                    cmd += f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                }
+            }
+            trainInstallCmd.add(cmd);
+            EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                    .andReturn("Success")
+                    .once();
+            mMockDevice.reboot();
+            Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
+            activatedApex.add(
+                    new ApexInfo(
+                            SPLIT_APEX_PACKAGE_NAME,
+                            1,
+                            "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex"));
+            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
+            EasyMock.expect(mMockDevice.uninstallPackage(SPLIT_APK_PACKAGE_NAME))
+                    .andReturn(null)
+                    .once();
+            mMockDevice.reboot();
+            EasyMock.expectLastCall();
+            Set<String> installableModules = new HashSet<>();
+            installableModules.add(APEX_PACKAGE_NAME);
+            installableModules.add(SPLIT_APK_PACKAGE_NAME);
+            EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+
+            EasyMock.replay(mMockBuildInfo, mMockDevice);
+            mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+            mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+            Mockito.verify(mMockBundletoolUtil, times(1))
+                    .generateDeviceSpecFile(Mockito.any(ITestDevice.class));
+            // Extract splits 1 time to get the package name for the module, and again during
+            // installation.
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            EasyMock.verify(mMockBuildInfo, mMockDevice);
+        } finally {
+            FileUtil.recursiveDelete(trainFolder);
+            FileUtil.deleteFile(trainFolder);
+            FileUtil.deleteFile(mFakeApexApks);
+            FileUtil.deleteFile(mFakeApkApks);
+            FileUtil.recursiveDelete(fakeSplitApexApks);
+            FileUtil.deleteFile(fakeSplitApexApks);
+            FileUtil.recursiveDelete(fakeSplitApkApks);
+            FileUtil.deleteFile(fakeSplitApkApks);
+            FileUtil.deleteFile(mBundletoolJar);
+        }
+    }
+
+    @Test
+    public void testInstallUsingBundletool_AllFilesHaveAbsolutePath() throws Exception {
+        mFakeApexApks = File.createTempFile("fakeApex", ".apks");
+        mFakeApkApks = File.createTempFile("fakeApk", ".apks");
+        mInstallApexModuleTargetPreparer.addTestFile(mFakeApexApks);
+        mInstallApexModuleTargetPreparer.addTestFile(mFakeApkApks);
+
+        File fakeSplitApexApks = File.createTempFile("ApexSplits", "");
+        fakeSplitApexApks.delete();
+        fakeSplitApexApks.mkdir();
+        File splitApex = File.createTempFile("fakeSplitApex", ".apex", fakeSplitApexApks);
+
+        File fakeSplitApkApks = File.createTempFile("ApkSplits", "");
+        fakeSplitApkApks.delete();
+        fakeSplitApkApks.mkdir();
+        File splitApk1 = File.createTempFile("fakeSplitApk1", ".apk", fakeSplitApkApks);
+        mBundletoolJar = File.createTempFile("/fake/absolute/path/bundletool", ".jar");
+        File splitApk2 = File.createTempFile("fakeSplitApk2", ".apk", fakeSplitApkApks);
+        try {
+            mockCleanInstalledApexPackagesAndReboot();
+            when(mMockBundletoolUtil.generateDeviceSpecFile(Mockito.any(ITestDevice.class)))
+                    .thenReturn("serial.json");
+
+            assertTrue(fakeSplitApexApks != null);
+            assertTrue(fakeSplitApkApks != null);
+            assertTrue(mFakeApexApks != null);
+            assertTrue(mFakeApkApks != null);
+            assertEquals(1, fakeSplitApexApks.listFiles().length);
+            assertEquals(2, fakeSplitApkApks.listFiles().length);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApexApks);
+
+            when(mMockBundletoolUtil.extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class)))
+                    .thenReturn(fakeSplitApkApks);
+
+            mMockDevice.waitForDeviceAvailable();
+
+            List<String> trainInstallCmd = new ArrayList<>();
+            trainInstallCmd.add("install-multi-package");
+            trainInstallCmd.add(splitApex.getAbsolutePath());
+            String cmd = "";
+            for (File f : fakeSplitApkApks.listFiles()) {
+                if (!cmd.isEmpty()) {
+                    cmd += ":" + f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                } else {
+                    cmd += f.getParentFile().getAbsolutePath() + "/" + f.getName();
+                }
+            }
+            trainInstallCmd.add(cmd);
+            EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                    .andReturn("Success")
+                    .once();
+            mMockDevice.reboot();
+            Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
+            activatedApex.add(
+                    new ApexInfo(
+                            SPLIT_APEX_PACKAGE_NAME,
+                            1,
+                            "/data/apex/active/com.android.FAKE_APEX_PACKAGE_NAME@1.apex"));
+            EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex).times(3);
+            EasyMock.expect(mMockDevice.uninstallPackage(SPLIT_APK_PACKAGE_NAME))
+                    .andReturn(null)
+                    .once();
+            mMockDevice.reboot();
+            EasyMock.expectLastCall();
+            Set<String> installableModules = new HashSet<>();
+            installableModules.add(APEX_PACKAGE_NAME);
+            installableModules.add(SPLIT_APK_PACKAGE_NAME);
+            EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
+
+            EasyMock.replay(mMockBuildInfo, mMockDevice);
+            mInstallApexModuleTargetPreparer.setUp(mTestInfo);
+            mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
+            Mockito.verify(mMockBundletoolUtil, times(1))
+                    .generateDeviceSpecFile(Mockito.any(ITestDevice.class));
+            // Extract splits 1 time to get the package name for the module, and again during
+            // installation.
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApexApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            Mockito.verify(mMockBundletoolUtil, times(2))
+                    .extractSplitsFromApks(
+                            Mockito.eq(mFakeApkApks),
+                            Mockito.anyString(),
+                            Mockito.any(ITestDevice.class),
+                            Mockito.any(IBuildInfo.class));
+            EasyMock.verify(mMockBuildInfo, mMockDevice);
+        } finally {
+            FileUtil.deleteFile(mFakeApexApks);
+            FileUtil.deleteFile(mFakeApkApks);
+            FileUtil.recursiveDelete(fakeSplitApexApks);
+            FileUtil.deleteFile(fakeSplitApexApks);
+            FileUtil.recursiveDelete(fakeSplitApkApks);
+            FileUtil.deleteFile(fakeSplitApkApks);
+            FileUtil.deleteFile(mBundletoolJar);
+        }
+    }
+
+    @Test
     public void testInstallUsingBundletool_skipModuleNotPreloaded() throws Exception {
         mSetter.setOptionValue("ignore-if-module-not-preloaded", "true");
         mInstallApexModuleTargetPreparer.addTestFileName(SPLIT_APEX_APKS_NAME);
@@ -1069,6 +1717,7 @@
                     .once();
             Set<String> installableModules = new HashSet<>();
             installableModules.add(SPLIT_APK_PACKAGE_NAME);
+
             EasyMock.expect(mMockDevice.getInstalledPackageNames()).andReturn(installableModules);
 
             EasyMock.replay(mMockBuildInfo, mMockDevice);
@@ -1148,6 +1797,22 @@
         EasyMock.expectLastCall().once();
     }
 
+    private void mockCleanInstalledApexPackagesAndReboot() throws DeviceNotAvailableException {
+        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(2);
+        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(2);
+        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(2);
+        CommandResult res = new CommandResult();
+        res.setStdout("test.apex");
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
+        mMockDevice.reboot();
+        EasyMock.expectLastCall();
+    }
+
     @Test
     public void testSetupAndTearDown_noModulesPreloaded() throws Exception {
         mSetter.setOptionValue("ignore-if-module-not-preloaded", "true");
@@ -1183,19 +1848,7 @@
         mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
         // Module not preloaded.
         mInstallApexModuleTargetPreparer.addTestFileName(APK2_NAME);
-        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        CommandResult res = new CommandResult();
-        res.setStdout("test.apex");
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
-        mMockDevice.reboot();
-        EasyMock.expectLastCall();
+        mockCleanInstalledApexPackagesAndReboot();
         mockSuccessfulInstallMultiPackageAndReboot();
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(
@@ -1225,19 +1878,7 @@
         mInstallApexModuleTargetPreparer.addTestFileName(APEX_NAME);
         mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
         mInstallApexModuleTargetPreparer.addTestFileName(APK2_NAME);
-        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
-        EasyMock.expectLastCall().times(2);
-        CommandResult res = new CommandResult();
-        res.setStdout("test.apex");
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
-        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
-        mMockDevice.reboot();
-        EasyMock.expectLastCall();
+        mockCleanInstalledApexPackagesAndReboot();
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(
                 new ApexInfo(
diff --git a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
index 16ec66f..853803c 100644
--- a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
@@ -42,6 +42,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.io.IOException;
 import java.io.File;
 import java.util.Set;
 
@@ -645,6 +646,53 @@
         }
     }
 
+    /**
+     * Test that if multiple files exists after delayed partial download, push the one with matching
+     * ABI.
+     */
+    @Test
+    public void testPush_moduleName_files_abi_delayedDownload() throws Exception {
+        mOptionSetter.setOptionValue("push", "file->/data/local/tmp/file");
+        mPreparer.setAbi(new Abi("x86", "32"));
+
+        mPreparer.setInvocationContext(createModuleWithName("aaaaa"));
+        File tmpFolder = FileUtil.createTempDir("push-file-tests-dir");
+        IDeviceBuildInfo info =
+                new DeviceBuildInfo() {
+                    @Override
+                    public File stageRemoteFile(String fileName, File workingDir) {
+                        try {
+                            File file_64 =
+                                    new File(tmpFolder, "target/testcases/aaaaa/x86_64/file");
+                            FileUtil.mkdirsRWX(file_64.getParentFile());
+                            file_64.createNewFile();
+                            File file_32 = new File(tmpFolder, "target/testcases/aaaaa/x86/file");
+                            FileUtil.mkdirsRWX(file_32.getParentFile());
+                            file_32.createNewFile();
+                            // Return the file with mismatched ABI.
+                            return file_64;
+                        } catch (IOException e) {
+                            return null;
+                        }
+                    }
+                };
+        try {
+            info.setFile(BuildInfoFileKey.TESTDIR_IMAGE, tmpFolder, "v1");
+            EasyMock.expect(
+                            mMockDevice.pushFile(
+                                    EasyMock.eq(
+                                            new File(tmpFolder, "target/testcases/aaaaa/x86/file")),
+                                    EasyMock.eq("/data/local/tmp/file")))
+                    .andReturn(true);
+            mTestInfo.getContext().addDeviceBuildInfo("device", info);
+            EasyMock.replay(mMockDevice);
+            mPreparer.setUp(mTestInfo);
+            EasyMock.verify(mMockDevice);
+        } finally {
+            FileUtil.recursiveDelete(tmpFolder);
+        }
+    }
+
     @Test
     public void testPush_moduleName_ignored() throws Exception {
         mOptionSetter.setOptionValue("push", "lib64->/data/local/tmp/lib");
diff --git a/tests/src/com/android/tradefed/targetprep/PythonVirtualenvPreparerTest.java b/tests/src/com/android/tradefed/targetprep/PythonVirtualenvPreparerTest.java
index 133bad5..20754fc 100644
--- a/tests/src/com/android/tradefed/targetprep/PythonVirtualenvPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/PythonVirtualenvPreparerTest.java
@@ -21,7 +21,10 @@
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.createNiceMock;
 import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.replay;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
 
 import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.IBuildInfo;
@@ -30,6 +33,8 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.IRunUtil;
 
+import com.google.common.base.Throwables;
+
 import junit.framework.TestCase;
 
 import java.io.File;
@@ -105,4 +110,44 @@
         mPreparer.installDeps(buildInfo, mMockDevice);
         assertTrue(buildInfo.getFile("PYTHONPATH") == null);
     }
+
+    public void testStartVirtualenv_throwTSE_whenVirtualenvNotFound() throws Exception {
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout("bash: virtualenv: command not found");
+        expect(mMockRunUtil.runTimedCmd(anyLong(), eq("virtualenv"), eq("--version")))
+                .andReturn(result);
+        replay(mMockRunUtil);
+
+        try {
+            mPreparer.startVirtualenv(new BuildInfo(), mMockDevice);
+            fail("startVirtualenv succeeded despite a failed command");
+        } catch (TargetSetupError e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown:\n%s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("virtualenv is not installed."));
+        }
+    }
+
+    public void testStartVirtualenv_throwTSE_whenVirtualenvIsTooOld() throws Exception {
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout("virtualenv 16.7.10 from /path/to/site-packages/virtualenv/__init__.py");
+        expect(mMockRunUtil.runTimedCmd(anyLong(), eq("virtualenv"), eq("--version")))
+                .andReturn(result);
+        replay(mMockRunUtil);
+
+        try {
+            mPreparer.startVirtualenv(new BuildInfo(), mMockDevice);
+            fail("startVirtualenv succeeded despite a failed command");
+        } catch (TargetSetupError e) {
+            assertEquals(
+                    String.format(
+                            "An unexpected exception was thrown:\n%s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    "virtualenv is too old. Required: >=20.0.1, yours: 16.7.10");
+        }
+    }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/tradefed/targetprep/RunHostCommandTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunHostCommandTargetPreparerTest.java
index 3f7ae6d..a8f95cf 100644
--- a/tests/src/com/android/tradefed/targetprep/RunHostCommandTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/RunHostCommandTargetPreparerTest.java
@@ -20,24 +20,29 @@
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.device.IDeviceManager;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.IRunUtil;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Answers;
+import org.mockito.InOrder;
 import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.io.OutputStream;
 import java.util.Arrays;
@@ -45,21 +50,24 @@
 import java.util.List;
 
 /** Unit test for {@link RunHostCommandTargetPreparer}. */
-@RunWith(MockitoJUnitRunner.class)
+@RunWith(JUnit4.class)
 public final class RunHostCommandTargetPreparerTest {
 
     private static final String DEVICE_SERIAL = "123456";
     private static final String FULL_COMMAND = "command    \t\t\t  \t  argument $SERIAL";
 
-    @Mock private ITestDevice mDevice;
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private TestInformation mTestInfo;
+
     @Mock private RunHostCommandTargetPreparer.BgCommandLog mBgCommandLog;
     @Mock private IRunUtil mRunUtil;
+    @Mock private IDeviceManager mDeviceManager;
     private RunHostCommandTargetPreparer mPreparer;
-    private TestInformation mTestInfo;
 
     @Before
     public void setUp() {
-        when(mDevice.getSerialNumber()).thenReturn(DEVICE_SERIAL);
         mPreparer =
                 new RunHostCommandTargetPreparer() {
                     @Override
@@ -68,13 +76,16 @@
                     }
 
                     @Override
+                    IDeviceManager getDeviceManager() {
+                        return mDeviceManager;
+                    }
+
+                    @Override
                     protected List<BgCommandLog> createBgCommandLogs() {
                         return Collections.singletonList(mBgCommandLog);
                     }
                 };
-        IInvocationContext context = new InvocationContext();
-        context.addAllocatedDevice("device", mDevice);
-        mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
+        when(mTestInfo.getDevice().getSerialNumber()).thenReturn(DEVICE_SERIAL);
     }
 
     @Test
@@ -89,6 +100,10 @@
         // Verify timeout and command (split, removed whitespace, and device serial)
         mPreparer.setUp(mTestInfo);
         verify(mRunUtil).runTimedCmd(eq(10L), eq("command"), eq("argument"), eq(DEVICE_SERIAL));
+
+        // No flashing permit taken/returned by default
+        verify(mDeviceManager, never()).takeFlashingPermit();
+        verify(mDeviceManager, never()).returnFlashingPermit();
     }
 
     @Test
@@ -120,6 +135,24 @@
     }
 
     @Test
+    public void testSetUp_flashingPermit() throws Exception {
+        OptionSetter optionSetter = new OptionSetter(mPreparer);
+        optionSetter.setOptionValue("host-setup-command", FULL_COMMAND);
+        optionSetter.setOptionValue("use-flashing-permit", "true");
+
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        when(mRunUtil.runTimedCmd(anyLong(), any())).thenReturn(result);
+
+        // Verify command ran with flashing permit
+        mPreparer.setUp(mTestInfo);
+        InOrder inOrder = inOrder(mRunUtil, mDeviceManager);
+        inOrder.verify(mDeviceManager).takeFlashingPermit();
+        inOrder.verify(mRunUtil)
+                .runTimedCmd(anyLong(), eq("command"), eq("argument"), eq(DEVICE_SERIAL));
+        inOrder.verify(mDeviceManager).returnFlashingPermit();
+    }
+
+    @Test
     public void testTearDown() throws Exception {
         OptionSetter optionSetter = new OptionSetter(mPreparer);
         optionSetter.setOptionValue("host-teardown-command", FULL_COMMAND);
@@ -131,6 +164,10 @@
         // Verify timeout and command (split, removed whitespace, and device serial)
         mPreparer.tearDown(mTestInfo, null);
         verify(mRunUtil).runTimedCmd(eq(10L), eq("command"), eq("argument"), eq(DEVICE_SERIAL));
+
+        // No flashing permit taken/returned by default
+        verify(mDeviceManager, never()).takeFlashingPermit();
+        verify(mDeviceManager, never()).returnFlashingPermit();
     }
 
     @Test
@@ -146,6 +183,24 @@
     }
 
     @Test
+    public void testTearDown_flashingPermit() throws Exception {
+        OptionSetter optionSetter = new OptionSetter(mPreparer);
+        optionSetter.setOptionValue("host-teardown-command", FULL_COMMAND);
+        optionSetter.setOptionValue("use-flashing-permit", "true");
+
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        when(mRunUtil.runTimedCmd(anyLong(), any())).thenReturn(result);
+
+        // Verify command ran with flashing permit
+        mPreparer.tearDown(mTestInfo, null);
+        InOrder inOrder = inOrder(mRunUtil, mDeviceManager);
+        inOrder.verify(mDeviceManager).takeFlashingPermit();
+        inOrder.verify(mRunUtil)
+                .runTimedCmd(anyLong(), eq("command"), eq("argument"), eq(DEVICE_SERIAL));
+        inOrder.verify(mDeviceManager).returnFlashingPermit();
+    }
+
+    @Test
     public void testBgCommand() throws Exception {
         OptionSetter optionSetter = new OptionSetter(mPreparer);
         optionSetter.setOptionValue("host-background-command", FULL_COMMAND);
diff --git a/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
index a623201..236498a 100644
--- a/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -42,6 +43,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Answers;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -117,6 +119,9 @@
         verify(mRunUtil).runTimedCmd(10L, mScriptFile.getAbsolutePath());
         // Verify that script is executable
         assertTrue(mScriptFile.canExecute());
+        // No flashing permit taken/returned by default
+        verify(mDeviceManager, never()).takeFlashingPermit();
+        verify(mDeviceManager, never()).returnFlashingPermit();
     }
 
     @Test
@@ -169,4 +174,16 @@
         mPreparer.setUp(mTestInfo);
         verify(mRunUtil).setEnvVariable("PATH", expectedPath);
     }
+
+    @Test
+    public void testSetUp_flashingPermit() throws Exception {
+        mOptionSetter.setOptionValue("script-file", mScriptFile.getAbsolutePath());
+        mOptionSetter.setOptionValue("use-flashing-permit", "true");
+        // Verify script executed with flashing permit
+        mPreparer.setUp(mTestInfo);
+        InOrder inOrder = inOrder(mRunUtil, mDeviceManager);
+        inOrder.verify(mDeviceManager).takeFlashingPermit();
+        inOrder.verify(mRunUtil).runTimedCmd(anyLong(), eq(mScriptFile.getAbsolutePath()));
+        inOrder.verify(mDeviceManager).returnFlashingPermit();
+    }
 }
diff --git a/tests/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java
new file mode 100644
index 0000000..14e324b
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.targetprep;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.UserInfo;
+import com.android.tradefed.invoker.TestInformation;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class RunOnSecondaryUserTargetPreparerTest {
+
+    private static final String CREATED_USER_2_MESSAGE = "Created user id 2";
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private TestInformation mTestInfo;
+
+    private RunOnSecondaryUserTargetPreparer mPreparer;
+    private OptionSetter mOptionSetter;
+
+    @Before
+    public void setUp() throws Exception {
+        mPreparer = new RunOnSecondaryUserTargetPreparer();
+        mOptionSetter = new OptionSetter(mPreparer);
+    }
+
+    @Test
+    public void setUp_createsAndStartsSecondaryUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user secondary";
+        String expectedStartUserCommand = "am start-user -w 2";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_2_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedCreateUserCommand);
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedStartUserCommand);
+    }
+
+    @Test
+    public void setUp_secondaryUserAlreadyExists_doesNotCreateSecondaryUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(2, new UserInfo(2, "secondary", /* flag= */ 0, /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice(), never()).executeShellCommand(any());
+    }
+
+    @Test
+    public void setUp_secondaryUserAlreadyExists_runsTestAsExistingUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(3, new UserInfo(3, "secondary", /* flag= */ 0, /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.properties())
+                .put(RunOnWorkProfileTargetPreparer.RUN_TESTS_AS_USER_KEY, "3");
+    }
+
+    @Test
+    public void setUp_setsRunTestsAsUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user secondary";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_2_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.properties())
+                .put(RunOnSecondaryUserTargetPreparer.RUN_TESTS_AS_USER_KEY, "2");
+    }
+
+    @Test
+    public void setUp_secondaryUserAlreadyExists_installsPackagesInExistingUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(3, new UserInfo(3, "secondary", /* flag= */ 0, /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+        mOptionSetter.setOptionValue(
+                RunOnWorkProfileTargetPreparer.TEST_PACKAGE_NAME_OPTION, "com.android.testpackage");
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice())
+                .executeShellCommand("pm install-existing --user 3 com.android.testpackage");
+    }
+
+    @Test
+    public void setUp_installsPackagesInSecondaryUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user secondary";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_2_MESSAGE);
+        mOptionSetter.setOptionValue(
+                RunOnSecondaryUserTargetPreparer.TEST_PACKAGE_NAME_OPTION,
+                "com.android.testpackage");
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice())
+                .executeShellCommand("pm install-existing --user 2 com.android.testpackage");
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_disablesTearDown() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(3, new UserInfo(3, "secondary", /* flag= */ 0, /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+        mOptionSetter.setOptionValue("disable-tear-down", "false");
+
+        mPreparer.setUp(mTestInfo);
+
+        assertThat(mPreparer.isTearDownDisabled()).isTrue();
+    }
+
+    @Test
+    public void setUp_doesNotDisableTearDown() throws Exception {
+        String expectedCreateUserCommand = "pm create-user secondary";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_2_MESSAGE);
+        mOptionSetter.setOptionValue("disable-tear-down", "false");
+
+        mPreparer.setUp(mTestInfo);
+
+        assertThat(mPreparer.isTearDownDisabled()).isFalse();
+    }
+
+    @Test
+    public void tearDown_removesSecondaryUser() throws Exception {
+        when(mTestInfo.properties().get(RunOnSecondaryUserTargetPreparer.RUN_TESTS_AS_USER_KEY))
+                .thenReturn("2");
+
+        mPreparer.tearDown(mTestInfo, /* throwable= */ null);
+
+        verify(mTestInfo.getDevice()).removeUser(2);
+    }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java
new file mode 100644
index 0000000..5845cd2
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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.targetprep;
+
+import static com.android.tradefed.targetprep.RunOnWorkProfileTargetPreparer.RUN_TESTS_AS_USER_KEY;
+import static com.android.tradefed.targetprep.RunOnWorkProfileTargetPreparer.TEST_PACKAGE_NAME_OPTION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.UserInfo;
+import com.android.tradefed.invoker.TestInformation;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class RunOnWorkProfileTargetPreparerTest {
+    private static final String CREATED_USER_10_MESSAGE = "Created user id 10";
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private TestInformation mTestInfo;
+
+    private RunOnWorkProfileTargetPreparer mPreparer;
+    private OptionSetter mOptionSetter;
+
+    @Before
+    public void setUp() throws Exception {
+        mPreparer = new RunOnWorkProfileTargetPreparer();
+        mOptionSetter = new OptionSetter(mPreparer);
+    }
+
+    @Test
+    public void setUp_createsAndStartsWorkProfile() throws Exception {
+        String expectedCreateUserCommand = "pm create-user --profileOf 0 --managed work";
+        String expectedStartUserCommand = "am start-user -w 10";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedCreateUserCommand);
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedStartUserCommand);
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_doesNotCreateWorkProfile() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(
+                10,
+                new UserInfo(
+                        10,
+                        "work",
+                        /* flag= */ UserInfo.FLAG_MANAGED_PROFILE,
+                        /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice(), never()).executeShellCommand(any());
+    }
+
+    @Test
+    public void setUp_nonZeroCurrentUser_createsWorkProfileForCorrectUser() throws Exception {
+        when(mTestInfo.getDevice().getCurrentUser()).thenReturn(1);
+        String expectedCreateUserCommand = "pm create-user --profileOf 1 --managed work";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice()).executeShellCommand(expectedCreateUserCommand);
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_runsTestAsExistingUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(
+                11,
+                new UserInfo(
+                        11,
+                        "work",
+                        /* flag= */ UserInfo.FLAG_MANAGED_PROFILE,
+                        /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.properties()).put(RUN_TESTS_AS_USER_KEY, "11");
+    }
+
+    @Test
+    public void setUp_setsRunTestsAsUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user --profileOf 0 --managed work";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.properties()).put(RUN_TESTS_AS_USER_KEY, "10");
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_installsPackagesInExistingUser() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(
+                11,
+                new UserInfo(
+                        11,
+                        "work",
+                        /* flag= */ UserInfo.FLAG_MANAGED_PROFILE,
+                        /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+        mOptionSetter.setOptionValue(TEST_PACKAGE_NAME_OPTION, "com.android.testpackage");
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice())
+                .executeShellCommand("pm install-existing --user 11 com.android.testpackage");
+    }
+
+    @Test
+    public void setUp_installsPackagesInWorkUser() throws Exception {
+        String expectedCreateUserCommand = "pm create-user --profileOf 0 --managed work";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+        mOptionSetter.setOptionValue(TEST_PACKAGE_NAME_OPTION, "com.android.testpackage");
+
+        mPreparer.setUp(mTestInfo);
+
+        verify(mTestInfo.getDevice())
+                .executeShellCommand("pm install-existing --user 10 com.android.testpackage");
+    }
+
+    @Test
+    public void setUp_workProfileAlreadyExists_disablesTearDown() throws Exception {
+        Map<Integer, UserInfo> userInfos = new HashMap<>();
+        userInfos.put(
+                11,
+                new UserInfo(
+                        11,
+                        "work",
+                        /* flag= */ UserInfo.FLAG_MANAGED_PROFILE,
+                        /* isRunning= */ true));
+        when(mTestInfo.getDevice().getUserInfos()).thenReturn(userInfos);
+        mOptionSetter.setOptionValue("disable-tear-down", "false");
+
+        mPreparer.setUp(mTestInfo);
+
+        assertThat(mPreparer.isTearDownDisabled()).isTrue();
+    }
+
+    @Test
+    public void setUp_doesNotDisableTearDown() throws Exception {
+        String expectedCreateUserCommand = "pm create-user --profileOf 0 --managed work";
+        when(mTestInfo.getDevice().executeShellCommand(expectedCreateUserCommand))
+                .thenReturn(CREATED_USER_10_MESSAGE);
+        mOptionSetter.setOptionValue("disable-tear-down", "false");
+
+        mPreparer.setUp(mTestInfo);
+
+        assertThat(mPreparer.isTearDownDisabled()).isFalse();
+    }
+
+    @Test
+    public void tearDown_removesWorkUser() throws Exception {
+        when(mTestInfo.properties().get(RUN_TESTS_AS_USER_KEY)).thenReturn("10");
+
+        mPreparer.tearDown(mTestInfo, /* throwable= */ null);
+
+        verify(mTestInfo.getDevice()).removeUser(10);
+    }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/TestAppInstallSetupTest.java b/tests/src/com/android/tradefed/targetprep/TestAppInstallSetupTest.java
index 97dbc2b..7fa4796 100644
--- a/tests/src/com/android/tradefed/targetprep/TestAppInstallSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/TestAppInstallSetupTest.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.targetprep;
 
+import static com.android.tradefed.targetprep.TestAppInstallSetup.CHECK_MIN_SDK_OPTION;
 import static com.android.tradefed.targetprep.TestAppInstallSetup.TEST_FILE_NAME_OPTION;
 import static com.android.tradefed.targetprep.TestAppInstallSetup.THROW_IF_NOT_FOUND_OPTION;
 
@@ -23,8 +24,13 @@
 
 import static org.easymock.EasyMock.anyBoolean;
 import static org.easymock.EasyMock.anyObject;
+
+import static org.mockito.Mockito.doReturn;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.times;
 
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.command.remote.DeviceDescriptor;
@@ -36,6 +42,7 @@
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.testtype.Abi;
+import com.android.tradefed.util.AaptParser;
 import com.android.tradefed.util.FileUtil;
 
 import com.google.common.collect.ImmutableMap;
@@ -47,12 +54,14 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
 
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -68,6 +77,7 @@
     private File fakeApk;
     private File fakeApk2;
     private File mFakeBuildApk;
+    private AaptParser mMockAaptParser;
     private TestAppInstallSetup mPrep;
     private TestInformation mTestInfo;
     private IDeviceBuildInfo mMockBuildInfo;
@@ -125,6 +135,7 @@
         mTestSplitApkFiles.add(fakeApk2);
 
         mMockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+        mMockAaptParser = Mockito.mock(AaptParser.class);
         mMockTestDevice = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockTestDevice.getSerialNumber()).andStubReturn(SERIAL);
         EasyMock.expect(mMockTestDevice.getDeviceDescriptor()).andStubReturn(null);
@@ -572,6 +583,108 @@
         assertThat(installs).containsExactly(ImmutableSet.of(apkFile1, apkFile2));
     }
 
+    /**
+     * Tests that we throw exception with check-min-sdk when file fails to parse
+     *
+     * @throws Exception the expected exception
+     */
+    @Test
+    public void testResolveApkFiles_checkMinSdk_failParsing() throws Exception {
+        mPrep =
+                new TestAppInstallSetup() {
+                    @Override
+                    AaptParser doAaptParse(File apkFile) {
+                        return null;
+                    }
+                };
+        List<File> files = new ArrayList<>();
+        files.add(fakeApk);
+        OptionSetter setter = new OptionSetter(mPrep);
+        setter.setOptionValue(CHECK_MIN_SDK_OPTION, "true");
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
+        try {
+            mPrep.resolveApkFiles(mTestInfo, files);
+            fail("Should have thrown an exception");
+        } catch (TargetSetupError expected) {
+            assertEquals(
+                    String.format("Failed to extract info from `%s` using aapt", fakeApk.getName()),
+                    expected.getMessage());
+        } finally {
+            EasyMock.verify(mMockBuildInfo, mMockTestDevice);
+        }
+    }
+
+    /** Tests that we don't include the file if api level too low */
+    @Test
+    public void testResolveApkFiles_checkMinSdk_apiLow() throws Exception {
+        mPrep =
+                new TestAppInstallSetup() {
+                    @Override
+                    AaptParser doAaptParse(File apkFile) {
+                        return mMockAaptParser;
+                    }
+
+                    @Override
+                    protected String parsePackageName(
+                            File testAppFile, DeviceDescriptor deviceDescriptor) {
+                        return "fakePackageName";
+                    }
+                };
+        List<File> files = new ArrayList<>();
+        files.add(fakeApk);
+        OptionSetter setter = new OptionSetter(mPrep);
+        setter.setOptionValue(CHECK_MIN_SDK_OPTION, "true");
+        EasyMock.expect(mMockTestDevice.getSerialNumber()).andStubReturn(SERIAL);
+        EasyMock.expect(mMockTestDevice.getApiLevel()).andReturn(21).times(2);
+        doReturn(22).when(mMockAaptParser).getSdkVersion();
+
+        Map<File, String> expected = new LinkedHashMap<>();
+
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
+
+        Map<File, String> result = mPrep.resolveApkFiles(mTestInfo, files);
+
+        EasyMock.verify(mMockBuildInfo, mMockTestDevice);
+        Mockito.verify(mMockAaptParser, times(2)).getSdkVersion();
+        assertEquals(expected, result);
+    }
+
+    /** Tests that we include the file if level is appropriate */
+    @Test
+    public void testResolveApkFiles_checkMinSdk_apiOk() throws Exception {
+        mPrep =
+                new TestAppInstallSetup() {
+                    @Override
+                    AaptParser doAaptParse(File apkFile) {
+                        return mMockAaptParser;
+                    }
+
+                    @Override
+                    protected String parsePackageName(
+                            File testAppFile, DeviceDescriptor deviceDescriptor) {
+                        return "fakePackageName";
+                    }
+                };
+        List<File> files = new ArrayList<>();
+        files.add(fakeApk);
+        OptionSetter setter = new OptionSetter(mPrep);
+        setter.setOptionValue(CHECK_MIN_SDK_OPTION, "true");
+        EasyMock.expect(mMockTestDevice.getSerialNumber()).andStubReturn(SERIAL);
+        EasyMock.expect(mMockTestDevice.getApiLevel()).andReturn(23);
+        doReturn(22).when(mMockAaptParser).getSdkVersion();
+
+        Map<File, String> expected = new LinkedHashMap<>();
+        expected.put(fakeApk, "fakePackageName");
+
+        EasyMock.replay(mMockBuildInfo, mMockTestDevice);
+
+        Map<File, String> result = mPrep.resolveApkFiles(mTestInfo, files);
+
+        Mockito.verify(mMockAaptParser).getSdkVersion();
+        EasyMock.verify(mMockBuildInfo, mMockTestDevice);
+        assertEquals(result, expected);
+    }
+
     private static Path createSubDirectory(Path parent, String name) throws IOException {
         return Files.createDirectory(parent.resolve(name)).toAbsolutePath();
     }
diff --git a/tests/src/com/android/tradefed/testtype/ArtGTestTest.java b/tests/src/com/android/tradefed/testtype/ArtGTestTest.java
new file mode 100644
index 0000000..a30343e
--- /dev/null
+++ b/tests/src/com/android/tradefed/testtype/ArtGTestTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.testtype;
+
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.MockFileUtil;
+import com.android.tradefed.targetprep.ArtChrootPreparer;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.result.ITestInvocationListener;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ArtGTest}. */
+@RunWith(JUnit4.class)
+public class ArtGTestTest {
+    private ITestInvocationListener mMockInvocationListener = null;
+    private IShellOutputReceiver mMockReceiver = null;
+    private ITestDevice mMockITestDevice = null;
+    private GTest mGTest;
+    private TestInformation mTestInfo;
+
+    @Before
+    public void setUp() throws Exception {
+        mMockInvocationListener = EasyMock.createMock(ITestInvocationListener.class);
+        mMockReceiver = EasyMock.createMock(IShellOutputReceiver.class);
+        mMockITestDevice = EasyMock.createMock(ITestDevice.class);
+        mGTest =
+                new ArtGTest() {
+                    @Override
+                    IShellOutputReceiver createResultParser(
+                            String runName, ITestInvocationListener listener) {
+                        return mMockReceiver;
+                    }
+                };
+        mGTest.setDevice(mMockITestDevice);
+        mGTest.setNativeTestDevicePath(ArtChrootPreparer.CHROOT_PATH);
+        mTestInfo = TestInformation.newBuilder().build();
+
+        EasyMock.expect(mMockITestDevice.getSerialNumber()).andStubReturn("serial");
+    }
+
+    private void replayMocks() {
+        EasyMock.replay(mMockInvocationListener, mMockITestDevice);
+    }
+
+    private void verifyMocks() {
+        EasyMock.verify(mMockInvocationListener, mMockITestDevice);
+    }
+
+    @Test
+    public void testChroot_testRun() throws DeviceNotAvailableException {
+        final String nativeTestPath = ArtChrootPreparer.CHROOT_PATH;
+        final String test1 = "test1";
+        final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
+
+        MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1);
+        EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+
+        String[] files = new String[] {"test1"};
+        EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
+        mMockITestDevice.executeShellCommand(
+                EasyMock.startsWith("chroot /data/local/tmp/art-test-chroot /test1"),
+                EasyMock.anyObject(),
+                EasyMock.anyLong(),
+                EasyMock.anyObject(),
+                EasyMock.anyInt());
+
+        replayMocks();
+        mGTest.run(mTestInfo, mMockInvocationListener);
+        verifyMocks();
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java b/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
index 23d8b75..e7c4354 100644
--- a/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/ArtRunTestTest.java
@@ -25,7 +25,6 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
@@ -49,9 +48,6 @@
 @RunWith(JUnit4.class)
 public class ArtRunTestTest {
 
-    // Default run-test name.
-    private static final String RUN_TEST_NAME = "run-test";
-
     private ITestInvocationListener mMockInvocationListener;
     private IAbi mMockAbi;
     private ITestDevice mMockITestDevice;
@@ -60,9 +56,9 @@
     private ArtRunTest mArtRunTest;
     private OptionSetter mSetter;
     private TestInformation mTestInfo;
-    // Target tests directory.
-    private File mTmpTargetTestsDir;
-    // Expected output file (under the target tests directory).
+    // Test dependencies directory on host.
+    private File mTmpDepsDir;
+    // Expected output file (within the dependencies directory).
     private File mTmpExpectedFile;
 
     @Before
@@ -82,23 +78,22 @@
         mArtRunTest.setDevice(mMockITestDevice);
         mSetter = new OptionSetter(mArtRunTest);
 
-        // Set up target tests directory and expected output file.
-        mTmpTargetTestsDir = FileUtil.createTempDir("target_testcases");
-        File runTestDir = new File(mTmpTargetTestsDir, RUN_TEST_NAME);
-        runTestDir.mkdir();
-        mTmpExpectedFile = new File(runTestDir, "expected.txt");
-        FileWriter fw = new FileWriter(mTmpExpectedFile);
-        fw.write("output\n");
-        fw.close();
-
-        // Set the target tests directory in test information object.
-        mTestInfo = TestInformation.newBuilder().build();
-        mTestInfo.executionFiles().put(FilesKey.TARGET_TESTS_DIRECTORY, mTmpTargetTestsDir);
+        // Temporary test directory (e.g. for the expected-output file).
+        mTmpDepsDir = FileUtil.createTempDir("art-run-test-deps");
+        mTestInfo = TestInformation.newBuilder().setDependenciesFolder(mTmpDepsDir).build();
     }
 
     @After
     public void tearDown() {
-        FileUtil.recursiveDelete(mTmpTargetTestsDir);
+        FileUtil.recursiveDelete(mTmpDepsDir);
+    }
+
+    /** Helper creating an expected-output file within the (temporary) test directory. */
+    private void createExpectedOutputFile(String runTestName) throws IOException {
+        mTmpExpectedFile = new File(mTmpDepsDir, runTestName + "-expected.txt");
+        FileWriter fw = new FileWriter(mTmpExpectedFile);
+        fw.write("output\n");
+        fw.close();
     }
 
     /** Helper mocking writing the output of a test command. */
@@ -151,7 +146,9 @@
     @Test
     public void testRunSingleTest_unsetClasspathOption()
             throws ConfigurationException, DeviceNotAvailableException, IOException {
-        mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
+        final String runTestName = "test";
+        mSetter.setOptionValue("run-test-name", runTestName);
+        createExpectedOutputFile(runTestName);
 
         replayMocks();
         try {
@@ -163,21 +160,20 @@
         verifyMocks();
     }
 
-    /** Test the run method for a (single) test. */
-    @Test
-    public void testRunSingleTest()
+    /** Helper containing testing logic for a (single) test expected to run (and succeed). */
+    private void doTestRunSingleTest(final String runTestName, final String classpath)
             throws ConfigurationException, DeviceNotAvailableException, IOException {
-        mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
-        final String classpath = "/data/local/tmp/test/test.jar";
+        mSetter.setOptionValue("run-test-name", runTestName);
+        createExpectedOutputFile(runTestName);
         mSetter.setOptionValue("classpath", classpath);
 
         // Pre-test checks.
-        EasyMock.expect(mMockITestDevice.getSerialNumber()).andReturn("");
         EasyMock.expect(mMockAbi.getName()).andReturn("abi");
+        EasyMock.expect(mMockITestDevice.getSerialNumber()).andReturn("");
         String runName = "ArtRunTest_abi";
         // Beginning of test.
         mMockInvocationListener.testRunStarted(runName, 1);
-        TestDescription testId = new TestDescription(runName, RUN_TEST_NAME);
+        TestDescription testId = new TestDescription(runName, runTestName);
         mMockInvocationListener.testStarted(testId);
         String cmd = String.format("dalvikvm64 -classpath %s Main", classpath);
         // Test execution.
@@ -198,6 +194,31 @@
         verifyMocks();
     }
 
+    /** Helper containing testing logic for a (single) test expected not to run. */
+    private void doTestDoNotRunSingleTest(final String runTestName, final String classpath)
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        mSetter.setOptionValue("run-test-name", runTestName);
+        createExpectedOutputFile(runTestName);
+        mSetter.setOptionValue("classpath", classpath);
+
+        EasyMock.expect(mMockAbi.getName()).andReturn("abi");
+        replayMocks();
+
+        mArtRunTest.run(mTestInfo, mMockInvocationListener);
+
+        verifyMocks();
+    }
+
+    /** Test the run method for a (single) test. */
+    @Test
+    public void testRunSingleTest()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        final String runTestName = "test";
+        final String classpath = "/data/local/tmp/test/test.jar";
+
+        doTestRunSingleTest(runTestName, classpath);
+    }
+
     /**
      * Test the behavior of the run method when the output produced by the shell command on device
      * differs from the expected output.
@@ -205,17 +226,19 @@
     @Test
     public void testRunSingleTest_unexpectedOutput()
             throws ConfigurationException, DeviceNotAvailableException, IOException {
-        mSetter.setOptionValue("run-test-name", RUN_TEST_NAME);
+        final String runTestName = "test";
+        mSetter.setOptionValue("run-test-name", runTestName);
+        createExpectedOutputFile(runTestName);
         final String classpath = "/data/local/tmp/test/test.jar";
         mSetter.setOptionValue("classpath", classpath);
 
         // Pre-test checks.
-        EasyMock.expect(mMockITestDevice.getSerialNumber()).andReturn("");
         EasyMock.expect(mMockAbi.getName()).andReturn("abi");
+        EasyMock.expect(mMockITestDevice.getSerialNumber()).andReturn("");
         String runName = "ArtRunTest_abi";
         // Beginning of test.
         mMockInvocationListener.testRunStarted(runName, 1);
-        TestDescription testId = new TestDescription(runName, RUN_TEST_NAME);
+        TestDescription testId = new TestDescription(runName, runTestName);
         mMockInvocationListener.testStarted(testId);
         String cmd = String.format("dalvikvm64 -classpath %s Main", classpath);
         // Test execution.
@@ -224,7 +247,14 @@
         mockTestOutputWrite("unexpected\n");
         EasyMock.expect(mMockITestDevice.getSerialNumber()).andReturn("");
         // End of test.
-        mMockInvocationListener.testFailed(testId, "'unexpected\n' instead of 'output\n'");
+        String errorMessage =
+                "The test's standard output does not match the expected output:\n"
+                        + "--- expected.txt\n"
+                        + "+++ stdout\n"
+                        + "@@ -1,1 +1,1 @@\n"
+                        + "-output\n"
+                        + "+unexpected\n";
+        mMockInvocationListener.testFailed(testId, errorMessage);
         mMockInvocationListener.testEnded(
                 EasyMock.eq(testId), (HashMap<String, Metric>) EasyMock.anyObject());
         mMockInvocationListener.testRunEnded(
@@ -236,4 +266,44 @@
 
         verifyMocks();
     }
+
+    /** Test the run method for a (single) test contained in an include filter. */
+    @Test
+    public void testIncludeFilter()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        final String runTestName = "test";
+        final String classpath = "/data/local/tmp/test/test.jar";
+        // Add an include filter containing the test's name.
+        mArtRunTest.addIncludeFilter(runTestName);
+
+        doTestRunSingleTest(runTestName, classpath);
+    }
+
+    /** Test the run method for a (single) test contained in an exclude filter. */
+    @Test
+    public void testExcludeFilter()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        final String runTestName = "test";
+        final String classpath = "/data/local/tmp/test/test.jar";
+        // Add an exclude filter containing the test's name.
+        mArtRunTest.addExcludeFilter(runTestName);
+
+        doTestDoNotRunSingleTest(runTestName, classpath);
+    }
+
+    /**
+     * Test the run method for a (single) test contained both in an include and an exclude filter.
+     */
+    @Test
+    public void testIncludeAndExcludeFilter()
+            throws ConfigurationException, DeviceNotAvailableException, IOException {
+        final String runTestName = "test";
+        final String classpath = "/data/local/tmp/test/test.jar";
+        // Add an include filter containing the test's name.
+        mArtRunTest.addIncludeFilter(runTestName);
+        // Add an exclude filter containing the test's name.
+        mArtRunTest.addExcludeFilter(runTestName);
+
+        doTestDoNotRunSingleTest(runTestName, classpath);
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java b/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java
index 51877ce..8ce4d0a 100644
--- a/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GoogleBenchmarkTestTest.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.util.StringEscapeUtils;
 
 import junit.framework.TestCase;
 
@@ -348,8 +349,7 @@
         String filterFlag = mGoogleBenchmarkTest.getFilterFlagForFilters(filters);
         assertEquals(
                 String.format(
-                        " %s=%s",
-                        GoogleBenchmarkTest.GBENCHMARK_FILTER_OPTION, "filter1\\|filter2"),
+                        " %s=%s", GoogleBenchmarkTest.GBENCHMARK_FILTER_OPTION, "filter1|filter2"),
                 filterFlag);
     }
 
@@ -366,8 +366,7 @@
         String filterFlag = mGoogleBenchmarkTest.getFilterFlagForTests(tests);
         assertEquals(
                 String.format(
-                        " %s=%s",
-                        GoogleBenchmarkTest.GBENCHMARK_FILTER_OPTION, "^test1$\\|^test2$"),
+                        " %s=%s", GoogleBenchmarkTest.GBENCHMARK_FILTER_OPTION, "^test1$|^test2$"),
                 filterFlag);
     }
 
@@ -407,7 +406,10 @@
             String incFilterFlag =
                     mGoogleBenchmarkTest.getFilterFlagForFilters(
                             mGoogleBenchmarkTest.getIncludeFilters());
-            EasyMock.expect(mMockITestDevice.executeShellCommand(EasyMock.contains(incFilterFlag)))
+            EasyMock.expect(
+                            mMockITestDevice.executeShellCommand(
+                                    EasyMock.contains(
+                                            StringEscapeUtils.escapeShell(incFilterFlag))))
                     .andReturn(incTests);
         } else {
             EasyMock.expect(
@@ -422,14 +424,17 @@
             String excFilterFlag =
                     mGoogleBenchmarkTest.getFilterFlagForFilters(
                             mGoogleBenchmarkTest.getExcludeFilters());
-            EasyMock.expect(mMockITestDevice.executeShellCommand(EasyMock.contains(excFilterFlag)))
+            EasyMock.expect(
+                            mMockITestDevice.executeShellCommand(
+                                    EasyMock.contains(
+                                            StringEscapeUtils.escapeShell(excFilterFlag))))
                     .andReturn(excTests);
         }
         if (filteredTests != null && filteredTests.size() > 0) {
             // Runningt filtered tests
             String testFilterFlag = mGoogleBenchmarkTest.getFilterFlagForTests(filteredTests);
             mMockITestDevice.executeShellCommand(
-                    EasyMock.contains(testFilterFlag),
+                    EasyMock.contains(StringEscapeUtils.escapeShell(testFilterFlag)),
                     EasyMock.same(mMockReceiver),
                     EasyMock.anyLong(),
                     (TimeUnit) EasyMock.anyObject(),
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
index d9e1e3e..02359c9 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
@@ -16,6 +16,8 @@
 
 package com.android.tradefed.testtype;
 
+import static com.android.tradefed.testtype.InstrumentationTest.RUN_TESTS_AS_USER_KEY;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -28,6 +30,7 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -205,6 +208,27 @@
     }
 
     @Test
+    public void testRun_nullTestInfo() throws Exception {
+        mInstrumentationTest.run(/* testInfo= */ null, mMockListener);
+
+        verify(mMockTestDevice, atLeastOnce())
+                .runInstrumentationTests(
+                        any(IRemoteAndroidTestRunner.class), any(ITestInvocationListener.class));
+    }
+
+    @Test
+    public void testRun_runTestsAsUser() throws DeviceNotAvailableException {
+        mTestInfo.properties().put(RUN_TESTS_AS_USER_KEY, "10");
+        mInstrumentationTest.run(mTestInfo, mMockListener);
+
+        verify(mMockTestDevice, atLeastOnce())
+                .runInstrumentationTestsAsUser(
+                        any(IRemoteAndroidTestRunner.class),
+                        eq(10),
+                        any(ITestInvocationListener.class));
+    }
+
+    @Test
     public void testRun_bothAbi() throws DeviceNotAvailableException {
         mInstrumentationTest.setAbi(mock(IAbi.class));
         mInstrumentationTest.setForceAbi("test");
diff --git a/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
index de69a05..27e793b 100644
--- a/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
@@ -28,6 +28,9 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -78,6 +81,7 @@
     private File mMoblyTestDir;
     private File mMoblyBinary; // used by python-binaries option
     private File mMoblyBinary2; // used by par-file-name option
+    private File mVenvDir;
     private DeviceBuildInfo mMockBuildInfo;
 
     @Before
@@ -87,6 +91,10 @@
         mMockRunUtil = Mockito.mock(IRunUtil.class);
         mMockBuildInfo = Mockito.mock(DeviceBuildInfo.class);
         mSpyTest.setDevice(mMockDevice);
+
+        mVenvDir = FileUtil.createTempDir("venv");
+        new File(mVenvDir, "bin").mkdir();
+
         Mockito.doReturn(mMockRunUtil).when(mSpyTest).getRunUtil();
         Mockito.doReturn(DEFAULT_TIME_OUT).when(mSpyTest).getTestTimeout();
         Mockito.doReturn("not_adb").when(mSpyTest).getAdbPath();
@@ -99,6 +107,7 @@
     @After
     public void tearDown() throws Exception {
         FileUtil.recursiveDelete(mMoblyTestDir);
+        FileUtil.recursiveDelete(mVenvDir);
     }
 
     @Test
@@ -216,6 +225,71 @@
     }
 
     @Test
+    public void testRun_shouldActivateVenvAndCleanUp_whenVenvIsSet() throws Exception {
+        Mockito.when(mMockBuildInfo.getFile(eq("VIRTUAL_ENV"))).thenReturn(mVenvDir);
+        OptionSetter setter = new OptionSetter(mSpyTest);
+        setter.setOptionValue("python-binaries", mMoblyBinary.getAbsolutePath());
+        File testResult = new File(mSpyTest.getLogDirAbsolutePath(), TEST_RESULT_FILE_NAME);
+        Mockito.when(
+                        mMockRunUtil.runTimedCmd(
+                                anyLong(),
+                                anyString(),
+                                eq("--"),
+                                contains("--device_serial="),
+                                contains("--log_path=")))
+                .thenAnswer(
+                        new Answer<CommandResult>() {
+                            @Override
+                            public CommandResult answer(InvocationOnMock invocation)
+                                    throws Throwable {
+                                FileUtils.createFile(testResult, "");
+                                FileUtils.createFile(
+                                        new File(mSpyTest.getLogDirAbsolutePath(), "log"),
+                                        "log content");
+                                return new CommandResult(CommandStatus.SUCCESS);
+                            }
+                        });
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout(
+                "Name: pip\nLocation: "
+                        + new File(mVenvDir.getAbsolutePath(), "lib/python3.8/site-packages"));
+        Mockito.when(mMockRunUtil.runTimedCmd(anyLong(), anyString(), eq("show"), eq("pip")))
+                .thenReturn(result);
+
+        mSpyTest.run(Mockito.mock(ITestInvocationListener.class));
+
+        verify(mSpyTest.getRunUtil(), times(1))
+                .setEnvVariable(eq("VIRTUAL_ENV"), eq(mVenvDir.getAbsolutePath()));
+        assertFalse(mVenvDir.exists());
+    }
+
+    @Test
+    public void testRun_shouldNotActivateVenv_whenVenvIsNotSet() throws Exception {
+        FileUtil.recursiveDelete(mVenvDir);
+        OptionSetter setter = new OptionSetter(mSpyTest);
+        setter.setOptionValue("python-binaries", mMoblyBinary.getAbsolutePath());
+        File testResult = new File(mSpyTest.getLogDirAbsolutePath(), TEST_RESULT_FILE_NAME);
+        Mockito.when(mMockRunUtil.runTimedCmd(anyLong(), any()))
+                .thenAnswer(
+                        new Answer<CommandResult>() {
+                            @Override
+                            public CommandResult answer(InvocationOnMock invocation)
+                                    throws Throwable {
+                                FileUtils.createFile(testResult, "");
+                                FileUtils.createFile(
+                                        new File(mSpyTest.getLogDirAbsolutePath(), "log"),
+                                        "log content");
+                                return new CommandResult(CommandStatus.SUCCESS);
+                            }
+                        });
+
+        mSpyTest.run(Mockito.mock(ITestInvocationListener.class));
+
+        verify(mSpyTest.getRunUtil(), never())
+                .setEnvVariable(eq("VIRTUAL_ENV"), eq(mVenvDir.getAbsolutePath()));
+    }
+
+    @Test
     public void testBuildCommandLineArrayWithOutConfig() throws Exception {
         Mockito.doNothing().when(mSpyTest).reportLogs(any(), any());
         Mockito.doReturn(DEVICE_SERIAL).when(mMockDevice).getSerialNumber();
@@ -262,7 +336,11 @@
         Mockito.doNothing().when(mSpyTest).reportLogs(any(), any());
         mMockSummaryInputStream = Mockito.mock(InputStream.class);
         mMockParser = Mockito.mock(MoblyYamlResultParser.class);
-        mSpyTest.processYamlTestResults(mMockSummaryInputStream, mMockParser);
+        mSpyTest.processYamlTestResults(
+                mMockSummaryInputStream,
+                mMockParser,
+                Mockito.mock(ITestInvocationListener.class),
+                "runName");
         verify(mMockParser, times(1)).parse(mMockSummaryInputStream);
     }
 
diff --git a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
index 3cee111..1050460 100644
--- a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.ITestDevice;
@@ -263,13 +264,22 @@
 
     private GranularRetriableTestWrapper createGranularTestWrapper(
             IRemoteTest test, int maxRunCount) throws Exception {
-        return createGranularTestWrapper(test, maxRunCount, new ArrayList<>());
+        return createGranularTestWrapper(test, maxRunCount, new ArrayList<>(), null);
     }
 
     private GranularRetriableTestWrapper createGranularTestWrapper(
             IRemoteTest test, int maxRunCount, List<IMetricCollector> collectors) throws Exception {
+        return createGranularTestWrapper(test, maxRunCount, collectors, null);
+    }
+
+    private GranularRetriableTestWrapper createGranularTestWrapper(
+            IRemoteTest test,
+            int maxRunCount,
+            List<IMetricCollector> collectors,
+            ModuleDefinition module)
+            throws Exception {
         GranularRetriableTestWrapper granularTestWrapper =
-                new GranularRetriableTestWrapper(test, null, null, null, maxRunCount);
+                new GranularRetriableTestWrapper(test, module, null, null, null, maxRunCount);
         granularTestWrapper.setModuleId("test module");
         granularTestWrapper.setMarkTestsSkipped(false);
         granularTestWrapper.setMetricCollectors(collectors);
@@ -904,6 +914,101 @@
         EasyMock.verify(mMockDevice, mMockDevice2);
     }
 
+    /** Test to reset multi-devices at the last intra-module retry. */
+    @Test
+    public void testIntraModuleRun_resetMultiDevicesAtLastIntraModuleRetry() throws Exception {
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("reset-at-last-retry", "true");
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
+        FakeTest test = new FakeTest();
+        test.setRunFailure("I failed!");
+        ITestDevice noneAVDDevice = EasyMock.createMock(ITestDevice.class);
+
+        RemoteAndroidVirtualDevice avdDevice = Mockito.mock(RemoteAndroidVirtualDevice.class);
+        Mockito.when(avdDevice.powerwashGce()).thenReturn(true);
+
+        ModuleDefinition module = Mockito.mock(ModuleDefinition.class);
+        // Should call suite level preparers.
+        Mockito.when(module.runPreparation(true)).thenReturn(null);
+
+        mModuleInvocationContext.addAllocatedDevice("default-device1", noneAVDDevice);
+        mModuleInvocationContext.addAllocatedDevice("default-device2", avdDevice);
+        GranularRetriableTestWrapper granularTestWrapper =
+                createGranularTestWrapper(test, 3, new ArrayList<>(), module);
+        granularTestWrapper.setRetryDecision(decision);
+        EasyMock.expect(noneAVDDevice.getIDevice())
+                .andStubReturn(EasyMock.createMock(IDevice.class));
+        EasyMock.expect(noneAVDDevice.getSerialNumber()).andStubReturn("device-1");
+
+        EasyMock.replay(noneAVDDevice);
+        granularTestWrapper.run(mModuleInfo, new CollectingTestListener());
+        EasyMock.verify(noneAVDDevice);
+    }
+
+    /** Test to reset device at the last intra-module retry failed due to preparer failure. */
+    @Test
+    public void testIntraModuleRun_resetFailed_preparerFailure() throws Exception {
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("reset-at-last-retry", "true");
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
+        FakeTest test = new FakeTest();
+        test.setRunFailure("I failed!");
+
+        RemoteAndroidVirtualDevice avdDevice = Mockito.mock(RemoteAndroidVirtualDevice.class);
+        Mockito.when(avdDevice.powerwashGce()).thenReturn(true);
+
+        ModuleDefinition module = Mockito.mock(ModuleDefinition.class);
+        // Suite level preparers failed.
+        Mockito.when(module.runPreparation(true)).thenReturn(new RuntimeException());
+
+        mModuleInvocationContext.addAllocatedDevice("default-device2", avdDevice);
+        GranularRetriableTestWrapper granularTestWrapper =
+                createGranularTestWrapper(test, 3, new ArrayList<>(), module);
+        granularTestWrapper.setRetryDecision(decision);
+
+        try {
+            granularTestWrapper.run(mModuleInfo, new CollectingTestListener());
+            fail("Exception should be raised when reset is failed.");
+        } catch (DeviceNotAvailableException e) {
+            assertTrue(e.getMessage().startsWith("Failed to reset devices before retry: "));
+        }
+    }
+
+    /** Test to reset device at the last intra-module retry failed due to reset failure. */
+    @Test
+    public void testIntraModuleRun_resetFailed_powerwashFailure() throws Exception {
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("reset-at-last-retry", "true");
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
+        FakeTest test = new FakeTest();
+        test.setRunFailure("I failed!");
+
+        RemoteAndroidVirtualDevice device = Mockito.mock(RemoteAndroidVirtualDevice.class);
+        Mockito.when(device.powerwashGce()).thenReturn(false);
+        Mockito.when(device.getSerialNumber()).thenReturn("device1");
+
+        test.setDevice(device);
+        mModuleInvocationContext.addAllocatedDevice("default-device1", device);
+        GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 3);
+        granularTestWrapper.setRetryDecision(decision);
+
+        try {
+            granularTestWrapper.run(mModuleInfo, new CollectingTestListener());
+            fail("Exception should be raised when reset is failed.");
+        } catch (DeviceNotAvailableException e) {
+            assertEquals("Failed to powerwash device: device1", e.getMessage());
+        }
+    }
+
     /** Collector that track if it was called or not */
     public static class CalledMetricCollector extends BaseDeviceMetricCollector {
 
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleSplitterTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleSplitterTest.java
index 2b3db6b..5d43bb2 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleSplitterTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleSplitterTest.java
@@ -37,8 +37,11 @@
 import org.junit.runners.JUnit4;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 /** Unit tests for {@link ModuleSplitter}. */
 @RunWith(JUnit4.class)
@@ -46,6 +49,8 @@
 
     private static final String DEFAULT_DEVICE = ConfigurationDef.DEFAULT_DEVICE_NAME;
     private TestInformation mTestInfo = TestInformation.newBuilder().build();
+    private Map<String, List<ITargetPreparer>> mSuitePreparersPerDevice =
+            new HashMap<String, List<ITargetPreparer>>();
 
     /**
      * Tests that {@link ModuleSplitter#splitConfiguration(TestInformation, LinkedHashMap, int,
@@ -69,7 +74,8 @@
         setter.setOptionValue("not-shardable", "true");
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, true);
         // matching 1 for 1, config to ModuleDefinition since not shardable
         assertEquals(1, res.size());
         // The original target preparer is changed since we split multiple <test> tags.
@@ -105,7 +111,8 @@
         setter.setOptionValue("not-strict-shardable", "true");
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, true);
         // We are sharding since even if we are not-strict-shardable, we are in dynamic context
         assertEquals(10, res.size());
         // The original target preparer is changed since we split multiple <test> tags.
@@ -140,7 +147,8 @@
         setter.setOptionValue("not-strict-shardable", "true");
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, false, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, false, true);
         // matching 1 for 1, config to ModuleDefinition since not shardable
         assertEquals(1, res.size());
         // The original target preparer is changed since we split multiple <test> tags.
@@ -169,7 +177,8 @@
 
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, false);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, false);
         // matching 1 for 1, config to ModuleDefinition since no intra-module sharding
         assertEquals(1, res.size());
         // The original target preparer is changed since we split multiple <test> tags.
@@ -206,7 +215,8 @@
 
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, true);
         // matching 1 for 1, config to ModuleDefinition since not shardable
         assertEquals(1, res.size());
         // The original target preparer is not there, it has been copied
@@ -239,7 +249,8 @@
 
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, true, true);
         // matching 1 for 1, config to ModuleDefinition since did not shard
         assertEquals(1, res.size());
         // The original target preparer is not there, it has been copied
@@ -271,14 +282,25 @@
         setter.setOptionValue("num-shards", "6");
         config.setTest(test);
 
+        Map<String, List<ITargetPreparer>> suitePreparers =
+                new HashMap<String, List<ITargetPreparer>>();
+        ITargetPreparer preparer1 = new StubTargetPreparer();
+        ITargetPreparer preparer2 = new StubTargetPreparer();
+        List<ITargetPreparer> preparers = Arrays.asList(preparer1, preparer2);
+        suitePreparers.put(DEFAULT_DEVICE, preparers);
+
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, true, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, suitePreparers, 5, true, true);
         // matching 1 for 10 since tests sharding in 5 units times 2.
         assertEquals(10, res.size());
         // The original IRemoteTest does not exists anymore, new IRemoteTests have been created.
         for (ModuleDefinition m : res) {
             assertNotSame(test, m.getTests().get(0));
+            assertEquals(2, m.getSuitePreparerForDevice(DEFAULT_DEVICE).size());
+            assertNotSame(preparer1, m.getSuitePreparerForDevice(DEFAULT_DEVICE).get(0));
+            assertNotSame(preparer1, m.getSuitePreparerForDevice(DEFAULT_DEVICE).get(1));
         }
         assertTrue(config.getTests().isEmpty());
     }
@@ -302,7 +324,8 @@
 
         runConfig.put("module1", config);
         List<ModuleDefinition> res =
-                ModuleSplitter.splitConfiguration(mTestInfo, runConfig, 5, false, true);
+                ModuleSplitter.splitConfiguration(
+                        mTestInfo, runConfig, mSuitePreparersPerDevice, 5, false, true);
         // matching 1 for 6 since tests sharding in 6 tests.
         assertEquals(6, res.size());
         // The original IRemoteTest does not exists anymore, new IRemoteTests have been created.
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index 07e0807..81fa44f 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -20,6 +20,7 @@
 
 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
 import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.ConfigurationDef;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
@@ -87,6 +88,7 @@
     private IDeviceBuildInfo mBuildInfo;
     private ITestDevice mMockDevice;
     private TestInformation mTestInfo;
+    private IConfiguration mStubMainConfiguration;
 
     private static final String TEST_MAINLINE_CONFIG =
         "<configuration description=\"Runs a stub tests part of some suite\">\n"
@@ -115,6 +117,8 @@
         mMainlineRunner = new FakeMainlineTMSR();
         mMainlineRunner.setBuild(mBuildInfo);
         mMainlineRunner.setDevice(mMockDevice);
+        mStubMainConfiguration = new Configuration("stub", "stub");
+        mMainlineRunner.setConfiguration(mStubMainConfiguration);
         mMainlineOptionSetter = new OptionSetter(mMainlineRunner);
 
         IInvocationContext context = new InvocationContext();
diff --git a/tests/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandlerTest.java b/tests/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandlerTest.java
index 11dee18..4a5d707 100644
--- a/tests/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandlerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/params/MainlineModuleHandlerTest.java
@@ -62,7 +62,7 @@
     public void testApplySetup() {
         EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
         EasyMock.replay(mMockBuildInfo);
-        mHandler = new MainlineModuleHandler("mod1.apk", mAbi, mContext);
+        mHandler = new MainlineModuleHandler("mod1.apk", mAbi, mContext, false);
         mHandler.applySetup(mConfig);
         assertTrue(mConfig.getTargetPreparers().get(0) instanceof InstallApexModuleTargetPreparer);
         InstallApexModuleTargetPreparer preparer =
@@ -77,7 +77,7 @@
     public void testApplySetup_MultipleMainlineModules() {
         EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
         EasyMock.replay(mMockBuildInfo);
-        mHandler = new MainlineModuleHandler("mod1.apk+mod2.apex", mAbi, mContext);
+        mHandler = new MainlineModuleHandler("mod1.apk+mod2.apex", mAbi, mContext, false);
         mHandler.applySetup(mConfig);
         assertTrue(mConfig.getTargetPreparers().get(0) instanceof InstallApexModuleTargetPreparer);
         InstallApexModuleTargetPreparer preparer =
@@ -97,7 +97,7 @@
         try {
             EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn(null);
             EasyMock.replay(mMockBuildInfo);
-            mHandler = new MainlineModuleHandler("mod1.apk+mod2.apex", mAbi, mContext);
+            mHandler = new MainlineModuleHandler("mod1.apk+mod2.apex", mAbi, mContext, false);
             fail("Should have thrown an exception.");
         } catch (IllegalArgumentException expected) {
             // expected
diff --git a/tests/src/com/android/tradefed/util/AaptParserTest.java b/tests/src/com/android/tradefed/util/AaptParserTest.java
index 65bf70d..93caecf 100644
--- a/tests/src/com/android/tradefed/util/AaptParserTest.java
+++ b/tests/src/com/android/tradefed/util/AaptParserTest.java
@@ -168,6 +168,34 @@
         assertTrue(p.isRequestingLegacyStorage());
     }
 
+    public void testParseXmlTreeForAapt2_withRequestLegacyFlagTrue() {
+        AaptParser p = new AaptParser();
+        p.parseXmlTree(
+                "N: android=http://schemas.android.com/apk/res/android\n"
+                        + "  E: manifest (line=2)\n"
+                        + "    A: http://schemas.android.com/apk/res/android:versionCode(0x0101021b)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/android:versionName(0x0101021c)=\"R\" (Raw: \"R\")\n"
+                        + "    A: http://schemas.android.com/apk/res/android:compileSdkVersion(0x01010572)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/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: http://schemas.android.com/apk/res/android:minSdkVersion(0x0101020c)=(type 0x10)0x1c\n"
+                        + "      A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=\"R\" (Raw: \"R\")\n"
+                        + "    E: application (line=12)\n"
+                        + "      A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=(type 0x10)0x1e\n"
+                        + "      A: http://schemas.android.com/apk/res/android:supportsRtl(0x010103af)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/android:extractNativeLibs(0x010104ea)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/android:appComponentFactory(0x0101057a)=\"androidx.core.app"
+                        + ".CoreComponentFactory\" (Raw: \"androidx.core.app"
+                        + ".CoreComponentFactory\")\n"
+                        + "      A: http://schemas.android.com/apk/res/android:requestLegacyExternalStorage(0x01010603)=(type 0x12)"
+                        + "0xffffffff\n");
+        assertTrue(p.isRequestingLegacyStorage());
+    }
+
     public void testParseXmlTree_withRequestLegacyFlagFalse() {
         AaptParser p = new AaptParser();
         p.parseXmlTree(
@@ -196,6 +224,34 @@
         assertFalse(p.isRequestingLegacyStorage());
     }
 
+    public void testParseXmlTreeForAapt2_withRequestLegacyFlagFalse() {
+        AaptParser p = new AaptParser();
+        p.parseXmlTree(
+                "N: android=http://schemas.android.com/apk/res/android\n"
+                        + "  E: manifest (line=2)\n"
+                        + "    A: http://schemas.android.com/apk/res/android:versionCode(0x0101021b)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/android:versionName(0x0101021c)=\"R\" (Raw: \"R\")\n"
+                        + "    A: http://schemas.android.com/apk/res/android:compileSdkVersion(0x01010572)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/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: http://schemas.android.com/apk/res/android:minSdkVersion(0x0101020c)=(type 0x10)0x1c\n"
+                        + "      A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=\"R\" (Raw: \"R\")\n"
+                        + "    E: application (line=12)\n"
+                        + "      A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=(type 0x10)0x1e\n"
+                        + "      A: http://schemas.android.com/apk/res/android:supportsRtl(0x010103af)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/android:extractNativeLibs(0x010104ea)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/android:appComponentFactory(0x0101057a)=\"androidx.core.app"
+                        + ".CoreComponentFactory\" (Raw: \"androidx.core.app"
+                        + ".CoreComponentFactory\")\n"
+                        + "      A: http://schemas.android.com/apk/res/android:requestLegacyExternalStorage(0x01010603)=(type 0x12)"
+                        + "0x0\n");
+        assertFalse(p.isRequestingLegacyStorage());
+    }
+
     public void testParseXmlTree_withoutRequestLegacyFlag() {
         AaptParser p = new AaptParser();
         p.parseXmlTree(
@@ -221,7 +277,32 @@
         assertFalse(p.isRequestingLegacyStorage());
     }
 
-    public void testParseXmlTree_withUsesPermissionManageExternalStorage() {
+    public void testParseXmlTreeForAapt2_withoutRequestLegacyFlag() {
+        AaptParser p = new AaptParser();
+        p.parseXmlTree(
+                "N: android=http://schemas.android.com/apk/res/android\n"
+                        + "  E: manifest (line=2)\n"
+                        + "    A: http://schemas.android.com/apk/res/androidandroid:versionCode(0x0101021b)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/androidandroid:versionName(0x0101021c)=\"R\" (Raw: \"R\")\n"
+                        + "    A: http://schemas.android.com/apk/res/androidandroid:compileSdkVersion(0x01010572)=(type 0x10)0x1d\n"
+                        + "    A: http://schemas.android.com/apk/res/androidandroid: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: http://schemas.android.com/apk/res/androidandroid:minSdkVersion(0x0101020c)=(type 0x10)0x1c\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:targetSdkVersion(0x01010270)=\"R\" (Raw: \"R\")\n"
+                        + "    E: application (line=12)\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:targetSdkVersion(0x01010270)=(type 0x10)0x1e\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:supportsRtl(0x010103af)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:extractNativeLibs(0x010104ea)=(type 0x12)0xffffffff\n"
+                        + "      A: http://schemas.android.com/apk/res/androidandroid:appComponentFactory(0x0101057a)=\"androidx.core.app"
+                        + ".CoreComponentFactory\" (Raw: \"androidx.core.app");
+        assertFalse(p.isRequestingLegacyStorage());
+    }
+
+    public void testParse_withUsesPermissionManageExternalStorage() {
         AaptParser p = new AaptParser();
         p.parse(
                 "package: name='com.android.foo' versionCode='217173' versionName='1.7173' "
@@ -234,7 +315,7 @@
         assertTrue(p.isUsingPermissionManageExternalStorage());
     }
 
-    public void testParseXmlTree_withoutUsesPermissionManageExternalStorage() {
+    public void testParse_withoutUsesPermissionManageExternalStorage() {
         AaptParser p = new AaptParser();
         p.parse(
                 "package: name='com.android.foo' versionCode='217173' versionName='1.7173' "
diff --git a/tests/src/com/android/tradefed/util/ProtoUtilTest.java b/tests/src/com/android/tradefed/util/ProtoUtilTest.java
new file mode 100644
index 0000000..2e42eee
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/ProtoUtilTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import com.android.tradefed.util.test.ProtoUtilTestProto.TestMessage;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Unit tests for {@link ProtoUtil} */
+@RunWith(Parameterized.class)
+public class ProtoUtilTest {
+    @Parameter(0)
+    public String mTestName; // Unused, for identifying tests only.
+
+    @Parameter(1)
+    public TestMessage mMessage;
+
+    @Parameter(2)
+    public List<String> mReferences;
+
+    @Parameter(3)
+    public List<String> mExpectedResults;
+
+    @Parameters(name = "{0}#{index}")
+    public static Iterable<Object[]> data() {
+        List<Object[]> parameters = new ArrayList<>();
+        parameters.add(
+                new Object[] {
+                    "returnsMessageAsStringForEmptyReference",
+                    TestMessage.newBuilder().setIntField(7).build(),
+                    new ArrayList<String>(),
+                    Arrays.asList(TestMessage.newBuilder().setIntField(7).build().toString())
+                });
+        parameters.add(
+                new Object[] {
+                    "singleLevel",
+                    TestMessage.newBuilder().setIntField(7).build(),
+                    Arrays.asList("int_field"),
+                    Arrays.asList("7")
+                });
+        parameters.add(
+                new Object[] {
+                    "singleLevel",
+                    TestMessage.newBuilder().setStringField("string").build(),
+                    Arrays.asList("string_field"),
+                    Arrays.asList("string")
+                });
+        parameters.add(
+                new Object[] {
+                    "singleLevel",
+                    TestMessage.newBuilder()
+                            .setMessageField(TestMessage.SubMessage.newBuilder().setIntField(7))
+                            .build(),
+                    Arrays.asList("message_field"),
+                    Arrays.asList(TestMessage.SubMessage.newBuilder().setIntField(7).toString())
+                });
+        parameters.add(
+                new Object[] {
+                    "singleLevelRepeated",
+                    TestMessage.newBuilder()
+                            .addAllRepeatedStringField(Arrays.asList("string1", "string2"))
+                            .build(),
+                    Arrays.asList("repeated_string_field"),
+                    Arrays.asList("string1", "string2")
+                });
+        parameters.add(
+                new Object[] {
+                    "multiLevel",
+                    TestMessage.newBuilder()
+                            .setMessageField(TestMessage.SubMessage.newBuilder().setIntField(7))
+                            .build(),
+                    Arrays.asList("message_field", "int_field"),
+                    Arrays.asList("7")
+                });
+        parameters.add(
+                new Object[] {
+                    "multiLevelRepeated",
+                    TestMessage.newBuilder()
+                            .setMessageField(
+                                    TestMessage.SubMessage.newBuilder()
+                                            .addAllRepeatedStringField(
+                                                    Arrays.asList("string1", "string2")))
+                            .build(),
+                    Arrays.asList("message_field", "repeated_string_field"),
+                    Arrays.asList("string1", "string2")
+                });
+        parameters.add(
+                new Object[] {
+                    "multiLevelRepeated",
+                    TestMessage.newBuilder()
+                            .addAllRepeatedMessageField(
+                                    Arrays.asList(
+                                            TestMessage.SubMessage.newBuilder()
+                                                    .addAllRepeatedStringField(
+                                                            Arrays.asList("string1", "string2"))
+                                                    .build(),
+                                            TestMessage.SubMessage.newBuilder()
+                                                    .addAllRepeatedStringField(
+                                                            Arrays.asList("string3", "string4"))
+                                                    .build()))
+                            .build(),
+                    Arrays.asList("repeated_message_field", "repeated_string_field"),
+                    Arrays.asList("string1", "string2", "string3", "string4")
+                });
+        parameters.add(
+                new Object[] {
+                    "oneofSingleLevel",
+                    TestMessage.newBuilder().setOneofStringField("string").build(),
+                    Arrays.asList("oneof_string_field"),
+                    Arrays.asList("string")
+                });
+        parameters.add(
+                new Object[] {
+                    "oneofMultiLevel",
+                    TestMessage.newBuilder()
+                            .setOneofMessageField(
+                                    TestMessage.SubMessage.newBuilder()
+                                            .addAllRepeatedStringField(
+                                                    Arrays.asList("string1", "string2")))
+                            .build(),
+                    Arrays.asList("oneof_message_field", "repeated_string_field"),
+                    Arrays.asList("string1", "string2")
+                });
+        parameters.add(
+                new Object[] {
+                    "returnsEmptyForNonExistentFieldReference",
+                    TestMessage.newBuilder().setStringField("string").build(),
+                    Arrays.asList("not_a_field"),
+                    new ArrayList<String>()
+                });
+        parameters.add(
+                new Object[] {
+                    "returnsEmptyForNonExistentFieldReference",
+                    TestMessage.newBuilder()
+                            .setMessageField(TestMessage.SubMessage.newBuilder().setIntField(7))
+                            .build(),
+                    Arrays.asList("message_field", "not_a_field"),
+                    new ArrayList<String>()
+                });
+        parameters.add(
+                new Object[] {
+                    "returnsEmptyForNonExistentFieldReference",
+                    TestMessage.newBuilder()
+                            .setMessageField(TestMessage.SubMessage.newBuilder().setIntField(7))
+                            .build(),
+                    Arrays.asList("message_field", "int_field", "not_a_field"),
+                    new ArrayList<String>()
+                });
+        return parameters;
+    }
+
+    @Test
+    public void testParsing() {
+        assertArrayEquals(
+                mExpectedResults.toArray(),
+                ProtoUtil.getNestedFieldFromMessageAsStrings(mMessage, mReferences).toArray());
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/PythonVirtualenvHelperTest.java b/tests/src/com/android/tradefed/util/PythonVirtualenvHelperTest.java
new file mode 100644
index 0000000..aeb1c24
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/PythonVirtualenvHelperTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.util;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.*;
+
+import com.google.common.base.Throwables;
+
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.file.Paths;
+
+public class PythonVirtualenvHelperTest {
+
+    private File mVenvDir;
+
+    @After
+    public void tearDown() throws Exception {
+        FileUtil.recursiveDelete(mVenvDir);
+    }
+
+    @Test
+    public void testActivate_shouldThrowNPE_whenVirtualenvPathIsNull() throws Exception {
+        String nullVirtualenvPath = null;
+        IRunUtil runUtil = mock(RunUtil.class);
+
+        try {
+            PythonVirtualenvHelper.activate(runUtil, nullVirtualenvPath);
+            fail("Should have thrown an exception");
+        } catch (NullPointerException e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("Path to the Python virtual environment should not be null"));
+        }
+    }
+
+    @Test
+    public void testActivate_whenVirtualenvPathIsInvalid() throws Exception {
+        mVenvDir = FileUtil.createTempDir("venv");
+        mVenvDir.delete();
+        IRunUtil runUtil = mock(RunUtil.class);
+
+        try {
+            PythonVirtualenvHelper.activate(runUtil, mVenvDir.getAbsolutePath());
+            fail("Should have thrown an exception");
+        } catch (RuntimeException e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("Invalid python virtualenv path"));
+        }
+    }
+
+    @Test
+    public void testActivate_whenPythonBinNotFound() throws Exception {
+        mVenvDir = FileUtil.createTempDir("venv");
+        IRunUtil runUtil = mock(RunUtil.class);
+
+        try {
+            PythonVirtualenvHelper.activate(runUtil, mVenvDir.getAbsolutePath());
+            fail("Should have thrown an exception");
+        } catch (RuntimeException e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("Invalid python virtualenv path"));
+        }
+    }
+
+    @Test
+    public void testActivate_success() throws Exception {
+        mVenvDir = FileUtil.createTempDir("venv");
+        File pythonBin = new File(mVenvDir, "bin");
+        pythonBin.mkdir();
+        IRunUtil runUtil = mock(RunUtil.class);
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout(
+                "Name: pip\nLocation: "
+                        + Paths.get(mVenvDir.getAbsolutePath(), "lib/python3.8/site-packages"));
+        when(runUtil.runTimedCmd(anyLong(), anyString(), eq("show"), eq("pip"))).thenReturn(result);
+
+        PythonVirtualenvHelper.activate(runUtil, mVenvDir.getAbsolutePath());
+
+        verify(runUtil)
+                .setEnvVariable("PATH", pythonBin.getAbsolutePath() + ":" + System.getenv("PATH"));
+        verify(runUtil).setEnvVariable("VIRTUAL_ENV", mVenvDir.getAbsolutePath());
+        verify(runUtil)
+                .setEnvVariable(
+                        "PYTHONPATH",
+                        new File(mVenvDir, "lib/python3.8/site-packages").getAbsolutePath()
+                                + ":"
+                                + System.getenv("PYTHONPATH"));
+        verify(runUtil).unsetEnvVariable("PYTHONHOME");
+    }
+
+    @Test
+    public void testActivate_pipShowFails() throws Exception {
+        mVenvDir = FileUtil.createTempDir("venv");
+        File pythonBin = new File(mVenvDir, "bin");
+        pythonBin.mkdir();
+        IRunUtil runUtil = mock(RunUtil.class);
+        when(runUtil.runTimedCmd(anyLong(), anyString(), eq("show"), eq("pip")))
+                .thenReturn(new CommandResult());
+
+        try {
+            PythonVirtualenvHelper.activate(runUtil, mVenvDir.getAbsolutePath());
+            fail("Should have thrown an exception");
+        } catch (RuntimeException e) {
+            assertThat(
+                    String.format(
+                            "An unexpected exception was thrown, full stack trace: %s",
+                            Throwables.getStackTraceAsString(e)),
+                    e.getMessage(),
+                    containsString("pip3 show pip"));
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/RemoteZipTest.java b/tests/src/com/android/tradefed/util/RemoteZipTest.java
index 0176a07..1d6056a 100644
--- a/tests/src/com/android/tradefed/util/RemoteZipTest.java
+++ b/tests/src/com/android/tradefed/util/RemoteZipTest.java
@@ -103,7 +103,7 @@
 
             List<CentralDirectoryInfo> entries = remoteZip.getZipEntries();
 
-            assertEquals(7, entries.size());
+            assertEquals(8, entries.size());
             assertTrue(mExpectedEntries.containsAll(entries));
         } finally {
             FileUtil.recursiveDelete(destDir);
@@ -122,7 +122,7 @@
             destDir = FileUtil.createTempDir("test");
             RemoteZip remoteZip = new RemoteZip(REMOTE_FILE, mZipFileSize, mDownloader, true);
             List<CentralDirectoryInfo> entries = remoteZip.getZipEntries();
-            assertEquals(7, entries.size());
+            assertEquals(8, entries.size());
             assertTrue(mExpectedEntries.containsAll(entries));
         } finally {
             FileUtil.recursiveDelete(destDir);
@@ -151,7 +151,7 @@
             targetFile = Paths.get(destDir.getPath(), "executable", "executable_file").toFile();
             assertTrue(targetFile.exists());
             // File not in the list is not unzipped.
-            targetFile = Paths.get(destDir.getPath(), "empty_file").toFile();
+            targetFile = Paths.get(destDir.getPath(), "empty/empty_file").toFile();
             assertFalse(targetFile.exists());
         } finally {
             FileUtil.recursiveDelete(destDir);
diff --git a/tests/src/com/android/tradefed/util/SparseImageUtilTest.java b/tests/src/com/android/tradefed/util/SparseImageUtilTest.java
new file mode 100644
index 0000000..d8858e7
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/SparseImageUtilTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.util;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/** Unit tests for {@link SparseImageUtil} */
+@RunWith(JUnit4.class)
+public class SparseImageUtilTest {
+    private File mSparseImageFile;
+
+    @Before
+    public void setUp() throws IOException {
+        mSparseImageFile = FileUtil.createTempFile("sparse", ".img");
+        try (FileOutputStream out = new FileOutputStream(mSparseImageFile)) {
+            out.write(getSparseImageData());
+        }
+    }
+
+    @After
+    public void tearDown() {
+        FileUtil.deleteFile(mSparseImageFile);
+    }
+
+    /** Verify {@link com.android.tradefed.util.SparseImageUtil#isSparse}. */
+    @Test
+    public void testIsSparse() {
+        Assert.assertTrue(SparseImageUtil.isSparse(mSparseImageFile));
+    }
+
+    /** Verify {@link com.android.tradefed.util.SparseImageUtil#unsparse}. */
+    @Test
+    public void testUnsparse() throws IOException {
+        File unsparsedFile = FileUtil.createTempFile("unsparse", ".img");
+        byte[] unsparsedData = null;
+        try {
+            SparseImageUtil.unsparse(mSparseImageFile, unsparsedFile);
+            try (FileInputStream in = new FileInputStream(unsparsedFile)) {
+                unsparsedData = StreamUtil.getByteArrayListFromStream(in).getContents();
+            }
+            Assert.assertArrayEquals(getUnsparsedImageData(), unsparsedData);
+        } finally {
+            FileUtil.deleteFile(unsparsedFile);
+        }
+    }
+
+    /**
+     * Returns some sparse data.
+     *
+     * @see https://android.googlesource.com/platform/system/core/+/master/libsparse/sparse_format.h
+     */
+    private byte[] getSparseImageData() {
+        final int SPARSE_IMAGE_MAGIC = 0xED26FF3A;
+        ByteBuffer buffer = ByteBuffer.allocate(4096);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        // Header
+        buffer.putInt(SPARSE_IMAGE_MAGIC);
+        buffer.putShort((short) 1);
+        buffer.putShort((short) 0);
+        buffer.putShort((short) 28);
+        buffer.putShort((short) 12);
+        buffer.putInt(4); /* block size */
+        buffer.putInt(512 + 256); /* total blocks */
+        buffer.putInt(2); /* total chunks */
+        buffer.putInt(0); /* ignore check sum */
+        // RAW chunk, 2048 bytes of lorem ipsum
+        byte[] loremIpsum = getLoremIpsum();
+        buffer.putShort((short) 0xCAC1);
+        buffer.putShort((short) 0); /* padding */
+        buffer.putInt(loremIpsum.length / 4); /* data size in terms of number of blocks */
+        buffer.putInt(12 + loremIpsum.length); /* header size + data size */
+        buffer.put(loremIpsum);
+        // DONTCARE chunk, 1024 bytes of zeroes
+        byte[] zeroes = new byte[1024];
+        buffer.putShort((short) 0xCAC3);
+        buffer.putShort((short) 0); /* padding */
+        buffer.putInt(zeroes.length / 4); /* data size in terms of number of blocks */
+        buffer.putInt(12 + zeroes.length); /* header size + data size */
+        buffer.put(zeroes);
+        return Arrays.copyOf(buffer.array(), buffer.position());
+    }
+
+    private byte[] getUnsparsedImageData() {
+        byte[] loremIpsum = getLoremIpsum();
+        // Pad lorem ipsum with 1024 bytes of zeroes
+        return Arrays.copyOf(loremIpsum, loremIpsum.length + 1024);
+    }
+
+    /** Returns a chunk of text data. */
+    private byte[] getLoremIpsum() {
+        final int dataLen = 2048; /* Must be a multiple of 4 */
+        final String loremIpsumString =
+                "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
+                        + " incididunt ut labore et dolore magna aliqua. Enim neque volutpat ac"
+                        + " tincidunt vitae semper quis lectus. Est pellentesque elit ullamcorper"
+                        + " dignissim cras tincidunt lobortis feugiat vivamus. Vitae ultricies leo"
+                        + " integer malesuada nunc vel. Ultrices tincidunt arcu non sodales neque"
+                        + " sodales ut etiam sit. Arcu cursus vitae congue mauris rhoncus aenean."
+                        + " Consectetur a erat nam at lectus urna duis convallis convallis. Suscipit"
+                        + " tellus mauris a diam maecenas sed. At elementum eu facilisis sed odio."
+                        + " Neque sodales ut etiam sit.";
+        final byte[] loremIpsumBytes = loremIpsumString.getBytes();
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        while (buffer.size() < dataLen) {
+            buffer.write(loremIpsumBytes, 0, loremIpsumBytes.length);
+        }
+        return Arrays.copyOf(buffer.toByteArray(), dataLen);
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java b/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java
index 7a81c5e..f166b6a 100644
--- a/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java
+++ b/tests/src/com/android/tradefed/util/StringEscapeUtilsTest.java
@@ -88,4 +88,30 @@
         assertArrayEquals(new String[]{"foo", "bar bar"},
                 StringEscapeUtils.paramsToArgs(expected).toArray());
     }
+
+    /**
+     * Simple test that {@link StringEscapeUtils#escapeShell(String)} escapes the is greater than
+     * sign.
+     */
+    @Test
+    public void testEscapesGreaterSigns() {
+        String escaped_str = StringEscapeUtils.escapeShell(">greater>signs");
+        assertEquals("\\>greater\\>signs", escaped_str);
+    }
+
+    /**
+     * Simple test that {@link StringEscapeUtils#escapeShell(String)} escapes the is less than sign.
+     */
+    @Test
+    public void testEscapesLessSigns() {
+        String escaped_str = StringEscapeUtils.escapeShell("<less<signs");
+        assertEquals("\\<less\\<signs", escaped_str);
+    }
+
+    /** Simple test that {@link StringEscapeUtils#escapeShell(String)} escapes the or sign. */
+    @Test
+    public void testEscapesOrSigns() {
+        String escaped_str = StringEscapeUtils.escapeShell("|or|signs");
+        assertEquals("\\|or\\|signs", escaped_str);
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/ZipUtilTest.java b/tests/src/com/android/tradefed/util/ZipUtilTest.java
index 33841a0..cabf7fa 100644
--- a/tests/src/com/android/tradefed/util/ZipUtilTest.java
+++ b/tests/src/com/android/tradefed/util/ZipUtilTest.java
@@ -216,8 +216,8 @@
                             partialZipFile,
                             endCentralDirInfo,
                             endCentralDirInfo.getCentralDirOffset());
-            // The zip file has 3 folders, 4 files.
-            assertEquals(7, zipEntries.size());
+            // The zip file has 4 folders, 4 files.
+            assertEquals(8, zipEntries.size());
 
             CentralDirectoryInfo zipEntry;
             LocalFileHeader localFileHeader;
@@ -228,7 +228,7 @@
             zipEntry =
                     zipEntries
                             .stream()
-                            .filter(e -> e.getFileName().equals("empty_file"))
+                            .filter(e -> e.getFileName().equals("empty/empty_file"))
                             .findFirst()
                             .get();
             targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
@@ -243,6 +243,7 @@
             // Verify file permissions - readonly - 644 rw-r--r--
             permissions = Files.getPosixFilePermissions(targetFile.toPath());
             assertEquals(PosixFilePermissions.fromString("rw-r--r--"), permissions);
+            assertTrue(targetFile.isFile());
 
             // Unzip text file
             zipEntry =
@@ -377,8 +378,8 @@
                             endCentralDirInfo,
                             endCentralDirInfo.getCentralDirOffset(),
                             true);
-            // The zip file has 3 folders, 4 files.
-            assertEquals(7, zipEntries.size());
+            // The zip file has 4 folders, 4 files.
+            assertEquals(8, zipEntries.size());
 
             CentralDirectoryInfo zipEntry;
             LocalFileHeader localFileHeader;
@@ -389,7 +390,7 @@
             zipEntry =
                     zipEntries
                             .stream()
-                            .filter(e -> e.getFileName().equals("empty_file"))
+                            .filter(e -> e.getFileName().equals("empty/empty_file"))
                             .findFirst()
                             .get();
             targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
diff --git a/tests/src/com/android/tradefed/util/executor/ParallelDeviceExecutorTest.java b/tests/src/com/android/tradefed/util/executor/ParallelDeviceExecutorTest.java
index db019b6..b3ba079 100644
--- a/tests/src/com/android/tradefed/util/executor/ParallelDeviceExecutorTest.java
+++ b/tests/src/com/android/tradefed/util/executor/ParallelDeviceExecutorTest.java
@@ -31,6 +31,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.TimeUnit;
 
 /** Unit tests for {@link ParallelDeviceExecutor}. */
@@ -93,4 +94,23 @@
         assertTrue(mExecutor.getErrors().get(0).getMessage().contains("one"));
         assertTrue(mExecutor.getErrors().get(1).getMessage().contains("two"));
     }
+
+    @Test
+    public void testExecution_timeout() {
+        List<Callable<Boolean>> callableTasks = new ArrayList<>();
+        for (ITestDevice device : mDevices) {
+            callableTasks.add(
+                    () -> {
+                        Thread.sleep(1000L);
+                        return true;
+                    });
+        }
+
+        List<Boolean> results = mExecutor.invokeAll(callableTasks, 1L, TimeUnit.MILLISECONDS);
+        assertEquals(0, results.size());
+        assertTrue(mExecutor.hasErrors());
+        assertEquals(2, mExecutor.getErrors().size());
+        assertTrue(mExecutor.getErrors().get(0) instanceof CancellationException);
+        assertTrue(mExecutor.getErrors().get(1) instanceof CancellationException);
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java b/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java
index dd43440..f45238a 100644
--- a/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java
+++ b/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java
@@ -176,12 +176,14 @@
                         any(CollectingByteOutputReceiver.class));
         List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
         // Resulting list should have two metrics.
-        assertThat(data.size()).comparesEqualTo(2);
+        assertThat(data.size()).isEquivalentAccordingToCompareTo(2);
         // The first metric should correspond to METRIC_2_* as its timestamp is earlier.
-        assertThat(data.get(0).getElapsedTimestampNanos()).comparesEqualTo(METRIC_2_NANOS);
+        assertThat(data.get(0).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_2_NANOS);
         assertThat(data.get(0).getAtom().hasBleScanResultReceived()).isTrue();
         // The second metric should correspond to METRIC_1_*.
-        assertThat(data.get(1).getElapsedTimestampNanos()).comparesEqualTo(METRIC_1_NANOS);
+        assertThat(data.get(1).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_1_NANOS);
         assertThat(data.get(1).getAtom().hasBleScanStateChanged()).isTrue();
     }
 
@@ -204,7 +206,7 @@
                         any(CollectingByteOutputReceiver.class));
         List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
         // Resulting list should be empty.
-        assertThat(data.size()).comparesEqualTo(0);
+        assertThat(data.size()).isEquivalentAccordingToCompareTo(0);
     }
 
     /**
@@ -230,12 +232,14 @@
                         any(CollectingByteOutputReceiver.class));
         List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
         // Resulting list should have two metrics.
-        assertThat(data.size()).comparesEqualTo(2);
+        assertThat(data.size()).isEquivalentAccordingToCompareTo(2);
         // The first metric should correspond to METRIC_2_* as its timestamp is earlier.
-        assertThat(data.get(0).getElapsedTimestampNanos()).comparesEqualTo(METRIC_2_NANOS);
+        assertThat(data.get(0).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_2_NANOS);
         assertThat(data.get(0).getAtom().hasBleScanResultReceived()).isTrue();
         // The second metric should correspond to METRIC_1_*.
-        assertThat(data.get(1).getElapsedTimestampNanos()).comparesEqualTo(METRIC_1_NANOS);
+        assertThat(data.get(1).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_1_NANOS);
         assertThat(data.get(1).getAtom().hasBleScanStateChanged()).isTrue();
     }
 
@@ -261,12 +265,14 @@
                         any(CollectingByteOutputReceiver.class));
         List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
         // Resulting list should have two metrics.
-        assertThat(data.size()).comparesEqualTo(2);
+        assertThat(data.size()).isEquivalentAccordingToCompareTo(2);
         // The first metric should correspond to METRIC_1_* as its timestamp is earlier.
-        assertThat(data.get(0).getElapsedTimestampNanos()).comparesEqualTo(METRIC_1_NANOS);
+        assertThat(data.get(0).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_1_NANOS);
         assertThat(data.get(0).getAtom().hasBleScanStateChanged()).isTrue();
         // The second metric should correspond to METRIC_3_*.
-        assertThat(data.get(1).getElapsedTimestampNanos()).comparesEqualTo(METRIC_3_NANOS);
+        assertThat(data.get(1).getElapsedTimestampNanos())
+                .isEquivalentAccordingToCompareTo(METRIC_3_NANOS);
         assertThat(data.get(1).getAtom().hasBleScanStateChanged()).isTrue();
     }
 
diff --git a/util-apps/ContentProvider/main/AndroidManifest.xml b/util-apps/ContentProvider/main/AndroidManifest.xml
index ac37e68..15cb3fa 100644
--- a/util-apps/ContentProvider/main/AndroidManifest.xml
+++ b/util-apps/ContentProvider/main/AndroidManifest.xml
@@ -19,6 +19,7 @@
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
 
     <application>
         <provider