Snap for 9679998 from 75c1d6970331a21bb5780b5abfc5c2443393ed80 to sdk-release

Change-Id: I90baae1dad065d66fcf8b285dab075f9acbabfec
diff --git a/Android.bp b/Android.bp
index face49d..cbe5c43 100644
--- a/Android.bp
+++ b/Android.bp
@@ -34,6 +34,7 @@
         include_dirs: ["external/protobuf/src"],
         type: "full",
     },
+    // b/267831518: Pin tradefed and dependencies to Java 11.
     java_version: "11",
 }
 
@@ -52,6 +53,7 @@
         "guava",
         "javax-annotation-api-prebuilt-host-jar",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
     java_version: "11",
 }
 
@@ -75,6 +77,7 @@
         "guava",
         "javax-annotation-api-prebuilt-host-jar",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
     java_version: "11",
 }
 
@@ -93,6 +96,7 @@
         "guava",
         "javax-annotation-api-prebuilt-host-jar",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
     java_version: "11",
 }
 
@@ -111,6 +115,7 @@
         "guava",
         "javax-annotation-api-prebuilt-host-jar",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
     java_version: "11",
 }
 
@@ -124,13 +129,15 @@
     java_resources: [
         ":TradefedContentProvider",
         ":TelephonyUtility",
-        ":WifiUtil"
+        ":WifiUtil",
+        ":test-services.apk",
     ],
     static_libs: [
         "tradefed-lib-core",
         "tradefed-test-framework",
     ],
     manifest: "MANIFEST.mf",
+    // b/267831518: Pin tradefed and dependencies to Java 11.
     java_version: "11",
 }
 
@@ -148,6 +155,7 @@
         "tradefed-test-framework",
     ],
     manifest: "MANIFEST.mf",
+    // b/267831518: Pin tradefed and dependencies to Java 11.
     java_version: "11",
 }
 
@@ -202,6 +210,7 @@
     libs: [
         "loganalysis",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
     java_version: "11",
 }
 
diff --git a/aoa_helper/Android.bp b/aoa_helper/Android.bp
index 5493761..73cacf3 100644
--- a/aoa_helper/Android.bp
+++ b/aoa_helper/Android.bp
@@ -29,12 +29,16 @@
     static_libs: [
         "jna-prebuilt",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
 }
 
 java_test_host {
     name: "aoa-helper-test",
     visibility: ["//visibility:private"],
     srcs: ["javatests/**/*.java"],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     test_options: {
         unit_test: true,
     },
diff --git a/clearcut_client/Android.bp b/clearcut_client/Android.bp
index 6e37229..34bba40 100644
--- a/clearcut_client/Android.bp
+++ b/clearcut_client/Android.bp
@@ -34,4 +34,6 @@
         "tradefed-common-util",
         "devtools-annotations-prebuilt",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
 }
diff --git a/common_util/Android.bp b/common_util/Android.bp
index 3a60e1f..f4abe34 100644
--- a/common_util/Android.bp
+++ b/common_util/Android.bp
@@ -27,6 +27,7 @@
         "//tools/tradefederation/core/test_result_interfaces",
     ],
     defaults: ["tradefed_defaults"],
+    java_version: "11",
     srcs: [
         "com/**/*.java",
     ],
diff --git a/common_util/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/common_util/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
index 63cb72d..beb9c9d 100644
--- a/common_util/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
+++ b/common_util/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -45,8 +45,10 @@
         AUTO_RETRY_TIME("auto_retry_time_ms", true),
         BACKFILL_BUILD_INFO("backfill_build_info", false),
         STAGE_TESTS_TIME("stage_tests_time_ms", true),
+        STAGE_REMOTE_TIME("stage_remote_time_ms", true),
         STAGE_TESTS_BYTES("stage_tests_bytes", true),
         STAGE_TESTS_INDIVIDUAL_DOWNLOADS("stage_tests_individual_downloads", true),
+        STAGE_UNDEFINED_DEPENDENCY("stage_undefined_dependency", true),
         SERVER_REFERENCE("server_reference", false),
         INSTRUMENTATION_RERUN_FROM_FILE("instrumentation_rerun_from_file", true),
         INSTRUMENTATION_RERUN_SERIAL("instrumentation_rerun_serial", true),
@@ -165,6 +167,7 @@
         CF_INSTANCE_COUNT("cf_instance_count", false),
         CF_OXYGEN_SERVER_URL("cf_oxygen_server_url", false),
         CF_OXYGEN_SESSION_ID("cf_oxygen_session_id", false),
+        CF_OXYGEN_VERSION("cf_oxygen_version", false),
         CRASH_FAILURES("crash_failures", true),
         UNCAUGHT_CRASH_FAILURES("uncaught_crash_failures", true),
         TEST_CRASH_FAILURES("test_crash_failures", true),
@@ -218,6 +221,7 @@
 
         MODULE_SETUP_PAIR("tf_module_setup_pair_timestamp", true),
         MODULE_TEARDOWN_PAIR("tf_module_teardown_pair_timestamp", true),
+        STATUS_CHECKER_PAIR("status_checker_pair", true),
 
         LAB_PREPARER_NOT_ILAB("lab_preparer_not_ilab", true),
         TARGET_PREPARER_IS_ILAB("target_preparer_is_ilab", true),
@@ -246,9 +250,14 @@
         // Ab downloader metrics
         AB_DOWNLOAD_SIZE_ELAPSED_TIME("ab_download_size_elapsed_time", true),
 
+        DUPLICATE_MAPPING_DIFFERENT_OPTIONS("duplicate_mapping_different_options", true),
+
         HAS_ANY_RUN_FAILURES("has_any_run_failures", false),
         TOTAL_TEST_COUNT("total_test_count", true),
 
+        // Metrics to store Device failure signatures
+        DEVICE_ERROR_SIGNATURES("device_failure_signatures", true),
+
         // Following are trace events also reporting as metrics
         invocation_warm_up("invocation_warm_up", true),
         dynamic_download("dynamic_download", true),
@@ -267,6 +276,7 @@
         test_cleanup("test_cleanup", true),
         log_and_release_device("log_and_release_device", true),
         invocation_events_processing("invocation_events_processing", true),
+        stage_suite_test_artifacts("stage_suite_test_artifacts", true),
         ;
 
         private final String mKeyName;
diff --git a/common_util/com/android/tradefed/invoker/tracing/ActiveTrace.java b/common_util/com/android/tradefed/invoker/tracing/ActiveTrace.java
index 2a495a2..9f52cfc 100644
--- a/common_util/com/android/tradefed/invoker/tracing/ActiveTrace.java
+++ b/common_util/com/android/tradefed/invoker/tracing/ActiveTrace.java
@@ -105,6 +105,13 @@
     }
 
     /**
+     * thread id of the thread that initiated the tracing.
+     */
+    public long reportingThreadId() {
+        return tid;
+    }
+
+    /**
      * Very basic event reporting to do START / END of traces.
      *
      * @param categories Category associated with event
diff --git a/common_util/com/android/tradefed/invoker/tracing/CloseableTraceScope.java b/common_util/com/android/tradefed/invoker/tracing/CloseableTraceScope.java
index d9952f4..ca7d638 100644
--- a/common_util/com/android/tradefed/invoker/tracing/CloseableTraceScope.java
+++ b/common_util/com/android/tradefed/invoker/tracing/CloseableTraceScope.java
@@ -74,7 +74,7 @@
                 category, name, threadId, threadName, TrackEvent.Type.TYPE_SLICE_END);
         Optional<InvocationMetricKey> optionalKey =
                 Enums.getIfPresent(InvocationMetricKey.class, name);
-        if (optionalKey.isPresent()) {
+        if (optionalKey.isPresent() && Thread.currentThread().getId() == trace.reportingThreadId()) {
             InvocationMetricLogger.addInvocationPairMetrics(
                     optionalKey.get(), startTime, System.currentTimeMillis());
         }
diff --git a/common_util/com/android/tradefed/result/LogDataType.java b/common_util/com/android/tradefed/result/LogDataType.java
index d75d38e..2d78dc4 100644
--- a/common_util/com/android/tradefed/result/LogDataType.java
+++ b/common_util/com/android/tradefed/result/LogDataType.java
@@ -15,11 +15,8 @@
  */
 package com.android.tradefed.result;
 
-/**
- * Represents the data type of log data.
- */
+/** Represents the data type of log data. */
 public enum LogDataType {
-
     TEXT("txt", "text/plain", false, true),
     UIX("uix", "text/xml", false, true),
     XML("xml", "text/xml", false, true),
@@ -74,7 +71,7 @@
     CPU_INFO("txt", "text/plain", false, true), // dumpsys cpuinfo
     JACOCO_CSV("csv", "text/csv", false, true), // JaCoCo coverage report in CSV format
     JACOCO_XML("xml", "text/xml", false, true), // JaCoCo coverage report in XML format
-    JACOCO_EXEC("exec", "application/octet-stream", false, false), //JaCoCo coverage execution file
+    JACOCO_EXEC("exec", "application/octet-stream", false, false), // JaCoCo coverage execution file
     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),
@@ -92,6 +89,7 @@
             true), // ScreenshotTest proto result
     CUTTLEFISH_LOG("txt", "text/plain", true, true), // Log from cuttlefish instance
     TOMBSTONEZ("zip", "application/zip", true, false),
+    BT_SNOOP_LOG("log", "application/octet-stream", false, false), // Bluetooth HCI snoop logs
     /* Unknown file type */
     UNKNOWN("dat", "text/plain", false, false);
 
diff --git a/common_util/com/android/tradefed/result/error/TestErrorIdentifier.java b/common_util/com/android/tradefed/result/error/TestErrorIdentifier.java
index e293cac..0a9d417 100644
--- a/common_util/com/android/tradefed/result/error/TestErrorIdentifier.java
+++ b/common_util/com/android/tradefed/result/error/TestErrorIdentifier.java
@@ -33,7 +33,9 @@
     UNEXPECTED_MOBLY_CONFIG(530_010, FailureStatus.CUSTOMER_ISSUE),
     UNEXPECTED_MOBLY_BEHAVIOR(530_011, FailureStatus.CUSTOMER_ISSUE),
     HOST_COMMAND_FAILED(530_012, FailureStatus.CUSTOMER_ISSUE),
-    TEST_PHASE_TIMED_OUT(530_013, FailureStatus.TIMED_OUT);
+    TEST_PHASE_TIMED_OUT(530_013, FailureStatus.TIMED_OUT),
+    TEST_FILTER_NEEDS_UPDATE(530_014, FailureStatus.SYSTEM_UNDER_TEST_CRASHED),
+    TEST_TIMEOUT(530_015, FailureStatus.TIMED_OUT);
 
     private final long code;
     private final @Nonnull FailureStatus status;
diff --git a/device_build_interfaces/Android.bp b/device_build_interfaces/Android.bp
index 3ebcf09..656b3bf 100644
--- a/device_build_interfaces/Android.bp
+++ b/device_build_interfaces/Android.bp
@@ -37,4 +37,6 @@
         "tf-remote-client",
         "tradefed-result-interfaces",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
 }
diff --git a/device_build_interfaces/com/android/tradefed/device/INativeDevice.java b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
index dded229..a14fe9c 100644
--- a/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
@@ -476,15 +476,32 @@
             throws DeviceNotAvailableException;
 
     /**
-     * Helper method which executes a fastboot command as a system command with a default timeout
-     * of 2 minutes.
-     * <p/>
-     * Expected to be used when device is already in fastboot mode.
+     * Helper method which executes a adb command as a system command with a specified timeout.
+     *
+     * <p>{@link #executeShellCommand(String)} should be used instead wherever possible, as that
+     * method provides better failure detection and performance.
+     *
+     * @param timeout the time in milliseconds before the device is considered unresponsive, 0L for
+     *     no timeout
+     * @param envMap environment to set for the command
+     * @param commandArgs the adb command and arguments to run
+     * @return the stdout from command. <code>null</code> if command failed to execute.
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *     recovered.
+     */
+    public String executeAdbCommand(long timeout, Map<String, String> envMap, String... commandArgs)
+            throws DeviceNotAvailableException;
+
+    /**
+     * Helper method which executes a fastboot command as a system command with a default timeout of
+     * 2 minutes.
+     *
+     * <p>Expected to be used when device is already in fastboot mode.
      *
      * @param commandArgs the fastboot command and arguments to run
      * @return the CommandResult containing output of command
      * @throws DeviceNotAvailableException if connection with device is lost and cannot be
-     * recovered.
+     *     recovered.
      */
     public CommandResult executeFastbootCommand(String... commandArgs)
             throws DeviceNotAvailableException;
diff --git a/device_build_interfaces/com/android/tradefed/device/UserInfo.java b/device_build_interfaces/com/android/tradefed/device/UserInfo.java
index 853f1a0..cac498c 100644
--- a/device_build_interfaces/com/android/tradefed/device/UserInfo.java
+++ b/device_build_interfaces/com/android/tradefed/device/UserInfo.java
@@ -30,6 +30,7 @@
     public static final int FLAG_EPHEMERAL = 0x00000100;
     public static final int FLAG_MANAGED_PROFILE = 0x00000020;
     public static final int USER_SYSTEM = 0;
+    public static final int FLAG_MAIN = 0x00004000;
 
     public static final int FLAGS_NOT_SECONDARY =
             FLAG_PRIMARY | FLAG_MANAGED_PROFILE | FLAG_GUEST | FLAG_RESTRICTED;
@@ -51,6 +52,11 @@
         PRIMARY,
         /** system user = user 0 */
         SYSTEM,
+        /**
+         * user flagged as main user on the device; on non-hsum main user = system user = user 0 on
+         * hsum main user = first human user.
+         */
+        MAIN,
         /** secondary user, i.e. non-primary and non-system. */
         SECONDARY,
         /** managed profile user, e.g. work profile. */
@@ -72,6 +78,10 @@
             return this == SYSTEM;
         }
 
+        public boolean isMain() {
+            return this == MAIN;
+        }
+
         public boolean isSecondary() {
             return this == SECONDARY;
         }
@@ -120,6 +130,10 @@
         return mUserId == USER_SYSTEM;
     }
 
+    public boolean isMain() {
+        return (mFlag & FLAG_MAIN) == FLAG_MAIN;
+    }
+
     public boolean isManagedProfile() {
         return (mFlag & FLAG_MANAGED_PROFILE) == FLAG_MANAGED_PROFILE;
     }
@@ -139,6 +153,8 @@
                 return isPrimary();
             case SYSTEM:
                 return isSystem();
+            case MAIN:
+                return isMain();
             case SECONDARY:
                 return isSecondary();
             case MANAGED_PROFILE:
diff --git a/external_dependencies/Android.bp b/external_dependencies/Android.bp
index 54fed3d..834e8ee 100644
--- a/external_dependencies/Android.bp
+++ b/external_dependencies/Android.bp
@@ -21,4 +21,6 @@
     srcs: [
         "com/**/*.java",
     ],
-}
\ No newline at end of file
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
+}
diff --git a/invocation_interfaces/Android.bp b/invocation_interfaces/Android.bp
index 8d86706..94e9e50 100644
--- a/invocation_interfaces/Android.bp
+++ b/invocation_interfaces/Android.bp
@@ -34,4 +34,6 @@
         "tradefed-result-interfaces",
         "tradefed-device-build-interfaces",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
 }
diff --git a/invocation_interfaces/com/android/tradefed/invoker/ExecutionProperties.java b/invocation_interfaces/com/android/tradefed/invoker/ExecutionProperties.java
index 0c058c1..906fadf 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/ExecutionProperties.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/ExecutionProperties.java
@@ -117,4 +117,9 @@
     public void clear() {
         mProperties.clear();
     }
+
+    @Override
+    public String toString() {
+        return "ExecutionProperties: " + mProperties;
+    }
 }
diff --git a/invocation_interfaces/com/android/tradefed/invoker/TestInformation.java b/invocation_interfaces/com/android/tradefed/invoker/TestInformation.java
index 12360ae..f2748be 100644
--- a/invocation_interfaces/com/android/tradefed/invoker/TestInformation.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/TestInformation.java
@@ -18,6 +18,8 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.util.FileUtil;
 
 import java.io.File;
@@ -232,6 +234,8 @@
                 // approach to do individual download from remote artifact.
                 // Try to stage the files from remote zip files.
                 file = getBuildInfo().stageRemoteFile(fileName, testsDir);
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.STAGE_UNDEFINED_DEPENDENCY, fileName);
             }
             return file;
         }
diff --git a/invocation_interfaces/com/android/tradefed/result/TestRunResult.java b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
index ecfcded..e12a855 100644
--- a/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
+++ b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
@@ -119,7 +119,7 @@
     }
 
     /** Gets the set of tests in given statuses. */
-    private Set<TestDescription> getTestsInState(List<TestStatus> statuses) {
+    public Set<TestDescription> getTestsInState(List<TestStatus> statuses) {
         Set<TestDescription> tests = new LinkedHashSet<>();
         for (Map.Entry<TestDescription, TestResult> testEntry : getTestResults().entrySet()) {
             TestStatus status = testEntry.getValue().getStatus();
diff --git a/isolation/Android.bp b/isolation/Android.bp
index 8c9a77f..4e746f6 100644
--- a/isolation/Android.bp
+++ b/isolation/Android.bp
@@ -32,6 +32,8 @@
         "commons-cli-1.2",
     ],
     jarjar_rules: "jarjar_rules.txt",
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
 }
 
 java_library_host {
@@ -50,4 +52,6 @@
         include_dirs: ["external/protobuf/src"],
         type: "full",
     },
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
 }
diff --git a/javatests/Android.bp b/javatests/Android.bp
index 92c0324..1a8344f 100644
--- a/javatests/Android.bp
+++ b/javatests/Android.bp
@@ -20,6 +20,8 @@
     name: "tradefed-test-protos",
     visibility: ["//visibility:private"],
     srcs: ["res/**/*.proto"],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     libs: [
         "libprotobuf-java-full",
     ],
@@ -36,6 +38,9 @@
     // Only compile source java files in this lib.
     srcs: ["com/**/*.java"],
 
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
+
     java_resource_dirs: ["res"],
     java_resources: [
         ":SimpleFailingTest",
diff --git a/javatests/com/android/tradefed/UnitTests.java b/javatests/com/android/tradefed/UnitTests.java
index ba416ee..47950b8 100644
--- a/javatests/com/android/tradefed/UnitTests.java
+++ b/javatests/com/android/tradefed/UnitTests.java
@@ -108,6 +108,7 @@
 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.BluetoothHciSnoopLogCollectorTest;
 import com.android.tradefed.device.metric.BugreportzOnFailureCollectorTest;
 import com.android.tradefed.device.metric.BugreportzOnTestCaseFailureCollectorTest;
 import com.android.tradefed.device.metric.ClangCodeCoverageCollectorTest;
@@ -282,6 +283,7 @@
 import com.android.tradefed.targetprep.TestAppInstallSetupTest;
 import com.android.tradefed.targetprep.TestFilePushSetupTest;
 import com.android.tradefed.targetprep.UserCleanerTest;
+import com.android.tradefed.targetprep.VisibleBackgroundUserPreparerTest;
 import com.android.tradefed.targetprep.adb.AdbStopServerPreparerTest;
 import com.android.tradefed.targetprep.app.NoApkTestSkipperTest;
 import com.android.tradefed.targetprep.multi.MergeMultiBuildTargetPreparerTest;
@@ -369,6 +371,7 @@
 import com.android.tradefed.testtype.suite.params.ModuleParametersHelperTest;
 import com.android.tradefed.testtype.suite.params.RunOnSdkSandboxHandlerTest;
 import com.android.tradefed.testtype.suite.params.SecondaryUserHandlerTest;
+import com.android.tradefed.testtype.suite.params.SecondaryUserOnSecondaryDisplayHandlerTest;
 import com.android.tradefed.testtype.suite.params.multiuser.RunOnSecondaryUserParameterHandlerTest;
 import com.android.tradefed.testtype.suite.params.multiuser.RunOnWorkProfileParameterHandlerTest;
 import com.android.tradefed.testtype.suite.retry.ResultsPlayerTest;
@@ -595,6 +598,7 @@
     AtraceRunMetricCollectorTest.class,
     AutoLogCollectorTest.class,
     BaseDeviceMetricCollectorTest.class,
+    BluetoothHciSnoopLogCollectorTest.class,
     BugreportzOnTestCaseFailureCollectorTest.class,
     BugreportzOnFailureCollectorTest.class,
     ClangCodeCoverageCollectorTest.class,
@@ -742,6 +746,7 @@
     AoaTargetPreparerTest.class,
     AppSetupTest.class,
     BaseTargetPreparerTest.class,
+    VisibleBackgroundUserPreparerTest.class,
     CreateUserPreparerTest.class,
     DefaultTestsZipInstallerTest.class,
     DeviceFlashPreparerTest.class,
@@ -928,6 +933,7 @@
     RunOnSecondaryUserParameterHandlerTest.class,
     RunOnWorkProfileParameterHandlerTest.class,
     SecondaryUserHandlerTest.class,
+    SecondaryUserOnSecondaryDisplayHandlerTest.class,
 
     // testtype/suite/retry
     ResultsPlayerTest.class,
diff --git a/javatests/com/android/tradefed/device/DeviceStateMonitorTest.java b/javatests/com/android/tradefed/device/DeviceStateMonitorTest.java
index 04ae37d..c521d0c 100644
--- a/javatests/com/android/tradefed/device/DeviceStateMonitorTest.java
+++ b/javatests/com/android/tradefed/device/DeviceStateMonitorTest.java
@@ -197,7 +197,7 @@
                         return new CollectingOutputReceiver() {
                             @Override
                             public String getOutput() {
-                                return "/system/bin/adb";
+                                return "uid=0(root)";
                             }
                         };
                     }
@@ -231,7 +231,7 @@
                             @Override
                             public String getOutput() {
                                 if (mAtomicBoolean.get()) {
-                                  return "/system/bin/adb";
+                                    return "uid=0(root)";
                                 }
                                 return "not found";
                             }
diff --git a/javatests/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java b/javatests/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
index b41b484..55d683f 100644
--- a/javatests/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
+++ b/javatests/com/android/tradefed/device/LocalAndroidVirtualDeviceTest.java
@@ -96,7 +96,7 @@
         }
 
         @Override
-        IRunUtil createRunUtil() {
+        protected IRunUtil createRunUtil() {
             Assert.assertNotNull("Unexpected method call to createRunUtil.", currentRunUtil);
             IRunUtil returnValue = currentRunUtil;
             currentRunUtil = null;
diff --git a/javatests/com/android/tradefed/device/TestDeviceFuncTest.java b/javatests/com/android/tradefed/device/TestDeviceFuncTest.java
index 2c7f4b8..854af8e 100644
--- a/javatests/com/android/tradefed/device/TestDeviceFuncTest.java
+++ b/javatests/com/android/tradefed/device/TestDeviceFuncTest.java
@@ -44,7 +44,6 @@
 import com.android.tradefed.util.ProcessInfo;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
-import android.platform.test.annotations.FlakyTest;
 
 import org.junit.Assume;
 import org.junit.Before;
@@ -665,7 +664,6 @@
     }
 
     /** Test device soft-restart detection API. */
-    @FlakyTest
     @Test
     public void testDeviceSoftRestart() throws DeviceNotAvailableException {
         CLog.i("testDeviceSoftRestartSince");
diff --git a/javatests/com/android/tradefed/device/cloud/GceAvdInfoTest.java b/javatests/com/android/tradefed/device/cloud/GceAvdInfoTest.java
index f4dc517..18624ac 100644
--- a/javatests/com/android/tradefed/device/cloud/GceAvdInfoTest.java
+++ b/javatests/com/android/tradefed/device/cloud/GceAvdInfoTest.java
@@ -488,6 +488,7 @@
                     + " random_key:\"this-is-12345678\"\n"
                     + " leased_device_spec:{type:TESTTYPE build_artifacts:{build_id:\"P1234567\""
                     + " build_target:\"target\" build_branch:\"testBranch\"}}"
+                    + " oxygen_version:\"v20220509-0008-rc01-cl447382102\"  "
                     + " debug_info:{reserved_cores:1 region:\"test-region\" environment:\"test\"}";
         CommandResult res = Mockito.mock(CommandResult.class);
         Mockito.doAnswer(
diff --git a/javatests/com/android/tradefed/device/cloud/OxygenUtilTest.java b/javatests/com/android/tradefed/device/cloud/OxygenUtilTest.java
index 80784d8..f62a1b2 100644
--- a/javatests/com/android/tradefed/device/cloud/OxygenUtilTest.java
+++ b/javatests/com/android/tradefed/device/cloud/OxygenUtilTest.java
@@ -34,6 +34,7 @@
 import org.mockito.Mockito;
 
 import java.io.File;
+import java.util.List;
 
 /** Unit tests for {@link OxygenUtil}. */
 @RunWith(JUnit4.class)
@@ -88,4 +89,51 @@
                 OxygenUtil.getDefaultLogType("invocation_started_bugreport_123456.txt"),
                 LogDataType.BUGREPORT);
     }
+
+    /** Test collectErrorSignatures. */
+    @Test
+    public void testCollectErrorSignatures() throws Exception {
+        File tmpDir = null;
+        try {
+            tmpDir = FileUtil.createTempDir("logs");
+            File file1 = FileUtil.createTempFile("launcher.log", ".randomstring", tmpDir);
+            String content =
+                    "some content\n"
+                            + "some Address already in use\n"
+                            + "some vcpu hw run failure: 0x7.\n"
+                            + "tailing string";
+            FileUtil.writeToFile(content, file1);
+            List<String> signatures = OxygenUtil.collectErrorSignatures(tmpDir);
+            assertEquals("crosvm_vcpu_hw_run_failure_7", signatures.get(0));
+            assertEquals("launch_cvd_port_collision", signatures.get(1));
+        } finally {
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
+
+    /** Test collectDeviceLaunchMetrics. */
+    @Test
+    public void testCollectDeviceLaunchMetrics() throws Exception {
+        File tmpDir = null;
+        try {
+            tmpDir = FileUtil.createTempDir("logs");
+            File file1 = FileUtil.createTempFile("vdl_stdout.txt", ".randomstring", tmpDir);
+            String content =
+                    "some content\n2023/02/09 21:25:25 launch_cvd exited."
+                            + "2023/02/09 21:25:30   Ended At  | Duration | Event Name\n"
+                            + "2023/02/09 21:25:30      62.21  |    0.00  | SetupDependencies\n"
+                            + "2023/02/09 21:25:30      62.55  |    0.33  | CuttlefishCommon\n"
+                            + "2023/02/09 21:25:30     186.84  |  124.63  | LaunchDevice\n"
+                            + "2023/02/09 21:25:30     186.84  |  186.84  |"
+                            + " CuttlefishLauncherMainstart\n"
+                            + "tailing string";
+            FileUtil.writeToFile(content, file1);
+            long[] metrics = OxygenUtil.collectDeviceLaunchMetrics(tmpDir);
+            assertEquals(61880, metrics[0]);
+            assertEquals(124630, metrics[1]);
+
+        } finally {
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
 }
diff --git a/javatests/com/android/tradefed/device/metric/BluetoothHciSnoopLogCollectorTest.java b/javatests/com/android/tradefed/device/metric/BluetoothHciSnoopLogCollectorTest.java
new file mode 100644
index 0000000..27a68db
--- /dev/null
+++ b/javatests/com/android/tradefed/device/metric/BluetoothHciSnoopLogCollectorTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2023 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.mockito.Mockito.doAnswer;
+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.ddmlib.IDevice;
+import com.android.tradefed.config.ConfigurationDef;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.TestDeviceState;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.proto.TfMetricProtoUtil;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public final class BluetoothHciSnoopLogCollectorTest {
+    private BluetoothHciSnoopLogCollector mCollector;
+    @Mock ITestInvocationListener mMockListener;
+    private IInvocationContext mContext;
+    @Mock ITestDevice mMockDevice;
+    @Mock IDevice mMockIDevice;
+    private List<ITestDevice> mDevices;
+    private ITestInvocationListener listener;
+
+    private static final String TEST_RUN_NAME = "runName";
+    private static final int TEST_RUN_COUNT = 1;
+    private static final long TEST_START_TIME = 0;
+    private static final long TEST_END_TIME = 50;
+    private static final long TEST_RUN_END_TIME = 100;
+    private static final String PULL_DIRECTORY = "/data/misc/bluetooth/testreporting";
+    private static final String PULL_DIRECTORY_BASENAME = "testreporting";
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = Mockito.spy(new InvocationContext());
+        mMockDevice = Mockito.mock(ITestDevice.class);
+        mDevices = List.of(mMockDevice);
+        doReturn(mDevices).when(mContext).getDevices();
+
+        mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
+        when(mContext.getDevices()).thenReturn(mDevices);
+        mCollector = Mockito.spy(new BluetoothHciSnoopLogCollector());
+        doNothing()
+                .when(mCollector)
+                .executeShellCommand(Mockito.any(ITestDevice.class), Mockito.anyString());
+        doReturn("/data/misc/bluetooth/testreporting").when(mCollector).getReportingDir();
+        when(mMockDevice.getCurrentUser()).thenReturn(0);
+        when(mMockDevice.getIDevice()).thenReturn(mMockIDevice);
+        when(mMockDevice.setProperty(Mockito.anyString(), Mockito.anyString())).thenReturn(true);
+        when(mMockDevice.executeShellV2Command(Mockito.anyString()))
+                .thenReturn(new CommandResult(CommandStatus.SUCCESS));
+        when(mMockDevice.getDeviceState()).thenReturn(TestDeviceState.ONLINE);
+
+        listener = mCollector.init(mContext, mMockListener);
+    }
+
+    /**
+     * Test that if the pattern of a metric match the requested pattern we attempt to pull it as a
+     * log file.
+     */
+    @Test
+    public void testPullFileAndLog() throws Exception {
+        // Pattern of file(s)/dir(s) to pull.
+        String pullPatternKey = "log1";
+        String pullPatternPath = "/data/local/tmp/log1.txt";
+
+        // Set up the metric collector's param.
+        OptionSetter setter = new OptionSetter(mCollector);
+        setter.setOptionValue("pull-pattern-keys", pullPatternKey);
+        setter.setOptionValue("clean-up", "true");
+        setter.setOptionValue("collect-on-run-ended-only", "false");
+
+        HashMap<String, Metric> metrics = new HashMap<>();
+        metrics.put(pullPatternKey, TfMetricProtoUtil.stringToMetric(pullPatternPath));
+        metrics.put("another_metrics", TfMetricProtoUtil.stringToMetric("57"));
+
+        ArgumentCaptor<HashMap<String, Metric>> capture = ArgumentCaptor.forClass(HashMap.class);
+
+        when(mMockDevice.pullFile(Mockito.eq(pullPatternPath), Mockito.eq(0)))
+                .thenReturn(new File("file"));
+
+        TestDescription test = new TestDescription("class", "test");
+        listener.testRunStarted(TEST_RUN_NAME, TEST_RUN_COUNT);
+        listener.testStarted(test, TEST_START_TIME);
+        listener.testEnded(test, TEST_END_TIME, metrics);
+        listener.testRunEnded(TEST_RUN_END_TIME, metrics);
+
+        verify(mMockListener)
+                .testRunStarted(
+                        Mockito.eq(TEST_RUN_NAME),
+                        Mockito.eq(TEST_RUN_COUNT),
+                        Mockito.eq(0),
+                        Mockito.anyLong());
+        verify(mMockListener).testStarted(test, TEST_START_TIME);
+        verify(mMockDevice).deleteFile(pullPatternPath);
+        verify(mMockDevice).pullFile(Mockito.eq(pullPatternPath), Mockito.anyInt());
+        verify(mCollector).retrieveFile(Mockito.any(), Mockito.anyString(), Mockito.anyInt());
+        verify(mMockListener)
+                .testEnded(Mockito.eq(test), Mockito.eq(TEST_END_TIME), capture.capture());
+        verify(mMockListener).testRunEnded(TEST_RUN_END_TIME, metrics);
+        HashMap<String, Metric> metricCaptured = capture.getValue();
+        assertEquals(
+                "57", metricCaptured.get("another_metrics").getMeasurements().getSingleString());
+        assertEquals(
+                pullPatternPath,
+                metricCaptured.get(pullPatternKey).getMeasurements().getSingleString());
+    }
+
+    /** Test that we attempt to pull metrics (in the watched directory) as a log file. */
+    @Test
+    public void testPullDirMultipleSnoopLogs() throws Exception {
+        // Dir to pull.
+        String pullDirectoryPath = "/tmp/my/dir";
+
+        // Set up the metric collector's param.
+        OptionSetter setter = new OptionSetter(mCollector);
+        setter.setOptionValue("directory-keys", pullDirectoryPath);
+        setter.setOptionValue("clean-up", "true");
+        setter.setOptionValue("collect-on-run-ended-only", "false");
+
+        HashMap<String, Metric> metrics = new HashMap<>();
+        metrics.put("another_metrics", TfMetricProtoUtil.stringToMetric("57"));
+
+        ArgumentCaptor<HashMap<String, Metric>> capture = ArgumentCaptor.forClass(HashMap.class);
+
+        // Inject a file into the temp destination directory on the host, to simulate the behaviour
+        // in FilePullerDeviceMetricCollector.pullMetricDirectory().
+        doAnswer(
+                        invocation -> {
+                            String keyDirectory = (String) invocation.getArgument(0);
+                            File tmpDestDir = (File) invocation.getArgument(1);
+                            Path logFileInTmpDestDir =
+                                    Files.createTempFile(
+                                            tmpDestDir.toPath(), "class-test-", ".log");
+
+                            return true;
+                        })
+                .when(mMockDevice)
+                .pullDir(Mockito.anyString(), Mockito.any(File.class));
+
+        listener.testRunStarted(TEST_RUN_NAME, TEST_RUN_COUNT);
+
+        TestDescription test1 = new TestDescription("class", "test1");
+        listener.testStarted(test1, TEST_START_TIME);
+        listener.testEnded(test1, TEST_END_TIME, metrics);
+
+        TestDescription test2 = new TestDescription("class", "test2");
+        listener.testStarted(test2, TEST_START_TIME);
+        listener.testEnded(test2, TEST_END_TIME, metrics);
+
+        TestDescription test3 = new TestDescription("class", "test3");
+        listener.testStarted(test3, TEST_START_TIME);
+        listener.testEnded(test3, TEST_END_TIME, metrics);
+
+        listener.testRunEnded(TEST_RUN_END_TIME, metrics);
+
+        verify(mMockListener)
+                .testRunStarted(
+                        Mockito.eq(TEST_RUN_NAME),
+                        Mockito.eq(TEST_RUN_COUNT),
+                        Mockito.eq(0),
+                        Mockito.anyLong());
+
+        verify(mMockListener).testStarted(test1, TEST_START_TIME);
+        verify(mMockListener).testStarted(test2, TEST_START_TIME);
+        verify(mMockListener).testStarted(test3, TEST_START_TIME);
+        verify(mMockListener, times(3))
+                .testLog(Mockito.anyString(), Mockito.eq(LogDataType.BT_SNOOP_LOG), Mockito.any());
+        verify(mMockListener)
+                .testEnded(Mockito.eq(test1), Mockito.eq(TEST_END_TIME), capture.capture());
+        verify(mMockListener)
+                .testEnded(Mockito.eq(test2), Mockito.eq(TEST_END_TIME), capture.capture());
+        verify(mMockListener)
+                .testEnded(Mockito.eq(test3), Mockito.eq(TEST_END_TIME), capture.capture());
+        verify(mMockListener).testRunEnded(TEST_RUN_END_TIME, metrics);
+        HashMap<String, Metric> metricCaptured = capture.getValue();
+        assertEquals(
+                "57", metricCaptured.get("another_metrics").getMeasurements().getSingleString());
+    }
+}
diff --git a/javatests/com/android/tradefed/device/metric/ClangCodeCoverageCollectorTest.java b/javatests/com/android/tradefed/device/metric/ClangCodeCoverageCollectorTest.java
index 42afdee..5b7b951 100644
--- a/javatests/com/android/tradefed/device/metric/ClangCodeCoverageCollectorTest.java
+++ b/javatests/com/android/tradefed/device/metric/ClangCodeCoverageCollectorTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.times;
@@ -64,6 +65,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
@@ -197,6 +199,7 @@
     public void testRun_logsCoverageFile() throws Exception {
         mCoverageOptionsSetter.setOptionValue("coverage", "true");
         mCoverageOptionsSetter.setOptionValue("coverage-toolchain", "CLANG");
+        mCoverageOptionsSetter.setOptionValue("pull-timeout", "314159");
 
         // Setup mocks.
         doReturn(true).when(mMockDevice).isAdbRoot();
@@ -216,6 +219,15 @@
         mListener.testRunEnded(ELAPSED_TIME, mMetrics);
         mListener.invocationEnded(ELAPSED_TIME);
 
+        // Verify the timeout is set.
+        verify(mMockDevice, times(1))
+                .executeShellV2Command(
+                        eq("find /data/misc/trace -name '*.profraw' | tar -czf - -T - 2>/dev/null"),
+                        any(),
+                        any(),
+                        eq(314159L),
+                        eq(TimeUnit.MILLISECONDS),
+                        eq(1));
         // Verify that the command line contains the files above.
         List<String> command = mCommandArgumentCaptor.getCommand();
         checkListContainsSuffixes(
diff --git a/javatests/com/android/tradefed/device/metric/EmulatorMemoryCpuCapturerTest.java b/javatests/com/android/tradefed/device/metric/EmulatorMemoryCpuCapturerTest.java
index a347335..babc54e 100644
--- a/javatests/com/android/tradefed/device/metric/EmulatorMemoryCpuCapturerTest.java
+++ b/javatests/com/android/tradefed/device/metric/EmulatorMemoryCpuCapturerTest.java
@@ -18,20 +18,37 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.doReturn;
+
+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.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
 
 @RunWith(JUnit4.class)
 public class EmulatorMemoryCpuCapturerTest {
 
     private EmulatorMemoryCpuCapturer mEmulatorMemoryCpuCapturer;
+    @Mock IRunUtil mMockRunUtil;
 
     @Before
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
         // just capture the current process'es id
-        mEmulatorMemoryCpuCapturer = new EmulatorMemoryCpuCapturer(ProcessHandle.current().pid());
+        mEmulatorMemoryCpuCapturer =
+                new EmulatorMemoryCpuCapturer(ProcessHandle.current().pid()) {
+                    @Override
+                    protected IRunUtil getRunUtil() {
+                        return mMockRunUtil;
+                    }
+                };
     }
 
     @Test
@@ -67,10 +84,18 @@
     /** functional test for getting cpu usage using the current java processes' pid. */
     @Test
     public void getCpuUsage() {
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout("%CPU\n35.4");
+        doReturn(result)
+                .when(mMockRunUtil)
+                .runTimedCmd(
+                        Mockito.anyLong(),
+                        Mockito.eq("ps"),
+                        Mockito.eq("-o"),
+                        Mockito.eq("%cpu"),
+                        Mockito.eq("-p"),
+                        Mockito.any());
         float cpu = mEmulatorMemoryCpuCapturer.getCpuUsage();
-        // arbitrarily check bounds to make sure returned value is reasonable
-        assertThat(cpu).isGreaterThan(1);
-        // ensure less than 2 GB memory
-        assertThat(cpu).isLessThan(1000);
+        assertThat(cpu).isEqualTo(35.4F);
     }
 }
diff --git a/javatests/com/android/tradefed/device/metric/GcovKernelCodeCoverageCollectorTest.java b/javatests/com/android/tradefed/device/metric/GcovKernelCodeCoverageCollectorTest.java
index 0b1e2fd..987335e 100644
--- a/javatests/com/android/tradefed/device/metric/GcovKernelCodeCoverageCollectorTest.java
+++ b/javatests/com/android/tradefed/device/metric/GcovKernelCodeCoverageCollectorTest.java
@@ -20,9 +20,7 @@
 
 import static org.junit.Assert.assertTrue;
 
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.startsWith;
 import static org.mockito.Mockito.doReturn;
@@ -70,7 +68,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
-import java.util.concurrent.TimeUnit;
 
 /** Unit tests for {@link GcovKernelCodeCoverageCollector}. */
 @RunWith(JUnit4.class)
@@ -152,9 +149,25 @@
                         GcovKernelCodeCoverageCollector.MAKE_TEMP_DIR_COMMAND))
                 .thenReturn(successResultWithDir);
 
-        // collectGcovDebugfsCoverage() gather coverage happy path: success
+        // collectGcovDebugfsCoverage() make gcda temp dir happy path: success
         when(mMockDevice.executeShellV2Command(
-                        startsWith("find /d/gcov"), anyLong(), any(TimeUnit.class)))
+                        startsWith(
+                                GcovKernelCodeCoverageCollector.MAKE_GCDA_TEMP_DIR_COMMAND_FMT
+                                        .substring(0, 8))))
+                .thenReturn(mSuccessResult);
+
+        // collectGcovDebugfsCoverage() copy gcov data happy path: success
+        when(mMockDevice.executeShellV2Command(
+                        startsWith(
+                                GcovKernelCodeCoverageCollector.COPY_GCOV_DATA_COMMAND_FMT
+                                        .substring(0, 6))))
+                .thenReturn(mSuccessResult);
+
+        // collectGcovDebugfsCoverage() tar gcov data happy path: success
+        when(mMockDevice.executeShellV2Command(
+                        startsWith(
+                                GcovKernelCodeCoverageCollector.TAR_GCOV_DATA_COMMAND_FMT.substring(
+                                        0, 8))))
                 .thenReturn(mSuccessResult);
 
         // device.pullFile() happy path: always return a file with the given name
@@ -266,7 +279,7 @@
     public void resetGcovCountsFail_noTar() throws Exception {
         var moduleName = name.getMethodName();
 
-        // Set mount command to fail
+        // Set reset command to fail
         when(mMockDevice.executeShellV2Command(
                         GcovKernelCodeCoverageCollector.RESET_GCOV_COUNTS_COMMAND))
                 .thenReturn(mFailedResult);
@@ -279,7 +292,7 @@
     public void makeTempDirFail_noTar() throws Exception {
         var moduleName = name.getMethodName();
 
-        // Set mount command to fail
+        // Set make temp dir command to fail
         when(mMockDevice.executeShellV2Command(
                         GcovKernelCodeCoverageCollector.MAKE_TEMP_DIR_COMMAND))
                 .thenReturn(mFailedResult);
@@ -289,12 +302,44 @@
     }
 
     @Test
-    public void gatherCoverageFail_noTar() throws Exception {
+    public void makeGcdaTempDirFail_noTar() throws Exception {
         var moduleName = name.getMethodName();
 
-        // Set mount command to fail
+        // Set make gcda temp dir command to fail
         when(mMockDevice.executeShellV2Command(
-                        startsWith("find /d/gcov"), anyLong(), any(TimeUnit.class)))
+                        startsWith(
+                                GcovKernelCodeCoverageCollector.MAKE_GCDA_TEMP_DIR_COMMAND_FMT
+                                        .substring(0, 8))))
+                .thenReturn(mFailedResult);
+
+        configuredRun(List.of(moduleName), 1, false);
+        assertThat(mFakeListener.getLogs()).hasSize(0);
+    }
+
+    @Test
+    public void copyGcovDataFail_noTar() throws Exception {
+        var moduleName = name.getMethodName();
+
+        // Set copy gcov data command to fail
+        when(mMockDevice.executeShellV2Command(
+                        startsWith(
+                                GcovKernelCodeCoverageCollector.COPY_GCOV_DATA_COMMAND_FMT
+                                        .substring(0, 6))))
+                .thenReturn(mFailedResult);
+
+        configuredRun(List.of(moduleName), 1, false);
+        assertThat(mFakeListener.getLogs()).hasSize(0);
+    }
+
+    @Test
+    public void tarGcovDataFail_noTar() throws Exception {
+        var moduleName = name.getMethodName();
+
+        // Set tar gcov data command to fail
+        when(mMockDevice.executeShellV2Command(
+                        startsWith(
+                                GcovKernelCodeCoverageCollector.TAR_GCOV_DATA_COMMAND_FMT.substring(
+                                        0, 8))))
                 .thenReturn(mFailedResult);
 
         configuredRun(List.of(moduleName), 1, false);
diff --git a/javatests/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java b/javatests/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
index 0a6e3b6..77546bc 100644
--- a/javatests/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
+++ b/javatests/com/android/tradefed/device/metric/JavaCodeCoverageCollectorTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -168,6 +169,7 @@
     @Test
     public void testRunEnded_rootEnabled_logsCoverageMeasurement() throws Exception {
         enableJavaCoverage();
+        mCoverageOptionsSetter.setOptionValue("pull-timeout", "314159");
 
         // Setup mocks.
         HashMap<String, Metric> runMetrics = createMetricsWithCoverageMeasurement(DEVICE_PATH);
@@ -181,6 +183,16 @@
         mCodeCoverageCollector.testRunStarted(RUN_NAME, TEST_COUNT);
         mCodeCoverageCollector.testRunEnded(ELAPSED_TIME, runMetrics);
 
+        // Verify timeout is set.
+        verify(mMockDevice, times(1))
+                .executeShellV2Command(
+                        eq("find /data/misc/trace -name '*.ec' | tar -czf - -T - 2>/dev/null"),
+                        any(),
+                        any(),
+                        eq(314159L),
+                        eq(TimeUnit.MILLISECONDS),
+                        eq(1));
+
         // Verify testLog(..) was called with the coverage file.
         verify(mFakeListener)
                 .testLog(anyString(), eq(LogDataType.COVERAGE), eq(COVERAGE_MEASUREMENT));
diff --git a/javatests/com/android/tradefed/device/metric/ShowmapPullerMetricCollectorTest.java b/javatests/com/android/tradefed/device/metric/ShowmapPullerMetricCollectorTest.java
index 17d3868..d19b728 100644
--- a/javatests/com/android/tradefed/device/metric/ShowmapPullerMetricCollectorTest.java
+++ b/javatests/com/android/tradefed/device/metric/ShowmapPullerMetricCollectorTest.java
@@ -280,6 +280,44 @@
     }
 
     @Test
+    public void testNoProcessFlow() throws Exception {
+        OptionSetter setter = new OptionSetter(mShowmapMetricCollector);
+        setter.setOptionValue("collect-on-run-ended-only", "false");
+        setter.setOptionValue("pull-pattern-keys", "showmap_output_file");
+        setter.setOptionValue("showmap-process-name", "");
+        FileWriter writer = new FileWriter(mTmpFile);
+        String log =
+                String.join(
+                        "\n",
+                        ">>> system_server (6910) <<<",
+                        "size      RSS      PSS    clean    dirty    clean    dirty",
+                        "-------- -------- --------",
+                        "10 20 30 40 50 60 70 80 90    100   110  120  130 140 15 rw- obj1",
+                        "-------- -------- --------",
+                        "   >>> netd (7038) <<<   ",
+                        "size      RSS      PSS    clean    dirty    clean    dirty",
+                        "-------- -------- --------",
+                        "100 2021 3033 4092 500 6 7  8 9 100 110 120 130 140 15 rw- obj123",
+                        "-------- -------- --------");
+        writer.write(log);
+        writer.close();
+        TestDescription testDesc = new TestDescription("xyz", "abc");
+        mShowmapMetricCollector.testStarted(testDesc);
+
+        HashMap<String, Metric> currentMetrics = new HashMap<>();
+        currentMetrics.put(
+                "showmap_output_file",
+                TfMetricProtoUtil.stringToMetric("/sdcard/test_results/showmap.txt"));
+        Mockito.when(mMockDevice.pullFile(Mockito.eq("/sdcard/test_results/showmap.txt")))
+                .thenReturn(mTmpFile);
+
+        mShowmapMetricCollector.testEnded(testDesc, currentMetrics);
+        Assert.assertEquals(1, currentMetrics.size());
+        Assert.assertEquals(
+                null, currentMetrics.get("showmap_granular_system_server_total_object_count"));
+    }
+
+    @Test
     public void testErrorFlow() throws Exception {
         OptionSetter setter = new OptionSetter(mShowmapMetricCollector);
         setter.setOptionValue("collect-on-run-ended-only", "false");
diff --git a/javatests/com/android/tradefed/invoker/InvocationExecutionTest.java b/javatests/com/android/tradefed/invoker/InvocationExecutionTest.java
index 20f17f8..2c4c016 100644
--- a/javatests/com/android/tradefed/invoker/InvocationExecutionTest.java
+++ b/javatests/com/android/tradefed/invoker/InvocationExecutionTest.java
@@ -184,8 +184,7 @@
         private boolean mFirstInit = true;
 
         @Override
-        public ITestInvocationListener init(
-                IInvocationContext context, ITestInvocationListener listener)
+        public void extraInit(IInvocationContext context, ITestInvocationListener listener)
                 throws DeviceNotAvailableException {
             if (mFirstInit) {
                 sTotalInit++;
@@ -193,7 +192,6 @@
             } else {
                 fail("Init should only be called once per instance.");
             }
-            return super.init(context, listener);
         }
     }
 
diff --git a/javatests/com/android/tradefed/lite/Android.bp b/javatests/com/android/tradefed/lite/Android.bp
index dfb390d..19b7070 100644
--- a/javatests/com/android/tradefed/lite/Android.bp
+++ b/javatests/com/android/tradefed/lite/Android.bp
@@ -21,6 +21,8 @@
     name: "IsolatedSampleTests",
     defaults: ["tradefed_defaults"],
     srcs: ["./SampleTests.java"],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     static_libs: [
         "junit"
     ]
diff --git a/javatests/com/android/tradefed/presubmit/TestMappingsValidation.java b/javatests/com/android/tradefed/presubmit/TestMappingsValidation.java
index 547daab..f5dce86 100644
--- a/javatests/com/android/tradefed/presubmit/TestMappingsValidation.java
+++ b/javatests/com/android/tradefed/presubmit/TestMappingsValidation.java
@@ -15,9 +15,10 @@
  */
 package com.android.tradefed.presubmit;
 
-import static java.lang.String.format;
 import static org.junit.Assert.fail;
 
+import static java.lang.String.format;
+
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.ConfigurationException;
@@ -27,6 +28,8 @@
 import com.android.tradefed.config.IConfigurationFactory;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.PushFilePreparer;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.suite.ITestSuite;
@@ -38,10 +41,16 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
-
 import com.google.gson.Gson;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -51,14 +60,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.junit.Assume;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
 
 /**
  * Validation tests to run against the TEST_MAPPING files in tests_mappings.zip to ensure they
@@ -191,8 +194,9 @@
      * targets.
      */
     @Test
-    public void testValidateTestEntry() {
+    public void testValidateTestEntry() throws Exception {
         List<String> errors = new ArrayList<>();
+        Set<String> checkedModule = new HashSet<>();
         for (String testGroup : allTests.keySet()) {
             if (!mTestGroupToValidate.contains(testGroup)) {
                 CLog.d("Skip checking tests with group: %s", testGroup);
@@ -215,6 +219,7 @@
                                         testInfo.getName(), testInfo.getSources()));
                     }
                 }
+                checkedModule.add(moduleName);
             }
         }
         if (!errors.isEmpty()) {
@@ -235,6 +240,7 @@
                 fail(error);
             }
         }
+        validateConfigsOfSharedPool(checkedModule);
     }
 
     /**
@@ -531,4 +537,60 @@
         }
         return errors;
     }
+
+    private void validateConfigsOfSharedPool(Set<String> checkedModule) throws Exception {
+        File configZip = deviceBuildInfo.getFile("general-tests_configs.zip");
+        File deviceConfigZip = deviceBuildInfo.getFile("device-tests_configs.zip");
+        Assume.assumeTrue(configZip != null);
+        List<String> testConfigs = new ArrayList<>();
+        List<File> dirToLoad = new ArrayList<>();
+        File testConfigDir = ZipUtil2.extractZipToTemp(configZip, "general-tests_configs");
+        File deviceTestConfigDir = null;
+        dirToLoad.add(testConfigDir);
+        if (deviceConfigZip != null) {
+            deviceTestConfigDir =
+                    ZipUtil2.extractZipToTemp(deviceConfigZip, "device-tests_configs");
+            dirToLoad.add(deviceTestConfigDir);
+        }
+        try {
+            testConfigs.addAll(ConfigurationUtil.getConfigNamesFromDirs(null, dirToLoad));
+            CLog.d("Checking modules: %s. And configs: %s", checkedModule, testConfigs);
+            List<String> errors = new ArrayList<>();
+            for (String configName : testConfigs) {
+                String fileName = FileUtil.getBaseName(new File(configName).getName());
+                if (!checkedModule.contains(fileName)) {
+                    continue;
+                }
+                try {
+                    IConfiguration config =
+                            mConfigFactory.createConfigurationFromArgs(new String[] {configName});
+                    for (ITargetPreparer prep : config.getTargetPreparers()) {
+                        if (prep instanceof PushFilePreparer) {
+                            PushFilePreparer pushPreparer = (PushFilePreparer) prep;
+                            if (pushPreparer.shouldRemountSystem()
+                                    || pushPreparer.shouldRemountVendor()) {
+                                // TODO: Throw exception instead
+                                CLog.e(
+                                        // throw new ConfigurationException(
+                                        String.format(
+                                                "%s: Shouldn't use 'remount-system' or"
+                                                        + " 'remount-vendor' in shared test mapping"
+                                                        + " pools.",
+                                                fileName));
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    errors.add(e.toString());
+                }
+            }
+
+            if (!errors.isEmpty()) {
+                fail(Joiner.on("\n").join(errors));
+            }
+        } finally {
+            FileUtil.recursiveDelete(testConfigDir);
+            FileUtil.recursiveDelete(deviceTestConfigDir);
+        }
+    }
 }
diff --git a/javatests/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java b/javatests/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
index ff28bc3..bcaa8e6 100644
--- a/javatests/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
+++ b/javatests/com/android/tradefed/result/suite/XmlSuiteResultFormatterTest.java
@@ -277,6 +277,67 @@
         assertFalse(result.isRunComplete());
     }
 
+    @Test
+    public void testFailuresReporting_largeStackTrace() throws Exception {
+        mResultHolder.context = mContext;
+
+        List<TestRunResult> runResults = new ArrayList<>();
+        runResults.add(createFakeResult("module1", 2, 1, 0, 0, 1024 * 1024, false, false));
+        mResultHolder.runResults = runResults;
+
+        Map<String, IAbi> modulesAbi = new HashMap<>();
+        modulesAbi.put("module1", new Abi("armeabi-v7a", "32"));
+        mResultHolder.modulesAbi = modulesAbi;
+
+        mResultHolder.completeModules = 2;
+        mResultHolder.totalModules = 1;
+        mResultHolder.passedTests = 2;
+        mResultHolder.failedTests = 1;
+        mResultHolder.startTime = 0L;
+        mResultHolder.endTime = 10L;
+        File res = mFormatter.writeResults(mResultHolder, mResultDir);
+        String content = FileUtil.readStringFromFile(res);
+
+        assertXmlContainsNode(content, "Result/Module");
+        assertXmlContainsAttribute(content, "Result/Module/TestCase", "name", "com.class.module1");
+        assertXmlContainsAttribute(
+                content, "Result/Module/TestCase/Test", "name", "module1.method0");
+        assertXmlContainsAttribute(
+                content, "Result/Module/TestCase/Test", "name", "module1.method1");
+        // Check that failures are showing in the xml for the test cases with error identifiers
+        assertXmlContainsAttribute(
+                content, "Result/Module/TestCase/Test", "name", "module1.failed0");
+        assertXmlContainsAttribute(content, "Result/Module/TestCase/Test", "result", "fail");
+        assertXmlContainsAttribute(
+                content, "Result/Module/TestCase/Test/Failure", "message", "module1 failed.");
+        assertXmlContainsAttribute(
+                content,
+                "Result/Module/TestCase/Test/Failure",
+                "error_name",
+                TestErrorIdentifier.TEST_ABORTED.name());
+        assertXmlContainsAttribute(
+                content,
+                "Result/Module/TestCase/Test/Failure",
+                "error_code",
+                Long.toString(TestErrorIdentifier.TEST_ABORTED.code()));
+        assertXmlContainsValue(
+                content,
+                "Result/Module/TestCase/Test/Failure/StackTrace",
+                mFormatter.sanitizeXmlContent("module1 failed." + "\nstack".repeat(174760) + "\n"));
+        // Test that we can read back the informations
+        SuiteResultHolder holder = mFormatter.parseResults(mResultDir, false);
+        assertEquals(holder.completeModules, mResultHolder.completeModules);
+        assertEquals(holder.totalModules, mResultHolder.totalModules);
+        assertEquals(holder.passedTests, mResultHolder.passedTests);
+        assertEquals(holder.failedTests, mResultHolder.failedTests);
+        assertEquals(holder.startTime, mResultHolder.startTime);
+        assertEquals(holder.endTime, mResultHolder.endTime);
+        assertEquals(
+                holder.modulesAbi.get("armeabi-v7a module1"),
+                mResultHolder.modulesAbi.get("module1"));
+        assertEquals(holder.runResults.size(), mResultHolder.runResults.size());
+    }
+
     /** Test that assumption failures and ignored tests are correctly reported in the xml. */
     @Test
     public void testAssumptionFailures_Ignore_Reporting() throws Exception {
@@ -731,6 +792,26 @@
             int testIgnored,
             boolean withMetrics,
             boolean withBadKey) {
+        return createFakeResult(
+                runName,
+                passed,
+                failed,
+                assumptionFailures,
+                testIgnored,
+                2,
+                withMetrics,
+                withBadKey);
+    }
+
+    private TestRunResult createFakeResult(
+            String runName,
+            int passed,
+            int failed,
+            int assumptionFailures,
+            int testIgnored,
+            int stackDepth,
+            boolean withMetrics,
+            boolean withBadKey) {
         TestRunResult fakeRes = new TestRunResult();
         fakeRes.testRunStarted(runName, passed + failed);
         for (int i = 0; i < passed; i++) {
@@ -745,7 +826,8 @@
             fakeRes.testStarted(description);
             // Include a null character \0 that is not XML supported
             FailureDescription failureDescription =
-                    FailureDescription.create(runName + " failed.\nstack\nstack\0")
+                    FailureDescription.create(
+                                    runName + " failed." + "\nstack".repeat(stackDepth) + "\0")
                             .setErrorIdentifier(TestErrorIdentifier.TEST_ABORTED);
             fakeRes.testFailed(description, failureDescription);
             HashMap<String, Metric> metrics = new HashMap<String, Metric>();
@@ -763,7 +845,8 @@
             TestDescription description =
                     new TestDescription("com.class." + runName, runName + ".assumpFail" + i);
             fakeRes.testStarted(description);
-            fakeRes.testAssumptionFailure(description, runName + " failed.\nstack\nstack");
+            fakeRes.testAssumptionFailure(
+                    description, runName + " failed." + "\nstack".repeat(stackDepth));
             fakeRes.testEnded(description, new HashMap<String, Metric>());
         }
         for (int i = 0; i < testIgnored; i++) {
diff --git a/javatests/com/android/tradefed/retry/BaseRetryDecisionTest.java b/javatests/com/android/tradefed/retry/BaseRetryDecisionTest.java
index 285e67b..df3c8d6 100644
--- a/javatests/com/android/tradefed/retry/BaseRetryDecisionTest.java
+++ b/javatests/com/android/tradefed/retry/BaseRetryDecisionTest.java
@@ -258,6 +258,15 @@
         verify(mMockDevice).reboot();
     }
 
+    @Test
+    public void shouldRetryPreparation_NOT_ISOLATED() throws Exception {
+        ModuleDefinition module1 = Mockito.mock(ModuleDefinition.class);
+        OptionSetter setter = new OptionSetter(mRetryDecision);
+        RetryPreparationDecision res = mRetryDecision.shouldRetryPreparation(module1, 0, 3);
+        assertFalse(res.shouldRetry());
+        assertTrue(res.shouldFailRun());
+    }
+
     private TestRunResult createResult(FailureDescription failure1, FailureDescription failure2) {
         return createResult(failure1, failure2, null);
     }
diff --git a/javatests/com/android/tradefed/targetprep/CreateUserPreparerTest.java b/javatests/com/android/tradefed/targetprep/CreateUserPreparerTest.java
index 69f1549..7c78328 100644
--- a/javatests/com/android/tradefed/targetprep/CreateUserPreparerTest.java
+++ b/javatests/com/android/tradefed/targetprep/CreateUserPreparerTest.java
@@ -15,12 +15,11 @@
  */
 package com.android.tradefed.targetprep;
 
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
+import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
+import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.NativeDevice;
@@ -28,27 +27,35 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.ITestDeviceMockHelper;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.Map;
 
 /** Unit tests for {@link CreateUserPreparer}. */
-@RunWith(JUnit4.class)
-public class CreateUserPreparerTest {
+@RunWith(MockitoJUnitRunner.class)
+public final class CreateUserPreparerTest {
 
+    @Mock private ITestDevice mMockDevice;
+
+    private OptionSetter mSetter;
+
+    private ITestDeviceMockHelper mTestDeviceMockHelper;
     private CreateUserPreparer mPreparer;
-    private ITestDevice mMockDevice;
     private TestInformation mTestInfo;
 
     @Before
-    public void setUp() {
-        mMockDevice = Mockito.mock(ITestDevice.class);
+    public void setFixtures() throws Exception {
+        mTestDeviceMockHelper = new ITestDeviceMockHelper(mMockDevice);
         mPreparer = new CreateUserPreparer();
+        mSetter = new OptionSetter(mPreparer);
+
         IInvocationContext context = new InvocationContext();
         context.addAllocatedDevice("device", mMockDevice);
         mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
@@ -56,23 +63,23 @@
 
     @Test
     public void testSetUp_tearDown() throws Exception {
-        doReturn(10).when(mMockDevice).getCurrentUser();
-        doReturn(5).when(mMockDevice).createUser(Mockito.any());
-        doReturn(true).when(mMockDevice).switchUser(5);
-        doReturn(true).when(mMockDevice).startUser(5, true);
+        mockGetCurrentUser(10);
+        mockCreateUser(5);
+        mockSwitchUser(5);
+        mockStartUser(5);
         mPreparer.setUp(mTestInfo);
+        verifyUserCreated();
+        verifyUserSwitched(5);
 
-        doReturn(true).when(mMockDevice).removeUser(5);
-        doReturn(true).when(mMockDevice).switchUser(10);
-        mPreparer.tearDown(mTestInfo, null);
+        mockSwitchUser(10);
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
+        verifyUserRemoved(5);
+        verifyUserSwitched(10);
     }
 
     @Test
     public void testSetUp_tearDown_reuseTestUser() throws Exception {
-        // Set the reuse-test-user to true.
-        CreateUserPreparer preparer = new CreateUserPreparer();
-        OptionSetter setter = new OptionSetter(preparer);
-        setter.setOptionValue("reuse-test-user", "true");
+        setParam("reuse-test-user", "true");
 
         Map<Integer, UserInfo> existingUsers = Map.of(
                 0, new UserInfo(
@@ -86,29 +93,26 @@
                     /* flags= */ 0,
                     /* isRunning= */ false));
 
-        doReturn(existingUsers).when(mMockDevice).getUserInfos();
-        doReturn(0).when(mMockDevice).getCurrentUser();
-        doReturn(true).when(mMockDevice).switchUser(13);
-        doReturn(true).when(mMockDevice).startUser(13, true);
-        doReturn(true).when(mMockDevice).switchUser(0);
+        mockGetUserInfos(existingUsers);
+        mockGetCurrentUser(0);
+        mockSwitchUser(13);
+        mockStartUser(13);
+        mockSwitchUser(0);
 
-        preparer.setUp(mTestInfo);
+        mPreparer.setUp(mTestInfo);
         // We should reuse the existing, not create a new user.
-        verify(mMockDevice, never()).createUser(Mockito.any());
-        verify(mMockDevice).switchUser(13);
+        verifyNoUserCreated();
+        verifyUserSwitched(13);
 
-        preparer.tearDown(mTestInfo, null);
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
         // We should keep the user for the next module to reuse.
-        verify(mMockDevice, never()).removeUser(13);
-        verify(mMockDevice).switchUser(0);
+        verifyUserNotRemoved(13);
+        verifyUserSwitched(0);
     }
 
     @Test
     public void testSetUp_tearDown_reuseTestUser_noExistingTestUser() throws Exception {
-        // Set the reuse-test-user to true.
-        CreateUserPreparer preparer = new CreateUserPreparer();
-        OptionSetter setter = new OptionSetter(preparer);
-        setter.setOptionValue("reuse-test-user", "true");
+        setParam("reuse-test-user", "true");
 
         Map<Integer, UserInfo> existingUsers = Map.of(
                 0, new UserInfo(
@@ -117,36 +121,32 @@
                     /* flags= */ 0x00000013,
                     /* isRunning= */ true));
 
-        doReturn(existingUsers).when(mMockDevice).getUserInfos();
-        doReturn(0).when(mMockDevice).getCurrentUser();
-        doReturn(12).when(mMockDevice).createUser(Mockito.any());
-        doReturn(true).when(mMockDevice).switchUser(12);
-        doReturn(true).when(mMockDevice).startUser(12, true);
-        doReturn(true).when(mMockDevice).switchUser(0);
+        mockGetUserInfos(existingUsers);
+        mockGetCurrentUser(0);
+        mockCreateUser(12);
+        mockSwitchUser(12);
+        mockStartUser(12);
+        mockSwitchUser(0);
 
-        preparer.setUp(mTestInfo);
-        verify(mMockDevice).createUser(Mockito.any());
-        verify(mMockDevice).switchUser(12);
+        mPreparer.setUp(mTestInfo);
+        verifyUserCreated();
+        verifyUserSwitched(12);
 
-        preparer.tearDown(mTestInfo, null);
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
         // Newly created user is kept to reuse it in the next run.
-        verify(mMockDevice, never()).removeUser(12);
-        verify(mMockDevice).switchUser(0);
+        verifyUserNotRemoved(12);
+        verifyUserSwitched(0);
     }
 
     @Test
     public void testSetUp_tearDown_noCurrent() throws Exception {
-        doReturn(NativeDevice.INVALID_USER_ID).when(mMockDevice).getCurrentUser();
-        try {
-            mPreparer.setUp(mTestInfo);
-            fail("Should have thrown an exception.");
-        } catch (TargetSetupError expected) {
-            // Expected
-        }
+        mockGetCurrentUser(NativeDevice.INVALID_USER_ID);
+
+        assertThrows(TargetSetupError.class, () -> mPreparer.setUp(mTestInfo));
 
         mPreparer.tearDown(mTestInfo, null);
-        verify(mMockDevice, never()).removeUser(Mockito.anyInt());
-        verify(mMockDevice, never()).switchUser(Mockito.anyInt());
+        verifyNoUserRemoved();
+        verifyNoUserSwitched();
     }
 
     @Test
@@ -168,41 +168,92 @@
                     /* flags= */ 0,
                     /* isRunning= */ false));
 
-        doReturn(3).when(mMockDevice).getMaxNumberOfUsersSupported();
-        doReturn(existingUsers).when(mMockDevice).getUserInfos();
-        doThrow(new IllegalStateException("failed to create"))
-                .when(mMockDevice)
-                .createUser(Mockito.any());
+        mockGetMaxNumberOfUsersSupported(3);
+        mockGetUserInfos(existingUsers);
+        Exception cause = mockCreateUserFailure("D'OH!");
 
-        try {
-            mPreparer.setUp(mTestInfo);
-            fail("Should have thrown an exception.");
-        } catch (TargetSetupError expected) {
-            // Expected
-        }
+        TargetSetupError e = assertThrows(TargetSetupError.class, () -> mPreparer.setUp(mTestInfo));
+        assertThat(e).hasCauseThat().isSameInstanceAs(cause);
+
         // verify that it removed the existing tradefed users.
-        verify(mMockDevice).removeUser(11);
-        verify(mMockDevice).removeUser(13);
+        verifyUserRemoved(11);
+        verifyUserRemoved(13);
     }
 
     @Test
-    public void testSetUp_failed() throws Exception {
-        doThrow(new IllegalStateException("failed to create"))
-                .when(mMockDevice)
-                .createUser(Mockito.any());
+    public void testSetUp_createUserfailed() throws Exception {
+        Exception cause = mockCreateUserFailure("D'OH!");
 
-        try {
-            mPreparer.setUp(mTestInfo);
-            fail("Should have thrown an exception.");
-        } catch (TargetSetupError expected) {
-            // Expected
-        }
+        TargetSetupError e = assertThrows(TargetSetupError.class, () -> mPreparer.setUp(mTestInfo));
+
+        assertThat(e).hasCauseThat().isSameInstanceAs(cause);
     }
 
     @Test
     public void testTearDown_only() throws Exception {
-        mPreparer.tearDown(mTestInfo, null);
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
 
-        verify(mMockDevice, never()).removeUser(Mockito.anyInt());
+        verifyNoUserRemoved();
+    }
+
+    private void setParam(String key, String value) throws ConfigurationException {
+        CLog.i("Setting param: '%s'='%s'", key, value);
+        mSetter.setOptionValue(key, value);
+    }
+
+    private void mockGetCurrentUser(int userId) throws Exception {
+        mTestDeviceMockHelper.mockGetCurrentUser(userId);
+    }
+
+    private void mockGetUserInfos(Map<Integer, UserInfo> existingUsers) throws Exception {
+        mTestDeviceMockHelper.mockGetUserInfos(existingUsers);
+    }
+
+    private void mockGetMaxNumberOfUsersSupported(int max) throws Exception {
+        mTestDeviceMockHelper.mockGetMaxNumberOfUsersSupported(max);
+    }
+
+    private void mockSwitchUser(int userId) throws Exception {
+        mTestDeviceMockHelper.mockSwitchUser(userId);
+    }
+
+    private void mockStartUser(int userId) throws Exception {
+        mTestDeviceMockHelper.mockStartUser(userId);
+    }
+
+    private void mockCreateUser(int userId) throws Exception {
+        mTestDeviceMockHelper.mockCreateUser(userId);
+    }
+
+    private IllegalStateException mockCreateUserFailure(String message) throws Exception {
+        return mTestDeviceMockHelper.mockCreateUserFailure(message);
+    }
+
+    private void verifyNoUserCreated() throws Exception {
+        mTestDeviceMockHelper.verifyNoUserCreated();
+    }
+
+    private void verifyUserCreated() throws Exception {
+        mTestDeviceMockHelper.verifyUserCreated();
+    }
+
+    private void verifyNoUserSwitched() throws Exception {
+        mTestDeviceMockHelper.verifyNoUserSwitched();
+    }
+
+    private void verifyUserSwitched(int userId) throws Exception {
+        mTestDeviceMockHelper.verifyUserSwitched(userId);
+    }
+
+    private void verifyNoUserRemoved() throws Exception {
+        mTestDeviceMockHelper.verifyNoUserRemoved();
+    }
+
+    private void verifyUserRemoved(int userId) throws Exception {
+        mTestDeviceMockHelper.verifyUserRemoved(userId);
+    }
+
+    private void verifyUserNotRemoved(int userId) throws Exception {
+        mTestDeviceMockHelper.verifyUserNotRemoved(userId);
     }
 }
diff --git a/javatests/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java b/javatests/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
index 518420f..5283284 100644
--- a/javatests/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
+++ b/javatests/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
@@ -234,6 +234,7 @@
         mSetter = new OptionSetter(mInstallApexModuleTargetPreparer);
         mSetter.setOptionValue("cleanup-apks", "true");
         mSetter.setOptionValue("apex-staging-wait-time", APEX_STAGING_WAIT_TIME);
+        mSetter.setOptionValue("apex-rollback-wait-time", APEX_STAGING_WAIT_TIME);
     }
 
     @After
@@ -815,7 +816,7 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         verifySuccessfulInstallPackages(Arrays.asList(mFakeApex));
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(1);
         verify(mMockDevice, times(2)).reboot();
         verify(mMockDevice, times(3)).getActiveApexes();
     }
@@ -851,7 +852,7 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         verifySuccessfulInstallPackages(Arrays.asList(mFakeApex));
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(1);
         verify(mMockDevice, times(2)).reboot();
         verify(mMockDevice, times(3)).getActiveApexes();
     }
@@ -869,7 +870,7 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         verifySuccessfulInstallPackages(Arrays.asList(mFakeApex));
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(1);
         verify(mMockDevice, times(2)).reboot();
         verify(mMockDevice, times(3)).getActiveApexes();
     }
@@ -883,7 +884,7 @@
         when(mMockDevice.getInstalledPackageNames()).thenReturn(new HashSet<>());
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(1);
         verify(mMockDevice, times(1)).reboot();
         verify(mMockDevice, times(2)).getActiveApexes();
     }
@@ -1037,7 +1038,7 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         verifySuccessfulInstallPackages(Arrays.asList(mFakeApex));
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(1);
         verify(mMockDevice, times(2)).reboot();
         verify(mMockDevice, times(3)).getActiveApexes();
     }
@@ -1065,7 +1066,7 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(1);
         verify(mMockDevice, times(2)).reboot();
         verify(mMockDevice, times(1)).uninstallPackage(APK_PACKAGE_NAME);
         verify(mMockDevice, times(2)).getActiveApexes();
@@ -1094,7 +1095,7 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(1);
         verify(mMockDevice, times(2)).reboot();
         verify(mMockDevice, times(1)).uninstallPackage(APK_PACKAGE_NAME);
         verify(mMockDevice, times(1)).uninstallPackage(APK2_PACKAGE_NAME);
@@ -1204,8 +1205,8 @@
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
         verifySuccessfulInstallPackages(Arrays.asList(mFakeApex));
-        verifyCleanInstalledApexPackages();
-        verify(mMockDevice, times(3)).reboot();
+        verifyCleanInstalledApexPackages(2);
+        verify(mMockDevice, times(4)).reboot();
         verify(mMockDevice, times(3)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice).waitForDeviceAvailable();
@@ -1237,9 +1238,9 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(2);
         verifySuccessfulInstallMultiPackages();
-        verify(mMockDevice, times(3)).reboot();
+        verify(mMockDevice, times(4)).reboot();
         verify(mMockDevice, times(3)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice, times(1)).getInstalledPackageNames();
@@ -1266,8 +1267,8 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
-        verify(mMockDevice, times(3)).reboot();
+        verifyCleanInstalledApexPackages(2);
+        verify(mMockDevice, times(4)).reboot();
         verify(mMockDevice, times(3)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice, times(1)).getInstalledPackageNames();
@@ -1294,8 +1295,8 @@
                             .contains("Failed to push local"));
         }
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
-        verify(mMockDevice, times(2)).reboot();
+        verifyCleanInstalledApexPackages(2);
+        verify(mMockDevice, times(3)).reboot();
         verify(mMockDevice, times(2)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice, times(1)).getInstalledPackageNames();
@@ -1330,8 +1331,8 @@
                                             parent_session_creation_res.getStdout())));
         }
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
-        verify(mMockDevice, times(2)).reboot();
+        verifyCleanInstalledApexPackages(2);
+        verify(mMockDevice, times(3)).reboot();
         verify(mMockDevice, times(2)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice, times(1)).getInstalledPackageNames();
@@ -1367,8 +1368,8 @@
                             .contains("Failed to create child session for"));
         }
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
-        verify(mMockDevice, times(2)).reboot();
+        verifyCleanInstalledApexPackages(2);
+        verify(mMockDevice, times(3)).reboot();
         verify(mMockDevice, times(2)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice, times(1)).getInstalledPackageNames();
@@ -1419,8 +1420,8 @@
                                             write_to_session_res.getStdout())));
         }
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
-        verify(mMockDevice, times(2)).reboot();
+        verifyCleanInstalledApexPackages(2);
+        verify(mMockDevice, times(3)).reboot();
         verify(mMockDevice, times(2)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice, times(1)).getInstalledPackageNames();
@@ -1482,8 +1483,8 @@
                         add_to_session_res.getStderr(), add_to_session_res.getStdout())));
         }
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
-        verify(mMockDevice, times(2)).reboot();
+        verifyCleanInstalledApexPackages(2);
+        verify(mMockDevice, times(3)).reboot();
         verify(mMockDevice, times(2)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice, times(1)).getInstalledPackageNames();
@@ -1546,8 +1547,8 @@
                                                     commit_session_res.getStdout())));
         }
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
-        verify(mMockDevice, times(2)).reboot();
+        verifyCleanInstalledApexPackages(2);
+        verify(mMockDevice, times(3)).reboot();
         verify(mMockDevice, times(2)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice, times(1)).getInstalledPackageNames();
@@ -1571,7 +1572,7 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(1);
         verifySuccessfulInstallMultiPackages();
         verify(mMockDevice, times(3)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
@@ -1656,7 +1657,7 @@
 
             mInstallApexModuleTargetPreparer.setUp(mTestInfo);
             mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-            verifyCleanInstalledApexPackages();
+            verifyCleanInstalledApexPackages(2);
             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
@@ -1673,7 +1674,7 @@
                             anyString(),
                             Mockito.any(ITestDevice.class),
                             Mockito.any(IBuildInfo.class));
-            verify(mMockDevice, times(3)).reboot();
+            verify(mMockDevice, times(4)).reboot();
             verify(mMockDevice, times(1)).executeAdbCommand(trainInstallCmd.toArray(new String[0]));
             verify(mMockDevice, times(1))
                     .executeShellCommand("pm rollback-app " + SPLIT_APEX_PACKAGE_NAME);
@@ -1768,7 +1769,7 @@
 
             mInstallApexModuleTargetPreparer.setUp(mTestInfo);
             mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-            verifyCleanInstalledApexPackages();
+            verifyCleanInstalledApexPackages(2);
             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
@@ -1785,7 +1786,7 @@
                             anyString(),
                             Mockito.any(ITestDevice.class),
                             Mockito.any(IBuildInfo.class));
-            verify(mMockDevice, times(3)).reboot();
+            verify(mMockDevice, times(4)).reboot();
             verify(mMockDevice, times(1)).executeAdbCommand(trainInstallCmd.toArray(new String[0]));
             verify(mMockDevice, times(3)).getActiveApexes();
             verify(mMockDevice, times(1))
@@ -1882,7 +1883,7 @@
 
             mInstallApexModuleTargetPreparer.setUp(mTestInfo);
             mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-            verifyCleanInstalledApexPackages();
+            verifyCleanInstalledApexPackages(2);
             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
@@ -1899,7 +1900,7 @@
                             anyString(),
                             Mockito.any(ITestDevice.class),
                             Mockito.any(IBuildInfo.class));
-            verify(mMockDevice, times(3)).reboot();
+            verify(mMockDevice, times(4)).reboot();
             verify(mMockDevice, times(1)).executeAdbCommand(trainInstallCmd.toArray(new String[0]));
             verify(mMockDevice, times(3)).getActiveApexes();
             verify(mMockDevice, times(1))
@@ -1996,7 +1997,7 @@
 
             mInstallApexModuleTargetPreparer.setUp(mTestInfo);
             mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-            verifyCleanInstalledApexPackages();
+            verifyCleanInstalledApexPackages(2);
             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
@@ -2013,7 +2014,7 @@
                             anyString(),
                             Mockito.any(ITestDevice.class),
                             Mockito.any(IBuildInfo.class));
-            verify(mMockDevice, times(3)).reboot();
+            verify(mMockDevice, times(4)).reboot();
             verify(mMockDevice, times(1)).executeAdbCommand(trainInstallCmd.toArray(new String[0]));
             verify(mMockDevice, times(3)).getActiveApexes();
             verify(mMockDevice, times(1))
@@ -2283,7 +2284,7 @@
 
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
-        verifyCleanInstalledApexPackages();
+        verifyCleanInstalledApexPackages(1);
         verify(mMockDevice, times(1)).reboot();
         verify(mMockDevice, times(2)).getActiveApexes();
     }
@@ -2309,7 +2310,7 @@
         mInstallApexModuleTargetPreparer.setUp(mTestInfo);
         mInstallApexModuleTargetPreparer.tearDown(mTestInfo, null);
         verifySuccessfulInstallMultiPackages();
-        verify(mMockDevice, times(3)).reboot();
+        verify(mMockDevice, times(4)).reboot();
         verify(mMockDevice, times(3)).getActiveApexes();
         verify(mMockDevice, times(1)).executeShellCommand("pm rollback-app " + APEX_PACKAGE_NAME);
         verify(mMockDevice, times(1)).getInstalledPackageNames();
@@ -2483,10 +2484,10 @@
         when(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).thenReturn(res);
     }
 
-    private void verifyCleanInstalledApexPackages() throws DeviceNotAvailableException {
-        verify(mMockDevice, times(1)).deleteFile(APEX_DATA_DIR + "*");
-        verify(mMockDevice, times(1)).deleteFile(SESSION_DATA_DIR + "*");
-        verify(mMockDevice, times(1)).deleteFile(STAGING_DATA_DIR + "*");
+    private void verifyCleanInstalledApexPackages(int count) throws DeviceNotAvailableException {
+        verify(mMockDevice, times(count)).deleteFile(APEX_DATA_DIR + "*");
+        verify(mMockDevice, times(count)).deleteFile(SESSION_DATA_DIR + "*");
+        verify(mMockDevice, times(count)).deleteFile(STAGING_DATA_DIR + "*");
     }
 
     private void setActivatedApex() throws DeviceNotAvailableException {
diff --git a/javatests/com/android/tradefed/targetprep/RunCommandTargetPreparerTest.java b/javatests/com/android/tradefed/targetprep/RunCommandTargetPreparerTest.java
index d77b5a1..80d38fa 100644
--- a/javatests/com/android/tradefed/targetprep/RunCommandTargetPreparerTest.java
+++ b/javatests/com/android/tradefed/targetprep/RunCommandTargetPreparerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.tradefed.targetprep;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
@@ -83,6 +85,8 @@
         when(mMockDevice.executeShellV2Command(Mockito.eq(command))).thenReturn(res);
 
         mPreparer.setUp(mTestInfo);
+
+        assertThat(mPreparer.getCommands()).containsExactly(command);
     }
 
     /**
diff --git a/javatests/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java b/javatests/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java
index 4ff162c..d85d201 100644
--- a/javatests/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java
+++ b/javatests/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparerTest.java
@@ -16,9 +16,8 @@
 
 package com.android.tradefed.targetprep;
 
-import static com.android.tradefed.targetprep.RunOnSecondaryUserTargetPreparer.SKIP_TESTS_REASON_KEY;
-import static com.android.tradefed.targetprep.RunOnSecondaryUserTargetPreparer.TEST_PACKAGE_NAME_OPTION;
 import static com.android.tradefed.targetprep.RunOnSecondaryUserTargetPreparer.RUN_TESTS_AS_USER_KEY;
+import static com.android.tradefed.targetprep.RunOnSecondaryUserTargetPreparer.TEST_PACKAGE_NAME_OPTION;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -29,7 +28,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.UserInfo;
 import com.android.tradefed.invoker.TestInformation;
@@ -57,9 +55,6 @@
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private TestInformation mTestInfo;
 
-    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
-    private IConfiguration mConfiguration;
-
     private RunOnSecondaryUserTargetPreparer mPreparer;
     private OptionSetter mOptionSetter;
 
@@ -67,7 +62,6 @@
     public void setUp() throws Exception {
         mPreparer = new RunOnSecondaryUserTargetPreparer();
         mOptionSetter = new OptionSetter(mPreparer);
-        mPreparer.setConfiguration(mConfiguration);
 
         ArrayList<Integer> userIds = new ArrayList<>();
         userIds.add(0);
@@ -278,17 +272,8 @@
 
         mPreparer.setUp(mTestInfo);
 
-        verify(mConfiguration)
-                .injectOptionValue(eq("instrumentation-arg"), eq(SKIP_TESTS_REASON_KEY), any());
-    }
-
-    @Test
-    public void setUp_doesNotSupportAdditionalUsers_disablesTearDown() throws Exception {
-        when(mTestInfo.getDevice().getMaxNumberOfUsersSupported()).thenReturn(1);
-
-        mPreparer.setUp(mTestInfo);
-
-        assertThat(mPreparer.isTearDownDisabled()).isTrue();
+        verify(mTestInfo.properties())
+                .put(eq(RunOnSecondaryUserTargetPreparer.SKIP_TESTS_REASON_KEY), any());
     }
 
     @Test
@@ -317,7 +302,7 @@
 
         mPreparer.setUp(mTestInfo);
 
-        verify(mConfiguration, never())
-                .injectOptionValue(eq("instrumentation-arg"), eq(SKIP_TESTS_REASON_KEY), any());
+        verify(mTestInfo.properties(), never())
+                .put(eq(RunOnSecondaryUserTargetPreparer.SKIP_TESTS_REASON_KEY), any());
     }
 }
diff --git a/javatests/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java b/javatests/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java
index 2545359..441232a 100644
--- a/javatests/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java
+++ b/javatests/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparerTest.java
@@ -22,15 +22,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static junit.framework.Assert.fail;
-
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.UserInfo;
 import com.android.tradefed.invoker.TestInformation;
@@ -58,9 +56,6 @@
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private TestInformation mTestInfo;
 
-    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
-    private IConfiguration mConfiguration;
-
     private RunOnWorkProfileTargetPreparer mPreparer;
     private OptionSetter mOptionSetter;
 
@@ -88,7 +83,6 @@
     public void setUp() throws Exception {
         mPreparer = new RunOnWorkProfileTargetPreparer();
         mOptionSetter = new OptionSetter(mPreparer);
-        mPreparer.setConfiguration(mConfiguration);
 
         ArrayList<Integer> userIds = new ArrayList<>();
         userIds.add(0);
@@ -161,9 +155,9 @@
 
         try {
             mPreparer.setUp(mTestInfo);
-            fail();
+            fail("Should have thrown exception");
         } catch (IllegalStateException expected) {
-
+            // Expected
         }
     }
 
@@ -367,17 +361,7 @@
 
         mPreparer.setUp(mTestInfo);
 
-        verify(mConfiguration)
-                .injectOptionValue(eq("instrumentation-arg"), eq(SKIP_TESTS_REASON_KEY), any());
-    }
-
-    @Test
-    public void setUp_doesNotSupportManagedUsers_disablesTearDown() throws Exception {
-        when(mTestInfo.getDevice().hasFeature("android.software.managed_users")).thenReturn(false);
-
-        mPreparer.setUp(mTestInfo);
-
-        assertThat(mPreparer.isTearDownDisabled()).isTrue();
+        verify(mTestInfo.properties()).put(eq(SKIP_TESTS_REASON_KEY), any());
     }
 
     @Test
@@ -387,25 +371,7 @@
         mPreparer.setUp(mTestInfo);
 
         verify(mTestInfo.properties(), never()).put(eq(RUN_TESTS_AS_USER_KEY), any());
-    }
-
-    @Test
-    public void setUp_doesNotSupportAdditionalUsers_setsArgumentToSkipTests() throws Exception {
-        when(mTestInfo.getDevice().getMaxNumberOfUsersSupported()).thenReturn(1);
-
-        mPreparer.setUp(mTestInfo);
-
-        verify(mConfiguration)
-                .injectOptionValue(eq("instrumentation-arg"), eq(SKIP_TESTS_REASON_KEY), any());
-    }
-
-    @Test
-    public void setUp_doesNotSupportAdditionalUsers_disablesTearDown() throws Exception {
-        when(mTestInfo.getDevice().getMaxNumberOfUsersSupported()).thenReturn(1);
-
-        mPreparer.setUp(mTestInfo);
-
-        assertThat(mPreparer.isTearDownDisabled()).isTrue();
+        verify(mTestInfo.properties()).put(eq(SKIP_TESTS_REASON_KEY), any());
     }
 
     @Test
@@ -446,7 +412,6 @@
 
         mPreparer.setUp(mTestInfo);
 
-        verify(mConfiguration, never())
-                .injectOptionValue(eq("instrumentation-arg"), eq(SKIP_TESTS_REASON_KEY), any());
+        verify(mTestInfo.properties(), never()).put(eq(SKIP_TESTS_REASON_KEY), any());
     }
 }
diff --git a/javatests/com/android/tradefed/targetprep/VisibleBackgroundUserPreparerTest.java b/javatests/com/android/tradefed/targetprep/VisibleBackgroundUserPreparerTest.java
new file mode 100644
index 0000000..d0d0447
--- /dev/null
+++ b/javatests/com/android/tradefed/targetprep/VisibleBackgroundUserPreparerTest.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2023 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.VisibleBackgroundUserPreparer.RUN_TESTS_AS_USER_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.ITestDeviceMockHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/** Unit tests for {@link VisibleBackgroundUserPreparer} */
+@RunWith(MockitoJUnitRunner.class)
+public final class VisibleBackgroundUserPreparerTest {
+
+    @Mock private ITestDevice mMockDevice;
+
+    private OptionSetter mSetter;
+
+    private ITestDeviceMockHelper mTestDeviceMockHelper;
+    private VisibleBackgroundUserPreparer mPreparer;
+    private TestInformation mTestInfo;
+
+    @Before
+    public void setFixtures() throws Exception {
+        mTestDeviceMockHelper = new ITestDeviceMockHelper(mMockDevice);
+        mPreparer = new VisibleBackgroundUserPreparer();
+        mSetter = new OptionSetter(mPreparer);
+
+        IInvocationContext context = new InvocationContext();
+        context.addAllocatedDevice("device", mMockDevice);
+        mTestInfo = TestInformation.newBuilder().setInvocationContext(context).build();
+
+        mockIsVisibleBackgroundUsersSupported(true);
+    }
+
+    @Test
+    public void testSetUp_featureNotSupported() throws Exception {
+        mockIsVisibleBackgroundUsersSupported(false);
+
+        TargetSetupError e = assertThrows(TargetSetupError.class, () -> mPreparer.setUp(mTestInfo));
+        assertThat(e).hasMessageThat().contains("not supported");
+
+        verifyNoUserCreated();
+        verifyNoUserRemoved();
+        verifyNoUserSwitched();
+    }
+
+    @Test
+    public void testSetUp_tearDown_noDisplayAvailable() throws Exception {
+        mockListDisplayIdsForStartingVisibleBackgroundUsers(Collections.emptySet());
+        mockCreateUser(42);
+
+        TargetSetupError e = assertThrows(TargetSetupError.class, () -> mPreparer.setUp(mTestInfo));
+        assertThat(e).hasMessageThat().containsMatch("No display.*available.* .*42.*");
+
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
+        verifyUserRemoved(42);
+        verifyNoUserSwitched();
+    }
+
+    @Test
+    public void testSetUp_tearDown() throws Exception {
+        mockListDisplayIdsForStartingVisibleBackgroundUsers(orderedSetOf(108));
+        mockCreateUser(42);
+        mockStartUserVisibleOnBackground(42, 108);
+
+        mPreparer.setUp(mTestInfo);
+        verifyUserCreated();
+        verifyUserStartedVisibleOnBackground(42, 108);
+        verifyTestInfoProperty(RUN_TESTS_AS_USER_KEY, "42");
+        verifyNoUserSwitched();
+
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
+        verifyUserStopped(42);
+        verifyUserRemoved(42);
+        verifyNoUserSwitched();
+    }
+
+    @Test
+    public void testSetUp_specificDisplayByOption() throws Exception {
+        setParam("display-id", "108");
+        mockCreateUser(42);
+        mockStartUserVisibleOnBackground(42, 108);
+
+        mPreparer.setUp(mTestInfo);
+        verifyUserCreated();
+        verifyUserStartedVisibleOnBackground(42, 108);
+        verifyTestInfoProperty(RUN_TESTS_AS_USER_KEY, "42");
+        verifyNoUserSwitched();
+    }
+
+    @Test
+    public void testSetUp_specificDisplayBySetter() throws Exception {
+        setParam("display-id", "666");
+        mPreparer.setDisplayId(108);
+        mockCreateUser(42);
+        mockStartUserVisibleOnBackground(42, 108);
+
+        mPreparer.setUp(mTestInfo);
+        verifyUserCreated();
+        verifyUserStartedVisibleOnBackground(42, 108);
+        verifyTestInfoProperty(RUN_TESTS_AS_USER_KEY, "42");
+        verifyNoUserSwitched();
+    }
+
+    @Test
+    public void
+            testSetUp_useDefaultDisplayWhenVisibleBackgroundUsersOnDefaultDisplayIsNotSupported()
+                    throws Exception {
+        mockIsVisibleBackgroundUsersOnDefaultDisplaySupported(false);
+        mockListDisplayIdsForStartingVisibleBackgroundUsers(orderedSetOf(0, 108));
+        mockCreateUser(42);
+        mockStartUserVisibleOnBackground(42, 0);
+
+        mPreparer.setUp(mTestInfo);
+        verifyUserCreated();
+        verifyUserStartedVisibleOnBackground(42, 0);
+        verifyTestInfoProperty(RUN_TESTS_AS_USER_KEY, "42");
+        verifyNoUserSwitched();
+    }
+
+    @Test
+    public void
+            testSetUp_ignoreDefaultDisplayWhenVisibleBackgroundUsersOnDefaultDisplayIsSupported()
+                    throws Exception {
+        mockIsVisibleBackgroundUsersOnDefaultDisplaySupported(true);
+        mockListDisplayIdsForStartingVisibleBackgroundUsers(orderedSetOf(0, 108));
+        mockCreateUser(42);
+        mockStartUserVisibleOnBackground(42, 108);
+
+        mPreparer.setUp(mTestInfo);
+        verifyUserCreated();
+        verifyUserStartedVisibleOnBackground(42, 108);
+        verifyTestInfoProperty(RUN_TESTS_AS_USER_KEY, "42");
+        verifyNoUserSwitched();
+    }
+
+    @Test
+    public void testSetUp_onlyDefaultDisplayWhenVisibleBackgroundUsersOnDefaultDisplayIsSupported()
+            throws Exception {
+        mockIsVisibleBackgroundUsersOnDefaultDisplaySupported(true);
+        mockListDisplayIdsForStartingVisibleBackgroundUsers(orderedSetOf(0));
+        mockCreateUser(42);
+        mockStartUserVisibleOnBackground(42, 108);
+
+        assertThrows(TargetSetupError.class, () -> mPreparer.setUp(mTestInfo));
+
+        verifyNoUserStartedVisibleOnBackground();
+    }
+
+    @Test
+    public void testSetDisplay_invalidId() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mPreparer.setDisplayId(VisibleBackgroundUserPreparer.INVALID_DISPLAY));
+    }
+
+    @Test
+    public void testSetUp_tearDown_reuseTestUser_invisible() throws Exception {
+        setParam("reuse-test-user", "true");
+        mockListDisplayIdsForStartingVisibleBackgroundUsers(orderedSetOf(108));
+        Map<Integer, UserInfo> existingUsers =
+                Map.of(
+                        0,
+                                new UserInfo(
+                                        /* id= */ 0,
+                                        /* userName= */ null,
+                                        /* flags= */ 0x00000013,
+                                        /* isRunning= */ true),
+                        42,
+                                new UserInfo(
+                                        /* id= */ 42,
+                                        "tf_created_user",
+                                        /* flags= */ 0,
+                                        /* isRunning= */ false));
+        mockGetUserInfos(existingUsers);
+        mockStartUserVisibleOnBackground(42, 108);
+
+        mPreparer.setUp(mTestInfo);
+        // We should reuse the existing, not create a new user.
+        verifyNoUserCreated();
+        verifyUserStartedVisibleOnBackground(42, 108);
+        verifyNoUserSwitched();
+        verifyTestInfoProperty(RUN_TESTS_AS_USER_KEY, "42");
+
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
+        // We should keep the user for the next module to reuse.
+        verifyUserStopped(42);
+        verifyNoUserRemoved();
+        verifyNoUserSwitched();
+    }
+
+    @Test
+    public void testSetUp_tearDown_reuseTestUser_alreadyVisible() throws Exception {
+        setParam("reuse-test-user", "true");
+        mockListDisplayIdsForStartingVisibleBackgroundUsers(orderedSetOf(108));
+        Map<Integer, UserInfo> existingUsers =
+                Map.of(
+                        0,
+                                new UserInfo(
+                                        /* id= */ 0,
+                                        /* userName= */ null,
+                                        /* flags= */ 0x00000013,
+                                        /* isRunning= */ true),
+                        42,
+                                new UserInfo(
+                                        /* id= */ 42,
+                                        "tf_created_user",
+                                        /* flags= */ 0,
+                                        /* isRunning= */ false));
+        mockGetUserInfos(existingUsers);
+        mockIsUserVisibleOnDisplay(42, 108);
+
+        mPreparer.setUp(mTestInfo);
+        // We should reuse the existing, not create a new user.
+        verifyNoUserCreated();
+        verifyNoUserStartedVisibleOnBackground();
+        verifyNoUserStarted();
+        verifyNoUserSwitched();
+        verifyTestInfoProperty(RUN_TESTS_AS_USER_KEY, "42");
+
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
+        // We should keep the user for the next module to reuse.
+        verifyNoUserRemoved();
+        verifyNoUserSwitched();
+        verifyNoUserStopped();
+    }
+
+    @Test
+    public void testSetUp_tearDown_reuseTestUser_noExistingTestUser() throws Exception {
+        setParam("reuse-test-user", "true");
+        mockListDisplayIdsForStartingVisibleBackgroundUsers(orderedSetOf(108));
+        Map<Integer, UserInfo> existingUsers =
+                Map.of(
+                        0,
+                        new UserInfo(
+                                /* id= */ 0,
+                                /* userName= */ null,
+                                /* flags= */ 0x00000013,
+                                /* isRunning= */ true));
+        mockGetUserInfos(existingUsers);
+        mockCreateUser(42);
+        mockStartUserVisibleOnBackground(42, 108);
+
+        mPreparer.setUp(mTestInfo);
+        verifyUserCreated();
+        verifyUserStartedVisibleOnBackground(42, 108);
+        verifyTestInfoProperty(RUN_TESTS_AS_USER_KEY, "42");
+        verifyNoUserStarted();
+        verifyNoUserSwitched();
+
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
+        // Newly created user is kept to reuse it in the next run.
+        verifyUserNotRemoved(42);
+        verifyUserStopped(42);
+        verifyNoUserSwitched();
+    }
+
+    @Test
+    public void testSetUp_maxUsersReached() throws Exception {
+        Map<Integer, UserInfo> existingUsers =
+                Map.of(
+                        0,
+                                new UserInfo(
+                                        /* id= */ 0,
+                                        /* userName= */ null,
+                                        /* flags= */ 0x00000013,
+                                        /* isRunning= */ true),
+                        11,
+                                new UserInfo(
+                                        /* id= */ 11,
+                                        "tf_created_user",
+                                        /* flags= */ 0,
+                                        /* isRunning= */ true),
+                        13,
+                                new UserInfo(
+                                        /* id= */ 13,
+                                        "tf_created_user",
+                                        /* flags= */ 0,
+                                        /* isRunning= */ false));
+
+        mockGetMaxNumberOfUsersSupported(3);
+        mockGetUserInfos(existingUsers);
+        Exception cause = mockCreateUserFailure("D'OH!");
+
+        TargetSetupError e = assertThrows(TargetSetupError.class, () -> mPreparer.setUp(mTestInfo));
+        assertThat(e).hasCauseThat().isSameInstanceAs(cause);
+
+        // verify that it removed the existing tradefed users.
+        verifyUserRemoved(11);
+        verifyUserRemoved(13);
+    }
+
+    @Test
+    public void testSetUp_createUserfailed() throws Exception {
+        Exception cause = mockCreateUserFailure("D'OH!");
+
+        TargetSetupError e = assertThrows(TargetSetupError.class, () -> mPreparer.setUp(mTestInfo));
+
+        assertThat(e).hasCauseThat().isSameInstanceAs(cause);
+    }
+
+    @Test
+    public void testSetUp_starUserFailed() throws Exception {
+        mockListDisplayIdsForStartingVisibleBackgroundUsers(orderedSetOf(108));
+        mockCreateUser(12);
+        mockStartUserVisibleOnBackground(42, 108, /*result= */ false);
+
+        TargetSetupError e = assertThrows(TargetSetupError.class, () -> mPreparer.setUp(mTestInfo));
+
+        assertThat(e).hasMessageThat().containsMatch(".ailed.*start.*12.*108");
+    }
+
+    @Test
+    public void testTearDown_only() throws Exception {
+        mPreparer.tearDown(mTestInfo, /* e= */ null);
+
+        verifyNoUserRemoved();
+        verifyNoUserStopped();
+    }
+
+    private <T> Set<T> orderedSetOf(@SuppressWarnings("unchecked") T... elements) {
+        return new LinkedHashSet<>(Arrays.asList(elements));
+    }
+
+    private void setParam(String key, String value) throws ConfigurationException {
+        CLog.i("Setting param: '%s'='%s'", key, value);
+        mSetter.setOptionValue(key, value);
+    }
+
+    private void mockGetUserInfos(Map<Integer, UserInfo> existingUsers) throws Exception {
+        mTestDeviceMockHelper.mockGetUserInfos(existingUsers);
+    }
+
+    private void mockGetMaxNumberOfUsersSupported(int max) throws Exception {
+        mTestDeviceMockHelper.mockGetMaxNumberOfUsersSupported(max);
+    }
+
+    private void mockStartUserVisibleOnBackground(int userId, int displayId) throws Exception {
+        mTestDeviceMockHelper.mockStartUserVisibleOnBackground(userId, displayId);
+    }
+
+    private void mockStartUserVisibleOnBackground(int userId, int displayId, boolean result)
+            throws Exception {
+        mTestDeviceMockHelper.mockStartUserVisibleOnBackground(userId, displayId, result);
+    }
+
+    private void mockCreateUser(int userId) throws Exception {
+        mTestDeviceMockHelper.mockCreateUser(userId);
+    }
+
+    private IllegalStateException mockCreateUserFailure(String message) throws Exception {
+        return mTestDeviceMockHelper.mockCreateUserFailure(message);
+    }
+
+    private void mockIsVisibleBackgroundUsersSupported(boolean supported) throws Exception {
+        mTestDeviceMockHelper.mockIsVisibleBackgroundUsersSupported(supported);
+    }
+
+    private void mockIsVisibleBackgroundUsersOnDefaultDisplaySupported(boolean supported)
+            throws Exception {
+        mTestDeviceMockHelper.mockIsVisibleBackgroundUsersOnDefaultDisplaySupported(supported);
+    }
+
+    private void mockIsUserVisibleOnDisplay(int userId, int displayId) throws Exception {
+        mTestDeviceMockHelper.mockIsUserVisibleOnDisplay(userId, displayId);
+    }
+
+    private void mockListDisplayIdsForStartingVisibleBackgroundUsers(Set<Integer> displays)
+            throws Exception {
+        mTestDeviceMockHelper.mockListDisplayIdsForStartingVisibleBackgroundUsers(displays);
+    }
+
+    private void verifyNoUserCreated() throws Exception {
+        mTestDeviceMockHelper.verifyNoUserCreated();
+    }
+
+    private void verifyUserCreated() throws Exception {
+        mTestDeviceMockHelper.verifyUserCreated();
+    }
+
+    private void verifyNoUserSwitched() throws Exception {
+        mTestDeviceMockHelper.verifyNoUserSwitched();
+    }
+
+    private void verifyNoUserStarted() throws Exception {
+        mTestDeviceMockHelper.verifyNoUserStarted();
+    }
+
+    private void verifyNoUserStartedVisibleOnBackground() throws Exception {
+        mTestDeviceMockHelper.verifyNoUserStartedVisibleOnBackground();
+    }
+
+    private void verifyUserStartedVisibleOnBackground(int userId, int displayId) throws Exception {
+        mTestDeviceMockHelper.verifyUserStartedVisibleOnBackground(userId, displayId);
+    }
+
+    private void verifyUserStopped(int userId) throws Exception {
+        mTestDeviceMockHelper.verifyUserStopped(userId);
+    }
+
+    private void verifyNoUserStopped() throws Exception {
+        mTestDeviceMockHelper.verifyNoUserStopped();
+    }
+
+    private void verifyNoUserRemoved() throws Exception {
+        mTestDeviceMockHelper.verifyNoUserRemoved();
+    }
+
+    private void verifyUserRemoved(int userId) throws Exception {
+        mTestDeviceMockHelper.verifyUserRemoved(userId);
+    }
+
+    private void verifyUserNotRemoved(int userId) throws Exception {
+        mTestDeviceMockHelper.verifyUserNotRemoved(userId);
+    }
+
+    private void verifyTestInfoProperty(String key, String expectedValue) {
+        String actualValue = mTestInfo.properties().get(key);
+        assertWithMessage("value of property %s (all properties: %s)", key, mTestInfo.properties())
+                .that(actualValue)
+                .isEqualTo(expectedValue);
+    }
+}
diff --git a/javatests/com/android/tradefed/targetprep/sync/DeviceSyncHelperFuncTest.java b/javatests/com/android/tradefed/targetprep/sync/DeviceSyncHelperFuncTest.java
index fd2fe83..59412eb 100644
--- a/javatests/com/android/tradefed/targetprep/sync/DeviceSyncHelperFuncTest.java
+++ b/javatests/com/android/tradefed/targetprep/sync/DeviceSyncHelperFuncTest.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
@@ -37,6 +38,7 @@
     public void testSyncDevice() throws DeviceNotAvailableException {
         String buildId = getDevice().getProperty("ro.system.build.version.incremental");
         CLog.d("%s / buildid: %s", getDevice().getProperty("ro.build.fingerprint"), buildId);
+        printBuildProp(getDevice());
 
         File targetFiles = getBuild().getFile("target_files");
         if (targetFiles == null || !targetFiles.exists() || !targetFiles.isDirectory()) {
@@ -50,7 +52,14 @@
         String afterBuildId = getDevice().getProperty("ro.system.build.version.incremental");
         CLog.d("Initial build: %s. Final build: %s", buildId, afterBuildId);
         CLog.d("%s", getDevice().getProperty("ro.build.fingerprint"));
+        printBuildProp(getDevice());
 
         Truth.assertThat(buildId).isNotEqualTo(afterBuildId);
     }
+
+    private void printBuildProp(ITestDevice device) throws DeviceNotAvailableException {
+        String output = device.executeAdbCommand("shell", "cat", "/system/build.prop");
+        CLog.e("================ build.prop");
+        CLog.e(output);
+    }
 }
diff --git a/javatests/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java b/javatests/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
index 0dfb652..827be8f 100644
--- a/javatests/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
+++ b/javatests/com/android/tradefed/testtype/mobly/MoblyBinaryHostTestTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -64,7 +65,9 @@
 import java.io.Writer;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 /** Unit tests for {@link MoblyBinaryHostTest}. */
 @RunWith(JUnit4.class)
@@ -88,6 +91,7 @@
     private File mVenvDir;
     private DeviceBuildInfo mMockBuildInfo;
     private TestInformation mTestInfo;
+    private Set<String> mIncludeFilters = new LinkedHashSet<>();
 
     @Before
     public void setUp() throws Exception {
@@ -145,7 +149,7 @@
         mSpyTest.run(mTestInfo, Mockito.mock(ITestInvocationListener.class));
 
         verify(mSpyTest.getRunUtil()).runTimedCmd(anyLong(), any());
-        assertFalse(new File(mSpyTest.getLogDirAbsolutePath()).exists());
+        assertNull(mSpyTest.getLogDirFile());
     }
 
     @Test
@@ -184,7 +188,7 @@
         mSpyTest.run(mTestInfo, Mockito.mock(ITestInvocationListener.class));
 
         verify(mSpyTest.getRunUtil()).runTimedCmd(anyLong(), any());
-        assertFalse(new File(mSpyTest.getLogDirAbsolutePath()).exists());
+        assertNull(mSpyTest.getLogDirFile());
     }
 
     @Test
@@ -227,7 +231,7 @@
             assertThat(e)
                     .hasMessageThat()
                     .contains("Fail to find test summary file test_summary.yaml under directory");
-            assertFalse(new File(mSpyTest.getLogDirAbsolutePath()).exists());
+            assertNull(mSpyTest.getLogDirFile());
         }
     }
 
@@ -384,6 +388,28 @@
     }
 
     @Test
+    public void testBuildCommandLineArrayWithIncludeFilter() throws Exception {
+        Mockito.doReturn(DEVICE_SERIAL).when(mMockDevice).getSerialNumber();
+        Mockito.doReturn(LOG_PATH).when(mSpyTest).getLogDirAbsolutePath();
+        mIncludeFilters.addAll(
+            Arrays.asList("ExampleTest#test_print_addresses", "ExampleTest#test_le_connect"));
+        mSpyTest.addAllIncludeFilters(mIncludeFilters);
+        String[] cmdArray = mSpyTest.buildCommandLineArray(BINARY_PATH, "path");
+        Truth.assertThat(cmdArray)
+                .isEqualTo(
+                        new String[] {
+                            BINARY_PATH,
+                            "--",
+                            "--config=path",
+                            "--device_serial=" + DEVICE_SERIAL,
+                            "--log_path=" + LOG_PATH,
+                            "--tests",
+                            "ExampleTest.test_print_addresses",
+                            "ExampleTest.test_le_connect"
+                        });
+    }
+
+    @Test
     public void testProcessYamlTestResultsSuccess() throws Exception {
         Mockito.doNothing().when(mSpyTest).reportLogs(any(), any());
         mMockSummaryInputStream = Mockito.mock(InputStream.class);
diff --git a/javatests/com/android/tradefed/testtype/suite/ModuleListenerTest.java b/javatests/com/android/tradefed/testtype/suite/ModuleListenerTest.java
index dc8c882..b37f1bb 100644
--- a/javatests/com/android/tradefed/testtype/suite/ModuleListenerTest.java
+++ b/javatests/com/android/tradefed/testtype/suite/ModuleListenerTest.java
@@ -20,6 +20,8 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
@@ -43,7 +45,8 @@
     @Before
     public void setUp() {
         mStubListener = new ITestInvocationListener() {};
-        mListener = new ModuleListener(mStubListener);
+        IInvocationContext context = new InvocationContext();
+        mListener = new ModuleListener(mStubListener, context);
     }
 
     /** Test that a regular execution yield the proper number of tests. */
diff --git a/javatests/com/android/tradefed/testtype/suite/RemoteTestTimeOutEnforcerTest.java b/javatests/com/android/tradefed/testtype/suite/RemoteTestTimeOutEnforcerTest.java
index 46e929e..f79fe09 100644
--- a/javatests/com/android/tradefed/testtype/suite/RemoteTestTimeOutEnforcerTest.java
+++ b/javatests/com/android/tradefed/testtype/suite/RemoteTestTimeOutEnforcerTest.java
@@ -53,7 +53,6 @@
 
     @Before
     public void setUp() {
-        mListener = new ModuleListener(mock(ITestInvocationListener.class));
         mIRemoteTest = new StubTest();
         mConfigurationDescriptor = new ConfigurationDescriptor();
         mModuleDefinition = Mockito.mock(ModuleDefinition.class);
@@ -64,6 +63,8 @@
         Mockito.when(mModuleDefinition.getId()).thenReturn(mModuleName);
         Mockito.when(mModuleDefinition.getModuleInvocationContext())
                 .thenReturn(mModuleInvocationContext);
+        mListener =
+                new ModuleListener(mock(ITestInvocationListener.class), mModuleInvocationContext);
         mEnforcer =
                 new RemoteTestTimeOutEnforcer(mListener, mModuleDefinition, mIRemoteTest, mTimeout);
     }
diff --git a/javatests/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/javatests/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index 02fb490..9335673 100644
--- a/javatests/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/javatests/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -775,20 +775,34 @@
     }
 
     /**
-     * Test for {@link TestMappingSuiteRunner#dedupTestInfos(Set)} that tests with the same test
-     * options would be filtered out.
+     * Test for {@link TestMappingSuiteRunner#dedupTestInfos(File, Set)} that tests with the same
+     * test options would be filtered out.
      */
     @Test
     public void testDedupTestInfos() throws Exception {
         Set<TestInfo> testInfos = new HashSet<>();
         testInfos.add(createTestInfo("test", "path"));
         testInfos.add(createTestInfo("test", "path2"));
-        assertEquals(1, mRunner.dedupTestInfos(testInfos).size());
+        assertEquals(1, mRunner.dedupTestInfos(new File("anything"), testInfos).size());
 
         TestInfo anotherInfo = new TestInfo("test", "folder3", false);
         anotherInfo.addOption(new TestOption("include-filter", "value1"));
         testInfos.add(anotherInfo);
-        assertEquals(2, mRunner.dedupTestInfos(testInfos).size());
+        assertEquals(2, mRunner.dedupTestInfos(new File("anything"), testInfos).size());
+
+        // Aggregate the test-mapping sources with the same test options.
+        TestInfo anotherInfo2 = new TestInfo("test", "folder4", false);
+        anotherInfo2.addOption(new TestOption("include-filter", "value1"));
+        TestInfo anotherInfo3 = new TestInfo("test", "folder5", false);
+        anotherInfo3.addOption(new TestOption("include-filter", "value1"));
+        testInfos.clear();
+        testInfos = new HashSet<>(Arrays.asList(anotherInfo, anotherInfo2, anotherInfo3));
+        Set<TestInfo> dedupTestInfos = mRunner.dedupTestInfos(new File("anything"), testInfos);
+        assertEquals(1, dedupTestInfos.size());
+        TestInfo dedupTestInfo = dedupTestInfos.iterator().next();
+        Set<String> expected_sources =
+                new HashSet<>(Arrays.asList("folder3", "folder4", "folder5"));
+        assertEquals(expected_sources, dedupTestInfo.getSources());
     }
 
     /**
diff --git a/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserHandlerTest.java b/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserHandlerTest.java
index bb793a5..fcf0bd6 100644
--- a/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserHandlerTest.java
+++ b/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserHandlerTest.java
@@ -15,12 +15,13 @@
  */
 package com.android.tradefed.testtype.suite.params;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.targetprep.CreateUserPreparer;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.RunCommandTargetPreparer;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -29,7 +30,7 @@
 
 /** Unit tests for {@link SecondaryUserHandler}. */
 @RunWith(JUnit4.class)
-public class SecondaryUserHandlerTest {
+public final class SecondaryUserHandlerTest {
 
     private SecondaryUserHandler mHandler;
     private IConfiguration mModuleConfig;
@@ -44,15 +45,14 @@
     @Test
     public void testApplySetup() {
         TestFilterable test = new TestFilterable();
-        assertEquals(0, test.getExcludeAnnotations().size());
+        assertThat(test.getExcludeAnnotations()).isEmpty();
         mModuleConfig.setTest(test);
         mHandler.applySetup(mModuleConfig);
 
         // User zero is filtered
-        assertEquals(1, test.getExcludeAnnotations().size());
-        assertEquals(
-                "android.platform.test.annotations.SystemUserOnly",
-                test.getExcludeAnnotations().iterator().next());
+        assertThat(test.getExcludeAnnotations()).hasSize(1);
+        assertThat(test.getExcludeAnnotations().iterator().next())
+                .isEqualTo("android.platform.test.annotations.SystemUserOnly");
     }
 
     /**
@@ -62,6 +62,14 @@
     @Test
     public void testAddParameterSpecificConfig() {
         mHandler.addParameterSpecificConfig(mModuleConfig);
-        assertTrue(mModuleConfig.getTargetPreparers().get(0) instanceof CreateUserPreparer);
+        assertThat(mModuleConfig.getTargetPreparers()).hasSize(2);
+
+        ITargetPreparer preparer1 = mModuleConfig.getTargetPreparers().get(0);
+        assertThat(preparer1).isInstanceOf(CreateUserPreparer.class);
+        ITargetPreparer preparer2 = mModuleConfig.getTargetPreparers().get(1);
+        assertThat(preparer2).isInstanceOf(RunCommandTargetPreparer.class);
+        assertThat(((RunCommandTargetPreparer) preparer2).getCommands())
+                .containsExactlyElementsIn(
+                        SecondaryUserOnSecondaryDisplayHandler.LOCATION_COMMANDS);
     }
 }
diff --git a/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserOnDefaultDisplayHandlerTest.java b/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserOnDefaultDisplayHandlerTest.java
new file mode 100644
index 0000000..8fdc70f
--- /dev/null
+++ b/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserOnDefaultDisplayHandlerTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 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.suite.params;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.targetprep.CreateUserPreparer;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.RunCommandTargetPreparer;
+import com.android.tradefed.targetprep.VisibleBackgroundUserPreparer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SecondaryUserOnSecondaryDisplayHandler}. */
+@RunWith(JUnit4.class)
+public final class SecondaryUserOnDefaultDisplayHandlerTest {
+
+    private SecondaryUserOnSecondaryDisplayHandler mHandler;
+    private IConfiguration mModuleConfig;
+
+    @Before
+    public void setUp() {
+        mHandler = new SecondaryUserOnSecondaryDisplayHandler();
+        mModuleConfig = new Configuration("test", "test");
+    }
+
+    /** Test that when a module configuration go through the handler it gets tuned properly. */
+    @Test
+    public void testApplySetup() {
+        TestFilterable test = new TestFilterable();
+        assertThat(test.getExcludeAnnotations()).isEmpty();
+        mModuleConfig.setTest(test);
+        mHandler.applySetup(mModuleConfig);
+
+        // User zero is filtered
+        assertThat(test.getExcludeAnnotations()).hasSize(1);
+        assertThat(test.getExcludeAnnotations().iterator().next())
+                .isEqualTo("android.platform.test.annotations.SystemUserOnly");
+    }
+
+    @Test
+    public void testGetParameterIdentifier() {
+        assertThat(mHandler.getParameterIdentifier())
+                .isEqualTo("secondary_user_on_secondary_display");
+    }
+
+    /**
+     * Test that when a module configuration goes through the handler's
+     * addParameterSpecificConfiguration, {@link CreateUserPreparer} is added correctly.
+     */
+    @Test
+    public void testAddParameterSpecificConfig() {
+        mHandler.addParameterSpecificConfig(mModuleConfig);
+        assertThat(mModuleConfig.getTargetPreparers()).hasSize(2);
+
+        ITargetPreparer preparer1 = mModuleConfig.getTargetPreparers().get(0);
+        assertThat(preparer1).isInstanceOf(VisibleBackgroundUserPreparer.class);
+        VisibleBackgroundUserPreparer userPreparer = (VisibleBackgroundUserPreparer) preparer1;
+        assertThat(userPreparer.getDisplayId())
+                .isEqualTo(VisibleBackgroundUserPreparer.INVALID_DISPLAY);
+        ITargetPreparer preparer2 = mModuleConfig.getTargetPreparers().get(1);
+        assertThat(preparer2).isInstanceOf(RunCommandTargetPreparer.class);
+        assertThat(((RunCommandTargetPreparer) preparer2).getCommands())
+                .containsExactlyElementsIn(
+                        SecondaryUserOnSecondaryDisplayHandler.LOCATION_COMMANDS);
+    }
+}
diff --git a/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserOnSecondaryDisplayHandlerTest.java b/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserOnSecondaryDisplayHandlerTest.java
new file mode 100644
index 0000000..9bc8c31e
--- /dev/null
+++ b/javatests/com/android/tradefed/testtype/suite/params/SecondaryUserOnSecondaryDisplayHandlerTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 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.suite.params;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.targetprep.CreateUserPreparer;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.RunCommandTargetPreparer;
+import com.android.tradefed.targetprep.VisibleBackgroundUserPreparer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SecondaryUserOnSecondaryDisplayHandler}. */
+@RunWith(JUnit4.class)
+public final class SecondaryUserOnSecondaryDisplayHandlerTest {
+
+    private SecondaryUserOnDefaultDisplayHandler mHandler;
+    private IConfiguration mModuleConfig;
+
+    @Before
+    public void setUp() {
+        mHandler = new SecondaryUserOnDefaultDisplayHandler();
+        mModuleConfig = new Configuration("test", "test");
+    }
+
+    /** Test that when a module configuration go through the handler it gets tuned properly. */
+    @Test
+    public void testApplySetup() {
+        TestFilterable test = new TestFilterable();
+        assertThat(test.getExcludeAnnotations()).isEmpty();
+        mModuleConfig.setTest(test);
+        mHandler.applySetup(mModuleConfig);
+
+        // User zero is filtered
+        assertThat(test.getExcludeAnnotations()).hasSize(1);
+        assertThat(test.getExcludeAnnotations().iterator().next())
+                .isEqualTo("android.platform.test.annotations.SystemUserOnly");
+    }
+
+    @Test
+    public void testGetParameterIdentifier() {
+        assertThat(mHandler.getParameterIdentifier())
+                .isEqualTo("secondary_user_on_default_display");
+    }
+
+    /**
+     * Test that when a module configuration goes through the handler's
+     * addParameterSpecificConfiguration, {@link CreateUserPreparer} is added correctly.
+     */
+    @Test
+    public void testAddParameterSpecificConfig() {
+        mHandler.addParameterSpecificConfig(mModuleConfig);
+        assertThat(mModuleConfig.getTargetPreparers()).hasSize(2);
+
+        ITargetPreparer preparer1 = mModuleConfig.getTargetPreparers().get(0);
+        assertThat(preparer1).isInstanceOf(VisibleBackgroundUserPreparer.class);
+        ITargetPreparer preparer2 = mModuleConfig.getTargetPreparers().get(1);
+        assertThat(preparer2).isInstanceOf(RunCommandTargetPreparer.class);
+        assertThat(((RunCommandTargetPreparer) preparer2).getCommands())
+                .containsExactlyElementsIn(
+                        SecondaryUserOnSecondaryDisplayHandler.LOCATION_COMMANDS);
+    }
+}
diff --git a/javatests/com/android/tradefed/util/ITestDeviceMockHelper.java b/javatests/com/android/tradefed/util/ITestDeviceMockHelper.java
new file mode 100644
index 0000000..adcf57a
--- /dev/null
+++ b/javatests/com/android/tradefed/util/ITestDeviceMockHelper.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 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.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/** Helper class for mocking {@link ITestDevice} methods. */
+public final class ITestDeviceMockHelper {
+
+    private final ITestDevice mMockDevice;
+
+    public ITestDeviceMockHelper(ITestDevice mockDevice) {
+        mMockDevice = Objects.requireNonNull(mockDevice);
+    }
+
+    public void mockGetCurrentUser(int userId) throws Exception {
+        when(mMockDevice.getCurrentUser()).thenReturn(userId);
+    }
+
+    public void mockGetUserInfos(Map<Integer, UserInfo> existingUsers) throws Exception {
+        when(mMockDevice.getUserInfos()).thenReturn(existingUsers);
+    }
+
+    public void mockGetMaxNumberOfUsersSupported(int max) throws Exception {
+        when(mMockDevice.getMaxNumberOfUsersSupported()).thenReturn(max);
+    }
+
+    public void mockSwitchUser(int userId) throws Exception {
+        when(mMockDevice.switchUser(userId)).thenReturn(true);
+    }
+
+    public void mockStartUser(int userId) throws Exception {
+        when(mMockDevice.startUser(userId, /* waitFlag= */ true)).thenReturn(true);
+    }
+
+    public void mockStartUserVisibleOnBackground(int userId, int displayId) throws Exception {
+        mockStartUserVisibleOnBackground(userId, displayId, /* result= */ true);
+    }
+
+    public void mockStartUserVisibleOnBackground(int userId, int displayId, boolean result)
+            throws Exception {
+        when(mMockDevice.startVisibleBackgroundUser(userId, displayId, /* waitFlag= */ true))
+                .thenReturn(result);
+    }
+
+    public void mockCreateUser(int userId) throws Exception {
+        when(mMockDevice.createUser(any())).thenReturn(userId);
+    }
+
+    public IllegalStateException mockCreateUserFailure(String message) throws Exception {
+        IllegalStateException e = new IllegalStateException(message);
+        when(mMockDevice.createUser(any())).thenThrow(e);
+        return e;
+    }
+
+    public void mockIsVisibleBackgroundUsersSupported(boolean supported) throws Exception {
+        when(mMockDevice.isVisibleBackgroundUsersSupported()).thenReturn(supported);
+    }
+
+    public void mockIsVisibleBackgroundUsersOnDefaultDisplaySupported(boolean supported)
+            throws Exception {
+        when(mMockDevice.isVisibleBackgroundUsersOnDefaultDisplaySupported()).thenReturn(supported);
+    }
+
+    public void mockIsUserVisibleOnDisplay(int userId, int displayId) throws Exception {
+        when(mMockDevice.isUserVisibleOnDisplay(userId, displayId)).thenReturn(true);
+    }
+
+    public void mockListDisplayIdsForStartingVisibleBackgroundUsers(Set<Integer> displays)
+            throws Exception {
+        when(mMockDevice.listDisplayIdsForStartingVisibleBackgroundUsers()).thenReturn(displays);
+    }
+
+    public void verifyNoUserCreated() throws Exception {
+        verify(mMockDevice, never()).createUser(any());
+    }
+
+    public void verifyUserCreated() throws Exception {
+        verify(mMockDevice).createUser(any());
+    }
+
+    public void verifyNoUserSwitched() throws Exception {
+        verify(mMockDevice, never()).switchUser(anyInt());
+    }
+
+    public void verifyUserSwitched(int userId) throws Exception {
+        verify(mMockDevice).switchUser(userId);
+    }
+
+    public void verifyNoUserStarted() throws Exception {
+        verify(mMockDevice, never()).startUser(anyInt());
+        verify(mMockDevice, never()).startUser(anyInt(), anyBoolean());
+    }
+
+    public void verifyNoUserStartedVisibleOnBackground() throws Exception {
+        verify(mMockDevice, never()).startVisibleBackgroundUser(anyInt(), anyInt(), anyBoolean());
+    }
+
+    public void verifyUserStartedVisibleOnBackground(int userId, int displayId) throws Exception {
+        verify(mMockDevice).startVisibleBackgroundUser(userId, displayId, /* waitFlag= */ true);
+    }
+
+    public void verifyUserStopped(int userId) throws Exception {
+        verify(mMockDevice).stopUser(userId, /* waitFlag= */ true, /* forceFlag= */ true);
+    }
+
+    public void verifyNoUserStopped() throws Exception {
+        verify(mMockDevice, never()).stopUser(anyInt(), anyBoolean(), anyBoolean());
+    }
+
+    public void verifyNoUserRemoved() throws Exception {
+        verify(mMockDevice, never()).removeUser(anyInt());
+    }
+
+    public void verifyUserRemoved(int userId) throws Exception {
+        verify(mMockDevice).removeUser(userId);
+    }
+
+    public void verifyUserNotRemoved(int userId) throws Exception {
+        verify(mMockDevice, never()).removeUser(userId);
+    }
+
+    public void verifyListDisplayIdsForStartingVisibleBackgroundUsersNeverCalled()
+            throws Exception {
+        verify(mMockDevice, never()).listDisplayIdsForStartingVisibleBackgroundUsers();
+    }
+}
diff --git a/javatests/com/android/tradefed/util/testmapping/TestInfoTest.java b/javatests/com/android/tradefed/util/testmapping/TestInfoTest.java
index dbc97a5..591742b 100644
--- a/javatests/com/android/tradefed/util/testmapping/TestInfoTest.java
+++ b/javatests/com/android/tradefed/util/testmapping/TestInfoTest.java
@@ -50,5 +50,6 @@
                 "Host: false",
                 info.toString());
         assertEquals("test1 - false", info.getNameAndHostOnly());
+        assertEquals("test1[option1:value1, option2:value2]", info.getNameOption());
     }
 }
diff --git a/lite/Android.bp b/lite/Android.bp
index a67af7d..8d6b795 100644
--- a/lite/Android.bp
+++ b/lite/Android.bp
@@ -34,4 +34,6 @@
     static_libs: [
         "junit",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
 }
diff --git a/reference_tests/Android.bp b/reference_tests/Android.bp
index 0041a72..87adbcd 100644
--- a/reference_tests/Android.bp
+++ b/reference_tests/Android.bp
@@ -21,6 +21,8 @@
     srcs: [
         "src/java/com/android/tradefed/referencetests/SimpleFailingTest.java",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     static_libs: [
         "junit",
     ],
@@ -34,6 +36,8 @@
     srcs: [
         "src/java/com/android/tradefed/referencetests/OnePassOneFailParamTest.java",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     static_libs: [
         "junit",
     ],
@@ -47,6 +51,8 @@
     srcs: [
         "src/java/com/android/tradefed/referencetests/OnePassingOneFailingTest.java",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     static_libs: [
         "junit",
     ],
@@ -60,6 +66,8 @@
     srcs: [
         "src/java/com/android/tradefed/referencetests/SimplePassingTest.java",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     static_libs: [
         "junit",
     ],
@@ -73,6 +81,8 @@
     srcs: [
         "src/java/com/android/tradefed/referencetests/PassIgnoreAssumeTest.java",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     static_libs: [
         "junit",
     ],
@@ -88,6 +98,8 @@
         "src/java/com/android/tradefed/referencetests/OnePassingOneFailingTest.java",
         "src/java/com/android/tradefed/referencetests/SimpleFailingTest.java",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     libs: [
         "junit",
     ],
diff --git a/remote/Android.bp b/remote/Android.bp
index 5dabc9e..693de88 100644
--- a/remote/Android.bp
+++ b/remote/Android.bp
@@ -36,4 +36,6 @@
     "ddmlib-prebuilt",
     "devtools-annotations-prebuilt",
   ],
+  // b/267831518: Pin tradefed and dependencies to Java 11.
+  java_version: "11",
 }
diff --git a/res/perfetto/trace_config.textproto b/res/perfetto/trace_config.textproto
index 0c42c0d..bc63e5a 100644
--- a/res/perfetto/trace_config.textproto
+++ b/res/perfetto/trace_config.textproto
@@ -13,63 +13,126 @@
 # limitations under the License.
 # proto-message: TraceConfig
 
-# Enable periodic flushing of the trace buffer into the output file.
+# Trace config originally supplied in b/230177578.
+
+# Periodically writes the central tracing buffers (defined below) to disk.
 write_into_file: true
+file_write_period_ms: 2000
 
-# Writes the userspace buffer into the file every 1s.
-file_write_period_ms: 1000
+# Ensure worst-case reordering of events in the central tracing buffers.
+flush_period_ms: 8000
 
-# See b/126487238 - we need to guarantee ordering of events.
-flush_period_ms: 30000
-
-# The trace buffers needs to be big enough to hold |file_write_period_ms| of
+# The trace buffers need to be big enough to hold |file_write_period_ms| of
 # trace data. The trace buffer sizing depends on the number of trace categories
 # enabled and the device activity.
-
-# RSS events
-buffers {
-  size_kb: 16384
-  fill_policy: RING_BUFFER
+buffers: {
+  size_kb: 20480
+  fill_policy: DISCARD
 }
 
-# procfs polling
-buffers {
-  size_kb: 8192
-  fill_policy: RING_BUFFER
-}
-
-data_sources {
-  config {
+data_sources: {
+  config: {
     name: "linux.ftrace"
     target_buffer: 0
-    ftrace_config {
+    ftrace_config: {
       throttle_rss_stat: true
-      # These parameters affect only the kernel trace buffer size and how
-      # frequently it gets moved into the userspace buffer defined above.
-      buffer_size_kb: 16384
-      drain_period_ms: 250
+      compact_sched: {
+        enabled: true
+      }
 
-      # We need to do process tracking to ensure kernel ftrace events targeted at short-lived
-      # threads are associated correctly
-      ftrace_events: "task/task_newtask"
-      ftrace_events: "task/task_rename"
+      # core: scheduling
+      ftrace_events: "sched/sched_switch"
+      ftrace_events: "sched/sched_waking"
+
+      # core: process lifetime events
+      ftrace_events: "sched/sched_wakeup_new"
       ftrace_events: "sched/sched_process_exit"
       ftrace_events: "sched/sched_process_free"
+      ftrace_events: "task/task_newtask"
+      ftrace_events: "task/task_rename"
 
-      # Old (kernel) LMK
-      ftrace_events: "lowmemorykiller/lowmemory_kill"
+      # scheduling: why any given thread is blocked
+      ftrace_events: "sched/sched_blocked_reason"
+      # device suspend/resume events
+      ftrace_events: "power/suspend_resume"
 
-      # New (userspace) LMK
+      # RSS and ION buffer events
+      ftrace_events: "dmabuf_heap/dma_heap_stat"
+      ftrace_events: "fastrpc/fastrpc_dma_stat"
+      ftrace_events: "gpu_mem/gpu_mem_total"
+      ftrace_events: "ion/ion_stat"
+      ftrace_events: "kmem/ion_heap_grow"
+      ftrace_events: "kmem/ion_heap_shrink"
+      ftrace_events: "kmem/rss_stat"
+
+      # optional: LMK
       atrace_apps: "lmkd"
+      ftrace_events: "lowmemorykiller/lowmemory_kill"
+      ftrace_events: "oom/oom_score_adj_update"
+      ftrace_events: "oom/mark_victim"
+
+      # userspace events from system_server
+      atrace_categories: "ss"
+      atrace_apps: "system_server"
+      # userspace events from activity and window managers
+      atrace_categories: "am"
+      atrace_categories: "wm"
+      # userspace events from java and c runtimes
+      atrace_categories: "dalvik"
+      atrace_categories: "bionic"
+      # userspace events from systemui
+      atrace_apps: "com.android.systemui"
+
+      # groups that expand into power events (mix of userspace and ftrace)
+      atrace_categories: "freq"
+      # "thermal" removed for APIs <= 30, temporarily.
+      atrace_categories: "power"
+
+      # binder & HALs
+      atrace_categories: "aidl"
+      atrace_categories: "hal"
+      atrace_categories: "binder_driver"
+
+      # Other userspace event groups, see
+      # frameworks/native/cmds/atrace/atrace.cpp in the Android tree for
+      # available categories. The encoding of userspace events is very verbose
+      # so keep the list focused or you will need to readjust the buffer sizes
+      # to avoid data loss.
+      atrace_categories: "disk"
+      atrace_categories: "gfx"
+      atrace_categories: "res"
+      atrace_categories: "view"
+      atrace_categories: "idle"
+      atrace_categories: "webview"
+
+      # Default to enabling userspace events from all apps.
       atrace_apps: "*"
 
       # Following line will be used to inject additional configs. Do not remove or modify.
       # {injected_config}
-
-      atrace_categories: "am"
-      atrace_categories: "dalvik"
-      atrace_categories: "binder_driver"
     }
   }
 }
 
+data_sources {
+  config {
+    name: "linux.process_stats"
+    target_buffer: 0
+    # polled per-process memory counters and process/thread names.
+    # If you don't want the polled counters, remove the "process_stats_config"
+    # section, but keep the data source itself as it still provides on-demand
+    # thread/process naming for ftrace data below.
+    process_stats_config {
+      proc_stats_poll_ms: 500
+      scan_all_processes_on_start: true
+    }
+  }
+}
+
+data_sources: {
+  config {
+    # rendering: expected vs actual frame timeline tracks
+    name: "android.surfaceflinger.frametimeline"
+    target_buffer: 0
+  }
+}
diff --git a/src/com/android/tradefed/build/BuildInfo.java b/src/com/android/tradefed/build/BuildInfo.java
index 3f81bd2..af77b2c 100644
--- a/src/com/android/tradefed/build/BuildInfo.java
+++ b/src/com/android/tradefed/build/BuildInfo.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.service.TradefedFeatureClient;
@@ -711,7 +712,8 @@
                 InvocationMetricKey.STAGE_TESTS_INDIVIDUAL_DOWNLOADS, fileName);
         List<String> includeFilters = Arrays.asList(String.format("/%s?($|/)", fileName));
 
-        try (TradefedFeatureClient client = new TradefedFeatureClient()) {
+        try (CloseableTraceScope stage = new CloseableTraceScope("stageRemoteFile:" + fileName);
+                TradefedFeatureClient client = new TradefedFeatureClient()) {
             Map<String, String> args = new HashMap<>();
             args.put(ResolvePartialDownload.DESTINATION_DIR, workingDir.getAbsolutePath());
             args.put(ResolvePartialDownload.INCLUDE_FILTERS, String.join(";", includeFilters));
@@ -723,9 +725,12 @@
                             .map(p -> p.toString())
                             .collect(Collectors.joining(";"));
             args.put(ResolvePartialDownload.REMOTE_PATHS, remotePaths);
+            long startTime = System.currentTimeMillis();
             FeatureResponse rep =
                     client.triggerFeature(
                             ResolvePartialDownload.RESOLVE_PARTIAL_DOWNLOAD_FEATURE_NAME, args);
+            InvocationMetricLogger.addInvocationPairMetrics(
+                    InvocationMetricKey.STAGE_REMOTE_TIME, startTime, System.currentTimeMillis());
             if (rep.hasErrorInfo()) {
                 throw new HarnessRuntimeException(
                         rep.getErrorInfo().getErrorTrace(),
diff --git a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
index 2075100..845c577 100644
--- a/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
+++ b/src/com/android/tradefed/cluster/ClusterCommandScheduler.java
@@ -835,7 +835,12 @@
      */
     protected boolean dryRunCommand(final InvocationEventHandler handler, String[] args)
             throws ConfigurationException {
-        IConfiguration config = createConfiguration(args);
+        IConfiguration config = null;
+        try {
+            config = createConfiguration(args);
+        } catch (Throwable e) {
+            throw new ConfigurationException("Failed to create dry-run config", e);
+        }
         if (config.getCommandOptions().isDryRunMode()) {
             IInvocationContext context = new InvocationContext();
             context.addDeviceBuildInfo("stub", new BuildInfo());
diff --git a/src/com/android/tradefed/command/CommandOptions.java b/src/com/android/tradefed/command/CommandOptions.java
index a991c5b..b74e732 100644
--- a/src/com/android/tradefed/command/CommandOptions.java
+++ b/src/com/android/tradefed/command/CommandOptions.java
@@ -224,6 +224,16 @@
     )
     private Set<AutoLogCollector> mAutoCollectors = new LinkedHashSet<>();
 
+    @Option(
+            name = "experiment-enabled",
+            description = "A feature flag used to enable experimental flags.")
+    private boolean mExperimentEnabled = false;
+
+    @Option(
+            name = "experimental-flags",
+            description = "Map of experimental flags that can be used for feature gating projects.")
+    private Map<String, String> mExperimentalFlags = new LinkedHashMap<>();
+
     @Deprecated
     @Option(
         name = "logcat-on-failure",
@@ -597,6 +607,18 @@
 
     /** {@inheritDoc} */
     @Override
+    public boolean isExperimentEnabled() {
+        return mExperimentEnabled;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Map<String, String> getExperimentalFlags() {
+        return mExperimentalFlags;
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public boolean captureScreenshotOnFailure() {
         return mScreenshotOnFailure;
     }
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index a64c9ed..c169ccb 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -20,6 +20,7 @@
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.build.BuildRetrievalError;
+import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.clearcut.ClearcutClient;
 import com.android.tradefed.command.CommandFileParser.CommandLine;
 import com.android.tradefed.command.CommandFileWatcher.ICommandFileListener;
@@ -37,6 +38,7 @@
 import com.android.tradefed.config.IDeviceConfiguration;
 import com.android.tradefed.config.IGlobalConfiguration;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.config.RetryConfigurationFactory;
 import com.android.tradefed.config.SandboxConfigurationFactory;
 import com.android.tradefed.config.proxy.ProxyConfiguration;
@@ -553,6 +555,7 @@
         private static final String INVOC_END_EVENT_ID_KEY = "id";
         private static final String INVOC_END_EVENT_ELAPSED_KEY = "elapsed-time";
         private static final String INVOC_END_EVENT_TAG_KEY = "test-tag";
+        private static final String PRESUBMIT_BUILD_REGEX = "^P[0-9]+";
 
         private final IScheduledInvocationListener[] mListeners;
         private final IInvocationContext mInvocationContext;
@@ -598,6 +601,20 @@
                                 .getInvocationData()
                                 .containsKey(SubprocessTfLauncher.SUBPROCESS_TAG_NAME));
             }
+            // Set experimental flags for non-presubmit builds
+            if (config.getCommandOptions().isExperimentEnabled()
+                    && !isPresubmitBuild(mInvocationContext)) {
+                try {
+                    OptionSetter setter = new OptionSetter(config.getCommandOptions());
+                    for (Map.Entry<String, String> entry :
+                            config.getCommandOptions().getExperimentalFlags().entrySet()) {
+                        setter.setOptionValue(entry.getKey(), entry.getValue());
+                    }
+                } catch (ConfigurationException e) {
+                    CLog.e("Configuration Exception caught while setting experimental flags.");
+                    CLog.e(e);
+                }
+            }
             mStartTime = System.currentTimeMillis();
             ITestInvocation instance = getInvocation();
             try (CloseableTraceScope ignore = new CloseableTraceScope("init")) {
@@ -900,6 +917,18 @@
                 }
             }
         }
+
+        /**
+         * Checks if the current Invocation is for a pre-submit build or not.
+         *
+         * @param context {@link IInvocationContext} for the current test.
+         * @return returns true if invocation is for a pre-submit build, false otherwise.
+         */
+        private boolean isPresubmitBuild(IInvocationContext context) {
+            IBuildInfo build = context.getBuildInfo(context.getDevices().get(0));
+            Pattern pattern = Pattern.compile(PRESUBMIT_BUILD_REGEX);
+            return pattern.matcher(build.getBuildId()).matches();
+        }
     }
 
     /** Create a map of the devices state so they can be released appropriately. */
diff --git a/src/com/android/tradefed/command/ICommandOptions.java b/src/com/android/tradefed/command/ICommandOptions.java
index 804eb30..d9c950a 100644
--- a/src/com/android/tradefed/command/ICommandOptions.java
+++ b/src/com/android/tradefed/command/ICommandOptions.java
@@ -186,6 +186,12 @@
     /** Sets the set of auto log collectors that should be added to an invocation. */
     public void setAutoLogCollectors(Set<AutoLogCollector> autoLogCollectors);
 
+    /** Whether or not to enable experiments through experimental flags. */
+    public boolean isExperimentEnabled();
+
+    /** Returns the experimental flags map, that can be used to feature gate projects. */
+    public Map<String, String> getExperimentalFlags();
+
     /** Whether or not to capture a screenshot on test case failure */
     public boolean captureScreenshotOnFailure();
 
diff --git a/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java b/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
index 0dee110..f3e324b 100644
--- a/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/LocalAndroidVirtualDevice.java
@@ -34,7 +34,6 @@
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.MultiMap;
-import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.TarUtil;
 import com.android.tradefed.util.ZipUtil;
 
@@ -595,9 +594,4 @@
             }
         }
     }
-
-    @VisibleForTesting
-    IRunUtil createRunUtil() {
-        return new RunUtil();
-    }
 }
diff --git a/src/com/android/tradefed/device/ManagedDeviceList.java b/src/com/android/tradefed/device/ManagedDeviceList.java
index f0912e9..bcd15d9 100644
--- a/src/com/android/tradefed/device/ManagedDeviceList.java
+++ b/src/com/android/tradefed/device/ManagedDeviceList.java
@@ -22,6 +22,9 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.ConditionPriorityBlockingQueue.IMatcher;
 
+import com.google.api.client.util.Strings;
+import com.google.common.collect.ImmutableSet;
+
 import java.util.ArrayList;
 import java.util.ConcurrentModificationException;
 import java.util.Iterator;
@@ -29,6 +32,8 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.annotation.concurrent.GuardedBy;
 
@@ -42,6 +47,13 @@
  */
 class ManagedDeviceList implements Iterable<IManagedTestDevice> {
 
+    private static final Pattern IP_PATTERN =
+            Pattern.compile(ManagedTestDeviceFactory.IPADDRESS_PATTERN);
+    // Ip that are associated with special use cases and shouldn't look up the
+    // serial number. Usually because those are virtual devices and not wifi
+    // connected devices
+    private static final Set<String> RESERVED_IP = ImmutableSet.of("127.0.0.1", "0.0.0.0", "localhost");
+
     /**
      * A {@link IMatcher} for finding a {@link IManagedTestDevice} that can be allocated.
      * Will change the device state to ALLOCATED upon finding a successful match.
@@ -154,6 +166,28 @@
         return serial.length() > 1 && !serial.contains("?");
     }
 
+    private boolean isTcpDeviceSerial(String serialString) {
+        String[] serial = serialString.split(":");
+        if (serial.length == 2) {
+            if (RESERVED_IP.contains(serial[0])) {
+                return false;
+            }
+            // Check first part is an IP
+            Matcher match = IP_PATTERN.matcher(serial[0]);
+            if (!match.find()) {
+                return false;
+            }
+            // Check second part if a port
+            try {
+                Integer.parseInt(serial[1]);
+                return true;
+            } catch (NumberFormatException nfe) {
+                return false;
+            }
+        }
+        return false;
+    }
+
     /**
      * Update the {@link TestDevice#getDeviceState()} of devices as appropriate.
      *
@@ -237,12 +271,24 @@
      * @return the {@link IManagedTestDevice}.
      */
     public IManagedTestDevice findOrCreate(IDevice idevice) {
-        if (!isValidDeviceSerial(idevice.getSerialNumber())) {
+        String serial = idevice.getSerialNumber();
+        if (!isValidDeviceSerial(serial)) {
             return null;
         }
+        if (isTcpDeviceSerial(serial)) {
+            // Override serial for tcp devices into their real one
+            try {
+                String realSerial = idevice.getProperty("ro.serialno");
+                if (!Strings.isNullOrEmpty(realSerial)) {
+                    serial = realSerial.trim();
+                }
+            } catch (RuntimeException e) {
+                CLog.e(e);
+            }
+        }
         mListLock.lock();
         try {
-            IManagedTestDevice d = find(idevice.getSerialNumber());
+            IManagedTestDevice d = find(serial);
             if (d == null || DeviceAllocationState.Unavailable.equals(d.getAllocationState())) {
                 mList.remove(d);
                 d = mDeviceFactory.createDevice(idevice);
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 03bec0c..fe05eff 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -168,8 +168,6 @@
 
     /** The time in ms to wait for a device to become unavailable. Should usually be short */
     private static final int DEFAULT_UNAVAILABLE_TIMEOUT = 20 * 1000;
-    /** The time in ms to wait for a recovery that we skip because of the NONE mode */
-    static final int NONE_RECOVERY_MODE_DELAY = 1000;
 
     private static final String SIM_STATE_PROP = "gsm.sim.state";
     private static final String SIM_OPERATOR_PROP = "gsm.operator.alpha";
@@ -273,11 +271,13 @@
         private String[] mCmd;
         private long mTimeout;
         private boolean mIsShellCommand;
+        private Map<String, String> mEnvMap;
 
-        AdbAction(long timeout, String[] cmd, boolean isShell) {
+        AdbAction(long timeout, String[] cmd, boolean isShell, Map<String, String> envMap) {
             mTimeout = timeout;
             mCmd = cmd;
             mIsShellCommand = isShell;
+            mEnvMap = envMap;
         }
 
         private void logExceptionAndOutput(CommandResult result) {
@@ -288,7 +288,14 @@
 
         @Override
         public boolean run() throws TimeoutException, IOException {
-            CommandResult result = getRunUtil().runTimedCmd(mTimeout, mCmd);
+            IRunUtil runUtil = getRunUtil();
+            if (!mEnvMap.isEmpty()) {
+                runUtil = createRunUtil();
+            }
+            for (String key : mEnvMap.keySet()) {
+                runUtil.setEnvVariable(key, mEnvMap.get(key));
+            }
+            CommandResult result = runUtil.runTimedCmd(mTimeout, mCmd);
             // TODO: how to determine device not present with command failing for other reasons
             if (result.getStatus() == CommandStatus.EXCEPTION) {
                 logExceptionAndOutput(result);
@@ -406,6 +413,10 @@
         return RunUtil.getDefault();
     }
 
+    protected IRunUtil createRunUtil() {
+        return new RunUtil();
+    }
+
     /** Set the Clock instance to use. */
     @VisibleForTesting
     protected void setClock(Clock clock) {
@@ -2202,8 +2213,15 @@
     @Override
     public String executeAdbCommand(long timeout, String... cmdArgs)
             throws DeviceNotAvailableException {
+        return executeAdbCommand(getCommandTimeout(), new HashMap<>(), cmdArgs);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String executeAdbCommand(long timeout, Map<String, String> envMap, String... cmdArgs)
+            throws DeviceNotAvailableException {
         final String[] fullCmd = buildAdbCommand(cmdArgs);
-        AdbAction adbAction = new AdbAction(timeout, fullCmd, "shell".equals(cmdArgs[0]));
+        AdbAction adbAction = new AdbAction(timeout, fullCmd, "shell".equals(cmdArgs[0]), envMap);
         performDeviceAction(String.format("adb %s", cmdArgs[0]), adbAction, MAX_RETRY_ATTEMPTS);
         return adbAction.mOutput;
     }
@@ -2546,7 +2564,6 @@
     public boolean recoverDevice() throws DeviceNotAvailableException {
         if (mRecoveryMode.equals(RecoveryMode.NONE)) {
             CLog.i("Skipping recovery on %s", getSerialNumber());
-            getRunUtil().sleep(NONE_RECOVERY_MODE_DELAY);
             return false;
         }
         CLog.i("Attempting recovery on %s", getSerialNumber());
@@ -3080,6 +3097,10 @@
             CLog.d("No ANRs at %s", ANRS_PATH);
             return true;
         }
+        boolean root = enableAdbRoot();
+        if (!root) {
+            CLog.d("Skipping logAnrs, need to be root.");
+        }
         File localDir = null;
         long startTime = System.currentTimeMillis();
         try {
@@ -3448,7 +3469,14 @@
             enableAdbRoot();
             prePostBootSetup();
             for (String command : mOptions.getPostBootCommands()) {
-                executeShellCommand(command);
+                long start = System.currentTimeMillis();
+                try (CloseableTraceScope cmdTrace = new CloseableTraceScope(command)) {
+                    executeShellCommand(command);
+                }
+                if (command.startsWith("sleep")) {
+                    InvocationMetricLogger.addInvocationPairMetrics(
+                            InvocationMetricKey.host_sleep, start, System.currentTimeMillis());
+                }
             }
         } finally {
             long elapsed = System.currentTimeMillis() - startTime;
@@ -3912,7 +3940,7 @@
         }
         InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.ADB_ROOT_ROUTINE_COUNT, 1);
         long startTime = System.currentTimeMillis();
-        try {
+        try (CloseableTraceScope ignored = new CloseableTraceScope("adb_root")) {
             CLog.i("adb root on device %s", getSerialNumber());
             int attempts = MAX_RETRY_ATTEMPTS + 1;
             for (int i = 1; i <= attempts; i++) {
diff --git a/src/com/android/tradefed/device/NativeDeviceStateMonitor.java b/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
index 5fd6127..a4d90e1 100644
--- a/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
+++ b/src/com/android/tradefed/device/NativeDeviceStateMonitor.java
@@ -216,13 +216,14 @@
         Callable<BUSY_WAIT_STATUS> bootComplete =
                 () -> {
                     final CollectingOutputReceiver receiver = createOutputReceiver();
-                    final String cmd = "ls /system/bin/adb";
+                    final String cmd = "id";
                     try {
                         getIDevice()
                                 .executeShellCommand(
                                         cmd, receiver, MAX_OP_TIME, TimeUnit.MILLISECONDS);
                         String output = receiver.getOutput();
-                        if (output.contains("/system/bin/adb")) {
+                        if (output.contains("uid=")) {
+                            CLog.i("shell ready. id output: %s", output);
                             return BUSY_WAIT_STATUS.SUCCESS;
                         }
                     } catch (IOException
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index 1ee927e..ee79e07 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -162,7 +162,7 @@
     // Then there is time to run the actual task. Set the maximum timeout value big enough.
     private static final long MICRODROID_MAX_LIFETIME_MINUTES = 20;
 
-    private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
+    private static final long MICRODROID_DEFAULT_ADB_CONNECT_TIMEOUT_MINUTES = 5;
 
     private static final String EARLY_REBOOT = "Too early to call shutdown() or reboot()";
 
@@ -196,69 +196,81 @@
     private String internalInstallPackage(
             final File packageFile, final boolean reinstall, final List<String> extraArgs)
                     throws DeviceNotAvailableException {
-        List<String> args = new ArrayList<>(extraArgs);
-        if (packageFile.getName().endsWith(APEX_SUFFIX)) {
-            args.add(APEX_ARG);
-        }
-        // use array to store response, so it can be returned to caller
-        final String[] response = new String[1];
-        DeviceAction installAction =
-                new DeviceAction() {
-                    @Override
-                    public boolean run() throws InstallException {
-                        try {
-                            InstallReceiver receiver = createInstallReceiver();
-                            getIDevice()
-                                    .installPackage(
-                                            packageFile.getAbsolutePath(),
-                                            reinstall,
-                                            receiver,
-                                            INSTALL_TIMEOUT_MINUTES,
-                                            INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
-                                            TimeUnit.MINUTES,
-                                            args.toArray(new String[] {}));
-                            if (receiver.isSuccessfullyCompleted()) {
-                                response[0] = null;
-                            } else if (receiver.getErrorMessage() == null) {
-                                response[0] =
-                                        String.format(
-                                                "Installation of %s timed out",
-                                                packageFile.getAbsolutePath());
-                            } else {
-                                response[0] = receiver.getErrorMessage();
-                                if (response[0].contains("cmd: Failure calling service package")) {
-                                    String message =
+        long startTime = System.currentTimeMillis();
+        try {
+            List<String> args = new ArrayList<>(extraArgs);
+            if (packageFile.getName().endsWith(APEX_SUFFIX)) {
+                args.add(APEX_ARG);
+            }
+            // use array to store response, so it can be returned to caller
+            final String[] response = new String[1];
+            DeviceAction installAction =
+                    new DeviceAction() {
+                        @Override
+                        public boolean run() throws InstallException {
+                            try {
+                                InstallReceiver receiver = createInstallReceiver();
+                                getIDevice()
+                                        .installPackage(
+                                                packageFile.getAbsolutePath(),
+                                                reinstall,
+                                                receiver,
+                                                INSTALL_TIMEOUT_MINUTES,
+                                                INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
+                                                TimeUnit.MINUTES,
+                                                args.toArray(new String[] {}));
+                                if (receiver.isSuccessfullyCompleted()) {
+                                    response[0] = null;
+                                } else if (receiver.getErrorMessage() == null) {
+                                    response[0] =
                                             String.format(
-                                                    "Failed to install '%s'. Device might have"
-                                                            + " crashed, it returned: %s",
-                                                    packageFile.getName(), response[0]);
-                                    throw new DeviceRuntimeException(
-                                            message, DeviceErrorIdentifier.DEVICE_CRASHED);
+                                                    "Installation of %s timed out",
+                                                    packageFile.getAbsolutePath());
+                                } else {
+                                    response[0] = receiver.getErrorMessage();
+                                    if (response[0].contains(
+                                            "cmd: Failure calling service package")) {
+                                        String message =
+                                                String.format(
+                                                        "Failed to install '%s'. Device might have"
+                                                                + " crashed, it returned: %s",
+                                                        packageFile.getName(), response[0]);
+                                        throw new DeviceRuntimeException(
+                                                message, DeviceErrorIdentifier.DEVICE_CRASHED);
+                                    }
                                 }
+                            } catch (InstallException e) {
+                                String message = e.getMessage();
+                                if (message == null) {
+                                    message =
+                                            String.format(
+                                                    "InstallException during package installation. "
+                                                            + "cause: %s",
+                                                    StreamUtil.getStackTrace(e));
+                                }
+                                response[0] = message;
                             }
-                        } catch (InstallException e) {
-                            String message = e.getMessage();
-                            if (message == null) {
-                                message =
-                                        String.format(
-                                                "InstallException during package installation. "
-                                                        + "cause: %s",
-                                                StreamUtil.getStackTrace(e));
-                            }
-                            response[0] = message;
+                            return response[0] == null;
                         }
-                        return response[0] == null;
-                    }
-                };
-        CLog.v(
-                "Installing package file %s with args %s on %s",
-                packageFile.getAbsolutePath(), extraArgs.toString(), getSerialNumber());
-        performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
-                installAction, MAX_RETRY_ATTEMPTS);
-        List<File> packageFiles = new ArrayList<>();
-        packageFiles.add(packageFile);
-        allowLegacyStorageForApps(packageFiles);
-        return response[0];
+                    };
+            CLog.v(
+                    "Installing package file %s with args %s on %s",
+                    packageFile.getAbsolutePath(), extraArgs.toString(), getSerialNumber());
+            performDeviceAction(
+                    String.format("install %s", packageFile.getAbsolutePath()),
+                    installAction,
+                    MAX_RETRY_ATTEMPTS);
+            List<File> packageFiles = new ArrayList<>();
+            packageFiles.add(packageFile);
+            allowLegacyStorageForApps(packageFiles);
+            return response[0];
+        } finally {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1);
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.PACKAGE_INSTALL_TIME,
+                    System.currentTimeMillis() - startTime);
+        }
     }
 
     /**
@@ -301,71 +313,83 @@
 
     public String installPackage(final File packageFile, final File certFile,
             final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
-        // use array to store response, so it can be returned to caller
-        final String[] response = new String[1];
-        DeviceAction installAction =
-                new DeviceAction() {
-                    @Override
-                    public boolean run()
-                            throws InstallException, SyncException, IOException, TimeoutException,
-                                    AdbCommandRejectedException {
-                        // TODO: create a getIDevice().installPackage(File, File...) method when the
-                        // dist cert functionality is ready to be open sourced
-                        String remotePackagePath =
-                                getIDevice().syncPackageToDevice(packageFile.getAbsolutePath());
-                        String remoteCertPath =
-                                getIDevice().syncPackageToDevice(certFile.getAbsolutePath());
-                        // trick installRemotePackage into issuing a 'pm install <apk> <cert>'
-                        // command, by adding apk path to extraArgs, and using cert as the
-                        // 'apk file'.
-                        String[] newExtraArgs = new String[extraArgs.length + 1];
-                        System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
-                        newExtraArgs[newExtraArgs.length - 1] =
-                                String.format("\"%s\"", remotePackagePath);
-                        try {
-                            InstallReceiver receiver = createInstallReceiver();
-                            getIDevice()
-                                    .installRemotePackage(
-                                            remoteCertPath,
-                                            reinstall,
-                                            receiver,
-                                            INSTALL_TIMEOUT_MINUTES,
-                                            INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
-                                            TimeUnit.MINUTES,
-                                            newExtraArgs);
-                            if (receiver.isSuccessfullyCompleted()) {
-                                response[0] = null;
-                            } else if (receiver.getErrorMessage() == null) {
-                                response[0] =
-                                        String.format(
-                                                "Installation of %s timed out.",
-                                                packageFile.getAbsolutePath());
-                            } else {
-                                response[0] = receiver.getErrorMessage();
+        long startTime = System.currentTimeMillis();
+        try {
+            // use array to store response, so it can be returned to caller
+            final String[] response = new String[1];
+            DeviceAction installAction =
+                    new DeviceAction() {
+                        @Override
+                        public boolean run()
+                                throws InstallException, SyncException, IOException,
+                                        TimeoutException, AdbCommandRejectedException {
+                            // TODO: create a getIDevice().installPackage(File, File...) method when
+                            // the
+                            // dist cert functionality is ready to be open sourced
+                            String remotePackagePath =
+                                    getIDevice().syncPackageToDevice(packageFile.getAbsolutePath());
+                            String remoteCertPath =
+                                    getIDevice().syncPackageToDevice(certFile.getAbsolutePath());
+                            // trick installRemotePackage into issuing a 'pm install <apk> <cert>'
+                            // command, by adding apk path to extraArgs, and using cert as the
+                            // 'apk file'.
+                            String[] newExtraArgs = new String[extraArgs.length + 1];
+                            System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
+                            newExtraArgs[newExtraArgs.length - 1] =
+                                    String.format("\"%s\"", remotePackagePath);
+                            try {
+                                InstallReceiver receiver = createInstallReceiver();
+                                getIDevice()
+                                        .installRemotePackage(
+                                                remoteCertPath,
+                                                reinstall,
+                                                receiver,
+                                                INSTALL_TIMEOUT_MINUTES,
+                                                INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
+                                                TimeUnit.MINUTES,
+                                                newExtraArgs);
+                                if (receiver.isSuccessfullyCompleted()) {
+                                    response[0] = null;
+                                } else if (receiver.getErrorMessage() == null) {
+                                    response[0] =
+                                            String.format(
+                                                    "Installation of %s timed out.",
+                                                    packageFile.getAbsolutePath());
+                                } else {
+                                    response[0] = receiver.getErrorMessage();
+                                }
+                            } catch (InstallException e) {
+                                String message = e.getMessage();
+                                if (message == null) {
+                                    message =
+                                            String.format(
+                                                    "InstallException during package installation. "
+                                                            + "cause: %s",
+                                                    StreamUtil.getStackTrace(e));
+                                }
+                                response[0] = message;
+                            } finally {
+                                getIDevice().removeRemotePackage(remotePackagePath);
+                                getIDevice().removeRemotePackage(remoteCertPath);
                             }
-                        } catch (InstallException e) {
-                            String message = e.getMessage();
-                            if (message == null) {
-                                message =
-                                        String.format(
-                                                "InstallException during package installation. "
-                                                        + "cause: %s",
-                                                StreamUtil.getStackTrace(e));
-                            }
-                            response[0] = message;
-                        } finally {
-                            getIDevice().removeRemotePackage(remotePackagePath);
-                            getIDevice().removeRemotePackage(remoteCertPath);
+                            return true;
                         }
-                        return true;
-                    }
-                };
-        performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
-                installAction, MAX_RETRY_ATTEMPTS);
-        List<File> packageFiles = new ArrayList<>();
-        packageFiles.add(packageFile);
-        allowLegacyStorageForApps(packageFiles);
-        return response[0];
+                    };
+            performDeviceAction(
+                    String.format("install %s", packageFile.getAbsolutePath()),
+                    installAction,
+                    MAX_RETRY_ATTEMPTS);
+            List<File> packageFiles = new ArrayList<>();
+            packageFiles.add(packageFile);
+            allowLegacyStorageForApps(packageFiles);
+            return response[0];
+        } finally {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1);
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.PACKAGE_INSTALL_TIME,
+                    System.currentTimeMillis() - startTime);
+        }
     }
 
     /**
@@ -2337,6 +2361,7 @@
         } catch (IOException e) {
             throw new DeviceRuntimeException(
                     "Unable to get an unused port for Microdroid.",
+                    e,
                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
         }
 
@@ -2354,6 +2379,11 @@
                     "mkdir -p " + TEST_ROOT + " has failed: " + result,
                     DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
         }
+        for (File localFile : builder.mBootFiles.keySet()) {
+            String remoteFileName = builder.mBootFiles.get(localFile);
+            pushFile(localFile, TEST_ROOT + remoteFileName);
+        }
+
         // Push the apk file to the test directory
         if (builder.mApkFile != null) {
             pushFile(builder.mApkFile, TEST_ROOT + builder.mApkFile.getName());
@@ -2374,10 +2404,15 @@
         final String logPath = TEST_ROOT + "log.txt";
         final String debugFlag =
                 Strings.isNullOrEmpty(builder.mDebugLevel) ? "" : "--debug " + builder.mDebugLevel;
+        final String cpuFlag = builder.mNumCpus == null ? "" : "--cpus " + builder.mNumCpus;
         final String cpuAffinityFlag =
                 Strings.isNullOrEmpty(builder.mCpuAffinity)
                         ? ""
                         : "--cpu-affinity " + builder.mCpuAffinity;
+        final String cpuTopologyFlag =
+                Strings.isNullOrEmpty(builder.mCpuTopology)
+                        ? ""
+                        : "--cpu-topology " + builder.mCpuTopology;
 
         List<String> args =
                 new ArrayList<>(
@@ -2392,8 +2427,9 @@
                                 "--log " + logPath,
                                 "--mem " + builder.mMemoryMib,
                                 debugFlag,
-                                "--cpus " + builder.mNumCpus,
+                                cpuFlag,
                                 cpuAffinityFlag,
+                                cpuTopologyFlag,
                                 builder.mApkPath,
                                 outApkIdsigPath,
                                 instanceImg,
@@ -2431,7 +2467,9 @@
             }
         } catch (IOException ex) {
             throw new DeviceRuntimeException(
-                    "IOException trying to start a VM", DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
+                    "IOException trying to start a VM",
+                    ex,
+                    DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
         }
 
         // Redirect log.txt to logd using logwrapper
@@ -2445,7 +2483,7 @@
                     forwardFileToLog(logPath, "MicrodroidLog");
                 });
 
-        adbConnectToMicrodroid(cid, microdroidSerial, vmAdbPort);
+        adbConnectToMicrodroid(cid, microdroidSerial, vmAdbPort, builder.mAdbConnectTimeoutMs);
         TestDevice microdroid = (TestDevice) deviceManager.forceAllocateDevice(microdroidSerial);
         if (microdroid == null) {
             process.destroy();
@@ -2469,12 +2507,13 @@
      * Establish an adb connection to microdroid by letting Android forward the connection to
      * microdroid. Wait until the connection is established and microdroid is booted.
      */
-    private void adbConnectToMicrodroid(String cid, String microdroidSerial, int vmAdbPort) {
+    private void adbConnectToMicrodroid(
+            String cid, String microdroidSerial, int vmAdbPort, long adbConnectTimeoutMs) {
         MicrodroidHelper microdroidHelper = new MicrodroidHelper();
         IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance();
 
         long start = System.currentTimeMillis();
-        long timeoutMillis = MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000;
+        long timeoutMillis = adbConnectTimeoutMs;
         long elapsed = 0;
 
         final String serial = getSerialNumber();
@@ -2625,11 +2664,14 @@
         private String mConfigPath;
         private String mDebugLevel;
         private int mMemoryMib;
-        private int mNumCpus;
+        private Integer mNumCpus;
         private String mCpuAffinity;
+        private String mCpuTopology;
         private List<String> mExtraIdsigPaths;
         private boolean mProtectedVm;
         private Map<String, String> mTestDeviceOptions;
+        private Map<File, String> mBootFiles;
+        private long mAdbConnectTimeoutMs;
 
         /** Creates a builder for the given APK/apkPath and the payload config file in APK. */
         private MicrodroidBuilder(File apkFile, String apkPath, @Nonnull String configPath) {
@@ -2638,11 +2680,13 @@
             mConfigPath = configPath;
             mDebugLevel = null;
             mMemoryMib = 0;
-            mNumCpus = 1;
+            mNumCpus = null;
             mCpuAffinity = null;
             mExtraIdsigPaths = new ArrayList<>();
             mProtectedVm = false; // Vm is unprotected by default.
             mTestDeviceOptions = new LinkedHashMap<>();
+            mBootFiles = new LinkedHashMap<>();
+            mAdbConnectTimeoutMs = MICRODROID_DEFAULT_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000;
         }
 
         /** Creates a Microdroid builder for the given APK and the payload config file in APK. */
@@ -2659,7 +2703,11 @@
             return new MicrodroidBuilder(null, apkPath, configPath);
         }
 
-        /** Sets the debug level. Supported values: "none", "app_only", and "full". */
+        /**
+         * Sets the debug level.
+         *
+         * <p>Supported values: "none" and "full". Android T also supports "app_only".
+         */
         public MicrodroidBuilder debugLevel(String debugLevel) {
             mDebugLevel = debugLevel;
             return this;
@@ -2674,7 +2722,11 @@
             return this;
         }
 
-        /** Sets the number of vCPUs in the VM. Defaults to 1. */
+        /**
+         * Sets the number of vCPUs in the VM. Defaults to 1.
+         *
+         * <p>Only supported in Android T.
+         */
         public MicrodroidBuilder numCpus(int num) {
             mNumCpus = num;
             return this;
@@ -2685,12 +2737,20 @@
          * or CPU ranges to run vCPUs on. e.g. "0,1-3,5" to choose host CPUs 0, 1, 2, 3, and 5. Or
          * this can be a colon-separated list of assignments of vCPU to host CPU assignments. e.g.
          * "0=0:1=1:2=2" to map vCPU 0 to host CPU 0, and so on.
+         *
+         * <p>Only supported in Android T.
          */
         public MicrodroidBuilder cpuAffinity(String affinity) {
             mCpuAffinity = affinity;
             return this;
         }
 
+        /** Sets the CPU topology configuration. Supported values: "one_cpu" and "match_host". */
+        public MicrodroidBuilder cpuTopology(String cpuTopology) {
+            mCpuTopology = cpuTopology;
+            return this;
+        }
+
         /** Sets whether the VM will be protected or not. */
         public MicrodroidBuilder protectedVm(boolean isProtectedVm) {
             mProtectedVm = isProtectedVm;
@@ -2717,17 +2777,57 @@
             return this;
         }
 
+        /**
+         * Adds a file for booting to be pushed to {@link #TEST_ROOT}.
+         *
+         * <p>Use this method if an file is required for booting microdroid. Otherwise use {@link
+         * TestDevice#pushFile}.
+         *
+         * @param localFile The local file on the host
+         * @param remoteFileName The remote file name on the device
+         * @param the microdroid builder.
+         */
+        public MicrodroidBuilder addBootFile(File localFile, String remoteFileName) {
+            mBootFiles.put(localFile, remoteFileName);
+            return this;
+        }
+
+        /**
+         * Sets the timeout for adb connect to microdroid TestDevice in millis.
+         *
+         * @param timeoutMs The timeout in millis
+         */
+        public MicrodroidBuilder setAdbConnectTimeoutMs(long timeoutMs) {
+            mAdbConnectTimeoutMs = timeoutMs;
+            return this;
+        }
+
         /** Starts a Micrdroid TestDevice on the given TestDevice. */
         public ITestDevice build(@Nonnull TestDevice device) throws DeviceNotAvailableException {
-            if (mNumCpus < 1) {
-                throw new IllegalArgumentException("Number of vCPUs can not be less than 1.");
+            if (mNumCpus != null) {
+                if (device.getApiLevel() != 33) {
+                    throw new IllegalStateException(
+                            "Setting number of CPUs only supported with API level 33");
+                }
+                if (mNumCpus < 1) {
+                    throw new IllegalArgumentException("Number of vCPUs can not be less than 1.");
+                }
             }
 
-            if (mCpuAffinity != null
-                    && !Pattern.matches("[\\d]+(-[\\d]+)?(,[\\d]+(-[\\d]+)?)*", mCpuAffinity)
-                    && !Pattern.matches("[\\d]+=[\\d]+(:[\\d]+=[\\d]+)*", mCpuAffinity)) {
-                throw new IllegalArgumentException(
-                        "CPU affinity [" + mCpuAffinity + "]" + " is invalid");
+            if (!Strings.isNullOrEmpty(mCpuTopology)) {
+                device.checkApiLevelAgainstNextRelease("vm-cpu-topology", 34);
+            }
+
+            if (mCpuAffinity != null) {
+                if (device.getApiLevel() != 33) {
+                    throw new IllegalStateException(
+                            "Setting CPU affinity only supported with API level 33");
+                }
+                if (!Pattern.matches("[\\d]+(-[\\d]+)?(,[\\d]+(-[\\d]+)?)*", mCpuAffinity)
+                        && !Pattern.matches("[\\d]+=[\\d]+(:[\\d]+=[\\d]+)*", mCpuAffinity)) {
+                    throw new IllegalArgumentException(
+                            "CPU affinity [" + mCpuAffinity + "]" + " is invalid");
+                }
             }
 
             return device.startMicrodroid(this);
diff --git a/src/com/android/tradefed/device/cloud/GceAvdInfo.java b/src/com/android/tradefed/device/cloud/GceAvdInfo.java
index 20cfe28..659c1bf 100644
--- a/src/com/android/tradefed/device/cloud/GceAvdInfo.java
+++ b/src/com/android/tradefed/device/cloud/GceAvdInfo.java
@@ -397,7 +397,9 @@
         CLog.d("Parsing oxygen client output: %s", output);
 
         Pattern pattern =
-                Pattern.compile("session_id:\"(.*?)\".*?server_url:\"(.*?)\"", Pattern.DOTALL);
+                Pattern.compile(
+                        "session_id:\"(.*?)\".*?server_url:\"(.*?)\".*?oxygen_version:\"(.*?)\"",
+                        Pattern.DOTALL);
         Matcher matcher = pattern.matcher(output);
 
         List<GceAvdInfo> gceAvdInfos = new ArrayList<>();
@@ -405,6 +407,7 @@
         while (matcher.find()) {
             String sessionId = matcher.group(1);
             String serverUrl = matcher.group(2);
+            String oxygenVersion = matcher.group(3);
             gceAvdInfos.add(
                     new GceAvdInfo(
                             sessionId,
@@ -413,6 +416,8 @@
                             null,
                             null,
                             GceStatus.SUCCESS));
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.CF_OXYGEN_VERSION, oxygenVersion);
             deviceOffset++;
         }
         if (gceAvdInfos.isEmpty()) {
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index 5ffe6a1..990d407 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.error.HarnessRuntimeException;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ByteArrayInputStreamSource;
@@ -318,11 +319,14 @@
                             InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_FAILURE);
                 }
             }
-            InvocationMetricLogger.addInvocationMetrics(
-                    InvocationMetricKey.CF_OXYGEN_SESSION_ID, oxygenDeviceInfo.instanceName());
-            InvocationMetricLogger.addInvocationMetrics(
-                    InvocationMetricKey.CF_OXYGEN_SERVER_URL,
-                    oxygenDeviceInfo.hostAndPort().getHost());
+            // Lease may timeout and skip logging metrics if host is not set.
+            if (oxygenDeviceInfo.hostAndPort() != null) {
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.CF_OXYGEN_SESSION_ID, oxygenDeviceInfo.instanceName());
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.CF_OXYGEN_SERVER_URL,
+                        oxygenDeviceInfo.hostAndPort().getHost());
+            }
             return oxygenDeviceInfo;
         } finally {
             InvocationMetricLogger.addInvocationMetrics(
@@ -953,6 +957,30 @@
             remoteFile =
                     RemoteFileUtil.fetchRemoteDir(
                             gceAvd, options, runUtil, REMOTE_FILE_OP_TIMEOUT, remoteFilePath);
+
+            // Search log files for known failures for devices hosted by Oxygen
+            if (options.useOxygenProxy()) {
+                try (CloseableTraceScope ignore =
+                        new CloseableTraceScope("avd:collectErrorSignature")) {
+                    List<String> signatures = OxygenUtil.collectErrorSignatures(remoteFile);
+                    if (signatures.size() > 0) {
+                        InvocationMetricLogger.addInvocationMetrics(
+                                InvocationMetricKey.DEVICE_ERROR_SIGNATURES,
+                                String.join(",", signatures));
+                    }
+                }
+                try (CloseableTraceScope ignore =
+                        new CloseableTraceScope("avd:collectDeviceLaunchMetrics")) {
+                    long[] launchMetrics = OxygenUtil.collectDeviceLaunchMetrics(remoteFile);
+                    if (launchMetrics[0] > 0) {
+                        InvocationMetricLogger.addInvocationMetrics(
+                                InvocationMetricKey.CF_FETCH_ARTIFACT_TIME, launchMetrics[0]);
+                        InvocationMetricLogger.addInvocationMetrics(
+                                InvocationMetricKey.CF_LAUNCH_CVD_TIME, launchMetrics[1]);
+                    }
+                }
+            }
+
             // Default files under a directory to be CUTTLEFISH_LOG to avoid compression.
             type = LogDataType.CUTTLEFISH_LOG;
             if (remoteFile != null) {
diff --git a/src/com/android/tradefed/device/cloud/OxygenUtil.java b/src/com/android/tradefed/device/cloud/OxygenUtil.java
index 66e993f..9414ae7 100644
--- a/src/com/android/tradefed/device/cloud/OxygenUtil.java
+++ b/src/com/android/tradefed/device/cloud/OxygenUtil.java
@@ -23,10 +23,15 @@
 import com.android.tradefed.targetprep.TargetSetupError;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.GCSFileDownloader;
+import com.android.tradefed.util.Pair;
 
 import java.io.File;
 import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
+import java.util.Scanner;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -54,6 +59,28 @@
                                     LogDataType.TOMBSTONEZ))
                     .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
 
+    private static final Map<Pattern, Pair<Pattern, String>>
+            REMOTE_LOG_NAME_PATTERN_TO_ERROR_SIGNATURE_MAP =
+                    Stream.of(
+                                    new AbstractMap.SimpleEntry<>(
+                                            Pattern.compile("^launcher\\.log.*"),
+                                            Pair.create(
+                                                    Pattern.compile(".*Address already in use.*"),
+                                                    "launch_cvd_port_collision")),
+                                    new AbstractMap.SimpleEntry<>(
+                                            Pattern.compile("^launcher\\.log.*"),
+                                            Pair.create(
+                                                    Pattern.compile(".*vcpu hw run failure: 0x7.*"),
+                                                    "crosvm_vcpu_hw_run_failure_7")),
+                                    new AbstractMap.SimpleEntry<>(
+                                            Pattern.compile("^launcher\\.log.*"),
+                                            Pair.create(
+                                                    Pattern.compile(
+                                                            ".*Unable to connect to vsock"
+                                                                    + " server.*"),
+                                                    "unable_to_connect_to_vsock_server")))
+                            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
     /** Default constructor of OxygenUtil */
     public OxygenUtil() {
         mDownloader = new GCSFileDownloader(true);
@@ -143,4 +170,125 @@
                         logFileName));
         return LogDataType.UNKNOWN;
     }
+
+    /**
+     * Collect error signatures from logs.
+     *
+     * @param logDir directory of logs pulled from remote host.
+     */
+    public static List<String> collectErrorSignatures(File logDir) {
+        CLog.d("Collect error signature from logs under: %s.", logDir);
+        List<String> signatures = new ArrayList<>();
+        try {
+            Set<String> files = FileUtil.findFiles(logDir, ".*");
+            for (String f : files) {
+                File file = new File(f);
+                if (file.isDirectory()) {
+                    continue;
+                }
+                String fileName = file.getName();
+                List<Pair<Pattern, String>> pairs = new ArrayList<>();
+                for (Map.Entry<Pattern, Pair<Pattern, String>> entry :
+                        REMOTE_LOG_NAME_PATTERN_TO_ERROR_SIGNATURE_MAP.entrySet()) {
+                    Matcher matcher = entry.getKey().matcher(fileName);
+                    if (matcher.find()) {
+                        pairs.add(entry.getValue());
+                    }
+                }
+                if (pairs.size() == 0) {
+                    continue;
+                }
+                try (Scanner scanner = new Scanner(file)) {
+                    List<Pair<Pattern, String>> pairsToRemove = new ArrayList<>();
+                    while (scanner.hasNextLine()) {
+                        String line = scanner.nextLine();
+                        for (Pair<Pattern, String> pair : pairs) {
+                            if (pair.first.matcher(line).find()) {
+                                pairsToRemove.add(pair);
+                                signatures.add(pair.second);
+                            }
+                        }
+                        if (pairsToRemove.size() > 0) {
+                            pairs.removeAll(pairsToRemove);
+                            if (pairs.size() == 0) {
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            CLog.e("Failed to collect error signature.");
+            CLog.e(e);
+        }
+        Collections.sort(signatures);
+        return signatures;
+    }
+
+    /**
+     * Collect device launcher metrics from vdl_stdout.
+     *
+     * @param logDir directory of logs pulled from remote host.
+     */
+    public static long[] collectDeviceLaunchMetrics(File logDir) {
+        CLog.d("Collect device launcher metrics from logs under: %s.", logDir);
+        long[] metrics = {-1, -1};
+        try {
+            Set<String> files = FileUtil.findFiles(logDir, "^vdl_stdout\\.txt.*");
+            if (files.size() == 0) {
+                CLog.d("There is no vdl_stdout.txt found.");
+                return metrics;
+            }
+            File vdlStdout = new File(files.iterator().next());
+            double cuttlefishCommon = 0;
+            double launchDevice = 0;
+            double mainstart = 0;
+            Pattern cuttlefishCommonPatteren =
+                    Pattern.compile(".*\\|\\s*(\\d+\\.\\d+)\\s*\\|\\sCuttlefishCommon");
+            Pattern launchDevicePatteren =
+                    Pattern.compile(".*\\|\\s*(\\d+\\.\\d+)\\s*\\|\\sLaunchDevice");
+            Pattern mainstartPatteren =
+                    Pattern.compile(".*\\|\\s*(\\d+\\.\\d+)\\s*\\|\\sCuttlefishLauncherMainstart");
+            try (Scanner scanner = new Scanner(vdlStdout)) {
+                boolean metricsPending = false;
+                while (scanner.hasNextLine()) {
+                    String line = scanner.nextLine();
+                    if (!metricsPending) {
+                        if (line.indexOf("launch_cvd exited") != -1) {
+                            metricsPending = true;
+                        } else {
+                            continue;
+                        }
+                    }
+                    Matcher matcher;
+                    if (cuttlefishCommon == 0) {
+                        matcher = cuttlefishCommonPatteren.matcher(line);
+                        if (matcher.find()) {
+                            cuttlefishCommon = Double.parseDouble(matcher.group(1));
+                        }
+                    }
+                    if (launchDevice == 0) {
+                        matcher = launchDevicePatteren.matcher(line);
+                        if (matcher.find()) {
+                            launchDevice = Double.parseDouble(matcher.group(1));
+                        }
+                    }
+                    if (mainstart == 0) {
+                        matcher = mainstartPatteren.matcher(line);
+                        if (matcher.find()) {
+                            mainstart = Double.parseDouble(matcher.group(1));
+                        }
+                    }
+                }
+            }
+            if (mainstart > 0) {
+                metrics[0] = (long) ((mainstart - launchDevice - cuttlefishCommon) * 1000);
+                metrics[1] = (long) (launchDevice * 1000);
+            }
+        } catch (Exception e) {
+            CLog.e("Failed to parse device launch time from vdl_stdout.txt.");
+            CLog.e(e);
+        }
+        return metrics;
+    }
 }
diff --git a/src/com/android/tradefed/device/internal/DeviceResetFeature.java b/src/com/android/tradefed/device/internal/DeviceResetFeature.java
index f84d79a..3b42e2d 100644
--- a/src/com/android/tradefed/device/internal/DeviceResetFeature.java
+++ b/src/com/android/tradefed/device/internal/DeviceResetFeature.java
@@ -20,6 +20,7 @@
 import com.android.tradefed.config.IDeviceConfiguration;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.RemoteAndroidDevice;
+import com.android.tradefed.device.cloud.GceAvdInfo;
 import com.android.tradefed.device.cloud.NestedRemoteDevice;
 import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.invoker.TestInformation;
@@ -93,14 +94,13 @@
         try {
             mTestInformation.setActiveDeviceIndex(index);
             if (mTestInformation.getDevice() instanceof RemoteAndroidVirtualDevice) {
-                Integer offset =
-                        ((RemoteAndroidVirtualDevice) mTestInformation.getDevice())
-                                .getAvdInfo()
-                                .getDeviceOffset();
-                String user =
-                        ((RemoteAndroidVirtualDevice) mTestInformation.getDevice())
-                                .getAvdInfo()
-                                .getInstanceUser();
+                GceAvdInfo info =
+                        ((RemoteAndroidVirtualDevice) mTestInformation.getDevice()).getAvdInfo();
+                if (info == null) {
+                    throw new RuntimeException("GceAvdInfo was null. skipping");
+                }
+                Integer offset = info.getDeviceOffset();
+                String user = info.getInstanceUser();
                 CommandResult powerwashResult =
                         ((RemoteAndroidVirtualDevice) mTestInformation.getDevice())
                                 .powerwashGce(user, offset);
diff --git a/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java b/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
index 21c99e7..f8c1c7a 100644
--- a/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
@@ -99,7 +99,7 @@
     private boolean mDeviceNoAvailable = false;
 
     @Override
-    public ITestInvocationListener init(
+    public final ITestInvocationListener init(
             IInvocationContext context, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
         mContext = context;
@@ -110,9 +110,26 @@
         }
         mWasInitDone = true;
         mDeviceNoAvailable = false;
+        long start = System.currentTimeMillis();
+        try {
+            extraInit(context, listener);
+        } finally {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.COLLECTOR_TIME, System.currentTimeMillis() - start);
+        }
         return this;
     }
 
+    /**
+     * @param context
+     * @param listener
+     * @throws DeviceNotAvailableException
+     */
+    public void extraInit(IInvocationContext context, ITestInvocationListener listener)
+            throws DeviceNotAvailableException {
+        // Empty by default
+    }
+
     @Override
     public final List<ITestDevice> getDevices() {
         return mContext.getDevices();
@@ -257,6 +274,7 @@
 
     @Override
     public final void testModuleStarted(IInvocationContext moduleContext) {
+        long start = System.currentTimeMillis();
         try (CloseableTraceScope ignored =
                 new CloseableTraceScope("module_start_" + this.getClass().getSimpleName())) {
             onTestModuleStarted();
@@ -266,12 +284,15 @@
         } catch (Throwable t) {
             CLog.e(t);
         } finally {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.COLLECTOR_TIME, System.currentTimeMillis() - start);
             mForwarder.testModuleStarted(moduleContext);
         }
     }
 
     @Override
     public final void testModuleEnded() {
+        long start = System.currentTimeMillis();
         try (CloseableTraceScope ignored =
                 new CloseableTraceScope("module_end_" + this.getClass().getSimpleName())) {
             onTestModuleEnded();
@@ -280,6 +301,8 @@
         } catch (Throwable t) {
             CLog.e(t);
         } finally {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.COLLECTOR_TIME, System.currentTimeMillis() - start);
             mForwarder.testModuleEnded();
         }
     }
diff --git a/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java b/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java
index be7c87b..47c9a33 100644
--- a/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java
+++ b/src/com/android/tradefed/device/metric/ClangCodeCoverageCollector.java
@@ -34,6 +34,7 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
+import com.android.tradefed.testtype.coverage.CoverageOptions;
 import com.android.tradefed.util.AdbRootElevator;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -70,9 +71,6 @@
 
     private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace";
 
-    // Timeout for pulling coverage measurements from the device, in minutes.
-    private static final long TIMEOUT = 20;
-
     // Maximum number of profile files before writing the list to a file. Beyond this value,
     // llvm-profdata will use the -f option to read the list from a file to prevent exceeding
     // the command line length limit.
@@ -91,17 +89,19 @@
 
     private IConfiguration mConfiguration;
     private IRunUtil mRunUtil = RunUtil.getDefault();
+    // Timeout for pulling coverage measurements from the device, in milliseconds.
+    private long mTimeoutMilli = 20 * 60 * 1000;
     private File mLlvmProfileTool;
 
     private NativeCodeCoverageFlusher mFlusher;
 
     @Override
-    public ITestInvocationListener init(
-            IInvocationContext context, ITestInvocationListener listener)
+    public void extraInit(IInvocationContext context, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
-        super.init(context, listener);
+        super.extraInit(context, listener);
 
         verifyNotNull(mConfiguration);
+        setCoverageOptions(mConfiguration.getCoverageOptions());
 
         if (isClangCoverageEnabled()
                 && mConfiguration.getCoverageOptions().shouldResetCoverageBeforeTest()) {
@@ -112,8 +112,6 @@
                 }
             }
         }
-
-        return this;
     }
 
     @Override
@@ -180,8 +178,8 @@
                         ZIP_CLANG_FILES_COMMAND, // Command
                         null, // File pipe as input
                         out, // OutputStream to write to
-                        TIMEOUT, // Timeout in minutes
-                        TimeUnit.MINUTES, // Timeout units
+                        mTimeoutMilli, // Timeout in milliseconds
+                        TimeUnit.MILLISECONDS, // Timeout units
                         1); // Retry count
             }
 
@@ -318,4 +316,8 @@
             FileUtil.deleteFile(profileToolZip);
         }
     }
+
+    private void setCoverageOptions(CoverageOptions coverageOptions) {
+        mTimeoutMilli = coverageOptions.getPullTimeout();
+    }
 }
diff --git a/src/com/android/tradefed/device/metric/DeviceTraceCollector.java b/src/com/android/tradefed/device/metric/DeviceTraceCollector.java
index e3f5833..f42a6d0 100644
--- a/src/com/android/tradefed/device/metric/DeviceTraceCollector.java
+++ b/src/com/android/tradefed/device/metric/DeviceTraceCollector.java
@@ -40,10 +40,9 @@
     private String mInstrumentationPkgName;
 
     @Override
-    public ITestInvocationListener init(
-            IInvocationContext context, ITestInvocationListener listener)
+    public void extraInit(IInvocationContext context, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
-        super.init(context, listener);
+        super.extraInit(context, listener);
         for (ITestDevice device : getRealDevices()) {
             try {
                 Map<String, String> extraConfigs = new LinkedHashMap<>();
@@ -58,7 +57,6 @@
                         device.getSerialNumber(), e.getMessage());
             }
         }
-        return this;
     }
 
     @Override
diff --git a/src/com/android/tradefed/device/metric/EmulatorMemoryCpuCapturer.java b/src/com/android/tradefed/device/metric/EmulatorMemoryCpuCapturer.java
index c87cd5e..e5d43e7 100644
--- a/src/com/android/tradefed/device/metric/EmulatorMemoryCpuCapturer.java
+++ b/src/com/android/tradefed/device/metric/EmulatorMemoryCpuCapturer.java
@@ -22,6 +22,7 @@
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -77,8 +78,7 @@
 
     public float getCpuUsage() {
         CommandResult result =
-                RunUtil.getDefault()
-                        .runTimedCmd(20000L, "ps", "-o", "%cpu", "-p", Long.toString(mPid));
+                getRunUtil().runTimedCmd(20000L, "ps", "-o", "%cpu", "-p", Long.toString(mPid));
         if (result.getStatus() == CommandStatus.SUCCESS) {
             return parseCpuUsage(result.getStdout());
         } else {
@@ -87,6 +87,10 @@
         }
     }
 
+    protected IRunUtil getRunUtil() {
+        return RunUtil.getDefault();
+    }
+
     /**
      * Parse the cpu usage string.
      *
diff --git a/src/com/android/tradefed/device/metric/GcovCodeCoverageCollector.java b/src/com/android/tradefed/device/metric/GcovCodeCoverageCollector.java
index 4a73e23..6ad0d53 100644
--- a/src/com/android/tradefed/device/metric/GcovCodeCoverageCollector.java
+++ b/src/com/android/tradefed/device/metric/GcovCodeCoverageCollector.java
@@ -72,10 +72,9 @@
     private IRunUtil mRunUtil = RunUtil.getDefault();
 
     @Override
-    public ITestInvocationListener init(
-            IInvocationContext context, ITestInvocationListener listener)
+    public void extraInit(IInvocationContext context, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
-        super.init(context, listener);
+        super.extraInit(context, listener);
 
         if (isGcovCoverageEnabled()) {
             for (ITestDevice device : getRealDevices()) {
@@ -85,8 +84,6 @@
                 }
             }
         }
-
-        return this;
     }
 
     @VisibleForTesting
diff --git a/src/com/android/tradefed/device/metric/GcovKernelCodeCoverageCollector.java b/src/com/android/tradefed/device/metric/GcovKernelCodeCoverageCollector.java
index 0a03071..741ace1 100644
--- a/src/com/android/tradefed/device/metric/GcovKernelCodeCoverageCollector.java
+++ b/src/com/android/tradefed/device/metric/GcovKernelCodeCoverageCollector.java
@@ -40,7 +40,6 @@
 
 import java.io.File;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
 /**
  * A {@link com.android.tradefed.device.metric.BaseDeviceMetricCollector} that will pull gcov kernel
@@ -59,6 +58,9 @@
     public static final String RESET_GCOV_COUNTS_COMMAND =
             String.format("echo 1 > %s/gcov/reset", DEBUGFS_PATH);
     public static final String MAKE_TEMP_DIR_COMMAND = "mktemp -d -p /data/local/tmp/";
+    public static final String MAKE_GCDA_TEMP_DIR_COMMAND_FMT = "mkdir -p %s";
+    public static final String COPY_GCOV_DATA_COMMAND_FMT = "cp -rf %s/* %s";
+    public static final String TAR_GCOV_DATA_COMMAND_FMT = "tar -czf %s -C %s %s";
 
     private IConfiguration mConfiguration;
     private boolean mTestRunStartFail;
@@ -185,9 +187,17 @@
     }
 
     /**
-     * Gather overage data files off of the device. This logic is taken directly from the
-     * `gather_on_test.sh` script detailed here:
+     * Gather overage data files off of the device. This logic is was originally taken directly from
+     * the `gather_on_test.sh` script detailed here:
      * https://www.kernel.org/doc/html/v4.15/dev-tools/gcov.html#appendix-b-gather-on-test-sh
+     * However, in practice the `find` + `cat` approach ended up taking a lot of time. The reasoning
+     * given for this approach was because of issues with the `seq_file` interface. It turns out
+     * this issue no longer applies to the `cp` command (it still applies to the `tar`). Discussion
+     * on this can b e found here:
+     * https://github.com/linux-test-project/lcov/discussions/199#discussion-4895422
+     *
+     * <p>TODO: Revert this summary back to the original text, once upstream patch lands that
+     * updates `gather_on_test.sh` `cp` instead of `find` + `cat`.
      */
     private void collectGcovDebugfsCoverage(INativeDevice device, String name)
             throws DeviceNotAvailableException {
@@ -208,31 +218,40 @@
                         DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
             }
             String tempDir = result.getStdout().strip();
+
+            String gcda = "/d/gcov";
+            String gcdaTempDir = tempDir + gcda;
+            String makeGcdaTempDirCommand =
+                    String.format(MAKE_GCDA_TEMP_DIR_COMMAND_FMT, gcdaTempDir);
+            result = device.executeShellV2Command(makeGcdaTempDirCommand);
+            if (result.getStatus() != CommandStatus.SUCCESS) {
+                CLog.e("Failed to create gcda temp directory %s. %s", gcdaTempDir, result);
+                throw new DeviceRuntimeException(
+                        "'" + makeGcdaTempDirCommand + "' has failed: " + result,
+                        DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
+            }
+
             String tarName = String.format("%s.tar.gz", name);
             String tarFullPath = String.format("%s/%s", tempDir, tarName);
-            String gcda = "/d/gcov";
 
-            String gatherCommand =
-                    String.format(
-                            "find %s -type d -exec sh -c 'mkdir -p %s/$0' {} \\;; find %s -name"
-                                + " '*.gcda' -exec sh -c 'cat < $0 > '%s'/$0' {} \\;; find %s -name"
-                                + " '*.gcno' -exec sh -c 'cp -d $0 '%s'/$0' {} \\;; tar -czf %s -C"
-                                + " %s %s",
-                            gcda,
-                            tempDir,
-                            gcda,
-                            tempDir,
-                            gcda,
-                            tempDir,
-                            tarFullPath,
-                            tempDir,
-                            gcda.substring(1));
-
-            result = device.executeShellV2Command(gatherCommand, 10, TimeUnit.MINUTES);
+            String copyGcovDataCommand =
+                    String.format(COPY_GCOV_DATA_COMMAND_FMT, gcda, gcdaTempDir);
+            result = device.executeShellV2Command(copyGcovDataCommand);
             if (result.getStatus() != CommandStatus.SUCCESS) {
                 CLog.e("Failed to collect coverage files for %s. %s", name, result);
                 throw new DeviceRuntimeException(
-                        "'" + gatherCommand + "' has failed: " + result,
+                        "'" + copyGcovDataCommand + "' has failed: " + result,
+                        DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
+            }
+
+            String tarCommand =
+                    String.format(
+                            TAR_GCOV_DATA_COMMAND_FMT, tarFullPath, tempDir, gcda.substring(1));
+            result = device.executeShellV2Command(tarCommand);
+            if (result.getStatus() != CommandStatus.SUCCESS) {
+                CLog.e("Failed to tar collected files for %s. %s", name, result);
+                throw new DeviceRuntimeException(
+                        "'" + tarCommand + "' has failed: " + result,
                         DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
             }
 
diff --git a/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java b/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
index 1469016..c177036 100644
--- a/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
+++ b/src/com/android/tradefed/device/metric/JavaCodeCoverageCollector.java
@@ -70,21 +70,20 @@
     public static final String COMPRESS_COVERAGE_FILES =
             String.format("%s | tar -czf - -T - 2>/dev/null", FIND_COVERAGE_FILES);
 
-    // Timeout for pulling coverage files from the device, in minutes.
-    private static final long TIMEOUT_MINUTES = 20;
-
     private ExecFileLoader mExecFileLoader;
 
     private JavaCodeCoverageFlusher mFlusher;
     private IConfiguration mConfiguration;
+    // Timeout for pulling coverage files from the device, in milliseconds.
+    private long mTimeoutMilli = 20 * 60 * 1000;
 
     @Override
-    public ITestInvocationListener init(
-            IInvocationContext context, ITestInvocationListener listener)
+    public void extraInit(IInvocationContext context, ITestInvocationListener listener)
             throws DeviceNotAvailableException {
-        super.init(context, listener);
+        super.extraInit(context, listener);
 
         verifyNotNull(mConfiguration);
+        setCoverageOptions(mConfiguration.getCoverageOptions());
 
         if (isJavaCoverageEnabled()
                 && mConfiguration.getCoverageOptions().shouldResetCoverageBeforeTest()) {
@@ -94,8 +93,6 @@
                 }
             }
         }
-
-        return this;
     }
 
     @Override
@@ -170,8 +167,8 @@
                                     COMPRESS_COVERAGE_FILES,
                                     null,
                                     out,
-                                    TIMEOUT_MINUTES,
-                                    TimeUnit.MINUTES,
+                                    mTimeoutMilli,
+                                    TimeUnit.MILLISECONDS,
                                     1);
                     if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
                         CLog.e(
@@ -293,4 +290,8 @@
     private boolean shouldMergeCoverage() {
         return mConfiguration != null && mConfiguration.getCoverageOptions().shouldMergeCoverage();
     }
+
+    private void setCoverageOptions(CoverageOptions coverageOptions) {
+        mTimeoutMilli = coverageOptions.getPullTimeout();
+    }
 }
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index c0bb474..606e59b 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -1128,7 +1128,8 @@
                         continue;
                     }
                     CLog.d("Using RetryLogSaverResultForwarder to forward results.");
-                    ModuleListener mainGranularRunListener = new ModuleListener(null);
+                    ModuleListener mainGranularRunListener =
+                            new ModuleListener(null, info.getContext());
                     RetryLogSaverResultForwarder runListener =
                             initializeListeners(config, listener, mainGranularRunListener);
                     mainGranularRunListener.setAttemptIsolation(
diff --git a/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecution.java b/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecution.java
index dd2e08e..49ab1b4 100644
--- a/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecution.java
@@ -130,13 +130,29 @@
             IInvocationContext context, IConfiguration config, ITestLogger logger)
             throws DeviceNotAvailableException, TargetSetupError {
         if (shouldRunDeviceSpecificSetup(config)) {
-            if (getSandboxOptions(config).shouldParallelSetup()
-                    && getSandboxOptions(config).shouldUseNewFlagOrder()) {
+            boolean parallelSetup =
+                    getSandboxOptions(config).shouldParallelSetup()
+                            && getSandboxOptions(config).shouldUseNewFlagOrder();
+            if (parallelSetup) {
                 setupThread =
                         new SandboxSetupThread(mTestInfo, config, (ITestInvocationListener) logger);
                 setupThread.start();
             }
-            super.runDevicePreInvocationSetup(context, config, logger);
+            try {
+                super.runDevicePreInvocationSetup(context, config, logger);
+            } catch (DeviceNotAvailableException | TargetSetupError | RuntimeException e) {
+                if (parallelSetup) {
+                    // Join and clean up since run won't be called.
+                    try {
+                        setupThread.join();
+                    } catch (InterruptedException ie) {
+                        // Ignore
+                        CLog.e(e);
+                    }
+                    SandboxInvocationRunner.teardownSandbox(config);
+                }
+                throw e;
+            }
             if (!getSandboxOptions(config).shouldUseNewFlagOrder()) {
                 String commandLine = config.getCommandLine();
                 for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) {
diff --git a/src/com/android/tradefed/postprocessor/BasePostProcessor.java b/src/com/android/tradefed/postprocessor/BasePostProcessor.java
index f749d25..318c0c4 100644
--- a/src/com/android/tradefed/postprocessor/BasePostProcessor.java
+++ b/src/com/android/tradefed/postprocessor/BasePostProcessor.java
@@ -17,6 +17,8 @@
 
 import com.android.tradefed.config.Option;
 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.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
@@ -93,7 +95,10 @@
     /** {@inheritDoc} */
     @Override
     public final ITestInvocationListener init(ITestInvocationListener listener) {
+        long start = System.currentTimeMillis();
         setUp();
+        InvocationMetricLogger.addInvocationMetrics(
+                InvocationMetricKey.COLLECTOR_TIME, System.currentTimeMillis() - start);
         mForwarder = listener;
         return this;
     }
@@ -211,6 +216,7 @@
 
     @Override
     public final void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
+        long start = System.currentTimeMillis();
         mIsPostProcessing = true;
         try (CloseableTraceScope ignored =
                 new CloseableTraceScope("run_processor_" + this.getClass().getSimpleName())) {
@@ -232,6 +238,8 @@
             // Clear out the stored test and run logs.
             mTestLogs.clear();
             mRunLogs.clear();
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.COLLECTOR_TIME, System.currentTimeMillis() - start);
         }
         mIsPostProcessing = false;
         mForwarder.testRunEnded(elapsedTime, runMetrics);
@@ -284,6 +292,7 @@
     public final void testEnded(
             TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
         mIsPostProcessing = true;
+        long start = System.currentTimeMillis();
         try {
             HashMap<String, Metric> rawValues = getRawMetricsOnly(testMetrics);
             // Store the raw metrics from the test in storedTestMetrics for potential aggregation.
@@ -316,6 +325,9 @@
         } catch (RuntimeException e) {
             // Prevent exception from messing up the status reporting.
             CLog.e(e);
+        } finally {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.COLLECTOR_TIME, System.currentTimeMillis() - start);
         }
         mIsPostProcessing = false;
         mCurrentTest = null;
diff --git a/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java b/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java
index 9e70b6a..3040db6 100644
--- a/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java
+++ b/src/com/android/tradefed/result/JUnitToInvocationResultForwarder.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.result;
 
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 
@@ -40,6 +41,7 @@
 public class JUnitToInvocationResultForwarder implements TestListener {
 
     private final List<ITestInvocationListener> mInvocationListeners;
+    private CloseableTraceScope mMethodTrace = null;
 
     public JUnitToInvocationResultForwarder(ITestInvocationListener invocationListener) {
         mInvocationListeners = new ArrayList<ITestInvocationListener>(1);
@@ -77,9 +79,7 @@
     @Override
     public void endTest(Test test) {
         HashMap<String, Metric> emptyMap = new HashMap<>();
-        for (ITestInvocationListener listener : mInvocationListeners) {
-            listener.testEnded(getTestId(test), emptyMap);
-        }
+        endTest(test, emptyMap);
     }
 
     /**
@@ -89,8 +89,13 @@
      * @param metrics The metrics in a Map format to be passed to the results callback.
      */
     public void endTest(Test test, HashMap<String, Metric> metrics) {
+        TestDescription description = getTestId(test);
         for (ITestInvocationListener listener : mInvocationListeners) {
-            listener.testEnded(getTestId(test), metrics);
+            listener.testEnded(description, metrics);
+        }
+        if (mMethodTrace != null) {
+            mMethodTrace.close();
+            mMethodTrace = null;
         }
     }
 
@@ -115,8 +120,10 @@
     /** {@inheritDoc} */
     @Override
     public void startTest(Test test) {
+        TestDescription description = getTestId(test);
+        mMethodTrace = new CloseableTraceScope(description.getTestName());
         for (ITestInvocationListener listener : mInvocationListeners) {
-            listener.testStarted(getTestId(test));
+            listener.testStarted(description);
         }
     }
 
diff --git a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
index 92d76f9..d73908d 100644
--- a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
+++ b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
@@ -60,6 +60,11 @@
 
     private static final int MAX_CRASH_SIZE = 250000;
     private static final String MAX_CRASH_SIZE_MESSAGE = "\n<Truncated>";
+    // Message from crash collector that reflect an issue
+    private static final String FILTER_NOT_FOUND =
+            "java.lang.IllegalArgumentException: testfile not found:";
+    private static final String FILTER_NOT_READ =
+            "java.lang.IllegalArgumentException: Could not read test file";
 
     private Long mStartTime = null;
     private Long mLastStartTime = null;
@@ -133,10 +138,19 @@
         } else {
             errorMessage = extractCrashAndAddToMessage(errorMessage, mLastStartTime);
         }
-        error.setErrorMessage(errorMessage.trim());
+
         if (isCrash(errorMessage)) {
             error.setErrorIdentifier(DeviceErrorIdentifier.INSTRUMENTATION_CRASH);
+            // Special failure due to permission issue.
+            if (errorMessage.contains(FILTER_NOT_FOUND) || errorMessage.contains(FILTER_NOT_READ)) {
+                CLog.d("Detected a permission error with filters.");
+                // First stop retrying, it won't work
+                error.setRetriable(false);
+                error.setErrorIdentifier(TestErrorIdentifier.TEST_FILTER_NEEDS_UPDATE);
+                errorMessage = "See go/iae-testfile-not-found \n" + errorMessage;
+            }
         }
+        error.setErrorMessage(errorMessage.trim());
         // Add metrics for assessing uncaught IntrumentationTest crash failures.
         InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.CRASH_FAILURES, 1);
         if (error.getFailureStatus() == null) {
diff --git a/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java b/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
index 757df86..e85c79e 100644
--- a/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/StreamProtoResultReporter.java
@@ -25,6 +25,7 @@
 import java.io.IOException;
 import java.net.Socket;
 import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /** An implementation of {@link ProtoResultReporter} */
 public final class StreamProtoResultReporter extends ProtoResultReporter {
@@ -95,7 +96,7 @@
 
     @Override
     public void processFinalInvocationLogs(TestRecord invocationLogs) {
-        if (mResultWriterThread.mCancelled) {
+        if (mResultWriterThread.mCancelled.get()) {
             writeRecordToSocket(invocationLogs);
         } else {
             mToBeSent.add(invocationLogs);
@@ -105,7 +106,7 @@
     @Override
     public void processFinalProto(TestRecord finalRecord) {
         try {
-            if (mResultWriterThread.mCancelled) {
+            if (mResultWriterThread.mCancelled.get()) {
                 writeRecordToSocket(finalRecord);
             } else {
                 mToBeSent.add(finalRecord);
@@ -114,7 +115,7 @@
             // Upon invocation ended, trigger the end of the socket when the process finishes
             SocketFinisher thread = new SocketFinisher();
             Runtime.getRuntime().addShutdownHook(thread);
-            mResultWriterThread.mCancelled = true;
+            mResultWriterThread.mCancelled.set(true);
             try {
                 mResultWriterThread.join();
             } catch (InterruptedException e) {
@@ -162,7 +163,7 @@
     /** Send events from the event queue */
     private class ResultWriterThread extends Thread {
 
-        private boolean mCancelled = false;
+        private AtomicBoolean mCancelled = new AtomicBoolean(false);
 
         public ResultWriterThread() {
             super();
@@ -171,9 +172,9 @@
 
         @Override
         public void run() {
-            while (!mCancelled) {
+            while (!mCancelled.get()) {
                 flushEvents();
-                if (!mCancelled) {
+                if (!mCancelled.get()) {
                     RunUtil.getDefault().sleep(1000);
                 }
             }
diff --git a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
index 5b640b6..d9a696d 100644
--- a/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
+++ b/src/com/android/tradefed/result/suite/XmlSuiteResultFormatter.java
@@ -73,6 +73,9 @@
  */
 public class XmlSuiteResultFormatter implements IFormatterGenerator {
 
+    // The maximum size of a stack trace saved in the report.
+    private static final int STACK_TRACE_MAX_SIZE = 1024 * 1024;
+
     private static final String ENCODING = "UTF-8";
     private static final String TYPE = "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer";
     public static final String NS = null;
@@ -402,6 +405,7 @@
             }
             ErrorIdentifier errorIdentifier =
                     testResult.getValue().getFailure().getErrorIdentifier();
+            String truncatedStackTrace = getTruncatedStackTrace(fullStack, testResult.getKey());
             serializer.startTag(NS, FAILURE_TAG);
 
             serializer.attribute(NS, MESSAGE_ATTR, sanitizeXmlContent(message));
@@ -410,13 +414,31 @@
                 serializer.attribute(NS, ERROR_CODE_ATTR, Long.toString(errorIdentifier.code()));
             }
             serializer.startTag(NS, STACK_TAG);
-            serializer.text(sanitizeXmlContent(fullStack));
+            serializer.text(sanitizeXmlContent(truncatedStackTrace));
             serializer.endTag(NS, STACK_TAG);
 
             serializer.endTag(NS, FAILURE_TAG);
         }
     }
 
+    /** Truncates the full stack trace with maximum {@link STACK_TRACE_MAX_SIZE} characters. */
+    private static String getTruncatedStackTrace(String fullStackTrace, String testCaseName) {
+        if (fullStackTrace == null) {
+            return null;
+        }
+        if (fullStackTrace.length() > STACK_TRACE_MAX_SIZE) {
+            CLog.i(
+                    "The stack trace for test case %s contains %d characters, and has been"
+                            + " truncated to %d characters in %s.",
+                    testCaseName,
+                    fullStackTrace.length(),
+                    STACK_TRACE_MAX_SIZE,
+                    TEST_RESULT_FILE_NAME);
+            return fullStackTrace.substring(0, STACK_TRACE_MAX_SIZE);
+        }
+        return fullStackTrace;
+    }
+
     /** Add files captured by {@link TestFailureListener} on test failures. */
     private static void HandleLoggedFiles(
             XmlSerializer serializer, Entry<String, TestResult> testResult)
diff --git a/src/com/android/tradefed/retry/BaseRetryDecision.java b/src/com/android/tradefed/retry/BaseRetryDecision.java
index 8ee6419..e13e511 100644
--- a/src/com/android/tradefed/retry/BaseRetryDecision.java
+++ b/src/com/android/tradefed/retry/BaseRetryDecision.java
@@ -180,6 +180,14 @@
             return decision;
         }
 
+        // Resetting the device only happends when FULLY_ISOLATED is set, and that cleans up the
+        // device to pure state and re-run suite-level or module-level setup. Besides, it doesn't
+        // need to retry module for reboot isolation.
+        if (!IsolationGrade.FULLY_ISOLATED.equals(mRetryIsolationGrade)) {
+            CLog.i("Do not proceed on module retry because it's not set FULLY_ISOLATED.");
+            return decision;
+        }
+
         try {
             recoverStateOfDevices(getDevices(), attempt, module);
         } catch (DeviceNotAvailableException e) {
@@ -259,25 +267,24 @@
             return false;
         }
 
-        mStatistics.addResultsFromRun(previousResults);
+        boolean shouldRetry = false;
+        long retryStartTime = System.currentTimeMillis();
         if (test instanceof ITestFilterReceiver) {
             // TODO(b/77548917): Right now we only support ITestFilterReceiver. We should expect to
             // support ITestFile*Filter*Receiver in the future.
             ITestFilterReceiver filterableTest = (ITestFilterReceiver) test;
-            boolean shouldRetry = handleRetryFailures(filterableTest, previousResults);
+            shouldRetry = handleRetryFailures(filterableTest, previousResults);
             if (shouldRetry) {
                 // In case of retry, go through the recovery routine
                 recoverStateOfDevices(getDevices(), attemptJustExecuted, module);
             }
-            return shouldRetry;
         } else if (test instanceof IAutoRetriableTest) {
             // Routine for IRemoteTest that don't support filters but still needs retry.
             IAutoRetriableTest autoRetryTest = (IAutoRetriableTest) test;
-            boolean shouldRetry = autoRetryTest.shouldRetry(attemptJustExecuted, previousResults);
+            shouldRetry = autoRetryTest.shouldRetry(attemptJustExecuted, previousResults);
             if (shouldRetry) {
                 recoverStateOfDevices(getDevices(), attemptJustExecuted, module);
             }
-            return shouldRetry;
         } else {
             CLog.d(
                     "%s does not implement ITestFilterReceiver or IAutoRetriableTest, thus "
@@ -285,6 +292,12 @@
                     test);
             return false;
         }
+        long retryCost = System.currentTimeMillis() - retryStartTime;
+        if (!shouldRetry) {
+            retryCost = 0L;
+        }
+        mStatistics.addResultsFromRun(previousResults, retryCost, attemptJustExecuted);
+        return shouldRetry;
     }
 
     @Override
diff --git a/src/com/android/tradefed/retry/RetryStatistics.java b/src/com/android/tradefed/retry/RetryStatistics.java
index be4afca..0d0b343 100644
--- a/src/com/android/tradefed/retry/RetryStatistics.java
+++ b/src/com/android/tradefed/retry/RetryStatistics.java
@@ -17,7 +17,9 @@
 
 import com.android.tradefed.testtype.IRemoteTest;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Structure holding the statistics for a retry session of one {@link IRemoteTest}. Not all fields
@@ -26,6 +28,7 @@
 public class RetryStatistics {
     // The time spent in retry. Always populated if retries or iterations occurred
     public long mRetryTime = 0L;
+    public Map<Integer, Long> mAttemptIsolationCost = new HashMap<>();
 
     // Success and failure counts. Populated for RETRY_ANY_FAILURE.
     public long mRetrySuccess = 0L;
@@ -41,4 +44,15 @@
         }
         return aggregatedStats;
     }
+
+    public static long isolationCostPerAttempt(int attempt, List<RetryStatistics> stats) {
+        long isolationCost = 0L;
+        for (RetryStatistics s : stats) {
+            Long attemptCost = s.mAttemptIsolationCost.get(attempt);
+            if (attemptCost != null) {
+                isolationCost += attemptCost;
+            }
+        }
+        return isolationCost;
+    }
 }
diff --git a/src/com/android/tradefed/retry/RetryStatsHelper.java b/src/com/android/tradefed/retry/RetryStatsHelper.java
index f7f08e8..e4e17d3 100644
--- a/src/com/android/tradefed/retry/RetryStatsHelper.java
+++ b/src/com/android/tradefed/retry/RetryStatsHelper.java
@@ -32,9 +32,17 @@
 
     /** Add the results from the latest run to be tracked for statistics purpose. */
     public void addResultsFromRun(List<TestRunResult> mLatestResults) {
+        addResultsFromRun(mLatestResults, 0L, 0);
+    }
+
+    /** Add the results from the latest run to be tracked for statistics purpose. */
+    public void addResultsFromRun(List<TestRunResult> mLatestResults, long timeForIsolation, int attempt) {
         if (!mResults.isEmpty()) {
             updateSuccess(mResults.get(mResults.size() - 1), mLatestResults);
         }
+        if (timeForIsolation != 0L) {
+            mStats.mAttemptIsolationCost.put(attempt, timeForIsolation);
+        }
         mResults.add(mLatestResults);
     }
 
diff --git a/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java b/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java
index ef080bc..9e453cd 100644
--- a/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java
+++ b/src/com/android/tradefed/sandbox/SandboxInvocationRunner.java
@@ -39,6 +39,15 @@
         return runSandbox(info, config, listener);
     }
 
+    public static void teardownSandbox(IConfiguration config) {
+        ISandbox sandbox =
+                (ISandbox) config.getConfigurationObject(Configuration.SANDBOX_TYPE_NAME);
+        if (sandbox == null) {
+            throw new RuntimeException("Couldn't find the sandbox object.");
+        }
+        sandbox.tearDown();
+    }
+
     /** Preparation step of the sandbox */
     public static void prepareSandbox(
             TestInformation info, IConfiguration config, ITestInvocationListener listener)
@@ -49,7 +58,13 @@
             throw new RuntimeException("Couldn't find the sandbox object.");
         }
         PrettyPrintDelimiter.printStageDelimiter("Starting Sandbox Environment Setup");
-        Exception res = sandbox.prepareEnvironment(info.getContext(), config, listener);
+        Exception res = null;
+        try {
+            res = sandbox.prepareEnvironment(info.getContext(), config, listener);
+        } catch (RuntimeException e) {
+            sandbox.tearDown();
+            throw e;
+        }
         if (res != null) {
             CLog.w("Sandbox prepareEnvironment threw an Exception.");
             sandbox.tearDown();
diff --git a/src/com/android/tradefed/service/TradefedFeatureClient.java b/src/com/android/tradefed/service/TradefedFeatureClient.java
index 33845fb..9dbe801 100644
--- a/src/com/android/tradefed/service/TradefedFeatureClient.java
+++ b/src/com/android/tradefed/service/TradefedFeatureClient.java
@@ -17,6 +17,7 @@
 
 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.StreamUtil;
 
@@ -70,7 +71,8 @@
     private FeatureResponse triggerFeature(
             String featureName, String invocationReference, Map<String, String> args) {
         FeatureResponse response;
-        try {
+        try (CloseableTraceScope ignore =
+                new CloseableTraceScope("triggerFeature:" + featureName)) {
             CLog.d("invoking feature '%s'", featureName);
             FeatureRequest.Builder request =
                     FeatureRequest.newBuilder().setName(featureName).putAllArgs(args);
@@ -92,7 +94,17 @@
                                             .build())
                             .build();
         }
-        CLog.d("Feature name: %s. response: %s", featureName, response);
+        String message = String.format("Feature name: %s. response: %s", featureName, response);
+        if (response.hasErrorInfo()) {
+            StringBuilder callsite = new StringBuilder();
+            for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
+                callsite.append(e.toString());
+            }
+            message += String.format(". Callsite: %s", callsite);
+            CLog.w(message);
+        } else {
+            CLog.d(message);
+        }
         return response;
     }
 
diff --git a/src/com/android/tradefed/targetprep/CreateUserPreparer.java b/src/com/android/tradefed/targetprep/CreateUserPreparer.java
index c9c1260..41f3773 100644
--- a/src/com/android/tradefed/targetprep/CreateUserPreparer.java
+++ b/src/com/android/tradefed/targetprep/CreateUserPreparer.java
@@ -20,27 +20,23 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDevice;
-import com.android.tradefed.device.UserInfo;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 
-import java.util.ArrayList;
-import java.util.Map;
 
 /** Target preparer for creating user and cleaning it up at the end. */
 @OptionClass(alias = "create-user-preparer")
 public class CreateUserPreparer extends BaseTargetPreparer {
-    private static final String TF_CREATED_USER = "tf_created_user";
 
     @Option(
             name = "reuse-test-user",
             description =
                     "Whether or not to reuse already created tradefed test user, or remove them "
                             + " and re-create them between module runs.")
-    private boolean mReuseTestUser = false;
+    private boolean mReuseTestUser;
 
-    private Integer mOriginalUser = null;
-    private Integer mCreatedUserId = null;
+    private Integer mOriginalUser;
+    private Integer mCreatedUserId;
 
     @Override
     public void setUp(TestInformation testInfo)
@@ -52,9 +48,18 @@
             throw new TargetSetupError(
                     "Failed to get the current user.", device.getDeviceDescriptor());
         }
+        CLog.i("setUp(): mOriginalUser=%d, mReuseTestUser=%b", mOriginalUser, mReuseTestUser);
 
-        mCreatedUserId = createUser(device);
+        mCreatedUserId = UserCreationHelper.createUser(device, mReuseTestUser);
 
+        switchCurrentUser(device, mCreatedUserId);
+
+        device.waitForDeviceAvailable();
+        device.postBootSetup();
+    }
+
+    private void switchCurrentUser(ITestDevice device, int userId)
+            throws TargetSetupError, DeviceNotAvailableException {
         if (!device.startUser(mCreatedUserId, true)) {
             throw new TargetSetupError(
                     String.format("Failed to start to user '%s'", mCreatedUserId),
@@ -65,16 +70,12 @@
                     String.format("Failed to switch to user '%s'", mCreatedUserId),
                     device.getDeviceDescriptor());
         }
-        device.waitForDeviceAvailable();
-        device.postBootSetup();
     }
 
     @Override
     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
         if (mCreatedUserId == null) {
-            return;
-        }
-        if (mOriginalUser == null) {
+            CLog.d("Skipping teardown because no user was created");
             return;
         }
         if (e instanceof DeviceNotAvailableException) {
@@ -82,66 +83,24 @@
             return;
         }
         ITestDevice device = testInfo.getDevice();
-        if (!device.switchUser(mOriginalUser)) {
-            CLog.e("Failed to switch back to original user '%s'", mOriginalUser);
+
+        if (mOriginalUser == null) {
+            CLog.d("Skipping teardown because original user is null");
+            return;
         }
+        switchBackToOriginalUser(device);
+
         if (!mReuseTestUser) {
             device.removeUser(mCreatedUserId);
         }
     }
 
-    private int createUser(ITestDevice device)
-            throws DeviceNotAvailableException, TargetSetupError {
-        if (mReuseTestUser) {
-            Integer existingTFUser = findExistingTradefedUser(device);
-            if (existingTFUser != null) {
-                return existingTFUser;
-            }
+    private void switchBackToOriginalUser(ITestDevice device) throws DeviceNotAvailableException {
+        CLog.d(
+                "switchBackToOriginalUser(): switching current user from %d to user %d ",
+                mCreatedUserId, mOriginalUser);
+        if (!device.switchUser(mOriginalUser)) {
+            CLog.e("Failed to switch back to original user '%s'", mOriginalUser);
         }
-
-        cleanupOldUsersIfLimitReached(device);
-
-        try {
-            return device.createUser(TF_CREATED_USER);
-        } catch (IllegalStateException e) {
-            throw new TargetSetupError("Failed to create user.", e, device.getDeviceDescriptor());
-        }
-    }
-
-    private void cleanupOldUsersIfLimitReached(ITestDevice device)
-            throws DeviceNotAvailableException {
-        ArrayList<Integer> tfCreatedUsers = new ArrayList<>();
-        int existingUsersCount = 0;
-        for (Map.Entry<Integer, UserInfo> entry : device.getUserInfos().entrySet()) {
-            UserInfo userInfo = entry.getValue();
-            String userName = userInfo.userName();
-
-            if (!userInfo.isGuest()) {
-                // Guest users don't fall under the quota.
-                existingUsersCount++;
-            }
-            if (userName != null && userName.equals(TF_CREATED_USER)) {
-                tfCreatedUsers.add(entry.getKey());
-            }
-        }
-
-        if (existingUsersCount >= device.getMaxNumberOfUsersSupported()) {
-            // Reached the maximum number of users allowed. Remove stale users to free up space.
-            for (int userId : tfCreatedUsers) {
-                device.removeUser(userId);
-            }
-        }
-    }
-
-    private Integer findExistingTradefedUser(ITestDevice device)
-            throws DeviceNotAvailableException {
-        for (Map.Entry<Integer, UserInfo> entry : device.getUserInfos().entrySet()) {
-            String userName = entry.getValue().userName();
-
-            if (userName != null && userName.equals(TF_CREATED_USER)) {
-                return entry.getKey();
-            }
-        }
-        return null;
     }
 }
diff --git a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
index dd1bded..0a3ca9b 100644
--- a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
@@ -36,6 +36,7 @@
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
 import com.android.tradefed.result.error.InfraErrorIdentifier;
 import com.android.tradefed.targetprep.IDeviceFlasher.UserDataFlashOption;
+import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
@@ -48,6 +49,7 @@
 public abstract class DeviceFlashPreparer extends BaseTargetPreparer {
 
     private static final int BOOT_POLL_TIME_MS = 5 * 1000;
+    private static final long SNAPSHOT_CANCEL_TIMEOUT = 20000L;
 
     @Option(
         name = "device-boot-time",
@@ -119,6 +121,11 @@
                             + "should be flashed to")
     private String mRamdiskPartition = "boot";
 
+    @Option(
+            name = "cancel-ota-snapshot",
+            description = "In case an OTA snapshot is in progress, cancel it.")
+    private boolean mCancelSnapshot = false;
+
     /**
      * Sets the device boot time
      * <p/>
@@ -219,6 +226,18 @@
                 }
                 start = System.currentTimeMillis();
                 flasher.preFlashOperations(device, deviceBuild);
+                // After preFlashOperations device should be in bootloader
+                if (mCancelSnapshot && TestDeviceState.FASTBOOT.equals(device.getDeviceState())) {
+                    CommandResult res =
+                            device.executeFastbootCommand(
+                                    SNAPSHOT_CANCEL_TIMEOUT, "snapshot-update", "cancel");
+                    if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
+                        CLog.w(
+                                "Failed to cancel snapshot: %s.\nstdout:%s\nstderr:%s",
+                                res.getStatus(), res.getStdout(), res.getStderr());
+                    }
+                }
+
                 // Only #flash is included in the critical section
                 getHostOptions().takePermit(PermitLimitType.CONCURRENT_FLASHER);
                 queueTime = System.currentTimeMillis() - start;
diff --git a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
index aa89106..090782c 100644
--- a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
@@ -35,6 +35,7 @@
 import com.android.tradefed.util.RunUtil;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 
 import java.io.File;
 import java.io.IOException;
@@ -75,6 +76,10 @@
     protected static final String PARENT_SESSION_CREATION_CMD = "pm install-create --multi-package";
     protected static final String CHILD_SESSION_CREATION_CMD = "pm install-create";
     protected static final String APEX_OPTION = "--apex";
+    // The dump logic in {@link com.android.server.pm.ComputerEngine#generateApexPackageInfo} is
+    // invalid.
+    private static final ImmutableList<String> PACKAGES_WITH_INVALID_DUMP_INFO =
+            ImmutableList.of("com.google.mainline.primary.libs");
 
     private List<ApexInfo> mTestApexInfoList = new ArrayList<>();
     private List<String> mApexModulesToUninstall = new ArrayList<>();
@@ -97,7 +102,13 @@
             name = "apex-staging-wait-time",
             description = "The time in ms to wait for apex staged session ready.",
             isTimeVal = true)
-    private long mApexStagingWaitTime = 1 * 60 * 1000;
+    private long mApexStagingWaitTime = 0;
+
+    @Option(
+            name = "apex-rollback-wait-time",
+            description = "The time in ms to wait for apex rollback success.",
+            isTimeVal = true)
+    private long mApexRollbackWaitTime = 1 * 60 * 1000;
 
     @Option(
             name="extra-booting-wait-time",
@@ -198,7 +209,9 @@
      * @throws DeviceNotAvailableException if reboot fails.
      */
     private void activateStagedInstall(ITestDevice device) throws DeviceNotAvailableException {
-        RunUtil.getDefault().sleep(mApexStagingWaitTime);
+        if (mApexStagingWaitTime > 0) {
+            RunUtil.getDefault().sleep(mApexStagingWaitTime);
+        }
         device.reboot();
         // Some devices need extra waiting time after reboot to get fully ready.
         if (mExtraBootingWaitTime > 0) {
@@ -408,12 +421,16 @@
                         }
                     }
                     CLog.i("Wait for rollback fully done.");
-                    RunUtil.getDefault().sleep(mApexStagingWaitTime);
+                    RunUtil.getDefault().sleep(mApexRollbackWaitTime);
                     CLog.i("Device Rebooting");
                     device.reboot();
                     CLog.i("Reboot finished. Wait for rollback fully propagate.");
-                    RunUtil.getDefault().sleep(mApexStagingWaitTime);
+                    RunUtil.getDefault().sleep(mApexRollbackWaitTime);
                     device.waitForDeviceAvailable();
+                    // TODO(b/262626794): Remove after confirming with framework team about the
+                    // behavior of rollbaking mainline modules.
+                    CLog.i("Clean up staged and active session for mainline test mapping.");
+                    cleanUpStagedAndActiveSession(device);
                 }
             }
         }
@@ -624,8 +641,9 @@
                     device.getDeviceDescriptor(),
                     DeviceErrorIdentifier.APK_INSTALLATION_FAILED);
             }
-            RunUtil.getDefault().sleep(mApexStagingWaitTime);
-
+            if (mApexStagingWaitTime > 0) {
+                RunUtil.getDefault().sleep(mApexStagingWaitTime);
+            }
             if (log.contains("Success")) {
                 CLog.d(
                     "Train is staged successfully. Cmd: %s, Output: %s.",
@@ -651,7 +669,9 @@
                         device.getDeviceDescriptor(),
                         DeviceErrorIdentifier.FAIL_PUSH_FILE);
             } else {
-              CLog.d("%s pushed successfully to %s.", moduleFile.getName(), MODULE_PUSH_REMOTE_PATH + moduleFile.getName());
+                CLog.d(
+                        "%s pushed successfully to %s.",
+                        moduleFile.getName(), MODULE_PUSH_REMOTE_PATH + moduleFile.getName());
             }
             if (moduleFile.getName().endsWith(APK_SUFFIX)) {
                 String packageName = parsePackageName(moduleFile, device.getDeviceDescriptor());
@@ -670,7 +690,9 @@
             CLog.d("Parent session %s created successfully. ", parentSessionId);
         } else {
             throw new TargetSetupError(
-                    String.format("Failed to create parent session. Error: %s, Stdout: %s", res.getStderr(), res.getStdout()),
+                    String.format(
+                            "Failed to create parent session. Error: %s, Stdout: %s",
+                            res.getStderr(), res.getStdout()),
                     device.getDeviceDescriptor());
         }
 
@@ -678,20 +700,45 @@
             String childSessionId = null;
             if (moduleFile.getName().endsWith(APEX_SUFFIX)) {
                 if (mEnableRollback) {
-                    res = device.executeShellV2Command(String.format("%s %s %s %s | egrep -o -e '[0-9]+'", CHILD_SESSION_CREATION_CMD, APEX_OPTION, STAGED_INSTALL_OPTION, ENABLE_ROLLBACK_INSTALL_OPTION));
+                    res =
+                            device.executeShellV2Command(
+                                    String.format(
+                                            "%s %s %s %s | egrep -o -e '[0-9]+'",
+                                            CHILD_SESSION_CREATION_CMD,
+                                            APEX_OPTION,
+                                            STAGED_INSTALL_OPTION,
+                                            ENABLE_ROLLBACK_INSTALL_OPTION));
                 } else {
-                    res = device.executeShellV2Command(String.format("%s %s %s | egrep -o -e '[0-9]+'", CHILD_SESSION_CREATION_CMD, APEX_OPTION, STAGED_INSTALL_OPTION));
+                    res =
+                            device.executeShellV2Command(
+                                    String.format(
+                                            "%s %s %s | egrep -o -e '[0-9]+'",
+                                            CHILD_SESSION_CREATION_CMD,
+                                            APEX_OPTION,
+                                            STAGED_INSTALL_OPTION));
                 }
             } else {
                 if (mEnableRollback) {
-                    res = device.executeShellV2Command(String.format("%s %s %s | egrep -o -e '[0-9]+'", CHILD_SESSION_CREATION_CMD, STAGED_INSTALL_OPTION, ENABLE_ROLLBACK_INSTALL_OPTION));
+                    res =
+                            device.executeShellV2Command(
+                                    String.format(
+                                            "%s %s %s | egrep -o -e '[0-9]+'",
+                                            CHILD_SESSION_CREATION_CMD,
+                                            STAGED_INSTALL_OPTION,
+                                            ENABLE_ROLLBACK_INSTALL_OPTION));
                 } else {
-                    res = device.executeShellV2Command(String.format("%s %s | egrep -o -e '[0-9]+'", CHILD_SESSION_CREATION_CMD, STAGED_INSTALL_OPTION));
+                    res =
+                            device.executeShellV2Command(
+                                    String.format(
+                                            "%s %s | egrep -o -e '[0-9]+'",
+                                            CHILD_SESSION_CREATION_CMD, STAGED_INSTALL_OPTION));
                 }
             }
             if (res.getStatus() == CommandStatus.SUCCESS) {
                 childSessionId = res.getStdout();
-                CLog.d("Child session %s created successfully for %s. ", childSessionId, moduleFile.getName());
+                CLog.d(
+                        "Child session %s created successfully for %s. ",
+                        childSessionId, moduleFile.getName());
             } else {
                 throw new TargetSetupError(
                         String.format(
@@ -707,29 +754,33 @@
                                     parsePackageName(moduleFile, device.getDeviceDescriptor()),
                                     MODULE_PUSH_REMOTE_PATH + moduleFile.getName()));
             if (res.getStatus() == CommandStatus.SUCCESS) {
-                CLog.d("Successfully wrote %s to session %s. ", moduleFile.getName(), childSessionId);
+                CLog.d(
+                        "Successfully wrote %s to session %s. ",
+                        moduleFile.getName(), childSessionId);
             } else {
                 throw new TargetSetupError(
                         String.format("Failed to write %s to session %s. Error: %s, Stdout: %s",
                             moduleFile.getName(), childSessionId, res.getStderr(), res.getStdout()),
                     device.getDeviceDescriptor());
             }
-            res = device.executeShellV2Command(
+            res =
+                    device.executeShellV2Command(
                             String.format(
-                                    "pm install-add-session " + parentSessionId + " " + childSessionId));
+                                    "pm install-add-session "
+                                            + parentSessionId
+                                            + " "
+                                            + childSessionId));
             if (res.getStatus() != CommandStatus.SUCCESS) {
                 throw new TargetSetupError(
-                        String.format("Failed to add child session %s to parent session %s. Error: %s, Stdout: %s",
-                            childSessionId, parentSessionId, res.getStderr(), res.getStdout()),
-                    device.getDeviceDescriptor());
+                        String.format(
+                                "Failed to add child session %s to parent session %s. Error: %s,"
+                                        + " Stdout: %s",
+                                childSessionId, parentSessionId, res.getStderr(), res.getStdout()),
+                        device.getDeviceDescriptor());
             }
         }
         res = device.executeShellV2Command("pm install-commit " + parentSessionId);
 
-        // Wait until all apexes are fully staged and ready.
-        // TODO: should have adb level solution b/130039562
-        RunUtil.getDefault().sleep(mApexStagingWaitTime);
-
         if (res.getStatus() == CommandStatus.SUCCESS) {
             CLog.d("Train is staged successfully. Stdout: %s.", res.getStdout());
         } else {
@@ -1026,6 +1077,10 @@
         for (ApexInfo testApexInfo : mTestApexInfoList) {
             if (!activatedApexInfo.containsKey(testApexInfo.name)) {
                 failToActivateApex.add(testApexInfo);
+            } else if (PACKAGES_WITH_INVALID_DUMP_INFO.contains(testApexInfo.name)) {
+                // Skip checking version or sourceDir if we can't get the valid info.
+                // ToDo(b/265785212): Remove this if bug fixed.
+                continue;
             } else if (activatedApexInfo.get(testApexInfo.name).versionCode
                     != testApexInfo.versionCode) {
                 failToActivateApex.add(testApexInfo);
diff --git a/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java b/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java
index b543552..7b4d591 100644
--- a/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.targetprep;
 
+import com.android.annotations.VisibleForTesting;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.BackgroundDeviceAction;
@@ -159,6 +160,11 @@
         mCommands.add(cmd);
     }
 
+    @VisibleForTesting
+    public List<String> getCommands() {
+        return mCommands;
+    }
+
     /**
      * Returns the device to apply the preparer on.
      *
diff --git a/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java b/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
index 53e2fa3..9f21002 100644
--- a/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/RunOnSecondaryUserTargetPreparer.java
@@ -16,9 +16,6 @@
 
 package com.android.tradefed.targetprep;
 
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.IConfiguration;
-import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -46,8 +43,7 @@
  * running on the device can read this argument to respond to this state.
  */
 @OptionClass(alias = "run-on-secondary-user")
-public class RunOnSecondaryUserTargetPreparer extends BaseTargetPreparer
-        implements IConfigurationReceiver {
+public class RunOnSecondaryUserTargetPreparer extends BaseTargetPreparer {
 
     @VisibleForTesting static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
 
@@ -55,8 +51,6 @@
 
     @VisibleForTesting static final String SKIP_TESTS_REASON_KEY = "skip-tests-reason";
 
-    private IConfiguration mConfiguration;
-
     private int userIdToDelete = -1;
     private int originalUserId;
 
@@ -69,14 +63,6 @@
     private List<String> mTestPackages = new ArrayList<>();
 
     @Override
-    public void setConfiguration(IConfiguration configuration) {
-        if (configuration == null) {
-            throw new NullPointerException("configuration must not be null");
-        }
-        mConfiguration = configuration;
-    }
-
-    @Override
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, DeviceNotAvailableException {
         int secondaryUserId = getSecondaryUserId(testInfo.getDevice());
@@ -85,7 +71,7 @@
             if (!assumeTrue(
                     canCreateAdditionalUsers(testInfo.getDevice(), 1),
                     "Device cannot support additional users",
-                    testInfo.getDevice())) {
+                    testInfo)) {
                 return;
             }
 
@@ -131,6 +117,12 @@
 
     @Override
     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        String value = testInfo.properties().remove(SKIP_TESTS_REASON_KEY);
+        if (value != null) {
+            // Skip teardown if a skip test reason was set.
+            return;
+        }
+
         testInfo.properties().remove(RUN_TESTS_AS_USER_KEY);
         int currentUser = testInfo.getDevice().getCurrentUser();
         if (currentUser != originalUserId) {
@@ -146,17 +138,9 @@
      *
      * <p>This will return {@code value} and, if it is not true, setup should be skipped.
      */
-    private boolean assumeTrue(boolean value, String reason, ITestDevice device)
-            throws TargetSetupError {
+    private boolean assumeTrue(boolean value, String reason, TestInformation testInfo) {
         if (!value) {
-            setDisableTearDown(true);
-            try {
-                mConfiguration.injectOptionValue(
-                        "instrumentation-arg", SKIP_TESTS_REASON_KEY, reason.replace(" ", "\\ "));
-            } catch (ConfigurationException e) {
-                throw new TargetSetupError(
-                        "Error setting skip-tests-reason", e, device.getDeviceDescriptor());
-            }
+            testInfo.properties().put(SKIP_TESTS_REASON_KEY, reason.replace(" ", "\\ "));
         }
 
         return value;
diff --git a/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java b/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
index 834593b..9a7b4eb 100644
--- a/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/RunOnWorkProfileTargetPreparer.java
@@ -16,9 +16,6 @@
 
 package com.android.tradefed.targetprep;
 
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.IConfiguration;
-import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -48,8 +45,7 @@
  * to this state.
  */
 @OptionClass(alias = "run-on-work-profile")
-public class RunOnWorkProfileTargetPreparer extends BaseTargetPreparer
-        implements IConfigurationReceiver {
+public class RunOnWorkProfileTargetPreparer extends BaseTargetPreparer {
 
     @VisibleForTesting static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
 
@@ -57,8 +53,6 @@
 
     @VisibleForTesting static final String SKIP_TESTS_REASON_KEY = "skip-tests-reason";
 
-    private IConfiguration mConfiguration;
-
     private int mUserIdToDelete = -1;
     private DeviceOwner mDeviceOwnerToSet = null;
 
@@ -81,17 +75,9 @@
     private List<String> mTestPackages = new ArrayList<>();
 
     @Override
-    public void setConfiguration(IConfiguration configuration) {
-        if (configuration == null) {
-            throw new NullPointerException("configuration must not be null");
-        }
-        mConfiguration = configuration;
-    }
-
-    @Override
     public void setUp(TestInformation testInfo)
             throws TargetSetupError, DeviceNotAvailableException {
-        if (!requireFeatures(testInfo.getDevice(), "android.software.managed_users")) {
+        if (!requireFeatures(testInfo, "android.software.managed_users")) {
             return;
         }
 
@@ -101,7 +87,7 @@
             if (!assumeTrue(
                     canCreateAdditionalUsers(testInfo.getDevice(), 1),
                     "Device cannot support additional users",
-                    testInfo.getDevice())) {
+                    testInfo)) {
                 return;
             }
 
@@ -162,6 +148,11 @@
 
     @Override
     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        String value = testInfo.properties().remove(SKIP_TESTS_REASON_KEY);
+        if (value != null) {
+            // Skip teardown if a skip test reason was set.
+            return;
+        }
         testInfo.properties().remove(RUN_TESTS_AS_USER_KEY);
         if (mUserIdToDelete != -1) {
             testInfo.getDevice().removeUser(mUserIdToDelete);
@@ -173,13 +164,13 @@
         }
     }
 
-    private boolean requireFeatures(ITestDevice device, String... features)
-            throws TargetSetupError, DeviceNotAvailableException {
+    private boolean requireFeatures(TestInformation testInfo, String... features)
+            throws DeviceNotAvailableException {
         for (String feature : features) {
             if (!assumeTrue(
-                    device.hasFeature(feature),
+                    testInfo.getDevice().hasFeature(feature),
                     "Device does not have feature " + feature,
-                    device)) {
+                    testInfo)) {
                 return false;
             }
         }
@@ -192,16 +183,10 @@
      *
      * <p>This will return {@code value} and, if it is not true, setup should be skipped.
      */
-    private boolean assumeTrue(boolean value, String reason, ITestDevice device)
-            throws TargetSetupError {
+    private boolean assumeTrue(boolean value, String reason, TestInformation testInfo) {
         if (!value) {
-            setDisableTearDown(true);
-            try {
-                mConfiguration.injectOptionValue(
-                        "instrumentation-arg", SKIP_TESTS_REASON_KEY, reason.replace(" ", "\\ "));
-            } catch (ConfigurationException e) {
-                throw new TargetSetupError(
-                        "Error setting skip-tests-reason", e, device.getDeviceDescriptor());
+            if (!value) {
+                testInfo.properties().put(SKIP_TESTS_REASON_KEY, reason.replace(" ", "\\ "));
             }
         }
 
diff --git a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
index 787c494..4359adf0 100644
--- a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
@@ -179,7 +179,7 @@
     private boolean mInstantMode = false;
 
     @Option(name = "aapt-version", description = "The version of AAPT for APK parsing.")
-    private AaptVersion mAaptVersion = AaptVersion.AAPT;
+    private AaptVersion mAaptVersion = AaptVersion.AAPT2;
 
     @Option(
             name = "force-install-mode",
diff --git a/src/com/android/tradefed/targetprep/UserCreationHelper.java b/src/com/android/tradefed/targetprep/UserCreationHelper.java
new file mode 100644
index 0000000..429ce8c
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/UserCreationHelper.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+// Not directly unit tested, but its clients are
+final class UserCreationHelper {
+
+    private static final String TF_CREATED_USER = "tf_created_user";
+
+    public static int createUser(ITestDevice device, boolean reuseTestUser)
+            throws DeviceNotAvailableException, TargetSetupError {
+        if (reuseTestUser) {
+            Integer existingTFUser = findExistingTradefedUser(device);
+            if (existingTFUser != null) {
+                return existingTFUser;
+            }
+        }
+
+        cleanupOldUsersIfLimitReached(device);
+
+        try {
+            return device.createUser(TF_CREATED_USER);
+        } catch (IllegalStateException e) {
+            throw new TargetSetupError("Failed to create user.", e, device.getDeviceDescriptor());
+        }
+    }
+
+    private static void cleanupOldUsersIfLimitReached(ITestDevice device)
+            throws DeviceNotAvailableException {
+        ArrayList<Integer> tfCreatedUsers = new ArrayList<>();
+        int existingUsersCount = 0;
+        for (Map.Entry<Integer, UserInfo> entry : device.getUserInfos().entrySet()) {
+            UserInfo userInfo = entry.getValue();
+            String userName = userInfo.userName();
+
+            if (!userInfo.isGuest()) {
+                // Guest users don't fall under the quota.
+                existingUsersCount++;
+            }
+            if (userName != null && userName.equals(TF_CREATED_USER)) {
+                tfCreatedUsers.add(entry.getKey());
+            }
+        }
+
+        if (existingUsersCount >= device.getMaxNumberOfUsersSupported()) {
+            // Reached the maximum number of users allowed. Remove stale users to free up space.
+            for (int userId : tfCreatedUsers) {
+                device.removeUser(userId);
+            }
+        }
+    }
+
+    private static Integer findExistingTradefedUser(ITestDevice device)
+            throws DeviceNotAvailableException {
+        for (Map.Entry<Integer, UserInfo> entry : device.getUserInfos().entrySet()) {
+            String userName = entry.getValue().userName();
+
+            if (userName != null && userName.equals(TF_CREATED_USER)) {
+                return entry.getKey();
+            }
+        }
+        return null;
+    }
+
+    private UserCreationHelper() {
+        throw new UnsupportedOperationException("provide only static methods");
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/VisibleBackgroundUserPreparer.java b/src/com/android/tradefed/targetprep/VisibleBackgroundUserPreparer.java
new file mode 100644
index 0000000..671e3d7
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/VisibleBackgroundUserPreparer.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2023 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.annotations.VisibleForTesting;
+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.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/** Target preparer for running tests in a user that is started in the visible in the background. */
+@OptionClass(alias = "visible-background-user-preparer")
+public class VisibleBackgroundUserPreparer extends BaseTargetPreparer {
+
+    @VisibleForTesting public static final int INVALID_DISPLAY = -1; // same as android.view.Display
+    @VisibleForTesting public static final int DEFAULT_DISPLAY = 0; // same as android.view.Display
+
+    // Needed when running tests on background user on visible display
+    @VisibleForTesting protected static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
+
+    @Option(
+            name = "reuse-test-user",
+            description =
+                    "Whether or not to reuse already created tradefed test user, or remove them "
+                            + " and re-create them between module runs.")
+    private boolean mReuseTestUser;
+
+    @Option(name = "display-id", description = "Which display to start the user visible on")
+    private int mDisplayId = INVALID_DISPLAY;
+
+    private Integer mUserId;
+    private boolean mUserAlreadyVisible;
+
+    @Override
+    public void setUp(TestInformation testInfo)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        ITestDevice device = testInfo.getDevice();
+        if (!device.isVisibleBackgroundUsersSupported()) {
+            throw new TargetSetupError("feature not supported", device.getDeviceDescriptor());
+        }
+        CLog.i("setUp(): mReuseTestUser=%b, mDisplayId=%d", mReuseTestUser, mDisplayId);
+
+        mUserId = UserCreationHelper.createUser(device, mReuseTestUser);
+
+        startUserVisibleOnBackground(testInfo, device, mUserId);
+
+        device.waitForDeviceAvailable();
+        device.postBootSetup();
+    }
+
+    public void setDisplayId(int displayId) {
+        if (displayId == INVALID_DISPLAY) {
+            throw new IllegalArgumentException(
+                    "Cannot set it as INVALID_DISPLAY (" + INVALID_DISPLAY + ")");
+        }
+        mDisplayId = displayId;
+    }
+
+    @VisibleForTesting
+    public @Nullable Integer getDisplayId() {
+        return mDisplayId;
+    }
+
+    private void startUserVisibleOnBackground(
+            TestInformation testInfo, ITestDevice device, int userId)
+            throws TargetSetupError, DeviceNotAvailableException {
+        int displayId = mDisplayId;
+        if (displayId == INVALID_DISPLAY) {
+            // If display is not explicitly set (by option / setter), get the first available one
+            Set<Integer> displays = device.listDisplayIdsForStartingVisibleBackgroundUsers();
+            CLog.d("Displays: %s", displays);
+            if (displays.isEmpty()) {
+                throw new TargetSetupError(
+                        String.format("No display available to start to user '%d'", userId),
+                        device.getDeviceDescriptor());
+            }
+            Iterator<Integer> iterator = displays.iterator();
+            displayId = iterator.next();
+            if (displayId == DEFAULT_DISPLAY
+                    && device.isVisibleBackgroundUsersOnDefaultDisplaySupported()) {
+                // Ignore default display - it's a special case where the display id should have
+                // been passed directly
+                CLog.d(
+                        "Ignoring DEFAULT_DISPLAY because device supports background users on"
+                                + " default display");
+                if (!iterator.hasNext()) {
+                    throw new TargetSetupError(
+                            String.format(
+                                    "Only DEFAULT_DISPLAY available to start to user '%d'", userId),
+                            device.getDeviceDescriptor());
+                }
+                displayId = iterator.next();
+            }
+        }
+
+        mUserAlreadyVisible = device.isUserVisibleOnDisplay(userId, displayId);
+        if (mUserAlreadyVisible) {
+            CLog.d(
+                    "startUserVisibleOnBackground(): user %d already visible on display %d",
+                    userId, displayId);
+        } else {
+            CLog.d(
+                    "startUserVisibleOnBackground(): starting user %d visible on display %d",
+                    userId, displayId);
+
+            if (!device.startVisibleBackgroundUser(userId, displayId, /* waitFlag= */ true)) {
+                throw new TargetSetupError(
+                        String.format(
+                                "Failed to start to user '%s' on display %d", mUserId, displayId),
+                        device.getDeviceDescriptor());
+            }
+        }
+
+        CLog.i("Setting test property %s=%d", RUN_TESTS_AS_USER_KEY, mUserId);
+        testInfo.properties().put(RUN_TESTS_AS_USER_KEY, Integer.toString(mUserId));
+    }
+
+    @Override
+    public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+        if (mUserId == null) {
+            CLog.d("Skipping teardown because no user was created or reused");
+            return;
+        }
+        if (e instanceof DeviceNotAvailableException) {
+            CLog.d("Skipping teardown due to dnae: %s", e.getMessage());
+            return;
+        }
+        ITestDevice device = testInfo.getDevice();
+
+        stopTestUser(device);
+
+        if (!mReuseTestUser) {
+            device.removeUser(mUserId);
+        }
+    }
+
+    private void stopTestUser(ITestDevice device) throws DeviceNotAvailableException {
+        if (mUserAlreadyVisible) {
+            CLog.d("stopTestUser(): user %d was already visible on start", mUserId);
+            return;
+        }
+        CLog.d("stopTestUser(): stopping user %d ", mUserId);
+        if (!device.stopUser(mUserId, /* waitFlag= */ true, /* forceFlag= */ true)) {
+            CLog.e("Failed to stop user '%d'", mUserId);
+        }
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/sync/DeviceSyncHelper.java b/src/com/android/tradefed/targetprep/sync/DeviceSyncHelper.java
index 594904f..7f5b29e 100644
--- a/src/com/android/tradefed/targetprep/sync/DeviceSyncHelper.java
+++ b/src/com/android/tradefed/targetprep/sync/DeviceSyncHelper.java
@@ -16,19 +16,21 @@
 package com.android.tradefed.targetprep.sync;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.IFileEntry;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.ITestDevice.RecoveryMode;
 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.RunUtil;
 
+import com.google.common.collect.ImmutableSet;
+
 import java.io.File;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -40,6 +42,10 @@
     private final ITestDevice mDevice;
     private final File mTargetFilesFolder;
 
+    // Partitions known by adb in "adb sync"
+    private static final Set<String> ADB_PARTITIONS =
+            ImmutableSet.of("data", "odm", "oem", "product", "system", "system_ext", "vendor");
+
     public DeviceSyncHelper(ITestDevice device, File targetFilesFolder) {
         mDevice = device;
         mTargetFilesFolder = targetFilesFolder;
@@ -62,7 +68,7 @@
     private Set<String> getPartitions(File rootFolder) throws IOException {
         File abPartitions = new File(rootFolder, "META/ab_partitions.txt");
         String partitionString = FileUtil.readStringFromFile(abPartitions);
-        return new HashSet<>(Arrays.asList(partitionString.split("\n")));
+        return new LinkedHashSet<>(Arrays.asList(partitionString.split("\n")));
     }
 
     private void lowerCaseDirectory(File rootFolder) {
@@ -86,12 +92,28 @@
         device.executeAdbCommand("shell", "stop");
         RunUtil.getDefault().sleep(20000L);
 
+        device.setRecoveryMode(RecoveryMode.NONE);
+        // Use adb sync when supported
+        try (CloseableTraceScope push = new CloseableTraceScope("sync all")) {
+            Map<String, String> env = new HashMap<>();
+            env.put("ANDROID_PRODUCT_OUT", mTargetFilesFolder.getAbsolutePath());
+            String output = device.executeAdbCommand(0L, env, "sync", "all");
+            if (output == null) {
+                throw new IOException("Failed to sync all");
+            }
+            CLog.d("%s", output);
+        }
+
         for (String partition : partitions) {
             File localToPush = new File(mTargetFilesFolder, partition);
             if (!localToPush.exists()) {
                 CLog.w("%s is in the partition but doesn't exist", partition);
                 continue;
             }
+            if (ADB_PARTITIONS.contains(partition)) {
+                continue;
+            }
+            // Push after deleting to ensure shell is fine
             try (CloseableTraceScope push = new CloseableTraceScope("push " + partition)) {
                 String output =
                         device.executeAdbCommand(0L, "push", localToPush.getAbsolutePath(), "/");
@@ -99,57 +121,15 @@
                     throw new IOException("Failed to push " + localToPush);
                 }
             }
-            try (CloseableTraceScope delete = new CloseableTraceScope("delete_extra_files")) {
-                List<String> removeFiles = syncFiles(device, localToPush, "/" + partition);
-                CLog.d("Files to be removed from device: %s", removeFiles);
-                for (String deviceFile : removeFiles) {
-                    device.executeShellCommand(String.format("rm -rf %s", deviceFile));
-                }
-            }
         }
 
         try (CloseableTraceScope reboot = new CloseableTraceScope("reboot")) {
-            device.executeAdbCommand("reboot");
-            device.waitForDeviceAvailable();
+            String output = device.executeAdbCommand("reboot");
+            CLog.d("reboot output: %s", output);
+            device.waitForDeviceNotAvailable(30 * 1000L);
+            device.waitForDeviceAvailable(15 * 60 * 1000);
         }
         device.enableAdbRoot();
     }
 
-    private List<String> syncFiles(ITestDevice device, File localFileDir, String deviceFilePath)
-            throws DeviceNotAvailableException {
-        CLog.i(
-                "Syncing %s to %s on device %s",
-                localFileDir.getAbsolutePath(), deviceFilePath, device.getSerialNumber());
-        IFileEntry remoteFileEntry = device.getFileEntry(deviceFilePath);
-        if (remoteFileEntry == null) {
-            CLog.e("Could not find remote file entry %s ", deviceFilePath);
-            remoteFileEntry = device.getFileEntry(deviceFilePath);
-            if (remoteFileEntry == null) {
-                CLog.e(
-                        "Could not find remote file entry %s a second time. doesExist: %s",
-                        deviceFilePath, device.doesFileExist(deviceFilePath, 0));
-                return new ArrayList<String>();
-            }
-        }
-        return syncFiles(device, localFileDir, remoteFileEntry);
-    }
-
-    private List<String> syncFiles(
-            ITestDevice device, File localFileDir, final IFileEntry remoteFileEntry)
-            throws DeviceNotAvailableException {
-        // find newer files to sync
-        // File[] localFiles = localFileDir.listFiles(new NoHiddenFilesFilter());
-        List<String> filePathsToRemove = new ArrayList<>();
-        for (IFileEntry entry : remoteFileEntry.getChildren(false)) {
-            File local = new File(localFileDir, entry.getName());
-            if (!local.exists()) {
-                filePathsToRemove.add(entry.getFullPath());
-            } else {
-                if (local.isDirectory()) {
-                    filePathsToRemove.addAll(syncFiles(device, local, entry));
-                }
-            }
-        }
-        return filePathsToRemove;
-    }
 }
diff --git a/src/com/android/tradefed/testtype/coverage/CoverageOptions.java b/src/com/android/tradefed/testtype/coverage/CoverageOptions.java
index fbdfd91..60a5bc2 100644
--- a/src/com/android/tradefed/testtype/coverage/CoverageOptions.java
+++ b/src/com/android/tradefed/testtype/coverage/CoverageOptions.java
@@ -80,6 +80,12 @@
                             + " \"foo.*\\.profraw\".  Default: \".*\\.profraw\"")
     private String mProfrawFilter = ".*\\.profraw";
 
+    @Option(
+            name = "pull-timeout",
+            isTimeVal = true,
+            description = "Timeout in milliseconds to pull coverage metrics from the device.")
+    private long mPullTimeout = 20 * 60 * 1000;
+
     /**
      * Returns whether coverage measurements should be collected from this run.
      *
@@ -150,4 +156,13 @@
     public String getProfrawFilter() {
         return mProfrawFilter;
     }
+
+    /**
+     * Returns the timeout in milliseconds for pulling coverage metrics from the device.
+     *
+     * @return a {@link long} as timeout in milliseconds.
+     */
+    public long getPullTimeout() {
+        return mPullTimeout;
+    }
 }
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index dbf9749..46757a7 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -46,6 +46,7 @@
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestCollector;
 import com.android.tradefed.testtype.ITestFilterReceiver;
+import com.android.tradefed.util.StreamUtil;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -98,6 +99,7 @@
 
     // Tracking of the metrics
     private RetryStatistics mRetryStats = null;
+    private int mCountRetryUsed = 0;
 
     public GranularRetriableTestWrapper(
             IRemoteTest test,
@@ -117,7 +119,11 @@
             int maxRunLimit) {
         mTest = test;
         mModule = module;
-        initializeGranularRunListener(mainListener);
+        IInvocationContext context = null;
+        if (module != null) {
+            context = module.getModuleInvocationContext();
+        }
+        initializeGranularRunListener(mainListener, context);
         mFailureListener = failureListener;
         mModuleLevelListeners = moduleLevelListeners;
         mMaxRunLimit = maxRunLimit;
@@ -187,14 +193,14 @@
     }
 
     /**
-     * Initialize granular run listener with {@link RemoteTestTimeOutEnforcer} if timeout is
-     * set.
+     * Initialize granular run listener with {@link RemoteTestTimeOutEnforcer} if timeout is set.
      *
      * @param listener The listener for each test run should be wrapped.
-     *
+     * @param moduleContext the invocation context of the module
      */
-    private void initializeGranularRunListener(ITestInvocationListener listener) {
-        mMainGranularRunListener = new ModuleListener(listener);
+    private void initializeGranularRunListener(
+            ITestInvocationListener listener, IInvocationContext moduleContext) {
+        mMainGranularRunListener = new ModuleListener(listener, moduleContext);
         if (mModule != null) {
             ConfigurationDescriptor configDesc =
                     mModule.getModuleInvocationContext().getConfigurationDescriptor();
@@ -306,6 +312,7 @@
                     }
                 }
                 firstCheck = false;
+                mCountRetryUsed++;
                 CLog.d("Intra-module retry attempt number %s", attemptNumber);
                 // Run the tests again
                 intraModuleRun(testInfo, allListeners, attemptNumber);
@@ -436,6 +443,10 @@
         return mMainGranularRunListener;
     }
 
+    public int getRetryCount() {
+        return mCountRetryUsed;
+    }
+
     @Override
     public void setCollectTestsOnly(boolean shouldCollectTest) {
         mCollectTestsOnly = shouldCollectTest;
@@ -444,7 +455,9 @@
     private FailureDescription createFromException(Throwable exception) {
         String message =
                 (exception.getMessage() == null)
-                        ? String.format("No error message reported for: %s", exception)
+                        ? String.format(
+                                "No error message reported for: %s",
+                                StreamUtil.getStackTrace(exception))
                         : exception.getMessage();
         FailureDescription failure =
                 CurrentInvocation.createFailure(message, null).setCause(exception);
diff --git a/src/com/android/tradefed/testtype/suite/ITestSuite.java b/src/com/android/tradefed/testtype/suite/ITestSuite.java
index f583731d..17d3c4d 100644
--- a/src/com/android/tradefed/testtype/suite/ITestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/ITestSuite.java
@@ -476,59 +476,73 @@
         }
         CLog.i(String.format("Start to stage test artifacts for %d modules.", modules.size()));
         long startTime = System.currentTimeMillis();
-        // Include the file if its path contains a folder name matching any of the module.
-        String moduleRegex =
-                modules.stream()
-                        .map(m -> String.format("/%s/", m))
-                        .collect(Collectors.joining("|"));
-        List<String> includeFilters = Arrays.asList(moduleRegex);
-        // Ignore config file as it's part of config zip artifact that's staged already.
-        List<String> excludeFilters = Arrays.asList("[.]config$");
-        if (mStageArtifactsViaFeature) {
-            try (TradefedFeatureClient client = new TradefedFeatureClient()) {
-                Map<String, String> args = new HashMap<>();
-                args.put(ResolvePartialDownload.DESTINATION_DIR, getTestsDir().getAbsolutePath());
-                args.put(ResolvePartialDownload.INCLUDE_FILTERS, String.join(";", includeFilters));
-                args.put(ResolvePartialDownload.EXCLUDE_FILTERS, String.join(";", excludeFilters));
-                // Pass the remote paths
-                String remotePaths =
-                        mBuildInfo.getRemoteFiles().stream()
-                                .map(p -> p.toString())
-                                .collect(Collectors.joining(";"));
-                args.put(ResolvePartialDownload.REMOTE_PATHS, remotePaths);
+        try (CloseableTraceScope ignored =
+                new CloseableTraceScope(
+                        InvocationMetricKey.stage_suite_test_artifacts.toString())) {
+            // Include the file if its path contains a folder name matching any of the module.
+            String moduleRegex =
+                    modules.stream()
+                            .map(m -> String.format("/%s/", m))
+                            .collect(Collectors.joining("|"));
+            List<String> includeFilters = Arrays.asList(moduleRegex);
+            // Ignore config file as it's part of config zip artifact that's staged already.
+            List<String> excludeFilters = Arrays.asList("[.]config$");
+            if (mStageArtifactsViaFeature) {
+                try (TradefedFeatureClient client = new TradefedFeatureClient()) {
+                    Map<String, String> args = new HashMap<>();
+                    args.put(
+                            ResolvePartialDownload.DESTINATION_DIR,
+                            getTestsDir().getAbsolutePath());
+                    args.put(
+                            ResolvePartialDownload.INCLUDE_FILTERS,
+                            String.join(";", includeFilters));
+                    args.put(
+                            ResolvePartialDownload.EXCLUDE_FILTERS,
+                            String.join(";", excludeFilters));
+                    // Pass the remote paths
+                    String remotePaths =
+                            mBuildInfo.getRemoteFiles().stream()
+                                    .map(p -> p.toString())
+                                    .collect(Collectors.joining(";"));
+                    args.put(ResolvePartialDownload.REMOTE_PATHS, remotePaths);
 
-                FeatureResponse rep =
-                        client.triggerFeature(
-                                ResolvePartialDownload.RESOLVE_PARTIAL_DOWNLOAD_FEATURE_NAME, args);
-                if (rep.hasErrorInfo()) {
-                    throw new HarnessRuntimeException(
-                            rep.getErrorInfo().getErrorTrace(),
-                            InfraErrorIdentifier.ARTIFACT_DOWNLOAD_ERROR);
-                }
-            } catch (FileNotFoundException e) {
-                throw new HarnessRuntimeException(
-                        e.getMessage(), e, InfraErrorIdentifier.ARTIFACT_DOWNLOAD_ERROR);
-            }
-        } else {
-            mDynamicResolver.setDevice(device);
-            mDynamicResolver.addExtraArgs(
-                    mMainConfiguration.getCommandOptions().getDynamicDownloadArgs());
-            for (File remoteFile : mBuildInfo.getRemoteFiles()) {
-                try {
-                    mDynamicResolver.resolvePartialDownloadZip(
-                            getTestsDir(), remoteFile.toString(), includeFilters, excludeFilters);
-                } catch (BuildRetrievalError | FileNotFoundException e) {
-                    String message =
-                            String.format(
-                                    "Failed to download partial zip from %s for modules: %s",
-                                    remoteFile, String.join(", ", modules));
-                    CLog.e(message);
-                    CLog.e(e);
-                    if (e instanceof IHarnessException) {
-                        throw new HarnessRuntimeException(message, (IHarnessException) e);
+                    FeatureResponse rep =
+                            client.triggerFeature(
+                                    ResolvePartialDownload.RESOLVE_PARTIAL_DOWNLOAD_FEATURE_NAME,
+                                    args);
+                    if (rep.hasErrorInfo()) {
+                        throw new HarnessRuntimeException(
+                                rep.getErrorInfo().getErrorTrace(),
+                                InfraErrorIdentifier.ARTIFACT_DOWNLOAD_ERROR);
                     }
+                } catch (FileNotFoundException e) {
                     throw new HarnessRuntimeException(
-                            message, e, InfraErrorIdentifier.ARTIFACT_DOWNLOAD_ERROR);
+                            e.getMessage(), e, InfraErrorIdentifier.ARTIFACT_DOWNLOAD_ERROR);
+                }
+            } else {
+                mDynamicResolver.setDevice(device);
+                mDynamicResolver.addExtraArgs(
+                        mMainConfiguration.getCommandOptions().getDynamicDownloadArgs());
+                for (File remoteFile : mBuildInfo.getRemoteFiles()) {
+                    try {
+                        mDynamicResolver.resolvePartialDownloadZip(
+                                getTestsDir(),
+                                remoteFile.toString(),
+                                includeFilters,
+                                excludeFilters);
+                    } catch (BuildRetrievalError | FileNotFoundException e) {
+                        String message =
+                                String.format(
+                                        "Failed to download partial zip from %s for modules: %s",
+                                        remoteFile, String.join(", ", modules));
+                        CLog.e(message);
+                        CLog.e(e);
+                        if (e instanceof IHarnessException) {
+                            throw new HarnessRuntimeException(message, (IHarnessException) e);
+                        }
+                        throw new HarnessRuntimeException(
+                                message, e, InfraErrorIdentifier.ARTIFACT_DOWNLOAD_ERROR);
+                    }
                 }
             }
         }
@@ -983,6 +997,8 @@
 
         // We report System checkers like tests.
         reportModuleCheckerResult(MODULE_CHECKER_PRE, moduleName, failures, startTime, listener);
+        InvocationMetricLogger.addInvocationPairMetrics(
+                InvocationMetricKey.STATUS_CHECKER_PAIR, startTime, System.currentTimeMillis());
         return properties;
     }
 
@@ -1044,6 +1060,8 @@
 
         // We report System checkers like tests.
         reportModuleCheckerResult(MODULE_CHECKER_POST, moduleName, failures, startTime, listener);
+        InvocationMetricLogger.addInvocationPairMetrics(
+                InvocationMetricKey.STATUS_CHECKER_PAIR, startTime, System.currentTimeMillis());
         return properties;
     }
 
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index 0475be3..a419a3e 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -132,6 +132,7 @@
     public static final String TEST_TIME = "TEST_TIME";
     public static final String MODULE_TEST_COUNT = "MODULE_TEST_COUNT";
     public static final String RETRY_TIME = "MODULE_RETRY_TIME";
+    public static final String ISOLATION_COST = "ISOLATION_COST";
     public static final String RETRY_SUCCESS_COUNT = "MODULE_RETRY_SUCCESS";
     public static final String RETRY_FAIL_COUNT = "MODULE_RETRY_FAILED";
 
@@ -515,6 +516,7 @@
         // Run the tests
         try {
             mStartTestTime = getCurrentTime();
+            int perModuleRetryQuota = mMaxRetry;
             while (true) {
                 IRemoteTest test = poll();
                 if (test == null) {
@@ -563,7 +565,7 @@
                                 failureListener,
                                 moduleLevelListeners,
                                 skipTestCases,
-                                maxRunLimit);
+                                perModuleRetryQuota);
                 mCurrentTestWrapper.setCollectTestsOnly(mCollectTestsOnly);
                 // Resolve the dynamic options for that one test.
                 preparationException =
@@ -605,6 +607,8 @@
                         // Keep track of each listener for attempts
                         mRunListenersResults.add(mCurrentTestWrapper.getResultListener());
                     }
+                    // Limit escalating retries across all sub-IRemoteTests
+                    perModuleRetryQuota -= mCurrentTestWrapper.getRetryCount();
 
                     mExpectedTests += mCurrentTestWrapper.getExpectedTestsCount();
                     // Get information about retry
@@ -811,15 +815,25 @@
         metricsProto.put(MODULE_TEST_COUNT, TfMetricProtoUtil.createSingleValue(numResults, "int"));
         // Report all the retry informations
         if (!mRetryStats.isEmpty()) {
-            RetryStatistics agg = RetryStatistics.aggregateStatistics(mRetryStats);
-            metricsProto.put(
-                    RETRY_TIME,
-                    TfMetricProtoUtil.createSingleValue(agg.mRetryTime, "milliseconds"));
-            metricsProto.put(
-                    RETRY_SUCCESS_COUNT,
-                    TfMetricProtoUtil.createSingleValue(agg.mRetrySuccess, ""));
-            metricsProto.put(
-                    RETRY_FAIL_COUNT, TfMetricProtoUtil.createSingleValue(agg.mRetryFailure, ""));
+            if (attempt != null) {
+                long cost = RetryStatistics.isolationCostPerAttempt(attempt, mRetryStats);
+                if (cost != 0L) {
+                    metricsProto.put(
+                            ISOLATION_COST,
+                            TfMetricProtoUtil.createSingleValue(cost, "milliseconds"));
+                }
+            } else {
+                RetryStatistics agg = RetryStatistics.aggregateStatistics(mRetryStats);
+                metricsProto.put(
+                        RETRY_TIME,
+                        TfMetricProtoUtil.createSingleValue(agg.mRetryTime, "milliseconds"));
+                metricsProto.put(
+                        RETRY_SUCCESS_COUNT,
+                        TfMetricProtoUtil.createSingleValue(agg.mRetrySuccess, ""));
+                metricsProto.put(
+                        RETRY_FAIL_COUNT,
+                        TfMetricProtoUtil.createSingleValue(agg.mRetryFailure, ""));
+            }
         }
 
         // Only report the mismatch if there were no error during the run.
diff --git a/src/com/android/tradefed/testtype/suite/ModuleListener.java b/src/com/android/tradefed/testtype/suite/ModuleListener.java
index 55e5e3e..5adf63a 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleListener.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleListener.java
@@ -17,6 +17,7 @@
 
 import com.android.ddmlib.Log.LogLevel;
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.logger.CurrentInvocation.IsolationGrade;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
@@ -44,7 +45,8 @@
     private TestStatus mTestStatus;
     private String mTrace;
     private int mTestsRan = 1;
-    private ITestInvocationListener mMainListener;
+    private final ITestInvocationListener mMainListener;
+    private final IInvocationContext mModuleContext;
 
     private boolean mCollectTestsOnly = false;
     /** Track runs in progress for logging purpose */
@@ -53,8 +55,9 @@
     private IsolationGrade mAttemptIsolation = IsolationGrade.NOT_ISOLATED;
 
     /** Constructor. */
-    public ModuleListener(ITestInvocationListener listener) {
+    public ModuleListener(ITestInvocationListener listener, IInvocationContext moduleContext) {
         mMainListener = listener;
+        mModuleContext = moduleContext;
         mRunInProgress = false;
         setIsAggregrateMetrics(true);
     }
@@ -87,13 +90,15 @@
         if (attemptNumber != 0) {
             mTestsRan = 1;
         }
-        CLog.d("ModuleListener.testRunStarted(%s, %s, %s)", name, numTests, attemptNumber);
+        CLog.d(
+                "ModuleListener.testRunStarted(%s, %s, %s) on %s",
+                name, numTests, attemptNumber, getSerial());
     }
 
     /** {@inheritDoc} */
     @Override
     public void testRunFailed(String errorMessage) {
-        CLog.d("ModuleListener.testRunFailed(%s)", errorMessage);
+        CLog.d("ModuleListener.testRunFailed(%s) on %s", errorMessage, getSerial());
         super.testRunFailed(errorMessage);
     }
 
@@ -101,17 +106,18 @@
     @Override
     public void testRunFailed(FailureDescription failure) {
         CLog.d(
-                "ModuleListener.testRunFailed(%s|%s|%s)",
+                "ModuleListener.testRunFailed(%s|%s|%s) on %s",
                 failure.getFailureStatus(),
                 failure.getErrorIdentifier(),
-                failure.getErrorMessage());
+                failure.getErrorMessage(),
+                getSerial());
         super.testRunFailed(failure);
     }
 
     /** {@inheritDoc} */
     @Override
     public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
-        CLog.d("ModuleListener.testRunEnded(%s)", elapsedTime);
+        CLog.d("ModuleListener.testRunEnded(%s) on %s", elapsedTime, getSerial());
 
         if (!IsolationGrade.NOT_ISOLATED.equals(mAttemptIsolation)) {
             runMetrics.put(
@@ -133,7 +139,7 @@
     @Override
     public void testStarted(TestDescription test, long startTime) {
         if (!mCollectTestsOnly) {
-            CLog.d("ModuleListener.testStarted(%s)", test.toString());
+            CLog.d("ModuleListener.testStarted(%s) on %s", test.toString(), getSerial());
         }
         mTestStatus = TestStatus.PASSED;
         mTrace = null;
@@ -155,7 +161,8 @@
             String runAndTestCase = String.format("%s%s", runName, testName.toString());
             String message =
                     String.format(
-                            "[%d/%d] %s %s", mTestsRan, getExpectedTests(), runAndTestCase, status);
+                            "[%d/%d] %s %s %s",
+                            mTestsRan, getExpectedTests(), getSerial(), runAndTestCase, status);
             if (mTrace != null) {
                 message += ": " + mTrace;
             }
@@ -261,4 +268,11 @@
             }
         }
     }
+
+    private String getSerial() {
+        if (mModuleContext == null || mModuleContext.getDevices().isEmpty()) {
+            return "";
+        }
+        return mModuleContext.getDevices().get(0).getSerialNumber();
+    }
 }
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index 3690770..a1d599e 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -20,17 +20,20 @@
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.error.HarnessRuntimeException;
+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.error.InfraErrorIdentifier;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil2;
 import com.android.tradefed.util.testmapping.TestInfo;
 import com.android.tradefed.util.testmapping.TestMapping;
 import com.android.tradefed.util.testmapping.TestOption;
-import com.android.tradefed.util.ZipUtil2;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import com.google.common.io.Files;
 
 import org.apache.commons.compress.archivers.zip.ZipFile;
@@ -41,6 +44,7 @@
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -287,7 +291,6 @@
             IAbi abi = configDescriptor.getAbi();
             // Get the parameterized module name by striping the abi information out.
             String moduleName = entry.getKey().replace(String.format("%s ", abi.getName()), "");
-            String configPath = moduleConfig.getName();
             Set<TestInfo> testInfos = getTestInfos(testInfosToRun, moduleName);
             // Only keep the same matching abi runner
             allTests.addAll(createIndividualTests(testInfos, moduleConfig, abi));
@@ -352,7 +355,8 @@
             configFile = null;
         }
         // De-duplicate test infos so that there won't be duplicate test options.
-        testInfos = dedupTestInfos(testInfos);
+        testInfos = dedupTestInfos(configFile, testInfos);
+
         for (TestInfo testInfo : testInfos) {
             // Clean up all the test options injected in SuiteModuleLoader.
             super.cleanUpSuiteSetup();
@@ -455,26 +459,52 @@
     }
 
     /**
-     * De-duplicate test infos with the same test options.
+     * De-duplicate test infos and aggregate test-mapping sources with the same test options.
      *
+     * @param config the config file being deduplicated
      * @param testInfos A {@code Set<TestInfo>} containing multiple test options.
      * @return A {@code Set<TestInfo>} of tests without duplicated test options.
      */
     @VisibleForTesting
-    Set<TestInfo> dedupTestInfos(Set<TestInfo> testInfos) {
+    Set<TestInfo> dedupTestInfos(File config, Set<TestInfo> testInfos) {
         Set<String> nameOptions = new HashSet<>();
         Set<TestInfo> dedupTestInfos = new HashSet<>();
+        Set<String> duplicateSources = new LinkedHashSet<String>();
         for (TestInfo testInfo : testInfos) {
-            String nameOption = testInfo.getName() + testInfo.getOptions().toString();
+            String nameOption = testInfo.getNameOption();
             if (!nameOptions.contains(nameOption)) {
                 dedupTestInfos.add(testInfo);
+                duplicateSources.addAll(testInfo.getSources());
                 nameOptions.add(nameOption);
+            } else {
+                aggregateTestInfo(testInfo, dedupTestInfos);
             }
         }
+
+        // If size above 1 that means we have duplicated modules with different options
+        if (dedupTestInfos.size() > 1) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.DUPLICATE_MAPPING_DIFFERENT_OPTIONS,
+                    String.format("%s:" + Joiner.on("+").join(duplicateSources), config));
+        }
         return dedupTestInfos;
     }
 
     /**
+     * Aggregate test-mapping sources of the test info with the same test options
+     *
+     * @param testInfo A {@code TestInfo} of duplicated test to be aggregated.
+     * @param dedupTestInfos A {@code Set<TestInfo>} of tests without duplicated test options.
+     */
+    private void aggregateTestInfo(TestInfo testInfo, Set<TestInfo> dedupTestInfos) {
+        for (TestInfo dedupTestInfo : dedupTestInfos) {
+            if (testInfo.getNameOption().equals(dedupTestInfo.getNameOption())) {
+                dedupTestInfo.addSources(testInfo.getSources());
+            }
+        }
+    }
+
+    /**
      * Get the test infos for the given module name.
      *
      * @param testInfos A {@code Set<TestInfo>} containing multiple test options.
diff --git a/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java b/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
index 1f8d666..be91ba7 100644
--- a/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
+++ b/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
@@ -18,33 +18,56 @@
 /** Special values associated with the suite "parameter" keys in the metadata of each module. */
 public enum ModuleParameters {
     /** describes a parameterization based on app that should be installed in instant mode. */
-    INSTANT_APP("instant_app", "instant_app_family"),
-    NOT_INSTANT_APP("not_instant_app", "instant_app_family"),
+    INSTANT_APP("instant_app", ModuleParameters.INSTANT_APP_FAMILY),
+    NOT_INSTANT_APP("not_instant_app", ModuleParameters.INSTANT_APP_FAMILY),
 
-    MULTI_ABI("multi_abi", "multi_abi_family"),
-    NOT_MULTI_ABI("not_multi_abi", "multi_abi_family"),
+    MULTI_ABI("multi_abi", ModuleParameters.MULTI_ABI_FAMILY),
+    NOT_MULTI_ABI("not_multi_abi", ModuleParameters.MULTI_ABI_FAMILY),
 
-    SECONDARY_USER("secondary_user", "secondary_user_family"),
-    NOT_SECONDARY_USER("not_secondary_user", "secondary_user_family"),
+    SECONDARY_USER("secondary_user", ModuleParameters.SECONDARY_USER_FAMILY),
+    NOT_SECONDARY_USER("not_secondary_user", ModuleParameters.SECONDARY_USER_FAMILY),
+
+    // Secondary user started on background, visible in a secondary display
+    SECONDARY_USER_ON_SECONDARY_DISPLAY(
+            "secondary_user_on_secondary_display",
+            ModuleParameters.SECONDARY_USER_ON_SECONDARY_DISPLAY_FAMILY),
+    NOT_SECONDARY_USER_ON_SECONDARY_DISPLAY(
+            "not_secondary_user_on_secondary_display",
+            ModuleParameters.SECONDARY_USER_ON_SECONDARY_DISPLAY_FAMILY),
+
+    // Secondary user started on background, visible in the default display
+    SECONDARY_USER_ON_DEFAULT_DISPLAY(
+            "secondary_user_on_defauilt_display",
+            ModuleParameters.SECONDARY_USER_ON_DEFAULT_DISPLAY_FAMILY),
+    NOT_SECONDARY_USER_ON_DEFAULT_DISPLAY(
+            "not_secondary_user_on_default_display",
+            ModuleParameters.SECONDARY_USER_ON_DEFAULT_DISPLAY_FAMILY),
 
     // Multi-user
-    MULTIUSER("multiuser", "multiuser_family"),
-    RUN_ON_WORK_PROFILE("run_on_work_profile", "run_on_work_profile_family"),
-    RUN_ON_SECONDARY_USER("run_on_secondary_user", "run_on_secondary_user_family"),
+    MULTIUSER("multiuser", ModuleParameters.MULTIUSER_FAMILY),
+    RUN_ON_WORK_PROFILE("run_on_work_profile", ModuleParameters.RUN_ON_WORK_PROFILE_FAMILY),
+    RUN_ON_SECONDARY_USER("run_on_secondary_user", ModuleParameters.RUN_ON_SECONDARY_USER_FAMILY),
 
     // Foldable mode
-    ALL_FOLDABLE_STATES("all_foldable_states", "foldable_family"),
-    NO_FOLDABLE_STATES("no_foldable_states", "foldable_family"),
+    ALL_FOLDABLE_STATES("all_foldable_states", ModuleParameters.FOLDABLE_STATES_FAMILY),
+    NO_FOLDABLE_STATES("no_foldable_states", ModuleParameters.FOLDABLE_STATES_FAMILY),
 
     // SDK sandbox mode
-    RUN_ON_SDK_SANDBOX("run_on_sdk_sandbox", "run_on_sdk_sandbox_family"),
-    NOT_RUN_ON_SDK_SANDBOX("not_run_on_sdk_sandbox", "run_on_sdk_sandbox_family");
+    RUN_ON_SDK_SANDBOX("run_on_sdk_sandbox", ModuleParameters.RUN_ON_SDK_SANDBOX_FAMILY),
+    NOT_RUN_ON_SDK_SANDBOX("not_run_on_sdk_sandbox", ModuleParameters.RUN_ON_SDK_SANDBOX_FAMILY);
 
     public static final String INSTANT_APP_FAMILY = "instant_app_family";
     public static final String MULTI_ABI_FAMILY = "multi_abi_family";
     public static final String SECONDARY_USER_FAMILY = "secondary_user_family";
+    public static final String SECONDARY_USER_ON_SECONDARY_DISPLAY_FAMILY =
+            "secondary_user_on_secondary_display_family";
+    public static final String SECONDARY_USER_ON_DEFAULT_DISPLAY_FAMILY =
+            "secondary_user_on_default_display_family";
     public static final String MULTIUSER_FAMILY = "multiuser_family";
     public static final String FOLDABLE_STATES_FAMILY = "foldable_family";
+    public static final String RUN_ON_SDK_SANDBOX_FAMILY = "run_on_sdk_sandbox_family";
+    public static final String RUN_ON_WORK_PROFILE_FAMILY = "run_on_work_profile_family";
+    public static final String RUN_ON_SECONDARY_USER_FAMILY = "run_on_secondary_user_family";
 
     private final String mName;
     /** Defines whether several module parameters are associated and mutually exclusive. */
diff --git a/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java b/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java
index f0fd57e..d885633 100644
--- a/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java
+++ b/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java
@@ -15,43 +15,51 @@
  */
 package com.android.tradefed.testtype.suite.params;
 
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.ALL_FOLDABLE_STATES;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.INSTANT_APP;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.MULTIUSER;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.MULTI_ABI;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.NOT_INSTANT_APP;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.NOT_MULTI_ABI;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.NOT_RUN_ON_SDK_SANDBOX;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.NOT_SECONDARY_USER;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.NOT_SECONDARY_USER_ON_DEFAULT_DISPLAY;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.NOT_SECONDARY_USER_ON_SECONDARY_DISPLAY;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.NO_FOLDABLE_STATES;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.RUN_ON_SDK_SANDBOX;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.RUN_ON_SECONDARY_USER;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.RUN_ON_WORK_PROFILE;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.SECONDARY_USER;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.SECONDARY_USER_ON_DEFAULT_DISPLAY;
+import static com.android.tradefed.testtype.suite.params.ModuleParameters.SECONDARY_USER_ON_SECONDARY_DISPLAY;
+
 import com.android.tradefed.testtype.suite.params.multiuser.RunOnSecondaryUserParameterHandler;
 import com.android.tradefed.testtype.suite.params.multiuser.RunOnWorkProfileParameterHandler;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 
 /** Helper to get the {@link IModuleParameterHandler} associated with the parameter. */
-public class ModuleParametersHelper {
+public final class ModuleParametersHelper {
 
-    private static Map<ModuleParameters, IModuleParameterHandler> sHandlerMap = new HashMap<>();
+    private static final Map<ModuleParameters, IModuleParameterHandler> sHandlerMap =
+            Map.of(
+                    INSTANT_APP, new InstantAppHandler(),
+                    NOT_INSTANT_APP, new NegativeHandler(),
+                    // line separator
+                    MULTI_ABI, new NegativeHandler(),
+                    NOT_MULTI_ABI, new NotMultiAbiHandler(),
+                    // line separator
+                    RUN_ON_WORK_PROFILE, new RunOnWorkProfileParameterHandler(),
+                    RUN_ON_SECONDARY_USER, new RunOnSecondaryUserParameterHandler(),
+                    // line separator
+                    NO_FOLDABLE_STATES, new NegativeHandler(),
+                    ALL_FOLDABLE_STATES, new FoldableExpandingHandler());
 
-    static {
-        sHandlerMap.put(ModuleParameters.INSTANT_APP, new InstantAppHandler());
-        sHandlerMap.put(ModuleParameters.NOT_INSTANT_APP, new NegativeHandler());
-
-        sHandlerMap.put(ModuleParameters.MULTI_ABI, new NegativeHandler());
-        sHandlerMap.put(ModuleParameters.NOT_MULTI_ABI, new NotMultiAbiHandler());
-
-        sHandlerMap.put(
-                ModuleParameters.RUN_ON_WORK_PROFILE, new RunOnWorkProfileParameterHandler());
-        sHandlerMap.put(
-                ModuleParameters.RUN_ON_SECONDARY_USER, new RunOnSecondaryUserParameterHandler());
-
-        sHandlerMap.put(ModuleParameters.NO_FOLDABLE_STATES, new NegativeHandler());
-        sHandlerMap.put(ModuleParameters.ALL_FOLDABLE_STATES, new FoldableExpandingHandler());
-    }
-
-    private static Map<ModuleParameters, Set<ModuleParameters>> sGroupMap = new HashMap<>();
-
-    static {
-        sGroupMap.put(
-                ModuleParameters.MULTIUSER,
-                Set.of(
-                        ModuleParameters.RUN_ON_WORK_PROFILE,
-                        ModuleParameters.RUN_ON_SECONDARY_USER));
-    }
+    private static final Map<ModuleParameters, Set<ModuleParameters>> sGroupMap =
+            Map.of(MULTIUSER, Set.of(RUN_ON_WORK_PROFILE, RUN_ON_SECONDARY_USER));
 
     /**
      * Optional parameters are params that will not automatically be created when the module
@@ -59,19 +67,28 @@
      * set of parameterization that is less commonly requested to run. They could be upgraded to
      * main parameters in the future by moving them above.
      */
-    private static Map<ModuleParameters, IModuleParameterHandler> sOptionalHandlerMap = new HashMap<>();
+    private static final Map<ModuleParameters, IModuleParameterHandler> sOptionalHandlerMap =
+            Map.of(
+                    SECONDARY_USER,
+                    new SecondaryUserHandler(),
+                    NOT_SECONDARY_USER,
+                    new NegativeHandler(),
+                    SECONDARY_USER_ON_SECONDARY_DISPLAY,
+                    new SecondaryUserOnSecondaryDisplayHandler(),
+                    NOT_SECONDARY_USER_ON_SECONDARY_DISPLAY,
+                    new NegativeHandler(),
+                    SECONDARY_USER_ON_DEFAULT_DISPLAY,
+                    new SecondaryUserOnDefaultDisplayHandler(),
+                    NOT_SECONDARY_USER_ON_DEFAULT_DISPLAY,
+                    new NegativeHandler(),
+                    RUN_ON_SDK_SANDBOX,
+                    new RunOnSdkSandboxHandler(),
+                    NOT_RUN_ON_SDK_SANDBOX,
+                    new NegativeHandler());
 
-    static {
-        sOptionalHandlerMap.put(ModuleParameters.SECONDARY_USER, new SecondaryUserHandler());
-        sOptionalHandlerMap.put(ModuleParameters.NOT_SECONDARY_USER, new NegativeHandler());
-        sOptionalHandlerMap.put(ModuleParameters.RUN_ON_SDK_SANDBOX, new RunOnSdkSandboxHandler());
-        sOptionalHandlerMap.put(ModuleParameters.NOT_RUN_ON_SDK_SANDBOX, new NegativeHandler());
-    }
-
-    private static Map<ModuleParameters, Set<ModuleParameters>> sOptionalGroupMap = new HashMap<>();
-
-    static {
-    }
+    // NOTE: sOptionalGroupMap is currently empty, but used on resolveParam(), so don't remove it
+    private static Map<ModuleParameters, Set<ModuleParameters>> sOptionalGroupMap =
+            Collections.emptyMap();
 
     /**
      * Get the all {@link ModuleParameters} which are sub-params of a given {@link
diff --git a/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java b/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
index 7684b1b..cd821d6 100644
--- a/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
+++ b/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
@@ -20,34 +20,73 @@
 import com.android.tradefed.targetprep.CreateUserPreparer;
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.targetprep.RunCommandTargetPreparer;
+import com.android.tradefed.targetprep.VisibleBackgroundUserPreparer;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestAnnotationFilterReceiver;
 
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import javax.annotation.Nullable;
+
 /** Handler for {@link ModuleParameters#SECONDARY_USER}. */
 public class SecondaryUserHandler implements IModuleParameterHandler {
+
+    @VisibleForTesting
+    static final List<String> LOCATION_COMMANDS =
+            Arrays.asList(
+                    "settings put secure location_providers_allowed +network",
+                    "settings put secure location_providers_allowed +gps");
+
+    private final boolean mStartUserVisibleOnBackground;
+    private final @Nullable Integer mDisplayId;
+
+    public SecondaryUserHandler() {
+        this(/*startUserVisibleOnBackground= */ false);
+    }
+
+    protected SecondaryUserHandler(boolean startUserVisibleOnBackground) {
+        this(startUserVisibleOnBackground, /* displayId= */ null);
+    }
+
+    protected SecondaryUserHandler(boolean startUserVisibleOnBackground, Integer displayId) {
+        mStartUserVisibleOnBackground = startUserVisibleOnBackground;
+        mDisplayId = displayId;
+    }
+
     @Override
     public String getParameterIdentifier() {
         return "secondary_user";
     }
 
-    /** {@inheritDoc} */
     @Override
-    public void addParameterSpecificConfig(IConfiguration moduleConfiguration) {
+    public final void addParameterSpecificConfig(IConfiguration moduleConfiguration) {
         for (IDeviceConfiguration deviceConfig : moduleConfiguration.getDeviceConfig()) {
             List<ITargetPreparer> preparers = deviceConfig.getTargetPreparers();
             // The first things module will do is switch to a secondary user
-            preparers.add(0, new CreateUserPreparer());
+            ITargetPreparer userPreparer;
+            if (mStartUserVisibleOnBackground) {
+                userPreparer = new VisibleBackgroundUserPreparer();
+                if (mDisplayId != null) {
+                    ((VisibleBackgroundUserPreparer) userPreparer).setDisplayId(mDisplayId);
+                }
+            } else {
+                userPreparer = new CreateUserPreparer();
+            }
+            preparers.add(0, userPreparer);
             // Add a preparer to setup the location settings on the new user
-            preparers.add(1, createLocationPreparer());
+            RunCommandTargetPreparer locationPreparer = new RunCommandTargetPreparer();
+            LOCATION_COMMANDS.forEach(cmd -> locationPreparer.addRunCommand(cmd));
+            preparers.add(1, locationPreparer);
         }
     }
 
     @Override
-    public void applySetup(IConfiguration moduleConfiguration) {
+    public final void applySetup(IConfiguration moduleConfiguration) {
         // Add filter to exclude @SystemUserOnly
         for (IRemoteTest test : moduleConfiguration.getTests()) {
             if (test instanceof ITestAnnotationFilterReceiver) {
@@ -63,11 +102,4 @@
             }
         }
     }
-
-    private RunCommandTargetPreparer createLocationPreparer() {
-        RunCommandTargetPreparer location = new RunCommandTargetPreparer();
-        location.addRunCommand("settings put secure location_providers_allowed +gps");
-        location.addRunCommand("settings put secure location_providers_allowed +network");
-        return location;
-    }
 }
diff --git a/src/com/android/tradefed/testtype/suite/params/SecondaryUserOnDefaultDisplayHandler.java b/src/com/android/tradefed/testtype/suite/params/SecondaryUserOnDefaultDisplayHandler.java
new file mode 100644
index 0000000..952de95
--- /dev/null
+++ b/src/com/android/tradefed/testtype/suite/params/SecondaryUserOnDefaultDisplayHandler.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 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.suite.params;
+
+import com.android.tradefed.targetprep.VisibleBackgroundUserPreparer;
+
+/** Handler for {@link ModuleParameters#SECONDARY_USER_ON_DEFAULT_DISPLAY}. */
+public final class SecondaryUserOnDefaultDisplayHandler extends SecondaryUserHandler {
+
+    public SecondaryUserOnDefaultDisplayHandler() {
+        super(
+                /*startUserVisibleOnBackground= */ true,
+                VisibleBackgroundUserPreparer.DEFAULT_DISPLAY);
+    }
+
+    @Override
+    public String getParameterIdentifier() {
+        return "secondary_user_on_default_display";
+    }
+}
diff --git a/src/com/android/tradefed/testtype/suite/params/SecondaryUserOnSecondaryDisplayHandler.java b/src/com/android/tradefed/testtype/suite/params/SecondaryUserOnSecondaryDisplayHandler.java
new file mode 100644
index 0000000..a12d605
--- /dev/null
+++ b/src/com/android/tradefed/testtype/suite/params/SecondaryUserOnSecondaryDisplayHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 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.suite.params;
+
+/** Handler for {@link ModuleParameters#SECONDARY_USER_ON_SECONDARY_DISPLAY}. */
+public final class SecondaryUserOnSecondaryDisplayHandler extends SecondaryUserHandler {
+
+    public SecondaryUserOnSecondaryDisplayHandler() {
+        super(/*startUserVisibleOnBackground= */ true);
+    }
+
+    @Override
+    public String getParameterIdentifier() {
+        return "secondary_user_on_secondary_display";
+    }
+}
diff --git a/src/com/android/tradefed/util/BuildTestsZipUtils.java b/src/com/android/tradefed/util/BuildTestsZipUtils.java
index ae33b8a..63ab25b 100644
--- a/src/com/android/tradefed/util/BuildTestsZipUtils.java
+++ b/src/com/android/tradefed/util/BuildTestsZipUtils.java
@@ -17,6 +17,8 @@
 
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.targetprep.AltDirBehavior;
 
 import java.io.File;
@@ -146,6 +148,8 @@
         if (testsDir != null) {
             File apkFile = buildInfo.stageRemoteFile(apkFileName, testsDir);
             if (apkFile != null) {
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.STAGE_UNDEFINED_DEPENDENCY, apkFileName);
                 return apkFile;
             }
         }
diff --git a/src/com/android/tradefed/util/SubprocessTestResultsParser.java b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
index 7a1d697..6e4ef15 100644
--- a/src/com/android/tradefed/util/SubprocessTestResultsParser.java
+++ b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
@@ -416,7 +416,7 @@
                 TestRunEndedEventInfo rei = new TestRunEndedEventInfo(new JSONObject(eventJson));
                 // TODO: Parse directly as proto.
                 mListener.testRunEnded(
-                        rei.mTime, TfMetricProtoUtil.upgradeConvert(rei.mRunMetrics));
+                        rei.mTime, TfMetricProtoUtil.upgradeConvert(rei.mRunMetrics, true));
             } finally {
                 mCurrentRunName = null;
                 mCurrentTestCase = null;
diff --git a/src/com/android/tradefed/util/testmapping/TestInfo.java b/src/com/android/tradefed/util/testmapping/TestInfo.java
index 5e8b8a5..a280b37 100644
--- a/src/com/android/tradefed/util/testmapping/TestInfo.java
+++ b/src/com/android/tradefed/util/testmapping/TestInfo.java
@@ -84,6 +84,11 @@
         return String.format("%s - %s", mName, mHostOnly);
     }
 
+    /** Get a {@link String} represent the test name and its options. */
+    public String getNameOption() {
+        return String.format("%s%s", mName, mOptions.toString());
+    }
+
     /** Get a {@link Set} of the keywords supported by the test. */
     public Set<String> getKeywords() {
         return new HashSet<>(mKeywords);
diff --git a/test_framework/Android.bp b/test_framework/Android.bp
index 14525d0..3a46381 100644
--- a/test_framework/Android.bp
+++ b/test_framework/Android.bp
@@ -37,4 +37,6 @@
         "tradefed-lib-core",
         "loganalysis",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
 }
diff --git a/test_framework/com/android/tradefed/device/metric/BluetoothHciSnoopLogCollector.java b/test_framework/com/android/tradefed/device/metric/BluetoothHciSnoopLogCollector.java
new file mode 100644
index 0000000..3ef9f09
--- /dev/null
+++ b/test_framework/com/android/tradefed/device/metric/BluetoothHciSnoopLogCollector.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2023 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.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceRuntimeException;
+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.result.FileInputStreamSource;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.util.BluetoothUtils;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * Collector to enable Bluetooth HCI snoop logging on the DUT and to collect the log for each test.
+ * The collector will configure and enable snoop logging for the test run and revert the settings
+ * after the test run.
+ */
+@OptionClass(alias = "bluetooth-hci-snoop-log-collector")
+public class BluetoothHciSnoopLogCollector extends FilePullerDeviceMetricCollector {
+
+    // Settings for HCI-snoop-log reporting.
+    private String reportingDir = null;
+    public static final String SNOOP_LOG_MODE_PROPERTY = "persist.bluetooth.btsnooplogmode";
+    private String initialSnoopLogMode = null;
+    // Snoop-log-file header is defined in:
+    // https://cs.android.com/android/platform/superproject/+/master:packages/modules/Bluetooth/system/gd/hal/snoop_logger_common.h
+    private static final int SNOOP_LOG_FILE_HEADER_BYTE_SIZE = 16;
+
+    @Override
+    public void onTestRunStart(DeviceMetricData runData) throws DeviceNotAvailableException {
+        // Remember the initial snoop-log mode on the device.
+        initialSnoopLogMode = getSnoopLogModeProperty();
+        // Enable snoop logging on device.
+        setSnoopLogModeProperty("full");
+        for (ITestDevice device : getRealDevices()) {
+            // Restart Bluetooth service, to allow the snoop-log setting to take effect.
+            disableBluetoothService(device);
+            enableBluetoothService(device);
+        }
+    }
+
+    @Override
+    public void onTestRunEnd(DeviceMetricData runData, final Map<String, Metric> currentRunMetrics)
+            throws DeviceNotAvailableException {
+        // Disable snoop logging on device.
+        setSnoopLogModeProperty(initialSnoopLogMode);
+        for (ITestDevice device : getRealDevices()) {
+            // Wind down Bluetooth service after testing.
+            disableBluetoothService(device);
+        }
+    }
+
+    @Override
+    public void onTestStart(DeviceMetricData testData) throws DeviceNotAvailableException {
+        // Create the reporting directory for test snoop log.
+        deleteReportingDirectory();
+        createReportingDirectory();
+        for (ITestDevice device : getRealDevices()) {
+            // Get a clean slate on the snoop log for the test by only preserving header.
+            executeShellCommand(
+                    device,
+                    String.format(
+                            "truncate -s %s %s",
+                            SNOOP_LOG_FILE_HEADER_BYTE_SIZE, BluetoothUtils.GOLD_BTSNOOP_LOG_PATH));
+        }
+    }
+
+    @Override
+    public void onTestEnd(
+            DeviceMetricData testData,
+            final Map<String, Metric> currentTestCaseMetrics,
+            TestDescription test)
+            throws DeviceNotAvailableException {
+        // Saving HCI snoop logs for the test.
+        String testName = test.toString();
+        String normalisedTestName = normaliseTestName(testName);
+
+        for (ITestDevice device : getRealDevices()) {
+            String serialNumber = device.getSerialNumber();
+            String testSnoopLogFilename =
+                    String.format(getHciSnoopLogPathFormat(), normalisedTestName, serialNumber);
+            executeShellCommand(
+                    device,
+                    String.format(
+                            "cp -p %s %s",
+                            BluetoothUtils.GOLD_BTSNOOP_LOG_PATH, testSnoopLogFilename));
+        }
+
+        super.onTestEnd(testData, currentTestCaseMetrics);
+    }
+
+    @Override
+    public final void processMetricFile(String key, File metricFile, DeviceMetricData runData) {
+        try (InputStreamSource source = new FileInputStreamSource(metricFile, true)) {
+            testLog(FileUtil.getBaseName(metricFile.getName()), LogDataType.BT_SNOOP_LOG, source);
+        }
+    }
+
+    // From
+    // tools/tradefederation/core/src/com/android/tradefed/device/metric/FilePullerLogCollector.java
+    @Override
+    public void processMetricDirectory(String key, File metricDirectory, DeviceMetricData runData) {
+        if (metricDirectory.listFiles() == null) {
+            CLog.e("metricDirectory.listFiles() is null.");
+            return;
+        }
+        for (File file : metricDirectory.listFiles()) {
+            if (file.isDirectory()) {
+                processMetricDirectory(key, file, runData);
+            } else {
+                processMetricFile(key, file, runData);
+            }
+        }
+        FileUtil.recursiveDelete(metricDirectory);
+    }
+
+    /** Retrieve the directory to report the HCI snoop logs to. */
+    public String getReportingDir() {
+        if (reportingDir == null) {
+            if (mDirectoryKeys.size() == 0) {
+                CLog.w("No directory key set.");
+            } else if (mDirectoryKeys.size() > 1) {
+                CLog.w("%s directory keys were set.", mDirectoryKeys.size());
+            }
+            // Assume that the first directory key contains the location to store the HCI snoop
+            // logs.
+            reportingDir = mDirectoryKeys.iterator().next();
+        }
+        return reportingDir;
+    }
+
+    /** Construct a filename path for HCI snoop logs, to be tagged with test name and device id. */
+    private String getHciSnoopLogPathFormat() throws DeviceNotAvailableException {
+        return getReportingDir() + "/%s-%s-btsnoop_hci.log";
+    }
+
+    private void createReportingDirectory() throws DeviceNotAvailableException {
+        for (ITestDevice device : getRealDevices()) {
+            executeShellCommand(device, "mkdir -p " + getReportingDir());
+        }
+    }
+
+    private void deleteReportingDirectory() throws DeviceNotAvailableException {
+        for (ITestDevice device : getRealDevices()) {
+            executeShellCommand(device, "rm -rf  " + getReportingDir());
+        }
+    }
+
+    private String getSnoopLogModeProperty() throws DeviceNotAvailableException {
+        for (ITestDevice device : getRealDevices()) {
+            return device.getProperty(SNOOP_LOG_MODE_PROPERTY);
+        }
+        return null;
+    }
+
+    private void setSnoopLogModeProperty(String mode) throws DeviceNotAvailableException {
+        if (mode == null) {
+            CLog.i("mode is null. Using empty string instead.");
+            mode = "";
+        }
+        for (ITestDevice device : getRealDevices()) {
+            boolean successfullySetPropOnDevice = device.setProperty(SNOOP_LOG_MODE_PROPERTY, mode);
+            if (!successfullySetPropOnDevice) {
+                CLog.w(
+                        "Failed to set property [%s] to [%s] on [%s].",
+                        SNOOP_LOG_MODE_PROPERTY, mode, device.getSerialNumber());
+            }
+        }
+    }
+
+    private void disableBluetoothService(ITestDevice device) throws DeviceNotAvailableException {
+        executeShellCommand(device, "cmd bluetooth_manager disable");
+        executeShellCommand(device, "cmd bluetooth_manager wait-for-state:STATE_OFF");
+    }
+
+    private void enableBluetoothService(ITestDevice device) throws DeviceNotAvailableException {
+        executeShellCommand(device, "cmd bluetooth_manager enable");
+        executeShellCommand(device, "cmd bluetooth_manager wait-for-state:STATE_ON");
+    }
+
+    /**
+     * Execute shell command on the device. If the execution failed (non-zero exit code), throw a
+     * {@link com.android.tradefed.device.DeviceRuntimeException}.
+     *
+     * @throws DeviceRuntimeException
+     */
+    protected void executeShellCommand(ITestDevice device, String command)
+            throws DeviceNotAvailableException {
+        CommandResult result = device.executeShellV2Command(command);
+        if (result.getExitCode() != 0) {
+            throw new DeviceRuntimeException(
+                    "Failed to execute command: " + command,
+                    DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
+        }
+    }
+
+    /**
+     * Normalise the test name to avoid using slash /. For instance, "A2DP_SNK#A2DP/SNK/AS/BV-01-I"
+     * would be updated to "A2DP_SNK#A2DP_SNK_AS_BV-01-I".
+     */
+    private String normaliseTestName(String testName) {
+        return testName.replace("/", "_");
+    }
+}
diff --git a/test_framework/com/android/tradefed/device/metric/ShowmapPullerMetricCollector.java b/test_framework/com/android/tradefed/device/metric/ShowmapPullerMetricCollector.java
index 1d278fd..183c79c 100644
--- a/test_framework/com/android/tradefed/device/metric/ShowmapPullerMetricCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/ShowmapPullerMetricCollector.java
@@ -249,12 +249,13 @@
      * @return true or false
      */
     private Boolean isProcessFound(String line) {
+        if (mProcessNames.isEmpty()) return false;
         boolean psResult;
         Pattern psPattern = Pattern.compile(PROCESS_NAME_REGEX);
         Matcher psMatcher = psPattern.matcher(line);
         if (psMatcher.find()) {
             processName = psMatcher.group(2);
-            psResult = mProcessNames.isEmpty() || mProcessNames.contains(processName);
+            psResult = mProcessNames.contains(processName);
             return psResult;
         }
         return false;
diff --git a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
index 17d371d..d824633 100644
--- a/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -27,6 +27,8 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.TestInformation;
+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.observatory.IDiscoverDependencies;
 import com.android.tradefed.result.error.DeviceErrorIdentifier;
@@ -327,6 +329,8 @@
                 // Try to stage the files from remote zip files.
                 src = buildInfo.stageRemoteFile(fileName, testDir);
                 if (src != null) {
+                    InvocationMetricLogger.addInvocationMetrics(
+                            InvocationMetricKey.STAGE_UNDEFINED_DEPENDENCY, fileName);
                     try {
                         // Search again with filtering on ABI
                         File srcWithAbi = FileUtil.findFile(fileName, mAbi, testDir);
@@ -486,4 +490,12 @@
         }
         return deps;
     }
+
+    public boolean shouldRemountSystem() {
+        return mRemountSystem;
+    }
+
+    public boolean shouldRemountVendor() {
+        return mRemountVendor;
+    }
 }
diff --git a/test_framework/com/android/tradefed/testtype/AndroidJUnitTest.java b/test_framework/com/android/tradefed/testtype/AndroidJUnitTest.java
index 4126519..2802e40 100644
--- a/test_framework/com/android/tradefed/testtype/AndroidJUnitTest.java
+++ b/test_framework/com/android/tradefed/testtype/AndroidJUnitTest.java
@@ -31,14 +31,20 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.targetprep.TestAppInstallSetup;
 import com.android.tradefed.util.ArrayUtil;
+import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.ListInstrumentationParser;
+import com.android.tradefed.util.ResourceUtil;
 
 import com.google.common.annotations.VisibleForTesting;
 
 import org.junit.runner.notification.RunListener;
 
 import java.io.File;
+import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -90,6 +96,8 @@
      */
     public static final String NEW_RUN_LISTENER_ORDER_KEY = "newRunListenerMode";
 
+    public static final String USE_TEST_STORAGE_SERVICE = "useTestStorageService";
+
     /** Options from the collector side helper library. */
     public static final String INCLUDE_COLLECTOR_FILTER_KEY = "include-filter-group";
 
@@ -142,6 +150,17 @@
     private String mTestFilterDir = "/data/local/tmp/ajur";
 
     @Option(
+            name = "test-storage-dir",
+            description = "The device directory path where test storage read files.")
+    private String mTestStorageInternalDir = "/sdcard/googletest/test_runfiles";
+
+    @Option(
+            name = "use-test-storage",
+            description =
+                    "If set to true, we will push filters to the test storage instead of disk.")
+    private boolean mUseTestStorage = false;
+
+    @Option(
         name = "ajur-max-shard",
         description = "The maximum number of shard we want to allow the AJUR test to shard into"
     )
@@ -334,6 +353,10 @@
         if (mIncludeTestFile != null && mIncludeTestFile.length() > 0) {
             mDeviceIncludeFile = mTestFilterDir.replaceAll("/$", "") + "/" + INCLUDE_FILE;
             pushTestFile(mIncludeTestFile, mDeviceIncludeFile, listener);
+            if (mUseTestStorage) {
+                pushTestFile(
+                        mIncludeTestFile, mTestStorageInternalDir + mDeviceIncludeFile, listener);
+            }
             pushedFile = true;
             // If an explicit include file filter is provided, do not use the package
             setTestPackageName(null);
@@ -343,14 +366,43 @@
         if (mExcludeTestFile != null && mExcludeTestFile.length() > 0) {
             mDeviceExcludeFile = mTestFilterDir.replaceAll("/$", "") + "/" + EXCLUDE_FILE;
             pushTestFile(mExcludeTestFile, mDeviceExcludeFile, listener);
+            if (mUseTestStorage) {
+                pushTestFile(
+                        mExcludeTestFile, mTestStorageInternalDir + mDeviceExcludeFile, listener);
+            }
             pushedFile = true;
         }
+        TestAppInstallSetup serviceInstaller = null;
+        if (pushedFile && mUseTestStorage) {
+            File testServices = null;
+            try {
+                testServices = FileUtil.createTempFile("services", ".apk");
+                boolean extracted =
+                        ResourceUtil.extractResourceAsFile(
+                                "/test-services-1.4.2.apk", testServices);
+                if (extracted) {
+                    serviceInstaller = new TestAppInstallSetup();
+                    serviceInstaller.addTestFile(testServices);
+                    serviceInstaller.setUp(testInfo);
+                } else {
+                    throw new IOException("Failed to extract test-services.apk");
+                }
+            } catch (IOException | TargetSetupError | BuildError e) {
+                CLog.e(e);
+                mUseTestStorage = false;
+            } finally {
+                FileUtil.deleteFile(testServices);
+            }
+        }
         if (mTotalShards > 0 && !isShardable() && mShardIndex != 0) {
             // If not shardable, only first shard can run.
             CLog.i("%s is not shardable.", getRunnerName());
             return;
         }
         super.run(testInfo, listener);
+        if (serviceInstaller != null) {
+            serviceInstaller.tearDown(testInfo, null);
+        }
         if (pushedFile) {
             // Remove the directory where the files where pushed
             removeTestFilterDir();
@@ -440,6 +492,10 @@
             runner.addInstrumentationArg(
                     NEW_RUN_LISTENER_ORDER_KEY, Boolean.toString(mNewRunListenerOrderMode));
         }
+        if (mUseTestStorage) {
+            runner.addInstrumentationArg(
+                    USE_TEST_STORAGE_SERVICE, Boolean.toString(mUseTestStorage));
+        }
         // Add the listeners received from Options
         addDeviceListeners(mExtraDeviceListeners);
     }
diff --git a/test_framework/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java b/test_framework/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java
index 4602742..113d7e7 100644
--- a/test_framework/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java
+++ b/test_framework/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
@@ -180,7 +181,7 @@
     }
 
     private Set<File> resolveRemoteFileForObject(Object obj) {
-        try {
+        try (CloseableTraceScope ignore = new CloseableTraceScope("junit4:resolveRemoteFiles")) {
             OptionSetter setter = new OptionSetter(obj);
             return setter.validateRemoteFilePath(createResolver());
         } catch (BuildRetrievalError | ConfigurationException e) {
diff --git a/test_framework/com/android/tradefed/testtype/GTestResultParser.java b/test_framework/com/android/tradefed/testtype/GTestResultParser.java
index 16cf9b0..2a5ef00 100644
--- a/test_framework/com/android/tradefed/testtype/GTestResultParser.java
+++ b/test_framework/com/android/tradefed/testtype/GTestResultParser.java
@@ -762,6 +762,10 @@
                 listener.testFailed(testId, testFailure);
                 listener.testEnded(testId, emptyMap);
             }
+            if (mMethodScope != null) {
+                mMethodScope.close();
+                mMethodScope = null;
+            }
             clearCurrentTestResult();
         }
         // Report the test run failed
diff --git a/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java b/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
index d063706..2cbb2bd 100644
--- a/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
+++ b/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.tracing.CloseableTraceScope;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
@@ -214,7 +215,7 @@
             CollectingOutputReceiver outputCollector = createOutputCollector();
             GoogleBenchmarkResultParser resultParser = createResultParser(runName, listener);
             listener.testRunStarted(runName, numTests);
-            try {
+            try (CloseableTraceScope ignore = new CloseableTraceScope(runName)) {
                 String cmd =
                         String.format(
                                 "%s%s%s %s",
diff --git a/test_framework/com/android/tradefed/testtype/HostTest.java b/test_framework/com/android/tradefed/testtype/HostTest.java
index 1d494a9..57def46 100644
--- a/test_framework/com/android/tradefed/testtype/HostTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostTest.java
@@ -704,7 +704,9 @@
                         new TestTimeoutEnforcer(
                                 mTestCaseTimeout.toMillis(), TimeUnit.MILLISECONDS, listener);
             }
-            return JUnitRunUtil.runTest(listener, junitTest, className, mTestInfo);
+            try (CloseableTraceScope ignored = new CloseableTraceScope(className)) {
+                return JUnitRunUtil.runTest(listener, junitTest, className, mTestInfo);
+            }
         }
     }
 
@@ -1381,7 +1383,7 @@
     }
 
     private Set<File> resolveRemoteFileForObject(Object obj) {
-        try {
+        try (CloseableTraceScope ignore = new CloseableTraceScope("infra:resolveRemoteFiles")) {
             OptionSetter setter = new OptionSetter(obj);
             return setter.validateRemoteFilePath(createResolver());
         } catch (BuildRetrievalError | ConfigurationException e) {
diff --git a/test_framework/com/android/tradefed/testtype/InstalledInstrumentationsTest.java b/test_framework/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
index 4451770..dade01d 100644
--- a/test_framework/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
+++ b/test_framework/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.testtype;
 
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationReceiver;
@@ -46,8 +47,10 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
@@ -196,6 +199,17 @@
                     "Create InstrumentationTest type rather than more recent AndroidJUnitTest.")
     private boolean mDowngradeInstrumentation = false;
 
+    @Option(
+            name = "test-storage-dir",
+            description = "The device directory path where test storage read files.")
+    private String mTestStorageInternalDir = "/sdcard/googletest/test_runfiles";
+
+    @Option(
+            name = "use-test-storage",
+            description =
+                    "If set to true, we will push filters to the test storage instead of disk.")
+    private boolean mUseTestStorage = false;
+
     private int mTotalShards = 0;
     private int mShardIndex = 0;
     private List<IMetricCollector> mMetricCollectorList = new ArrayList<>();
@@ -213,16 +227,27 @@
     public boolean shouldRetry(int attemptJustExecuted, List<TestRunResult> previousResults)
             throws DeviceNotAvailableException {
         boolean retry = false;
-        mRunTestsFailureMap = new HashMap<>();
+        if (mRunTestsFailureMap == null) {
+            mRunTestsFailureMap = new HashMap<>();
+        }
         for (TestRunResult run : previousResults) {
             if (run == null) {
                 continue;
             }
             if (run.isRunFailure() || run.hasFailedTests()) {
                 retry = true;
+                HashSet<TestDescription> excludes =
+                        new LinkedHashSet<>(
+                                run.getTestsInState(
+                                        Arrays.asList(
+                                                TestStatus.PASSED,
+                                                TestStatus.ASSUMPTION_FAILURE,
+                                                TestStatus.IGNORED)));
+                if (mRunTestsFailureMap.get(run.getName()) != null) {
+                    excludes.addAll(mRunTestsFailureMap.get(run.getName()));
+                }
                 // Exclude passed tests from rerunning
-                mRunTestsFailureMap.put(
-                        run.getName(), new LinkedHashSet<TestDescription>(run.getPassedTests()));
+                mRunTestsFailureMap.put(run.getName(), excludes);
             } else {
                 // Set null if we should not rerun it
                 mRunTestsFailureMap.put(run.getName(), null);
diff --git a/test_framework/com/android/tradefed/testtype/InstrumentationListener.java b/test_framework/com/android/tradefed/testtype/InstrumentationListener.java
index eea4590..ea56254 100644
--- a/test_framework/com/android/tradefed/testtype/InstrumentationListener.java
+++ b/test_framework/com/android/tradefed/testtype/InstrumentationListener.java
@@ -50,6 +50,8 @@
     // Message from ddmlib for ShellCommandUnresponsiveException
     private static final String DDMLIB_SHELL_UNRESPONSIVE =
             "Failed to receive adb shell test output within";
+    private static final String JUNIT4_TIMEOUT =
+            "org.junit.runners.model.TestTimedOutException: test timed out";
     // Message from ddmlib when there is a mismatch of test cases count
     private static final String DDMLIB_UNEXPECTED_COUNT = "Instrumentation reported numtests=";
 
@@ -117,6 +119,15 @@
     }
 
     @Override
+    public void testFailed(TestDescription test, FailureDescription failure) {
+        String message = failure.getErrorMessage();
+        if (message.startsWith(JUNIT4_TIMEOUT) || message.contains(DDMLIB_SHELL_UNRESPONSIVE)) {
+            failure.setErrorIdentifier(TestErrorIdentifier.TEST_TIMEOUT).setRetriable(false);
+        }
+        super.testFailed(test, failure);
+    }
+
+    @Override
     public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
         super.testEnded(test, endTime, testMetrics);
         if (mMethodScope != null) {
diff --git a/test_framework/com/android/tradefed/testtype/InstrumentationTest.java b/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
index 902148c..c17257c 100644
--- a/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
+++ b/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
@@ -98,6 +98,7 @@
 
     public static final String RUN_TESTS_AS_USER_KEY = "RUN_TESTS_AS_USER";
     public static final String RUN_TESTS_ON_SDK_SANDBOX = "RUN_TESTS_ON_SDK_SANDBOX";
+    private static final String SKIP_TESTS_REASON_KEY = "skip-tests-reason";
 
     @Option(
             name = "package",
@@ -746,6 +747,10 @@
                 createRemoteAndroidTestRunner(
                         mPackageName, mRunnerName, mDevice.getIDevice(), testInfo);
         setRunnerArgs(mRunner);
+        if (testInfo != null && testInfo.properties().containsKey(SKIP_TESTS_REASON_KEY)) {
+            mRunner.addInstrumentationArg(
+                    SKIP_TESTS_REASON_KEY, testInfo.properties().get(SKIP_TESTS_REASON_KEY));
+        }
 
         doTestRun(testInfo, listener);
         if (mInstallFile != null) {
diff --git a/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java b/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java
index 3f847e7..4b3a770 100644
--- a/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/IsolatedHostTest.java
@@ -322,6 +322,9 @@
 
         if (mRobolectricResources) {
             cmdArgs.addAll(compileRobolectricOptions());
+            // Prevent tradefed from eagerly loading classes, which may not load without shadows
+            // applied.
+            mExcludePaths.add("org/robolectric");
         }
 
         if (this.debug) {
diff --git a/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java
index 7b7db94..435d851 100644
--- a/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/mobly/MoblyBinaryHostTest.java
@@ -446,12 +446,25 @@
         listener.testRunEnded(0L, new HashMap<String, Metric>());
     }
 
+    private Set<String> cleanFilters(Set<String> filters) {
+        Set<String> new_filters = new LinkedHashSet<String>();
+        for (String filter : filters) {
+            new_filters.add(filter.replace("#", "."));
+        }
+        return new_filters;
+    }
+
     @VisibleForTesting
-    String getLogDirAbsolutePath() {
+    protected String getLogDirAbsolutePath() {
         return getLogDir().getAbsolutePath();
     }
 
     @VisibleForTesting
+    protected File getLogDirFile() {
+        return mLogDir;
+    }
+
+    @VisibleForTesting
     String getTestBed() {
         return mTestBed;
     }
@@ -478,6 +491,10 @@
             commandLine.add("--device_serial=" + device.getSerialNumber());
         }
         commandLine.add("--log_path=" + getLogDirAbsolutePath());
+        if (!mIncludeFilters.isEmpty()) {
+            commandLine.add("--tests");
+            commandLine.addAll(cleanFilters(mIncludeFilters));
+        }
         // Add all the other options
         commandLine.addAll(getTestOptions());
         return commandLine.toArray(new String[0]);
@@ -503,6 +520,8 @@
             }
         }
         FileUtil.recursiveDelete(logDir);
+        // reset log dir to be recreated for retries
+        mLogDir = null;
     }
 
     @VisibleForTesting
diff --git a/test_result_interfaces/Android.bp b/test_result_interfaces/Android.bp
index 73d097a..da4e22f 100644
--- a/test_result_interfaces/Android.bp
+++ b/test_result_interfaces/Android.bp
@@ -34,4 +34,6 @@
         "tradefed-common-util",
         "tradefed-protos",
     ],
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
 }
diff --git a/test_result_interfaces/com/android/tradefed/util/proto/TfMetricProtoUtil.java b/test_result_interfaces/com/android/tradefed/util/proto/TfMetricProtoUtil.java
index 799f3c2..a548ccb 100644
--- a/test_result_interfaces/com/android/tradefed/util/proto/TfMetricProtoUtil.java
+++ b/test_result_interfaces/com/android/tradefed/util/proto/TfMetricProtoUtil.java
@@ -71,13 +71,48 @@
      * limitations.
      */
     public static HashMap<String, Metric> upgradeConvert(Map<String, String> metrics) {
+        return upgradeConvert(metrics, false);
+    }
+
+    /**
+     * Conversion from Map<String, String> to HashMap<String, Metric>. In order to go to the new
+     * interface. Information might only be partially populated because of the old format
+     * limitations.
+     *
+     * @param smartNumbers convert numbers to int metrics
+     */
+    public static HashMap<String, Metric> upgradeConvert(
+            Map<String, String> metrics, boolean smartNumbers) {
         HashMap<String, Metric> newFormat = new LinkedHashMap<>();
         for (String key : metrics.keySet()) {
-            newFormat.put(key, stringToMetric(metrics.get(key)));
+            Metric metric = null;
+            String stringMetric = metrics.get(key);
+            if (smartNumbers) {
+                Long numMetric = isLong(stringMetric);
+                if (numMetric != null) {
+                    metric = createSingleValue(numMetric.longValue(), null);
+                }
+            }
+            // Default to String metric
+            if (metric == null) {
+                metric = stringToMetric(stringMetric);
+            }
+            newFormat.put(key, metric);
         }
         return newFormat;
     }
 
+    private static Long isLong(String strNum) {
+        if (strNum == null) {
+            return null;
+        }
+        try {
+            return Long.parseLong(strNum);
+        } catch (NumberFormatException nfe) {
+            return null;
+        }
+    }
+
     /**
      * Convert a simple String metric (old format) to a {@link Metric} (new format).
      *
diff --git a/util_apps/ContentProvider/hostsidetests/Android.bp b/util_apps/ContentProvider/hostsidetests/Android.bp
index 14d3680..7284d16 100644
--- a/util_apps/ContentProvider/hostsidetests/Android.bp
+++ b/util_apps/ContentProvider/hostsidetests/Android.bp
@@ -24,6 +24,9 @@
     // Only compile source java files in this jar.
     srcs: ["src/**/*.java"],
 
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
+
     libs: ["tradefed"],
 
     static_libs: [
diff --git a/util_apps/WifiUtil/Android.bp b/util_apps/WifiUtil/Android.bp
index 2f7e9eb..15d465f 100644
--- a/util_apps/WifiUtil/Android.bp
+++ b/util_apps/WifiUtil/Android.bp
@@ -22,6 +22,8 @@
     min_sdk_version: "7",
     target_sdk_version: "31",
     sdk_version: "current",
+    // b/267831518: Pin tradefed and dependencies to Java 11.
+    java_version: "11",
     manifest: "src/com/android/tradefed/utils/wifi/AndroidManifest.xml",
     optimize: {
         enabled: false,