Snap for 5735642 from 2dec0561511bd4f22d5d8a960c9c0ad1e2be4656 to sdk-release

Change-Id: Iba4e0c7540e1119604768ab1191cf877668f5d5e
diff --git a/.classpath b/.classpath
index 2f5448b..95c2966 100644
--- a/.classpath
+++ b/.classpath
@@ -1,10 +1,17 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
-	<classpathentry excluding="com/android/tradefed/testtype/junit4/LongevityHostRunner.java|com/android/tradefed/testtype/junit4/builder/DeviceJUnit4ClassRunnerBuilder.java" kind="src" path="src"/>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry excluding="Android.bp" kind="src" path="test_framework"/>
+	<classpathentry excluding="Android.bp" kind="src" path="invocation_interfaces"/>
+	<classpathentry excluding="Android.bp" kind="src" path="test_result_interfaces"/>
+	<classpathentry excluding="Android.bp" kind="src" path="clearcut_client"/>
 	<classpathentry kind="src" path="res"/>
 	<classpathentry kind="src" path="hamcrest"/>
 	<classpathentry kind="src" path="jline"/>
 	<classpathentry kind="src" path="junit"/>
+	<classpathentry excluding="Android.bp" kind="src" path="common_util"/>
+	<classpathentry kind="src" path="global_configuration"/>
+	<classpathentry excluding="Android.bp" kind="src" path="device_build_interfaces"/>
 	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/tf-remote-client"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/LongevityHostRunner"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
@@ -33,5 +40,6 @@
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/tools/common/google-api-services-storage/1.23.0/google-api-services-storage-v1-rev114-1.23.0.jar"/>
 	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/tools/tradefederation/core/tradefed-protos/linux_glibc_common/combined/tradefed-protos.jar"/>
 	<classpathentry kind="var" path="TRADEFED_ROOT/prebuilts/tools/common/m2/repository/com/google/code/gson/gson/2.8.0/gson-2.8.0.jar"/>
+	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/prebuilts/tools/common/m2/protobuf-java-util-prebuilt-jar/linux_glibc_common/combined/protobuf-java-util-prebuilt-jar.jar"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/Android.bp b/Android.bp
index 2b63059..4b6d838 100644
--- a/Android.bp
+++ b/Android.bp
@@ -73,23 +73,35 @@
     ],
 }
 
+// Main Target to build tradefed jar
 java_library_host {
     name: "tradefed",
     defaults: ["tradefed_defaults"],
-    srcs: [
-        "src/**/*.java",
-    ],
     java_resource_dirs: [
         "res",
     ],
-    openjdk9: {
-        javacflags: [
-            "--add-modules=java.xml.bind",
-        ],
-    },
     static_libs: [
+        "tradefed-lib-core",
+        "tradefed-test-framework",
+    ],
+    manifest: "MANIFEST.mf",
+}
+
+java_library_host {
+    name: "tradefed-lib-core",
+    defaults: ["tradefed_defaults"],
+    srcs: [
+        "src/**/*.java",
+        "global_configuration/**/*.java",
+    ],
+    static_libs: [
+        "tradefed-common-util",
+        "tradefed-clearcut-client",
+        "tradefed-result-interfaces",
+        "tradefed-device-build-interfaces",
+        "tradefed-invocation-interfaces",
+        "protobuf-java-util-prebuilt-jar",
         "aoa-helper",
-        "commons-compress-prebuilt",
         "error_prone_annotations-2.0.18",
         "google-api-java-client-min-repackaged",
         "google-api-services-compute",
@@ -112,7 +124,6 @@
     libs: [
         "loganalysis",
     ],
-    manifest: "MANIFEST.mf",
 }
 
 // Turn off various doclava warnings when generating
diff --git a/Android.mk b/Android.mk
index 8ebd686..25647f5 100644
--- a/Android.mk
+++ b/Android.mk
@@ -38,7 +38,7 @@
 tradefed-core: tradefed atest_tradefed tradefed-contrib tf-contrib-tests script_help
 
 .PHONY: tradefed-all
-tradefed-all: tradefed-core tradefed-tests tradefed_win verify loganalysis-tests
+tradefed-all: tradefed-core tradefed-tests tradefed_win loganalysis-tests
 
 # ====================================
 include $(CLEAR_VARS)
@@ -46,14 +46,14 @@
 
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_PREBUILT_EXECUTABLES := tradefed.sh tradefed_win.bat script_help.sh verify.sh run_tf_cmd.sh atest_tradefed.sh
+LOCAL_PREBUILT_EXECUTABLES := tradefed.sh tradefed_win.bat script_help.sh run_tf_cmd.sh atest_tradefed.sh
 include $(BUILD_HOST_PREBUILT)
 
 ########################################################
 # Zip up the built files and dist it as tradefed.zip
 
 tradefed_dist_host_jars := tradefed tradefed-tests loganalysis loganalysis-tests tf-remote-client tradefed-contrib tf-contrib-tests
-tradefed_dist_host_exes := tradefed.sh tradefed_win.bat script_help.sh verify.sh run_tf_cmd.sh atest_tradefed.sh
+tradefed_dist_host_exes := tradefed.sh tradefed_win.bat script_help.sh run_tf_cmd.sh atest_tradefed.sh
 tradefed_dist_test_apks := TradeFedUiTestApp TradeFedTestApp DeviceSetupUtil
 
 # Generate a src:dest list of copies to perform.
diff --git a/README.md b/README.md
index 50fc1d3..dbc3667 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Trade Federation (TF / tradefed)
+# Trade Federation (TF / Tradefed)
 
 TF is a test harness used to drive Android automated testing. It runs on test hosts
 and monitors the connected devices, handling test scheduling & execution and device
@@ -15,3 +15,5 @@
 More information at:
 https://source.android.com/devices/tech/test_infra/tradefed/
 
+See more details about Tradefed Architecture at:
+https://source.android.com/devices/tech/test_infra/tradefed/architecture
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 29bcb06..5b5df84 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,6 +1,10 @@
+// Below lists the TEST_MAPPING tests to do TF integration tests to make sure
+// the expectation of setup with different target_preparers + run are still good.
 {
   "presubmit": [
     {
+      // Instrumentation Test with Annotation Filter/PushFilePreparer/
+      // RumCommandTargetPreparer/TestFilePushSetup
       "name": "HelloWorldTests",
       "options": [
         {
@@ -13,15 +17,19 @@
       "host": true
     },
     {
+      // Gtest with FilePusher/ApkInstaller.
       "name": "CtsPerfettoTestCases"
     },
     {
+      // Instrumentation Test with FilePusher/ApkInstaller/RumCommandTargetPreparer.
       "name": "CtsApacheHttpLegacy27ApiSignatureTestCases"
     },
     {
+      // Gtest with FilePusher to push the whole testdata(libs/files).
       "name": "ziparchive-tests"
     },
     {
+      // Instrumentation Test with Class Filter/ApkInstaller.
       "name": "CtsDpiTestCases",
       "options": [
         {
@@ -30,6 +38,7 @@
       ]
     },
     {
+      // Jar Host with ApkInstaller.
       "name": "CtsSampleHostTestCases"
     }
   ]
diff --git a/TEST_MAPPING_README b/TEST_MAPPING_README
deleted file mode 100644
index d5cd4e7..0000000
--- a/TEST_MAPPING_README
+++ /dev/null
@@ -1,21 +0,0 @@
-Below lists the TEST_MAPPING tests to do TF integration tests to make sure
-the expectation of setup with different target_preparers + run are still good.
-
-* Gtest with FilePusher/ApkInstaller
-  * CtsPerfettoTestCases
-
-* Gtest with FilePusher to push the whole testdata(libs/files).
-  * ziparchive-tests
-
-* Instrumentation Test with Class Filter/ApkInstaller
-  * CtsDpiTestCases
-
-* Instrumentation Test with FilePusher/ApkInstaller/RumCommandTargetPreparer
-  * CtsApacheHttpLegacy27ApiSignatureTestCases
-
-* Instrumentation Test with Annotation Filter/PushFilePreparer/RumCommandTargetPreparer/
-* TestFilePushSetup
-  * HelloWorldTests
-
-* Jar Host with ApkInstaller
-  * CtsSampleHostTestCases
diff --git a/atest/Android.bp b/atest/Android.bp
index f93be04..98d9298 100644
--- a/atest/Android.bp
+++ b/atest/Android.bp
@@ -59,6 +59,9 @@
     libs: [
         "atest_proto",
     ],
+    data: [
+        "tools/updatedb_darwin.sh",
+    ],
     // Make atest's built name to atest-dev
     stem: "atest-dev",
     defaults: ["atest_py2_default"],
@@ -68,10 +71,11 @@
     name: "atest_module_info",
     defaults: ["atest_lib_default"],
     srcs: [
-        "module_info.py",
+        "atest_error.py",
         "atest_utils.py",
         "constants.py",
-        "constants_default.py"
+        "constants_default.py",
+        "module_info.py",
     ],
 }
 
@@ -100,6 +104,8 @@
     ],
 }
 
+// Exclude atest_updatedb_unittest due to it's a test for ATest's wrapper of updatedb, but there's
+// no updatedb binary on test server.
 python_test_host {
     name: "atest_unittests",
     main: "atest_run_unittests.py",
@@ -108,6 +114,7 @@
         "**/*.py",
     ],
     data: [
+        "tools/updatedb_darwin.sh",
         "unittest_data/**/*",
         "unittest_data/**/.*",
     ],
@@ -115,6 +122,7 @@
         "asuite_lib_test/*.py",
         "proto/*_pb2.py",
         "proto/__init__.py",
+        "tools/atest_updatedb_unittest.py",
     ],
     libs: [
         "py-mock",
@@ -180,6 +188,7 @@
     name: "asuite_cc_client",
     defaults: ["asuite_default"],
     srcs: [
+        "atest_error.py",
         "atest_utils.py",
         "constants.py",
         "constants_default.py",
diff --git a/atest/TEST_MAPPING b/atest/TEST_MAPPING
index 5ea63b3..cebe409 100644
--- a/atest/TEST_MAPPING
+++ b/atest/TEST_MAPPING
@@ -1,22 +1,29 @@
+// Below lists the TEST_MAPPING tests to do ASuite unittests to make sure
+// the expectation of ASuite are still good.
 {
   "presubmit": [
     {
+      // Host side ATest unittests.
       "name": "atest_unittests",
       "host": true
     },
     {
+      // Host side metrics tests.
       "name": "asuite_metrics_lib_tests",
       "host": true
     },
     {
+      // Host side metrics tests with Python3.
       "name": "asuite_metrics_lib_py3_tests",
       "host": true
     },
     {
+      // Host side clearcut tests.
       "name": "asuite_cc_lib_tests",
       "host": true
     },
     {
+      // Host side clearcut tests with Python3.
       "name": "asuite_cc_lib_py3_tests",
       "host": true
     }
diff --git a/atest/atest.py b/atest/atest.py
index 6ae9214..6baa28e 100755
--- a/atest/atest.py
+++ b/atest/atest.py
@@ -33,9 +33,11 @@
 import platform
 
 import atest_arg_parser
+import atest_error
 import atest_execution_info
 import atest_metrics
 import atest_utils
+import bug_detector
 import cli_translator
 # pylint: disable=import-error
 import constants
@@ -483,13 +485,19 @@
         results_dir: Path for saving atest logs.
         extra_args: Dict of extra args for test runners to utilize.
         test_infos: A list of TestInfos.
+
+    Returns:
+        A list of test commands.
     """
+    all_run_cmds = []
     for test_runner, tests in test_runner_handler.group_tests_by_test_runners(test_infos):
         runner = test_runner(results_dir)
         run_cmds = runner.generate_run_commands(tests, extra_args)
         for run_cmd in run_cmds:
+            all_run_cmds.append(run_cmd)
             print('Would run test via command: %s'
                   % (atest_utils.colorize(run_cmd, constants.GREEN)))
+    return all_run_cmds
 
 def _print_testable_modules(mod_info, suite):
     """Print the testable modules for a given suite.
@@ -534,6 +542,9 @@
         return constants.EXIT_CODE_SUCCESS
     build_targets = set()
     test_infos = set()
+    # Clear cache if user pass -c option
+    if args.clear_cache:
+        atest_utils.clean_test_info_caches(args.tests)
     if _will_run_tests(args):
         build_targets, test_infos = translator.translate(args)
         if not test_infos:
@@ -547,8 +558,22 @@
     build_targets |= test_runner_handler.get_test_runner_reqs(mod_info,
                                                               test_infos)
     extra_args = get_extra_args(args)
+    if args.update_cmd_mapping or args.verify_cmd_mapping:
+        args.dry_run = True
     if args.dry_run:
-        _dry_run(results_dir, extra_args, test_infos)
+        args.tests.sort()
+        dry_run_cmds = _dry_run(results_dir, extra_args, test_infos)
+        if args.verify_cmd_mapping:
+            try:
+                atest_utils.handle_test_runner_cmd(' '.join(args.tests),
+                                                   dry_run_cmds,
+                                                   do_verification=True)
+            except atest_error.DryRunVerificationError as e:
+                atest_utils.colorful_print(str(e), constants.RED)
+                return constants.EXIT_CODE_VERIFY_FAILURE
+        if args.update_cmd_mapping:
+            atest_utils.handle_test_runner_cmd(' '.join(args.tests),
+                                               dry_run_cmds)
         return constants.EXIT_CODE_SUCCESS
     if args.detect_regression:
         build_targets |= (regression_test_runner.RegressionTestRunner('')
@@ -601,6 +626,10 @@
                                                  RESULTS_DIR) as result_file:
         metrics_base.MetricsBase.tool_name = constants.TOOL_NAME
         EXIT_CODE = main(sys.argv[1:], RESULTS_DIR)
+        DETECTOR = bug_detector.BugDetector(sys.argv[1:], EXIT_CODE)
+        metrics.LocalDetectEvent(
+            detect_type=constants.DETECT_TYPE_BUG_DETECTED,
+            result=DETECTOR.caught_result)
         metrics_utils.send_exit_event(EXIT_CODE)
         if result_file:
             print('Execution detail has saved in %s' % result_file.name)
diff --git a/atest/atest_arg_parser.py b/atest/atest_arg_parser.py
index 23b2f8c..2c9711f 100644
--- a/atest/atest_arg_parser.py
+++ b/atest/atest_arg_parser.py
@@ -93,6 +93,17 @@
                           help='Run the test completely on the host without '
                                'a device. (Note: running a host test that '
                                'requires a device with --host will fail.)')
+        # Option for updating dry-run command mapping result.
+        self.add_argument('-u', '--update-cmd-mapping', action='store_true',
+                          help='Update the test command of input tests. '
+                               'Warning: result will be saved under '
+                               'tools/tradefederation/core/atest/test_data.')
+        # Option for verifying dry-run command mapping result.
+        self.add_argument('-y', '--verify-cmd-mapping', action='store_true',
+                          help='Verify the test command of input tests.')
+        # Option for clearing cache of input test reference .
+        self.add_argument('-c', '--clear-cache', action='store_true',
+                          help='Wipe out the test_infos cache of the test.')
         # This arg actually doesn't consume anything, it's primarily used for the
         # help description and creating custom_args in the NameSpace object.
         self.add_argument('--', dest='custom_args', nargs='*',
diff --git a/atest/atest_error.py b/atest/atest_error.py
index 06ebf19..7ab8b5f 100644
--- a/atest/atest_error.py
+++ b/atest/atest_error.py
@@ -61,3 +61,6 @@
 
 class XmlNotExistError(TestDiscoveryException):
     """Raised when the xml file does not exist."""
+
+class DryRunVerificationError(Exception):
+    """Base Exception if verification fail."""
diff --git a/atest/atest_utils.py b/atest/atest_utils.py
index 9e03e8e..983c935 100644
--- a/atest/atest_utils.py
+++ b/atest/atest_utils.py
@@ -18,9 +18,12 @@
 
 from __future__ import print_function
 
+import hashlib
 import itertools
+import json
 import logging
 import os
+import pickle
 import re
 import subprocess
 import sys
@@ -30,10 +33,15 @@
 except ImportError:
     from urllib.request import urlopen
 
+import atest_error
 import constants
 
-_MAKE_CMD = '%s/build/soong/soong_ui.bash' % os.environ.get(
-    constants.ANDROID_BUILD_TOP)
+from metrics import metrics_base
+
+_MAKE_CMD = ('%s/build/soong/soong_ui.bash' %
+             os.path.relpath(os.environ.get(constants.ANDROID_BUILD_TOP,
+                                            os.getcwd()),
+                             os.getcwd()))
 BUILD_CMD = [_MAKE_CMD, '--make-mode']
 _BASH_RESET_CODE = '\033[0m\n'
 # Arbitrary number to limit stdout for failed runs in _run_limited_output.
@@ -45,7 +53,12 @@
 # ex: [ 99% 39710/39711]
 _BUILD_COMPILE_STATUS = re.compile(r'\[\s*(\d{1,3}%\s+)?\d+/\d+\]')
 _BUILD_FAILURE = 'FAILED: '
-
+CMD_RESULT_PATH = os.path.join(os.environ.get(constants.ANDROID_BUILD_TOP,
+                                              os.getcwd()),
+                               'tools/tradefederation/core/atest/test_data',
+                               'sample_test_cmd_result.json')
+TEST_INFO_CACHE_ROOT = os.path.join(os.path.expanduser('~'), '.atest',
+                                    'info_cache')
 
 def _capture_fail_section(full_log):
     """Return the error message from the build output.
@@ -280,34 +293,26 @@
 
 
 def is_external_run():
+    # TODO(b/133905312): remove this function after aidegen calling
+    #       metrics_base.get_user_type directly.
     """Check is external run or not.
 
+    Determine the internal user by passing at least one check:
+      - whose git mail domain is from google
+      - whose hostname is from google
+    Otherwise is external user.
+
     Returns:
         True if this is an external run, False otherwise.
     """
-    try:
-        output = subprocess.check_output(['git', 'config', '--get', 'user.email'],
-                                         universal_newlines=True)
-        if output and output.strip().endswith(constants.INTERNAL_EMAIL):
-            return False
-    except OSError:
-        # OSError can be raised when running atest_unittests on a host
-        # without git being set up.
-        # This happens before atest._configure_logging is called to set up
-        # logging. Therefore, use print to log the error message, instead of
-        # logging.debug.
-        print('Unable to determine if this is an external run, git is not found.')
-    except subprocess.CalledProcessError:
-        print('Unable to determine if this is an external run, email is not '
-              'found in git config.')
-    return True
+    return metrics_base.get_user_type() == metrics_base.EXTERNAL_USER
 
 
 def print_data_collection_notice():
     """Print the data collection notice."""
     anonymous = ''
     user_type = 'INTERNAL'
-    if is_external_run():
+    if metrics_base.get_user_type() == metrics_base.EXTERNAL_USER:
         anonymous = ' anonymous'
         user_type = 'EXTERNAL'
     notice = ('  We collect%s usage statistics in accordance with our Content '
@@ -323,3 +328,149 @@
     colorful_print("Notice:", constants.RED)
     colorful_print("%s" % notice, constants.GREEN)
     print('==================\n')
+
+
+def handle_test_runner_cmd(input_test, test_cmds, do_verification=False,
+                           result_path=CMD_RESULT_PATH):
+    """Handle the runner command of input tests.
+
+    Args:
+        input_test: A string of input tests pass to atest.
+        test_cmds: A list of strings for running input tests.
+        do_verification: A boolean to indicate the action of this method.
+                         True: Do verification without updating result map and
+                               raise DryRunVerificationError if verifying fails.
+                         False: Update result map, if the former command is
+                                different with current command, it will confirm
+                                with user if they want to update or not.
+        result_path: The file path for saving result.
+    """
+    # Always sort test_cmds to make it comparable.
+    test_cmds.sort()
+    full_result_content = {}
+    if os.path.isfile(result_path):
+        with open(result_path) as json_file:
+            full_result_content = json.load(json_file)
+    former_test_cmds = full_result_content.get(input_test, [])
+    if former_test_cmds != test_cmds:
+        if do_verification:
+            raise atest_error.DryRunVerificationError('Dry run verification failed,'
+                                                      'former commands: %s' %
+                                                      former_test_cmds)
+        if former_test_cmds:
+            # If former_test_cmds is different from test_cmds, ask users if they
+            # are willing to update the result.
+            print('Former cmds = %s' % former_test_cmds)
+            print('Current cmds = %s' % test_cmds)
+            try:
+                # TODO(b/137156054):
+                # Move the import statement into a method for that distutils is
+                # not a built-in lib in older python3(b/137017806). Will move it
+                # back when embedded_launcher fully supports Python3.
+                from distutils.util import strtobool
+                if not strtobool(raw_input('Do you want to update former result'
+                                           'with the latest one?(Y/n)')):
+                    print('SKIP updating result!!!')
+                    return
+            except ValueError:
+                # Default action is updating the command result of the input_test.
+                # If the user input is unrecognizable telling yes or no,
+                # "Y" is implicitly applied.
+                pass
+    else:
+        # If current commands are the same as the formers, no need to update
+        # result.
+        return
+    full_result_content[input_test] = test_cmds
+    with open(result_path, 'w') as outfile:
+        json.dump(full_result_content, outfile, indent=0)
+        print('Save result mapping to %s' % result_path)
+
+def _get_hashed_file_name(main_file_name):
+    """Convert the input string to a md5-hashed string. If file_extension is
+       given, returns $(hashed_string).$(file_extension), otherwise
+       $(hashed_string).cache.
+
+    Args:
+        main_file_name: The input string need to be hashed.
+
+    Returns:
+        A string as hashed file name with .cache file extension.
+    """
+    hashed_fn = hashlib.md5(str(main_file_name).encode())
+    hashed_name = hashed_fn.hexdigest()
+    return hashed_name + '.cache'
+
+def get_test_info_cache_path(test_reference, cache_root=TEST_INFO_CACHE_ROOT):
+    """Get the cache path of the desired test_infos.
+
+    Args:
+        test_reference: A string of the test.
+        cache_root: Folder path where stores caches.
+
+    Returns:
+        A string of the path of test_info cache.
+    """
+    return os.path.join(cache_root,
+                        _get_hashed_file_name(test_reference))
+
+def update_test_info_cache(test_reference, test_infos,
+                           cache_root=TEST_INFO_CACHE_ROOT):
+    """Update cache content which stores a set of test_info objects through
+       pickle module, each test_reference will be saved as a cache file.
+
+    Args:
+        test_reference: A string referencing a test.
+        test_infos: A set of TestInfos.
+        cache_root: Folder path for saving caches.
+    """
+    if not os.path.isdir(cache_root):
+        os.makedirs(cache_root)
+    cache_path = get_test_info_cache_path(test_reference, cache_root)
+    # Save test_info to files.
+    try:
+        with open(cache_path, 'wb') as test_info_cache_file:
+            logging.debug('Saving cache %s.', cache_path)
+            pickle.dump(test_infos, test_info_cache_file)
+    except (pickle.PicklingError, TypeError, IOError) as err:
+        # Don't break anything, just log this error, maybe collect the exception
+        # by metrics in the future.
+        logging.debug('Exception raised: %s', err)
+
+def load_test_info_cache(test_reference, cache_root=TEST_INFO_CACHE_ROOT):
+    """Load cache by test_reference to a set of test_infos object.
+
+    Args:
+        test_reference: A string referencing a test.
+        cache_root: Folder path for finding caches.
+
+    Returns:
+        A list of TestInfo namedtuple if cache found, else None.
+    """
+    cache_file = get_test_info_cache_path(test_reference, cache_root)
+    if os.path.isfile(cache_file):
+        logging.debug('Loading cache %s.', cache_file)
+        try:
+            with open(cache_file, 'rb') as config_dictionary_file:
+                return pickle.load(config_dictionary_file)
+        except (pickle.UnpicklingError, EOFError, IOError) as err:
+            # Don't break anything, just log this error, maybe collect the
+            # exception by metrics in the future.
+            logging.debug('Exception raised: %s', err)
+    return None
+
+def clean_test_info_caches(tests, cache_root=TEST_INFO_CACHE_ROOT):
+    """Clean caches of input tests.
+
+    Args:
+        tests: A list of test references.
+        cache_root: Folder path for finding caches.
+    """
+    for test in tests:
+        cache_file = get_test_info_cache_path(test, cache_root)
+        if os.path.isfile(cache_file):
+            logging.debug('Removing cache: %s', cache_file)
+            try:
+                os.remove(cache_file)
+            except IOError as err:
+                logging.debug('Exception raised: %s', err)
diff --git a/atest/atest_utils_unittest.py b/atest/atest_utils_unittest.py
index 886f26b..b41f113 100755
--- a/atest/atest_utils_unittest.py
+++ b/atest/atest_utils_unittest.py
@@ -16,18 +16,37 @@
 
 """Unittests for atest_utils."""
 
+import hashlib
+import os
 import subprocess
 import sys
+import tempfile
 import unittest
 import mock
 
+import atest_error
 import atest_utils
+import unittest_utils
+from test_finders import test_info
 
 if sys.version_info[0] == 2:
     from StringIO import StringIO
 else:
     from io import StringIO
 
+TEST_MODULE_NAME_A = 'ModuleNameA'
+TEST_RUNNER_A = 'FakeTestRunnerA'
+TEST_BUILD_TARGET_A = set(['bt1', 'bt2'])
+TEST_DATA_A = {'test_data_a_1': 'a1',
+               'test_data_a_2': 'a2'}
+TEST_SUITE_A = 'FakeSuiteA'
+TEST_MODULE_CLASS_A = 'FAKE_MODULE_CLASS_A'
+TEST_INSTALL_LOC_A = set(['host', 'device'])
+TEST_INFO_A = test_info.TestInfo(TEST_MODULE_NAME_A, TEST_RUNNER_A,
+                                 TEST_BUILD_TARGET_A, TEST_DATA_A,
+                                 TEST_SUITE_A, TEST_MODULE_CLASS_A,
+                                 TEST_INSTALL_LOC_A)
+
 #pylint: disable=protected-access
 class AtestUtilsUnittests(unittest.TestCase):
     """Unit tests for atest_utils.py"""
@@ -193,26 +212,41 @@
         self.assertEqual(capture_output.getvalue(),
                          green_wrap_no_highlight_string)
 
+    @mock.patch('socket.gethostname')
     @mock.patch('subprocess.check_output')
-    def test_is_external_run(self, mock_output):
+    def test_is_external_run(self, mock_output, mock_hostname):
         """Test method is_external_run."""
         mock_output.return_value = ''
+        mock_hostname.return_value = ''
         self.assertTrue(atest_utils.is_external_run())
+
         mock_output.return_value = 'test@other.com'
+        mock_hostname.return_value = 'abc.com'
         self.assertTrue(atest_utils.is_external_run())
+
+        mock_output.return_value = 'test@other.com'
+        mock_hostname.return_value = 'abc.google.com'
+        self.assertFalse(atest_utils.is_external_run())
+
+        mock_output.return_value = 'test@other.com'
+        mock_hostname.return_value = 'abc.google.def.com'
+        self.assertTrue(atest_utils.is_external_run())
+
         mock_output.return_value = 'test@google.com'
         self.assertFalse(atest_utils.is_external_run())
+
         mock_output.side_effect = OSError()
         self.assertTrue(atest_utils.is_external_run())
+
         mock_output.side_effect = subprocess.CalledProcessError(1, 'cmd')
         self.assertTrue(atest_utils.is_external_run())
 
-    @mock.patch('atest_utils.is_external_run')
-    def test_print_data_collection_notice(self, mock_is_external_run):
+    @mock.patch('metrics.metrics_base.get_user_type')
+    def test_print_data_collection_notice(self, mock_get_user_type):
         """Test method print_data_collection_notice."""
 
-        # is_external_run return False.
-        mock_is_external_run.return_value = True
+        # get_user_type return 1(external).
+        mock_get_user_type.return_value = 1
         notice_str = ('\n==================\nNotice:\n'
                       '  We collect anonymous usage statistics'
                       ' in accordance with our'
@@ -228,8 +262,8 @@
         uncolored_string = notice_str
         self.assertEqual(capture_output.getvalue(), uncolored_string)
 
-        # is_external_run return False.
-        mock_is_external_run.return_value = False
+        # get_user_type return 0(internal).
+        mock_get_user_type.return_value = 0
         notice_str = ('\n==================\nNotice:\n'
                       '  We collect usage statistics'
                       ' in accordance with our'
@@ -245,6 +279,94 @@
         uncolored_string = notice_str
         self.assertEqual(capture_output.getvalue(), uncolored_string)
 
+    @mock.patch('__builtin__.raw_input')
+    @mock.patch('json.load')
+    def test_update_test_runner_cmd(self, mock_json_load_data, mock_raw_input):
+        """Test method handle_test_runner_cmd without enable do_verification."""
+        former_cmd_str = 'Former cmds ='
+        write_result_str = 'Save result mapping to test_result'
+        tmp_file = tempfile.NamedTemporaryFile()
+        input_cmd = 'atest_args'
+        runner_cmds = ['cmd1', 'cmd2']
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        # Previous data is empty. Should not enter strtobool.
+        # If entered, exception will be raised cause test fail.
+        mock_json_load_data.return_value = {}
+        atest_utils.handle_test_runner_cmd(input_cmd,
+                                           runner_cmds,
+                                           do_verification=False,
+                                           result_path=tmp_file.name)
+        sys.stdout = sys.__stdout__
+        self.assertEqual(capture_output.getvalue().find(former_cmd_str), -1)
+        # Previous data is the same as the new input. Should not enter strtobool.
+        # If entered, exception will be raised cause test fail
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        mock_json_load_data.return_value = {input_cmd:runner_cmds}
+        atest_utils.handle_test_runner_cmd(input_cmd,
+                                           runner_cmds,
+                                           do_verification=False,
+                                           result_path=tmp_file.name)
+        sys.stdout = sys.__stdout__
+        self.assertEqual(capture_output.getvalue().find(former_cmd_str), -1)
+        self.assertEqual(capture_output.getvalue().find(write_result_str), -1)
+        # Previous data has different cmds. Should enter strtobool not update,
+        # should not find write_result_str.
+        prev_cmds = ['cmd1']
+        mock_raw_input.return_value = 'n'
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        mock_json_load_data.return_value = {input_cmd:prev_cmds}
+        atest_utils.handle_test_runner_cmd(input_cmd,
+                                           runner_cmds,
+                                           do_verification=False,
+                                           result_path=tmp_file.name)
+        sys.stdout = sys.__stdout__
+        self.assertEqual(capture_output.getvalue().find(write_result_str), -1)
+
+    @mock.patch('json.load')
+    def test_verify_test_runner_cmd(self, mock_json_load_data):
+        """Test method handle_test_runner_cmd without enable update_result."""
+        tmp_file = tempfile.NamedTemporaryFile()
+        input_cmd = 'atest_args'
+        runner_cmds = ['cmd1', 'cmd2']
+        # Previous data is the same as the new input. Should not raise exception.
+        mock_json_load_data.return_value = {input_cmd:runner_cmds}
+        atest_utils.handle_test_runner_cmd(input_cmd,
+                                           runner_cmds,
+                                           do_verification=True,
+                                           result_path=tmp_file.name)
+        # Previous data has different cmds. Should enter strtobool and hit
+        # exception.
+        prev_cmds = ['cmd1']
+        mock_json_load_data.return_value = {input_cmd:prev_cmds}
+        self.assertRaises(atest_error.DryRunVerificationError,
+                          atest_utils.handle_test_runner_cmd,
+                          input_cmd,
+                          runner_cmds,
+                          do_verification=True,
+                          result_path=tmp_file.name)
+
+    def test_get_test_info_cache_path(self):
+        """Test method get_test_info_cache_path."""
+        input_file_name = 'mytest_name'
+        cache_root = '/a/b/c'
+        expect_hashed_name = ('%s.cache' % hashlib.md5(str(input_file_name).
+                                                       encode()).hexdigest())
+        self.assertEqual(os.path.join(cache_root, expect_hashed_name),
+                         atest_utils.get_test_info_cache_path(input_file_name,
+                                                              cache_root))
+
+    def test_get_and_load_cache(self):
+        """Test method update_test_info_cache and load_test_info_cache."""
+        test_reference = 'myTestRefA'
+        test_cache_dir = tempfile.mkdtemp()
+        atest_utils.update_test_info_cache(test_reference, TEST_INFO_A,
+                                           test_cache_dir)
+        unittest_utils.assert_equal_testinfos(
+            self, TEST_INFO_A,
+            atest_utils.load_test_info_cache(test_reference, test_cache_dir))
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/atest/bug_detector.py b/atest/bug_detector.py
new file mode 100644
index 0000000..211e0b5
--- /dev/null
+++ b/atest/bug_detector.py
@@ -0,0 +1,126 @@
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Classes for bug events history
+"""
+
+import datetime
+import json
+import os
+
+import constants
+
+_META_FILE = os.path.join(os.path.expanduser('~'),
+                          '.config', 'asuite', 'atest_history.json')
+_DETECT_OPTION_FILTER = ['-v', '--verbose']
+_DETECTED_SUCCESS = 1
+_DETECTED_FAIL = 0
+# constants of history key
+_LATEST_EXIT_CODE = 'latest_exit_code'
+_UPDATED_AT = 'updated_at'
+
+class BugDetector(object):
+    """Class for handling if a bug is detected by comparing test history."""
+
+    def __init__(self, argv, exit_code, history_file=None):
+        """BugDetector constructor
+
+        Args:
+            argv: A list of arguments.
+            exit_code: An integer of exit code.
+            history_file: A string of a given history file path.
+        """
+        self.detect_key = self.get_detect_key(argv)
+        self.exit_code = exit_code
+        self.file = history_file if history_file else _META_FILE
+        self.history = self.get_history()
+        self.caught_result = self.detect_bug_caught()
+        self.update_history()
+
+    def get_detect_key(self, argv):
+        """Get the key for history searching.
+
+        1. remove '-v' in argv to argv_no_verbose
+        2. sort the argv_no_verbose
+
+        Args:
+            argv: A list of arguments.
+
+        Returns:
+            A string of ordered command line.
+        """
+        argv_without_option = [x for x in argv if x not in _DETECT_OPTION_FILTER]
+        argv_without_option.sort()
+        return ' '.join(argv_without_option)
+
+    def get_history(self):
+        """Get a history object from a history file.
+
+        e.g.
+        {
+            "SystemUITests:.ScrimControllerTest":{
+                "latest_exit_code": 5, "updated_at": "2019-01-26T15:33:08.305026"},
+            "--host hello_world_test ":{
+                "latest_exit_code": 0, "updated_at": "2019-02-26T15:33:08.305026"},
+        }
+
+        Returns:
+            An object of loading from a history.
+        """
+        if os.path.exists(self.file):
+            with open(self.file) as json_file:
+                return json.load(json_file)
+        return {}
+
+    def detect_bug_caught(self):
+        """Detection of catching bugs.
+
+        When latest_exit_code and current exit_code are different, treat it
+        as a bug caught.
+
+        Returns:
+            A integer of detecting result, e.g.
+            1: success
+            0: fail
+        """
+        if not self.history:
+            return _DETECTED_FAIL
+        latest = self.history.get(self.detect_key, {})
+        if latest.get(_LATEST_EXIT_CODE, self.exit_code) == self.exit_code:
+            return _DETECTED_FAIL
+        return _DETECTED_SUCCESS
+
+    def update_history(self):
+        """Update the history file.
+
+        1. update latest_bug result to history cache.
+        2. trim history cache to size from oldest updated time.
+        3. write to the file.
+        """
+        latest_bug = {
+            self.detect_key: {
+                _LATEST_EXIT_CODE: self.exit_code,
+                _UPDATED_AT: datetime.datetime.now().isoformat()
+            }
+        }
+        self.history.update(latest_bug)
+        num_history = len(self.history)
+        if num_history > constants.UPPER_LIMIT:
+            sorted_history = sorted(self.history.items(),
+                                    key=lambda kv: kv[1][_UPDATED_AT])
+            self.history = dict(
+                sorted_history[(num_history - constants.TRIM_TO_SIZE):])
+        with open(self.file, 'w') as outfile:
+            json.dump(self.history, outfile, indent=0)
diff --git a/atest/bug_detector_unittest.py b/atest/bug_detector_unittest.py
new file mode 100644
index 0000000..a9356fc
--- /dev/null
+++ b/atest/bug_detector_unittest.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+#
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unittests for bug_detector."""
+
+import datetime
+import json
+import os
+import unittest
+import mock
+
+import bug_detector
+import constants
+import unittest_constants as uc
+
+TEST_DICT = {
+    'test1': {
+        'latest_exit_code': 5,
+        'updated_at': ''
+    },
+    'test2': {
+        'latest_exit_code': 0,
+        'updated_at': ''
+    }
+}
+
+class BugDetectorUnittest(unittest.TestCase):
+    """Unit test for bug_detector.py"""
+
+    def setUp(self):
+        """Set up stuff for testing."""
+        self.history_file = os.path.join(uc.TEST_DATA_DIR, 'bug_detector.json')
+        self.detector = bug_detector.BugDetector(['test1'], 5, self.history_file)
+        self._reset_history_file()
+        self.history_file2 = os.path.join(uc.TEST_DATA_DIR, 'bug_detector2.json')
+
+    def tearDown(self):
+        """Run after execution of every test"""
+        if os.path.isfile(self.history_file):
+            os.remove(self.history_file)
+        if os.path.isfile(self.history_file2):
+            os.remove(self.history_file2)
+
+    def _reset_history_file(self):
+        """Reset test history file."""
+        with open(self.history_file, 'w') as outfile:
+            json.dump(TEST_DICT, outfile)
+
+    def _make_test_file(self, file_size):
+        temp_history = {}
+        for i in range(file_size):
+            latest_bug = {
+                i: {
+                    'latest_exit_code': i,
+                    'updated_at': datetime.datetime.now().isoformat()
+                }
+            }
+            temp_history.update(latest_bug)
+        with open(self.history_file2, 'w') as outfile:
+            json.dump(temp_history, outfile, indent=0)
+
+    @mock.patch.object(bug_detector.BugDetector, 'update_history')
+    def test_get_detect_key(self, _):
+        """Test get_detect_key."""
+        # argv without -v
+        argv = ['test2', 'test1']
+        want_key = 'test1 test2'
+        dtr = bug_detector.BugDetector(argv, 0)
+        self.assertEqual(dtr.get_detect_key(argv), want_key)
+
+        # argv with -v
+        argv = ['-v', 'test2', 'test1']
+        want_key = 'test1 test2'
+        dtr = bug_detector.BugDetector(argv, 0)
+        self.assertEqual(dtr.get_detect_key(argv), want_key)
+
+        # argv with --verbose
+        argv = ['--verbose', 'test2', 'test3', 'test1']
+        want_key = 'test1 test2 test3'
+        dtr = bug_detector.BugDetector(argv, 0)
+        self.assertEqual(dtr.get_detect_key(argv), want_key)
+
+    def test_get_history(self):
+        """Test get_history."""
+        self.assertEqual(self.detector.get_history(), TEST_DICT)
+
+    @mock.patch.object(bug_detector.BugDetector, 'update_history')
+    def test_detect_bug_caught(self, _):
+        """Test detect_bug_caught."""
+        self._reset_history_file()
+        dtr = bug_detector.BugDetector(['test1'], 0, self.history_file)
+        success = 1
+        self.assertEqual(dtr.detect_bug_caught(), success)
+
+    def test_update_history(self):
+        """Test update_history."""
+        constants.UPPER_LIMIT = 10
+        constants.TRIM_TO_SIZE = 3
+
+        mock_file_size = 0
+        self._make_test_file(mock_file_size)
+        dtr = bug_detector.BugDetector(['test1'], 0, self.history_file2)
+        self.assertTrue(dtr.history.has_key('test1'))
+
+        # History is larger than constants.UPPER_LIMIT. Trim to size.
+        mock_file_size = 10
+        self._make_test_file(mock_file_size)
+        dtr = bug_detector.BugDetector(['test1'], 0, self.history_file2)
+        self.assertEqual(len(dtr.history), constants.TRIM_TO_SIZE)
+        keys = ['test1', '9', '8']
+        for key in keys:
+            self.assertTrue(dtr.history.has_key(key))
+
+        # History is not larger than constants.UPPER_LIMIT.
+        mock_file_size = 5
+        self._make_test_file(mock_file_size)
+        dtr = bug_detector.BugDetector(['test1'], 0, self.history_file2)
+        self.assertEqual(len(dtr.history), mock_file_size+1)
+        keys = ['test1', '4', '3', '2', '1', '0']
+        for key in keys:
+            self.assertTrue(dtr.history.has_key(key))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/cli_translator.py b/atest/cli_translator.py
index e02a339..b56e79a 100644
--- a/atest/cli_translator.py
+++ b/atest/cli_translator.py
@@ -23,6 +23,7 @@
 import json
 import logging
 import os
+import re
 import sys
 import time
 
@@ -39,6 +40,9 @@
 TEST_MAPPING = 'TEST_MAPPING'
 FUZZY_FINDER = 'FUZZY'
 
+# Pattern used to identify comments start with '//' or '#' in TEST_MAPPING.
+_COMMENTS_RE = re.compile(r'(?m)[\s\t]*(#|//).*|(\".*?\")')
+_COMMENTS = frozenset(['//', '#'])
 
 #pylint: disable=no-self-use
 class CLITranslator(object):
@@ -65,6 +69,7 @@
         """
         self.mod_info = module_info
 
+    # pylint: disable=too-many-locals
     def _find_test_infos(self, test, tm_test_detail):
         """Return set of TestInfos based on a given test.
 
@@ -88,29 +93,30 @@
             # test name, so the details can be set after test_info object
             # is created.
             try:
-                test_info = finder.find_method(finder.test_finder_instance,
-                                               test)
+                found_test_infos = finder.find_method(
+                    finder.test_finder_instance, test)
             except atest_error.TestDiscoveryException as e:
                 find_test_err_msg = e
-            if test_info:
-                if tm_test_detail:
-                    test_info.data[constants.TI_MODULE_ARG] = (
-                        tm_test_detail.options)
-                    test_info.from_test_mapping = True
-                    test_info.host = tm_test_detail.host
-                test_infos.add(test_info)
+            if found_test_infos:
+                for test_info in found_test_infos:
+                    if tm_test_detail:
+                        test_info.data[constants.TI_MODULE_ARG] = (
+                            tm_test_detail.options)
+                        test_info.from_test_mapping = True
+                        test_info.host = tm_test_detail.host
+                    test_infos.add(test_info)
                 test_found = True
                 finder_info = finder.finder_info
                 print("Found '%s' as %s" % (
                     atest_utils.colorize(test, constants.GREEN),
                     finder_info))
                 test_finders.append(finder_info)
-                test_info_str = str(test_info)
+                test_info_str = ','.join([str(x) for x in found_test_infos])
                 break
         if not test_found:
             f_results = self._fuzzy_search_and_msg(test, find_test_err_msg)
             if f_results:
-                test_infos.add(f_results)
+                test_infos.update(f_results)
                 test_found = True
                 test_finders.append(FUZZY_FINDER)
         metrics.FindTestFinishEvent(
@@ -120,6 +126,14 @@
             test_reference=test,
             test_finders=test_finders,
             test_info=test_info_str)
+        # Cache test_infos by default except running with TEST_MAPPING which may
+        # include customized flags and they are likely to mess up other
+        # non-test_mapping tests.
+        if test_infos and not tm_test_detail:
+            atest_utils.update_test_info_cache(test, test_infos)
+            print('Test info has been cached for speeding up the next run, if '
+                  'test info need to be updated, please add -c to clean the '
+                  'old cache.')
         return test_infos
 
     def _fuzzy_search_and_msg(self, test, find_test_err_msg):
@@ -130,7 +144,7 @@
             find_test_err_msg: A string of find test error message.
 
         Returns:
-            A TestInfos if found, otherwise None.
+            A list of TestInfos if found, otherwise None.
         """
         print('No test found for: %s' %
               atest_utils.colorize(test, constants.RED))
@@ -139,9 +153,10 @@
         mod_finder = module_finder.ModuleFinder(self.mod_info)
         results = mod_finder.get_fuzzy_searching_results(test)
         if len(results) == 1 and self._confirm_running(results):
-            test_info = mod_finder.find_test_by_module_name(results[0])
-            if test_info:
-                return test_info
+            found_test_infos = mod_finder.find_test_by_module_name(results[0])
+            # found_test_infos is a list with at most 1 element.
+            if found_test_infos:
+                return found_test_infos
         elif len(results) > 1:
             self._print_fuzzy_searching_results(results)
         else:
@@ -204,6 +219,30 @@
         for mod in results[:10]:
             atest_utils.colorful_print(mod, constants.GREEN)
 
+    def filter_comments(self, test_mapping_file):
+        """Remove comments in TEST_MAPPING file to valid format. Only '//' and
+        '#' are regarded as comments.
+
+        Args:
+            test_mapping_file: Path to a TEST_MAPPING file.
+
+        Returns:
+            Valid json string without comments.
+        """
+        def _replace(match):
+            """Replace comments if found matching the defined regular expression.
+
+            Args:
+                match: The matched regex pattern
+
+            Returns:
+                "" if it matches _COMMENTS, otherwise original string.
+            """
+            line = match.group(0).strip()
+            return "" if any(map(line.startswith, _COMMENTS)) else line
+        with open(test_mapping_file) as json_file:
+            return re.sub(_COMMENTS_RE, _replace, json_file.read())
+
     def _read_tests_in_test_mapping(self, test_mapping_file):
         """Read tests from a TEST_MAPPING file.
 
@@ -219,9 +258,7 @@
         """
         all_tests = {}
         imports = []
-        test_mapping_dict = None
-        with open(test_mapping_file) as json_file:
-            test_mapping_dict = json.load(json_file)
+        test_mapping_dict = json.loads(self.filter_comments(test_mapping_file))
         for test_group_name, test_list in test_mapping_dict.items():
             if test_group_name == constants.TEST_MAPPING_IMPORTS:
                 for import_detail in test_list:
@@ -302,11 +339,7 @@
                 grouped_tests.update(test_list)
 
         tests = set(merged_all_tests.get(test_group, []))
-        # Postsubmit tests shall include all presubmit tests as well.
-        if test_group == constants.TEST_GROUP_POSTSUBMIT:
-            tests.update(merged_all_tests.get(
-                constants.TEST_GROUP_PRESUBMIT, set()))
-        elif test_group == constants.TEST_GROUP_ALL:
+        if test_group == constants.TEST_GROUP_ALL:
             for grouped_tests in merged_all_tests.values():
                 tests.update(grouped_tests)
         return tests, merged_all_tests, all_imports
diff --git a/atest/cli_translator_unittest.py b/atest/cli_translator_unittest.py
index 5504fc2..1b6137b 100755
--- a/atest/cli_translator_unittest.py
+++ b/atest/cli_translator_unittest.py
@@ -17,6 +17,7 @@
 """Unittests for cli_translator."""
 
 import unittest
+import json
 import os
 import re
 import sys
@@ -82,6 +83,8 @@
         # Test mapping related args
         self.args.test_mapping = False
         self.args.include_subdirs = False
+        # Cache finder related args
+        self.args.clear_cache = False
         self.ctr.mod_info = mock.Mock
         self.ctr.mod_info.name_to_module_info = {}
 
@@ -99,11 +102,11 @@
                             mock_findtestbymodule, mock_raw_input):
         """Test _get_test_infos method."""
         ctr = cli_t.CLITranslator()
-        find_method_return_module_info = lambda x, y: uc.MODULE_INFO
+        find_method_return_module_info = lambda x, y: uc.MODULE_INFOS
         # pylint: disable=invalid-name
-        find_method_return_module_class_info = (lambda x, test: uc.MODULE_INFO
+        find_method_return_module_class_info = (lambda x, test: uc.MODULE_INFOS
                                                 if test == uc.MODULE_NAME
-                                                else uc.CLASS_INFO)
+                                                else uc.CLASS_INFOS)
         find_method_return_nothing = lambda x, y: None
         one_test = [uc.MODULE_NAME]
         mult_test = [uc.MODULE_NAME, uc.CLASS_NAME]
@@ -161,6 +164,54 @@
                     test_detail2.options,
                     test_info.data[constants.TI_MODULE_ARG])
 
+    @mock.patch.object(metrics, 'FindTestFinishEvent')
+    @mock.patch.object(test_finder_handler, 'get_find_methods_for_test')
+    def test_get_test_infos_2(self, mock_getfindmethods, _metrics):
+        """Test _get_test_infos method."""
+        ctr = cli_t.CLITranslator()
+        find_method_return_module_info2 = lambda x, y: uc.MODULE_INFOS2
+        find_method_ret_mod_cls_info2 = (
+            lambda x, test: uc.MODULE_INFOS2
+            if test == uc.MODULE_NAME else uc.CLASS_INFOS2)
+        one_test = [uc.MODULE_NAME]
+        mult_test = [uc.MODULE_NAME, uc.CLASS_NAME]
+        # Let's make sure we return what we expect.
+        expected_test_infos = {uc.MODULE_INFO, uc.MODULE_INFO2}
+        mock_getfindmethods.return_value = [
+            test_finder_base.Finder(None, find_method_return_module_info2,
+                                    None)]
+        unittest_utils.assert_strict_equal(
+            self, ctr._get_test_infos(one_test), expected_test_infos)
+        # Check we receive multiple test infos.
+        expected_test_infos = {uc.MODULE_INFO, uc.CLASS_INFO, uc.MODULE_INFO2,
+                               uc.CLASS_INFO2}
+        mock_getfindmethods.return_value = [
+            test_finder_base.Finder(None, find_method_ret_mod_cls_info2,
+                                    None)]
+        unittest_utils.assert_strict_equal(
+            self, ctr._get_test_infos(mult_test), expected_test_infos)
+        # Check the method works for test mapping.
+        test_detail1 = test_mapping.TestDetail(uc.TEST_MAPPING_TEST)
+        test_detail2 = test_mapping.TestDetail(uc.TEST_MAPPING_TEST_WITH_OPTION)
+        expected_test_infos = {uc.MODULE_INFO, uc.CLASS_INFO, uc.MODULE_INFO2,
+                               uc.CLASS_INFO2}
+        mock_getfindmethods.return_value = [
+            test_finder_base.Finder(None, find_method_ret_mod_cls_info2,
+                                    None)]
+        test_infos = ctr._get_test_infos(
+            mult_test, [test_detail1, test_detail2])
+        unittest_utils.assert_strict_equal(
+            self, test_infos, expected_test_infos)
+        for test_info in test_infos:
+            if test_info in [uc.MODULE_INFO, uc.MODULE_INFO2]:
+                self.assertEqual(
+                    test_detail1.options,
+                    test_info.data[constants.TI_MODULE_ARG])
+            elif test_info in [uc.CLASS_INFO, uc.CLASS_INFO2]:
+                self.assertEqual(
+                    test_detail2.options,
+                    test_info.data[constants.TI_MODULE_ARG])
+
     @mock.patch.object(cli_t.CLITranslator, '_get_test_infos',
                        side_effect=gettestinfos_side_effect)
     def test_translate_class(self, _info):
@@ -242,9 +293,7 @@
                 test_group=constants.TEST_GROUP_POSTSUBMIT,
                 file_name='test_mapping_sample', checked_files=set())
         expected_presubmit = set([TEST_1, TEST_2, TEST_5, TEST_7, TEST_9])
-        expected = set(
-            [TEST_1, TEST_2, TEST_3, TEST_5, TEST_6, TEST_7, TEST_8, TEST_9,
-             TEST_10])
+        expected = set([TEST_3, TEST_6, TEST_8, TEST_10])
         expected_all_tests = {'presubmit': expected_presubmit,
                               'postsubmit': set(
                                   [TEST_3, TEST_6, TEST_8, TEST_10]),
@@ -304,5 +353,22 @@
             uc.MODULE_NAME, uc.MODULE2_NAME)
         self.assertEquals(capture_output.getvalue(), output)
 
+    def test_filter_comments(self):
+        """Test filter_comments method"""
+        file_with_comments = os.path.join(TEST_MAPPING_TOP_DIR,
+                                          'folder6',
+                                          'test_mapping_sample_with_comments')
+        file_with_comments_golden = os.path.join(TEST_MAPPING_TOP_DIR,
+                                                 'folder6',
+                                                 'test_mapping_sample_golden')
+        test_mapping_dict = json.loads(
+            self.ctr.filter_comments(file_with_comments))
+        test_mapping_dict_gloden = None
+        with open(file_with_comments_golden) as json_file:
+            test_mapping_dict_gloden = json.load(json_file)
+
+        self.assertEqual(test_mapping_dict, test_mapping_dict_gloden)
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/atest/constants_default.py b/atest/constants_default.py
index abc6740..676f993 100644
--- a/atest/constants_default.py
+++ b/atest/constants_default.py
@@ -16,6 +16,7 @@
 Various globals used by atest.
 """
 
+import re
 
 MODE = 'DEFAULT'
 
@@ -52,6 +53,7 @@
 EXIT_CODE_ERROR = 3
 EXIT_CODE_TEST_NOT_FOUND = 4
 EXIT_CODE_TEST_FAILURE = 5
+EXIT_CODE_VERIFY_FAILURE = 6
 
 # Test finder constants.
 MODULE_CONFIG = 'AndroidTest.xml'
@@ -64,6 +66,8 @@
 MODULE_CLASS_NATIVE_TESTS = 'NATIVE_TESTS'
 MODULE_CLASS_JAVA_LIBRARIES = 'JAVA_LIBRARIES'
 MODULE_TEST_CONFIG = 'test_config'
+CC_EXT_RE = re.compile(r'.*\.(cc|cpp)$', re.I)
+JAVA_EXT_RE = re.compile(r'.*\.(java|kt)$', re.I)
 
 # Env constants
 ANDROID_BUILD_TOP = 'ANDROID_BUILD_TOP'
@@ -134,6 +138,7 @@
 EXTERNAL = 'EXTERNAL_RUN'
 INTERNAL = 'INTERNAL_RUN'
 INTERNAL_EMAIL = '@google.com'
+INTERNAL_HOSTNAME = '.google.com'
 CONTENT_LICENSES_URL = 'https://source.android.com/setup/start/licenses'
 CONTRIBUTOR_AGREEMENT_URL = {
     'INTERNAL': 'https://cla.developers.google.com/',
@@ -143,6 +148,15 @@
 TERMS_SERVICE_URL = 'https://policies.google.com/terms'
 TOOL_NAME = 'atest'
 
+# Detect type for local_detect_event.
+# Next expansion : DETECT_TYPE_XXX = 1
+DETECT_TYPE_BUG_DETECTED = 0
+# Considering a trade-off between speed and size, we set UPPER_LIMIT to 100000
+# to make maximum file space 10M(100000(records)*100(byte/record)) at most.
+# Therefore, to update history file will spend 1 sec at most in each run.
+UPPER_LIMIT = 100000
+TRIM_TO_SIZE = 50000
+
 # VTS plans
 VTS_STAGING_PLAN = 'vts-staging-default'
 
diff --git a/atest/metrics/metrics_base.py b/atest/metrics/metrics_base.py
index b716092..73027b4 100644
--- a/atest/metrics/metrics_base.py
+++ b/atest/metrics/metrics_base.py
@@ -15,12 +15,16 @@
 """
 Metrics base class.
 """
+
+from __future__ import print_function
+
 import logging
 import random
+import socket
+import subprocess
 import time
 import uuid
 
-import atest_utils
 import asuite_metrics
 import constants
 
@@ -43,6 +47,41 @@
     EXTERNAL_USER: 934
 }
 
+
+def get_user_type():
+    """Get user type.
+
+    Determine the internal user by passing at least one check:
+      - whose git mail domain is from google
+      - whose hostname is from google
+    Otherwise is external user.
+
+    Returns:
+        INTERNAL_USER if user is internal, EXTERNAL_USER otherwise.
+    """
+    try:
+        output = subprocess.check_output(['git', 'config', '--get', 'user.email'],
+                                         universal_newlines=True)
+        if output and output.strip().endswith(constants.INTERNAL_EMAIL):
+            return INTERNAL_USER
+    except OSError:
+        # OSError can be raised when running atest_unittests on a host
+        # without git being set up.
+        logging.debug('Unable to determine if this is an external run, git is '
+                      'not found.')
+    except subprocess.CalledProcessError:
+        logging.debug('Unable to determine if this is an external run, email '
+                      'is not found in git config.')
+    try:
+        hostname = socket.getfqdn()
+        if hostname and constants.INTERNAL_HOSTNAME in hostname:
+            return INTERNAL_USER
+    except IOError:
+        logging.debug('Unable to determine if this is an external run, '
+                      'hostname is not found.')
+    return EXTERNAL_USER
+
+
 class MetricsBase(object):
     """Class for separating allowed fields and sending metric."""
 
@@ -53,8 +92,7 @@
     #pylint: disable=broad-except
     except Exception:
         _user_key = asuite_metrics.DUMMY_UUID
-    _user_type = (EXTERNAL_USER if atest_utils.is_external_run()
-                  else INTERNAL_USER)
+    _user_type = get_user_type()
     _log_source = ATEST_LOG_SOURCE[_user_type]
     cc = clearcut_client.Clearcut(_log_source)
     tool_name = None
diff --git a/atest/test_data/sample_test_cmd_result.json b/atest/test_data/sample_test_cmd_result.json
new file mode 100644
index 0000000..9128dd1
--- /dev/null
+++ b/atest/test_data/sample_test_cmd_result.json
@@ -0,0 +1 @@
+{"hello_world_test": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter hello_world_test --log-level WARN"], "HelloWorldTests": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter HelloWorldTests --log-level WARN"], "packages/apps/Car/Messenger/tests/robotests/src/com/android/car/messenger/tts/TTSHelperTest.java": ["./build/soong/soong_ui.bash --make-mode RunCarMessengerRoboTests"], "CtsDeviceJankUi": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsJankDeviceTestCases --atest-include-filter CtsJankDeviceTestCases:android.jank.cts.ui.CtsDeviceJankUi --log-level WARN"], "CtsJankDeviceTestCases CtsSampleDeviceTestCases": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --include-filter CtsJankDeviceTestCases --log-level WARN"], "VtsCodelabHelloWorldTest": ["vts-tradefed run commandAndExit vts-staging-default -m VtsCodelabHelloWorldTest --skip-all-system-status-check --skip-preconditions --primary-abi-only"], "PacketFragmenterTest": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter net_test_hci --atest-include-filter net_test_hci:PacketFragmenterTest.* --log-level WARN"], "platform_testing/tests/example/native/Android.bp": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter hello_world_test --log-level WARN"], "android.jank.cts": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsJankDeviceTestCases --atest-include-filter CtsJankDeviceTestCases:android.jank.cts --log-level WARN"], "tools/tradefederation/core/res/config/native-benchmark.xml": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter native-benchmark --log-level WARN"], "native-benchmark": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter native-benchmark --log-level WARN"], "platform_testing/tests/example/native": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter hello_world_test --log-level WARN"], "PacketFragmenterTest#test_no_fragment_necessary,test_ble_fragment_necessary": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter net_test_hci --atest-include-filter net_test_hci:PacketFragmenterTest.test_ble_fragment_necessary:PacketFragmenterTest.test_no_fragment_necessary --log-level WARN"], "CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceReportLogTest": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --atest-include-filter CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceReportLogTest --log-level WARN"], "CtsSampleDeviceTestCases:SampleDeviceTest#testSharedPreferences": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --atest-include-filter CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceTest#testSharedPreferences --log-level WARN"], "CtsSampleDeviceTestCases:android.sample.cts": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --atest-include-filter CtsSampleDeviceTestCases:android.sample.cts --log-level WARN"], "CtsJankDeviceTestCases:CtsDeviceJankUi": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsJankDeviceTestCases --atest-include-filter CtsJankDeviceTestCases:android.jank.cts.ui.CtsDeviceJankUi --log-level WARN"], "CarMessengerRoboTests": ["./build/soong/soong_ui.bash --make-mode RunCarMessengerRoboTests"]}
\ No newline at end of file
diff --git a/atest/test_finder_handler.py b/atest/test_finder_handler.py
index 53ee79f..236f177 100644
--- a/atest/test_finder_handler.py
+++ b/atest/test_finder_handler.py
@@ -19,6 +19,7 @@
 import logging
 
 import atest_enum
+from test_finders import cache_finder
 from test_finders import test_finder_base
 from test_finders import suite_plan_finder
 from test_finders import tf_integration_finder
@@ -29,6 +30,7 @@
     suite_plan_finder.SuitePlanFinder,
     tf_integration_finder.TFIntegrationFinder,
     module_finder.ModuleFinder,
+    cache_finder.CacheFinder,
 }
 
 # Explanation of REFERENCE_TYPEs:
@@ -51,7 +53,7 @@
                                         'MODULE_PACKAGE', 'MODULE_FILE_PATH',
                                         'INTEGRATION_FILE_PATH', 'INTEGRATION',
                                         'SUITE', 'CC_CLASS', 'SUITE_PLAN',
-                                        'SUITE_PLAN_FILE_PATH'])
+                                        'SUITE_PLAN_FILE_PATH', 'CACHE'])
 
 _REF_TYPE_TO_FUNC_MAP = {
     _REFERENCE_TYPE.MODULE: module_finder.ModuleFinder.find_test_by_module_name,
@@ -70,6 +72,7 @@
     _REFERENCE_TYPE.SUITE_PLAN:suite_plan_finder.SuitePlanFinder.find_test_by_suite_name,
     _REFERENCE_TYPE.SUITE_PLAN_FILE_PATH:
         suite_plan_finder.SuitePlanFinder.find_test_by_suite_path,
+    _REFERENCE_TYPE.CACHE: cache_finder.CacheFinder.find_test_by_cache,
 }
 
 
@@ -124,15 +127,18 @@
         A list of possible REFERENCE_TYPEs (ints) for reference string.
     """
     if ref.startswith('.') or '..' in ref:
-        return [_REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+        return [_REFERENCE_TYPE.CACHE,
+                _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
                 _REFERENCE_TYPE.MODULE_FILE_PATH,
                 _REFERENCE_TYPE.SUITE_PLAN_FILE_PATH]
     if '/' in ref:
         if ref.startswith('/'):
-            return [_REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+            return [_REFERENCE_TYPE.CACHE,
+                    _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
                     _REFERENCE_TYPE.MODULE_FILE_PATH,
                     _REFERENCE_TYPE.SUITE_PLAN_FILE_PATH]
-        return [_REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+        return [_REFERENCE_TYPE.CACHE,
+                _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
                 _REFERENCE_TYPE.MODULE_FILE_PATH,
                 _REFERENCE_TYPE.INTEGRATION,
                 _REFERENCE_TYPE.SUITE_PLAN_FILE_PATH,
@@ -146,12 +152,14 @@
         if '.' in ref:
             if ref_end_is_upper:
                 # Module:fully.qualified.Class or Integration:fully.q.Class
-                return [_REFERENCE_TYPE.INTEGRATION,
+                return [_REFERENCE_TYPE.CACHE,
+                        _REFERENCE_TYPE.INTEGRATION,
                         _REFERENCE_TYPE.MODULE_CLASS]
             # Module:some.package
-            return [_REFERENCE_TYPE.MODULE_PACKAGE]
+            return [_REFERENCE_TYPE.CACHE, _REFERENCE_TYPE.MODULE_PACKAGE]
         # Module:Class or IntegrationName:Class
-        return [_REFERENCE_TYPE.INTEGRATION,
+        return [_REFERENCE_TYPE.CACHE,
+                _REFERENCE_TYPE.INTEGRATION,
                 _REFERENCE_TYPE.MODULE_CLASS]
     if '.' in ref:
         # The string of ref_end possibly includes specific mathods, e.g.
@@ -159,18 +167,21 @@
         if "#" in ref_end:
             ref_end = ref_end.split('#')[0]
         if ref_end in ('java', 'kt', 'bp', 'mk', 'cc', 'cpp'):
-            return [_REFERENCE_TYPE.MODULE_FILE_PATH]
+            return [_REFERENCE_TYPE.CACHE, _REFERENCE_TYPE.MODULE_FILE_PATH]
         if ref_end == 'xml':
-            return [_REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+            return [_REFERENCE_TYPE.CACHE,
+                    _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
                     _REFERENCE_TYPE.SUITE_PLAN_FILE_PATH]
         if ref_end_is_upper:
-            return [_REFERENCE_TYPE.QUALIFIED_CLASS]
-        return [_REFERENCE_TYPE.MODULE,
+            return [_REFERENCE_TYPE.CACHE, _REFERENCE_TYPE.QUALIFIED_CLASS]
+        return [_REFERENCE_TYPE.CACHE,
+                _REFERENCE_TYPE.MODULE,
                 _REFERENCE_TYPE.PACKAGE]
     # Note: We assume that if you're referencing a file in your cwd,
     # that file must have a '.' in its name, i.e. foo.java, foo.xml.
     # If this ever becomes not the case, then we need to include path below.
-    return [_REFERENCE_TYPE.INTEGRATION,
+    return [_REFERENCE_TYPE.CACHE,
+            _REFERENCE_TYPE.INTEGRATION,
             # TODO: Comment in SUITE when it's supported
             # _REFERENCE_TYPE.SUITE,
             _REFERENCE_TYPE.MODULE,
diff --git a/atest/test_finder_handler_unittest.py b/atest/test_finder_handler_unittest.py
index d834d05..8f5e822 100755
--- a/atest/test_finder_handler_unittest.py
+++ b/atest/test_finder_handler_unittest.py
@@ -93,119 +93,124 @@
         """Test _get_test_reference_types parses reference types correctly."""
         self.assertEqual(
             test_finder_handler._get_test_reference_types('ModuleOrClassName'),
-            [REF_TYPE.INTEGRATION, REF_TYPE.MODULE, REF_TYPE.SUITE_PLAN,
-             REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE,
+             REF_TYPE.SUITE_PLAN, REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('Module_or_Class_name'),
-            [REF_TYPE.INTEGRATION, REF_TYPE.MODULE, REF_TYPE.SUITE_PLAN,
-             REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE,
+             REF_TYPE.SUITE_PLAN, REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('SuiteName'),
-            [REF_TYPE.INTEGRATION, REF_TYPE.MODULE, REF_TYPE.SUITE_PLAN,
-             REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE,
+             REF_TYPE.SUITE_PLAN, REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('Suite-Name'),
-            [REF_TYPE.INTEGRATION, REF_TYPE.MODULE, REF_TYPE.SUITE_PLAN,
-             REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE,
+             REF_TYPE.SUITE_PLAN, REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('some.package'),
-            [REF_TYPE.MODULE, REF_TYPE.PACKAGE]
+            [REF_TYPE.CACHE, REF_TYPE.MODULE, REF_TYPE.PACKAGE]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('fully.q.Class'),
-            [REF_TYPE.QUALIFIED_CLASS]
+            [REF_TYPE.CACHE, REF_TYPE.QUALIFIED_CLASS]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('Integration.xml'),
-            [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+             REF_TYPE.SUITE_PLAN_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('SomeClass.java'),
-            [REF_TYPE.MODULE_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('SomeClass.kt'),
-            [REF_TYPE.MODULE_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('Android.mk'),
-            [REF_TYPE.MODULE_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('Android.bp'),
-            [REF_TYPE.MODULE_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('SomeTest.cc'),
-            [REF_TYPE.MODULE_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('SomeTest.cpp'),
-            [REF_TYPE.MODULE_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('SomeTest.cc#method'),
-            [REF_TYPE.MODULE_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('module:Class'),
-            [REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('module:f.q.Class'),
-            [REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('module:a.package'),
-            [REF_TYPE.MODULE_PACKAGE]
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_PACKAGE]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('.'),
-            [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
-             REF_TYPE.SUITE_PLAN_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+             REF_TYPE.MODULE_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('..'),
-            [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
-             REF_TYPE.SUITE_PLAN_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+             REF_TYPE.MODULE_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('./rel/path/to/test'),
-            [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
-             REF_TYPE.SUITE_PLAN_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+             REF_TYPE.MODULE_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('rel/path/to/test'),
-            [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
-             REF_TYPE.INTEGRATION, REF_TYPE.SUITE_PLAN_FILE_PATH]
-        )
-        self.assertEqual(
-            test_finder_handler._get_test_reference_types('/abs/path/to/test'),
-            [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+             REF_TYPE.MODULE_FILE_PATH, REF_TYPE.INTEGRATION,
              REF_TYPE.SUITE_PLAN_FILE_PATH]
         )
         self.assertEqual(
+            test_finder_handler._get_test_reference_types('/abs/path/to/test'),
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+             REF_TYPE.MODULE_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
+        )
+        self.assertEqual(
             test_finder_handler._get_test_reference_types('int/test'),
-            [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
-             REF_TYPE.INTEGRATION, REF_TYPE.SUITE_PLAN_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+             REF_TYPE.MODULE_FILE_PATH, REF_TYPE.INTEGRATION,
+             REF_TYPE.SUITE_PLAN_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('int/test:fully.qual.Class#m'),
-            [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
-             REF_TYPE.INTEGRATION, REF_TYPE.SUITE_PLAN_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+             REF_TYPE.MODULE_FILE_PATH, REF_TYPE.INTEGRATION,
+             REF_TYPE.SUITE_PLAN_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('int/test:Class#method'),
-            [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
-             REF_TYPE.INTEGRATION, REF_TYPE.SUITE_PLAN_FILE_PATH]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+             REF_TYPE.MODULE_FILE_PATH, REF_TYPE.INTEGRATION,
+             REF_TYPE.SUITE_PLAN_FILE_PATH]
         )
         self.assertEqual(
             test_finder_handler._get_test_reference_types('int_name_no_slash:Class#m'),
-            [REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
+            [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
         )
 
     def test_get_registered_find_methods(self):
diff --git a/atest/test_finders/cache_finder.py b/atest/test_finders/cache_finder.py
new file mode 100644
index 0000000..3937ef0
--- /dev/null
+++ b/atest/test_finders/cache_finder.py
@@ -0,0 +1,38 @@
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Cache Finder class.
+"""
+
+import atest_utils
+from test_finders import test_finder_base
+
+class CacheFinder(test_finder_base.TestFinderBase):
+    """Cache Finder class."""
+    NAME = 'CACHE'
+
+    def __init__(self, **kwargs):
+        super(CacheFinder, self).__init__()
+
+    def find_test_by_cache(self, test_reference):
+        """Find the matched test_infos in saved caches.
+
+        Args:
+            test_reference: A string of the path to the test's file or dir.
+
+        Returns:
+            A list of TestInfo namedtuple if cache found, else None.
+        """
+        return atest_utils.load_test_info_cache(test_reference)
diff --git a/atest/test_finders/cache_finder_unittest.py b/atest/test_finders/cache_finder_unittest.py
new file mode 100755
index 0000000..92de278
--- /dev/null
+++ b/atest/test_finders/cache_finder_unittest.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+#
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unittests for cache_finder."""
+
+import unittest
+import os
+import mock
+
+# pylint: disable=import-error
+import atest_utils
+import unittest_constants as uc
+from test_finders import cache_finder
+
+
+#pylint: disable=protected-access
+class CacheFinderUnittests(unittest.TestCase):
+    """Unit tests for cache_finder.py"""
+    def setUp(self):
+        """Set up stuff for testing."""
+        self.cache_finder = cache_finder.CacheFinder()
+
+    @mock.patch.object(atest_utils, 'get_test_info_cache_path')
+    def test_find_test_by_cache(self, mock_get_cache_path):
+        """Test find_test_by_cache method."""
+        cached_test = 'mytest1'
+        uncached_test = 'mytest2'
+        test_cache_root = os.path.join(uc.TEST_DATA_DIR, 'cache_root')
+        # Hit matched cache file, should return cached test infos.
+        mock_get_cache_path.return_value = os.path.join(
+            test_cache_root,
+            'cd66f9f5ad63b42d0d77a9334de6bb73.cache')
+        self.assertIsNotNone(self.cache_finder.find_test_by_cache(cached_test))
+        # Does not hit matched cache file, should return cached test infos.
+        mock_get_cache_path.return_value = os.path.join(
+            test_cache_root,
+            '39488b7ac83c56d5a7d285519fe3e3fd.cache')
+        self.assertIsNone(self.cache_finder.find_test_by_cache(uncached_test))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_finders/module_finder.py b/atest/test_finders/module_finder.py
index 6c3ecd6..91b10be 100644
--- a/atest/test_finders/module_finder.py
+++ b/atest/test_finders/module_finder.py
@@ -18,7 +18,6 @@
 
 import logging
 import os
-import re
 
 # pylint: disable=import-error
 import atest_error
@@ -31,9 +30,6 @@
 from test_runners import robolectric_test_runner
 from test_runners import vts_tf_test_runner
 
-_CC_EXT_RE = re.compile(r'.*(\.cc|\.cpp)$', re.I)
-_JAVA_EXT_RE = re.compile(r'.*(\.java|\.kt)$', re.I)
-
 _MODULES_IN = 'MODULES-IN-%s'
 _ANDROID_MK = 'Android.mk'
 
@@ -63,7 +59,7 @@
             path: String path of module to look for.
 
         Returns:
-            String of the module name.
+            A list of the module names.
         """
         testable_modules = []
         for mod in self.module_info.get_module_names(path):
@@ -72,7 +68,8 @@
             # the test and another to run it. For now, we are assuming they are
             # isolated in their own folders and will return if we find one.
             if self.module_info.is_robolectric_test(mod):
-                return mod
+                # return a list with one module name if it is robolectric.
+                return [mod]
             if self.module_info.is_testable_module(mod_info):
                 testable_modules.append(mod_info.get(constants.MODULE_NAME))
         return test_finder_utils.extract_test_from_tests(testable_modules)
@@ -111,12 +108,13 @@
             out_dir = os.path.relpath(out_dir, self.root_dir)
         vts_out_dir = os.path.join(out_dir, 'vts', 'android-vts', 'testcases')
         # Parse dependency of default staging plans.
-
-        xml_path = test_finder_utils.search_integration_dirs(
+        xml_paths = test_finder_utils.search_integration_dirs(
             constants.VTS_STAGING_PLAN,
             self.module_info.get_paths(constants.VTS_TF_MODULE))
-        vts_xmls = test_finder_utils.get_plans_from_vts_xml(xml_path)
+        vts_xmls = set()
         vts_xmls.add(config_file)
+        for xml_path in xml_paths:
+            vts_xmls |= test_finder_utils.get_plans_from_vts_xml(xml_path)
         for config_file in vts_xmls:
             # Add in vts test build targets.
             test.build_targets |= test_finder_utils.get_targets_from_vts_xml(
@@ -234,13 +232,13 @@
                 test_finder_utils.get_cc_filter(
                     kwargs.get('class_name', '*'), methods), frozenset())])
         # Path to java file.
-        elif file_name and _JAVA_EXT_RE.match(file_name):
+        elif file_name and constants.JAVA_EXT_RE.match(file_name):
             full_class_name = test_finder_utils.get_fully_qualified_class_name(
                 path)
             ti_filter = frozenset(
                 [test_info.TestFilter(full_class_name, methods)])
         # Path to cc file.
-        elif file_name and _CC_EXT_RE.match(file_name):
+        elif file_name and constants.CC_EXT_RE.match(file_name):
             if not test_finder_utils.has_cc_class(path):
                 raise atest_error.MissingCCTestCaseError(
                     "Can't find CC class in %s" % path)
@@ -254,7 +252,7 @@
               os.path.relpath(path, self.root_dir)):
             dir_items = [os.path.join(path, f) for f in os.listdir(path)]
             for dir_item in dir_items:
-                if _JAVA_EXT_RE.match(dir_item):
+                if constants.JAVA_EXT_RE.match(dir_item):
                     package_name = test_finder_utils.get_package_name(dir_item)
                     if package_name:
                         # methods should be empty frozenset for package.
@@ -283,7 +281,7 @@
             return os.path.join(rel_module_dir, constants.MODULE_CONFIG)
         return None
 
-    def _get_test_info(self, test_path, rel_config, module_name, test_filter):
+    def _get_test_infos(self, test_path, rel_config, module_name, test_filter):
         """Get test_info for test_path.
 
         Args:
@@ -293,24 +291,32 @@
             test_filter: A test info filter.
 
         Returns:
-            TestInfo namedtuple if found, else None.
+            A list of TestInfo namedtuple if found, else None.
         """
         if not rel_config:
             rel_config = self._get_rel_config(test_path)
             if not rel_config:
                 return None
-        if not module_name:
-            module_name = self._determine_testable_module(
+        if module_name:
+            module_names = [module_name]
+        else:
+            module_names = self._determine_testable_module(
                 os.path.dirname(rel_config))
-        # The real test config might be recorded in module-info.
-        rel_config = self._get_module_test_config(module_name,
-                                                  rel_config=rel_config)
-        return self._process_test_info(test_info.TestInfo(
-            test_name=module_name,
-            test_runner=self._TEST_RUNNER,
-            build_targets=set(),
-            data={constants.TI_FILTER: test_filter,
-                  constants.TI_REL_CONFIG: rel_config}))
+        test_infos = []
+        if module_names:
+            for mname in module_names:
+                # The real test config might be record in module-info.
+                rel_config = self._get_module_test_config(mname,
+                                                          rel_config=rel_config)
+                tinfo = self._process_test_info(test_info.TestInfo(
+                    test_name=mname,
+                    test_runner=self._TEST_RUNNER,
+                    build_targets=set(),
+                    data={constants.TI_FILTER: test_filter,
+                          constants.TI_REL_CONFIG: rel_config}))
+                if tinfo:
+                    test_infos.append(tinfo)
+        return test_infos
 
     def find_test_by_module_name(self, module_name):
         """Find test for the given module name.
@@ -319,7 +325,8 @@
             module_name: A string of the test's module name.
 
         Returns:
-            A populated TestInfo namedtuple if found, else None.
+            A list that includes only 1 populated TestInfo namedtuple
+            if found, otherwise None.
         """
         mod_info = self.module_info.get_module_info(module_name)
         if self.module_info.is_testable_module(mod_info):
@@ -327,12 +334,14 @@
             rel_config = os.path.join(mod_info['path'][0],
                                       constants.MODULE_CONFIG)
             rel_config = self._get_module_test_config(module_name, rel_config=rel_config)
-            return self._process_test_info(test_info.TestInfo(
+            tinfo = self._process_test_info(test_info.TestInfo(
                 test_name=module_name,
                 test_runner=self._TEST_RUNNER,
                 build_targets=set(),
                 data={constants.TI_REL_CONFIG: rel_config,
                       constants.TI_FILTER: frozenset()}))
+            if tinfo:
+                return [tinfo]
         return None
 
     def find_test_by_class_name(self, class_name, module_name=None,
@@ -350,7 +359,7 @@
             native test or not.
 
         Returns:
-            A populated TestInfo namedtuple if test found, else None.
+            A list of populated TestInfo namedtuple if test found, else None.
         """
         class_name, methods = test_finder_utils.split_methods(class_name)
         if rel_config:
@@ -358,22 +367,27 @@
                                       os.path.dirname(rel_config))
         else:
             search_dir = self.root_dir
-        test_path = test_finder_utils.find_class_file(search_dir, class_name,
-                                                      is_native_test)
-        if not test_path and rel_config:
+        test_paths = test_finder_utils.find_class_file(search_dir, class_name,
+                                                       is_native_test, methods)
+        if not test_paths and rel_config:
             logging.info('Did not find class (%s) under module path (%s), '
                          'researching from repo root.', class_name, rel_config)
-            test_path = test_finder_utils.find_class_file(self.root_dir,
-                                                          class_name,
-                                                          is_native_test)
-        if not test_path:
+            test_paths = test_finder_utils.find_class_file(self.root_dir,
+                                                           class_name,
+                                                           is_native_test,
+                                                           methods)
+        if not test_paths:
             return None
-        test_filter = self._get_test_info_filter(
-            test_path, methods, class_name=class_name,
-            is_native_test=is_native_test)
-        tinfo = self._get_test_info(test_path, rel_config, module_name,
-                                    test_filter)
-        return tinfo
+        tinfos = []
+        for test_path in test_paths:
+            test_filter = self._get_test_info_filter(
+                test_path, methods, class_name=class_name,
+                is_native_test=is_native_test)
+            tinfo = self._get_test_infos(test_path, rel_config,
+                                         module_name, test_filter)
+            if tinfo:
+                tinfos.extend(tinfo)
+        return tinfos
 
     def find_test_by_module_and_class(self, module_class):
         """Find the test info given a MODULE:CLASS string.
@@ -382,12 +396,14 @@
             module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD.
 
         Returns:
-            A populated TestInfo namedtuple if found, else None.
+            A list of populated TestInfo namedtuple if found, else None.
         """
         if ':' not in module_class:
             return None
         module_name, class_name = module_class.split(':')
-        module_info = self.find_test_by_module_name(module_name)
+        # module_infos is a list with at most 1 element.
+        module_infos = self.find_test_by_module_name(module_name)
+        module_info = module_infos[0] if module_infos else None
         if not module_info:
             return None
         # If the target module is NATIVE_TEST, search CC classes only.
@@ -414,7 +430,7 @@
             ref_config: Optional. A string of rel path of config.
 
         Returns:
-            A populated TestInfo namedtuple if found, else None.
+            A list of populated TestInfo namedtuple if found, else None.
         """
         _, methods = test_finder_utils.split_methods(package)
         if methods:
@@ -427,16 +443,20 @@
                                       os.path.dirname(rel_config))
         else:
             search_dir = self.root_dir
-        package_path = test_finder_utils.run_find_cmd(
+        package_paths = test_finder_utils.run_find_cmd(
             test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir,
             package.replace('.', '/'))
         # Package path will be the full path to the dir represented by package.
-        if not package_path:
+        if not package_paths:
             return None
         test_filter = frozenset([test_info.TestFilter(package, frozenset())])
-        tinfo = self._get_test_info(package_path, rel_config, module_name,
-                                    test_filter)
-        return tinfo
+        test_infos = []
+        for package_path in package_paths:
+            tinfo = self._get_test_infos(package_path, rel_config,
+                                         module_name, test_filter)
+            if tinfo:
+                test_infos.extend(tinfo)
+        return test_infos
 
     def find_test_by_module_and_package(self, module_package):
         """Find the test info given a MODULE:PACKAGE string.
@@ -445,10 +465,12 @@
             module_package: A string of form MODULE:PACKAGE
 
         Returns:
-            A populated TestInfo namedtuple if found, else None.
+            A list of populated TestInfo namedtuple if found, else None.
         """
         module_name, package = module_package.split(':')
-        module_info = self.find_test_by_module_name(module_name)
+        # module_infos is a list with at most 1 element.
+        module_infos = self.find_test_by_module_name(module_name)
+        module_info = module_infos[0] if module_infos else None
         if not module_info:
             return None
         return self.find_test_by_package_name(
@@ -470,7 +492,7 @@
             path: A string of the test's path.
 
         Returns:
-            A populated TestInfo namedtuple if test found, else None
+            A list of populated TestInfo namedtuple if test found, else None
         """
         logging.debug('Finding test by path: %s', path)
         path, methods = test_finder_utils.split_methods(path)
@@ -479,6 +501,9 @@
         path = os.path.realpath(path)
         if not os.path.exists(path):
             return None
+        if (methods and
+                not test_finder_utils.has_method_in_file(path, methods)):
+            return None
         dir_path, _ = test_finder_utils.get_dir_path_and_filename(path)
         # Module/Class
         rel_module_dir = test_finder_utils.find_parent_module_dir(
@@ -488,7 +513,7 @@
         rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
         test_filter = self._get_test_info_filter(path, methods,
                                                  rel_module_dir=rel_module_dir)
-        return self._get_test_info(path, rel_config, None, test_filter)
+        return self._get_test_infos(path, rel_config, None, test_filter)
 
     def find_test_by_cc_class_name(self, class_name, module_name=None,
                                    rel_config=None):
@@ -503,7 +528,7 @@
             rel_config: Optional. A string of module dir relative to repo root.
 
         Returns:
-            A populated TestInfo namedtuple if test found, else None.
+            A list of populated TestInfo namedtuple if test found, else None.
         """
         # Check if class_name is prepended with file name. If so, trim the
         # prefix and keep only the class_name.
diff --git a/atest/test_finders/module_finder_unittest.py b/atest/test_finders/module_finder_unittest.py
index 2d6124f..14041b0 100755
--- a/atest/test_finders/module_finder_unittest.py
+++ b/atest/test_finders/module_finder_unittest.py
@@ -110,14 +110,17 @@
                     'path': [uc.MODULE_DIR],
                     constants.MODULE_CLASS: []}
         self.mod_finder.module_info.get_module_info.return_value = mod_info
+        t_infos = self.mod_finder.find_test_by_module_name(uc.MODULE_NAME)
         unittest_utils.assert_equal_testinfos(
             self,
-            self.mod_finder.find_test_by_module_name(uc.MODULE_NAME),
+            t_infos[0],
             uc.MODULE_INFO)
         self.mod_finder.module_info.get_module_info.return_value = None
         self.mod_finder.module_info.is_testable_module.return_value = False
         self.assertIsNone(self.mod_finder.find_test_by_module_name('Not_Module'))
 
+    @mock.patch.object(test_finder_utils, 'has_method_in_file',
+                       return_value=True)
     @mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
                        return_value=False)
     @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
@@ -129,7 +132,7 @@
     #pylint: disable=unused-argument
     def test_find_test_by_class_name(self, _isdir, _isfile, _fqcn,
                                      mock_checkoutput, mock_build,
-                                     _vts):
+                                     _vts, _has_method_in_file):
         """Test find_test_by_class_name."""
         mock_build.return_value = uc.CLASS_BUILD_TARGETS
         self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
@@ -140,37 +143,42 @@
             constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
             constants.MODULE_NAME: uc.MODULE_NAME,
             constants.MODULE_CLASS: []}
+        t_infos = self.mod_finder.find_test_by_class_name(uc.CLASS_NAME)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_class_name(uc.CLASS_NAME), uc.CLASS_INFO)
+            self, t_infos[0], uc.CLASS_INFO)
 
         # with method
         mock_build.return_value = uc.MODULE_BUILD_TARGETS
         class_with_method = '%s#%s' % (uc.CLASS_NAME, uc.METHOD_NAME)
+        t_infos = self.mod_finder.find_test_by_class_name(class_with_method)
         unittest_utils.assert_equal_testinfos(
-            self,
-            self.mod_finder.find_test_by_class_name(class_with_method),
-            uc.METHOD_INFO)
+            self, t_infos[0], uc.METHOD_INFO)
         mock_build.return_value = uc.MODULE_BUILD_TARGETS
         class_methods = '%s,%s' % (class_with_method, uc.METHOD2_NAME)
+        t_infos = self.mod_finder.find_test_by_class_name(class_methods)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_class_name(class_methods),
+            self, t_infos[0],
             FLAT_METHOD_INFO)
         # module and rel_config passed in
         mock_build.return_value = uc.CLASS_BUILD_TARGETS
+        t_infos = self.mod_finder.find_test_by_class_name(
+            uc.CLASS_NAME, uc.MODULE_NAME, uc.CONFIG_FILE)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_class_name(
-                uc.CLASS_NAME, uc.MODULE_NAME, uc.CONFIG_FILE), uc.CLASS_INFO)
+            self, t_infos[0], uc.CLASS_INFO)
         # find output fails to find class file
         mock_checkoutput.return_value = ''
         self.assertIsNone(self.mod_finder.find_test_by_class_name('Not class'))
         # class is outside given module path
         mock_checkoutput.side_effect = classoutside_side_effect
-        unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_class_name(uc.CLASS_NAME,
+        t_infos = self.mod_finder.find_test_by_class_name(uc.CLASS_NAME,
                                                           uc.MODULE2_NAME,
-                                                          uc.CONFIG2_FILE),
+                                                          uc.CONFIG2_FILE)
+        unittest_utils.assert_equal_testinfos(
+            self, t_infos[0],
             CLASS_INFO_MODULE_2)
 
+    @mock.patch.object(test_finder_utils, 'has_method_in_file',
+                       return_value=True)
     @mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
                        return_value=False)
     @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
@@ -181,7 +189,7 @@
     #pylint: disable=unused-argument
     def test_find_test_by_module_and_class(self, _isfile, _fqcn,
                                            mock_checkoutput, mock_build,
-                                           _vts):
+                                           _vts, _has_method_in_file):
         """Test find_test_by_module_and_class."""
         # Native test was tested in test_find_test_by_cc_class_name().
         self.mod_finder.module_info.is_native_test.return_value = False
@@ -193,12 +201,12 @@
                     constants.MODULE_PATH: [uc.MODULE_DIR],
                     constants.MODULE_CLASS: []}
         self.mod_finder.module_info.get_module_info.return_value = mod_info
-        t_info = self.mod_finder.find_test_by_module_and_class(MODULE_CLASS)
-        unittest_utils.assert_equal_testinfos(self, t_info, uc.CLASS_INFO)
+        t_infos = self.mod_finder.find_test_by_module_and_class(MODULE_CLASS)
+        unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.CLASS_INFO)
         # with method
         mock_build.return_value = uc.MODULE_BUILD_TARGETS
-        t_info = self.mod_finder.find_test_by_module_and_class(MODULE_CLASS_METHOD)
-        unittest_utils.assert_equal_testinfos(self, t_info, uc.METHOD_INFO)
+        t_infos = self.mod_finder.find_test_by_module_and_class(MODULE_CLASS_METHOD)
+        unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.METHOD_INFO)
         self.mod_finder.module_info.is_testable_module.return_value = False
         # bad module, good class, returns None
         bad_module = '%s:%s' % ('BadMod', uc.CLASS_NAME)
@@ -232,13 +240,13 @@
                     constants.MODULE_PATH: [uc.CC_MODULE_DIR],
                     constants.MODULE_CLASS: []}
         self.mod_finder.module_info.get_module_info.return_value = mod_info
-        t_info = self.mod_finder.find_test_by_module_and_class(CC_MODULE_CLASS)
-        unittest_utils.assert_equal_testinfos(self, t_info, uc.CC_MODULE_CLASS_INFO)
+        t_infos = self.mod_finder.find_test_by_module_and_class(CC_MODULE_CLASS)
+        unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.CC_MODULE_CLASS_INFO)
         # with method
         mock_build.return_value = uc.MODULE_BUILD_TARGETS
         mock_fcf.side_effect = [None, None, '/']
-        t_info = self.mod_finder.find_test_by_module_and_class(CC_MODULE_CLASS_METHOD)
-        unittest_utils.assert_equal_testinfos(self, t_info, uc.CC_METHOD_INFO)
+        t_infos = self.mod_finder.find_test_by_module_and_class(CC_MODULE_CLASS_METHOD)
+        unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.CC_METHOD_INFO)
         # bad module, good class, returns None
         bad_module = '%s:%s' % ('BadMod', uc.CC_CLASS_NAME)
         self.mod_finder.module_info.get_module_info.return_value = None
@@ -264,8 +272,9 @@
             constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
             constants.MODULE_NAME: uc.MODULE_NAME,
             constants.MODULE_CLASS: []}
+        t_infos = self.mod_finder.find_test_by_package_name(uc.PACKAGE)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_package_name(uc.PACKAGE),
+            self, t_infos[0],
             uc.PACKAGE_INFO)
         # with method, should raise
         pkg_with_method = '%s#%s' % (uc.PACKAGE, uc.METHOD_NAME)
@@ -273,9 +282,10 @@
                           self.mod_finder.find_test_by_package_name,
                           pkg_with_method)
         # module and rel_config passed in
+        t_infos = self.mod_finder.find_test_by_package_name(
+            uc.PACKAGE, uc.MODULE_NAME, uc.CONFIG_FILE)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_package_name(
-                uc.PACKAGE, uc.MODULE_NAME, uc.CONFIG_FILE), uc.PACKAGE_INFO)
+            self, t_infos[0], uc.PACKAGE_INFO)
         # find output fails to find class file
         mock_checkoutput.return_value = ''
         self.assertIsNone(self.mod_finder.find_test_by_package_name('Not pkg'))
@@ -297,8 +307,8 @@
                     constants.MODULE_PATH: [uc.MODULE_DIR],
                     constants.MODULE_CLASS: []}
         self.mod_finder.module_info.get_module_info.return_value = mod_info
-        t_info = self.mod_finder.find_test_by_module_and_package(MODULE_PACKAGE)
-        unittest_utils.assert_equal_testinfos(self, t_info, uc.PACKAGE_INFO)
+        t_infos = self.mod_finder.find_test_by_module_and_package(MODULE_PACKAGE)
+        unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.PACKAGE_INFO)
         # with method, raises
         module_pkg_with_method = '%s:%s#%s' % (uc.MODULE2_NAME, uc.PACKAGE,
                                                uc.METHOD_NAME)
@@ -316,6 +326,8 @@
         self.mod_finder.module_info.get_module_info.return_value = mod_info
         self.assertIsNone(self.mod_finder.find_test_by_module_and_package(bad_pkg))
 
+    @mock.patch.object(test_finder_utils, 'has_method_in_file',
+                       return_value=True)
     @mock.patch.object(test_finder_utils, 'has_cc_class',
                        return_value=True)
     @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
@@ -330,7 +342,8 @@
     @mock.patch('os.path.exists')
     #pylint: disable=unused-argument
     def test_find_test_by_path(self, mock_pathexists, mock_dir, _isfile, _real,
-                               _fqcn, _vts, mock_build, _has_cc_class):
+                               _fqcn, _vts, mock_build, _has_cc_class,
+                               _has_method_in_file):
         """Test find_test_by_path."""
         self.mod_finder.module_info.is_robolectric_test.return_value = False
         self.mod_finder.module_info.has_test_config.return_value = True
@@ -354,23 +367,27 @@
 
         class_path = '%s.kt' % uc.CLASS_NAME
         mock_build.return_value = uc.CLASS_BUILD_TARGETS
+        t_infos = self.mod_finder.find_test_by_path(class_path)
         unittest_utils.assert_equal_testinfos(
-            self, uc.CLASS_INFO, self.mod_finder.find_test_by_path(class_path))
+            self, uc.CLASS_INFO, t_infos[0])
 
         class_path = '%s.java' % uc.CLASS_NAME
         mock_build.return_value = uc.CLASS_BUILD_TARGETS
+        t_infos = self.mod_finder.find_test_by_path(class_path)
         unittest_utils.assert_equal_testinfos(
-            self, uc.CLASS_INFO, self.mod_finder.find_test_by_path(class_path))
+            self, uc.CLASS_INFO, t_infos[0])
 
         class_with_method = '%s#%s' % (class_path, uc.METHOD_NAME)
         mock_build.return_value = uc.MODULE_BUILD_TARGETS
+        t_infos = self.mod_finder.find_test_by_path(class_with_method)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_path(class_with_method), uc.METHOD_INFO)
+            self, t_infos[0], uc.METHOD_INFO)
 
         class_with_methods = '%s,%s' % (class_with_method, uc.METHOD2_NAME)
         mock_build.return_value = uc.MODULE_BUILD_TARGETS
+        t_infos = self.mod_finder.find_test_by_path(class_with_methods)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_path(class_with_methods),
+            self, t_infos[0],
             FLAT_METHOD_INFO)
 
         # Cc path testing.
@@ -382,8 +399,9 @@
         mock_dir.return_value = uc.CC_MODULE_DIR
         class_path = '%s' % uc.CC_PATH
         mock_build.return_value = uc.CLASS_BUILD_TARGETS
+        t_infos = self.mod_finder.find_test_by_path(class_path)
         unittest_utils.assert_equal_testinfos(
-            self, uc.CC_PATH_INFO2, self.mod_finder.find_test_by_path(class_path))
+            self, uc.CC_PATH_INFO2, t_infos[0])
 
     @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets',
                        return_value=uc.MODULE_BUILD_TARGETS)
@@ -404,13 +422,15 @@
             constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
             constants.MODULE_NAME: uc.MODULE_NAME,
             constants.MODULE_CLASS: []}
+        t_infos = self.mod_finder.find_test_by_path(class_dir)
         unittest_utils.assert_equal_testinfos(
-            self, uc.PATH_INFO, self.mod_finder.find_test_by_path(class_dir))
+            self, uc.PATH_INFO, t_infos[0])
         # Dir with no java files in it, should run whole module
         empty_dir = os.path.join(uc.TEST_DATA_DIR, 'path_testing_empty')
+        t_infos = self.mod_finder.find_test_by_path(empty_dir)
         unittest_utils.assert_equal_testinfos(
             self, uc.EMPTY_PATH_INFO,
-            self.mod_finder.find_test_by_path(empty_dir))
+            t_infos[0])
         # Dir with cc files in it, should run as cc class
         class_dir = os.path.join(uc.TEST_DATA_DIR, 'cc_path_testing')
         self.mod_finder.module_info.get_module_names.return_value = [uc.CC_MODULE_NAME]
@@ -418,9 +438,12 @@
             constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
             constants.MODULE_NAME: uc.CC_MODULE_NAME,
             constants.MODULE_CLASS: []}
+        t_infos = self.mod_finder.find_test_by_path(class_dir)
         unittest_utils.assert_equal_testinfos(
-            self, uc.CC_PATH_INFO, self.mod_finder.find_test_by_path(class_dir))
+            self, uc.CC_PATH_INFO, t_infos[0])
 
+    @mock.patch.object(test_finder_utils, 'has_method_in_file',
+                       return_value=True)
     @mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
                        return_value=False)
     @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
@@ -430,7 +453,7 @@
     #pylint: disable=unused-argument
     def test_find_test_by_cc_class_name(self, _isdir, _isfile,
                                         mock_checkoutput, mock_build,
-                                        _vts):
+                                        _vts, _has_method):
         """Test find_test_by_cc_class_name."""
         mock_build.return_value = uc.CLASS_BUILD_TARGETS
         self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
@@ -441,36 +464,42 @@
             constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
             constants.MODULE_NAME: uc.CC_MODULE_NAME,
             constants.MODULE_CLASS: []}
+        t_infos = self.mod_finder.find_test_by_cc_class_name(uc.CC_CLASS_NAME)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_cc_class_name(uc.CC_CLASS_NAME), uc.CC_CLASS_INFO)
+            self, t_infos[0], uc.CC_CLASS_INFO)
 
         # with method
         mock_build.return_value = uc.MODULE_BUILD_TARGETS
         class_with_method = '%s#%s' % (uc.CC_CLASS_NAME, uc.CC_METHOD_NAME)
+        t_infos = self.mod_finder.find_test_by_cc_class_name(class_with_method)
         unittest_utils.assert_equal_testinfos(
             self,
-            self.mod_finder.find_test_by_cc_class_name(class_with_method),
+            t_infos[0],
             uc.CC_METHOD_INFO)
         mock_build.return_value = uc.MODULE_BUILD_TARGETS
         class_methods = '%s,%s' % (class_with_method, uc.CC_METHOD2_NAME)
+        t_infos = self.mod_finder.find_test_by_cc_class_name(class_methods)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_cc_class_name(class_methods),
+            self, t_infos[0],
             uc.CC_METHOD2_INFO)
         # module and rel_config passed in
         mock_build.return_value = uc.CLASS_BUILD_TARGETS
+        t_infos = self.mod_finder.find_test_by_cc_class_name(
+            uc.CC_CLASS_NAME, uc.CC_MODULE_NAME, uc.CC_CONFIG_FILE)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_cc_class_name(
-                uc.CC_CLASS_NAME, uc.CC_MODULE_NAME, uc.CC_CONFIG_FILE), uc.CC_CLASS_INFO)
+            self, t_infos[0], uc.CC_CLASS_INFO)
         # find output fails to find class file
         mock_checkoutput.return_value = ''
         self.assertIsNone(self.mod_finder.find_test_by_cc_class_name(
             'Not class'))
         # class is outside given module path
         mock_checkoutput.return_value = uc.CC_FIND_ONE
+        t_infos = self.mod_finder.find_test_by_cc_class_name(
+            uc.CC_CLASS_NAME,
+            uc.CC_MODULE2_NAME,
+            uc.CC_CONFIG2_FILE)
         unittest_utils.assert_equal_testinfos(
-            self, self.mod_finder.find_test_by_cc_class_name(uc.CC_CLASS_NAME,
-                                                             uc.CC_MODULE2_NAME,
-                                                             uc.CC_CONFIG2_FILE),
+            self, t_infos[0],
             CC_CLASS_INFO_MODULE_2)
 
     def test_get_testable_modules_with_ld(self):
diff --git a/atest/test_finders/suite_plan_finder.py b/atest/test_finders/suite_plan_finder.py
index 1fdccd2..a33da2d 100644
--- a/atest/test_finders/suite_plan_finder.py
+++ b/atest/test_finders/suite_plan_finder.py
@@ -105,7 +105,8 @@
             suite_path: A string of the path to the test's file or dir.
 
         Returns:
-            A populated TestInfo namedtuple if test found, else None.
+            A list of populated TestInfo namedtuple if test found, else None.
+            This is a list with at most 1 element.
         """
         path, _ = test_finder_utils.split_methods(suite_path)
         # Make sure we're looking for a config.
@@ -116,7 +117,7 @@
             path, self.suite_plan_dirs)
         if suite_plan_dir:
             rel_config = os.path.relpath(path, self.root_dir)
-            return self._get_test_info_from_path(rel_config)
+            return [self._get_test_info_from_path(rel_config)]
         return None
 
     def find_test_by_suite_name(self, suite_name):
@@ -133,19 +134,25 @@
             suite_name: A string of suite name.
 
         Returns:
-            A populated TestInfo namedtuple if suite_name matches
+            A list of populated TestInfo namedtuple if suite_name matches
             a suite in constants.SUITE_PLAN, else check if the file
             existing in the suite plan dirs, else return None.
         """
         logging.debug('Finding test by suite: %s', suite_name)
+        test_infos = []
         if suite_name in constants.SUITE_PLANS:
-            return test_info.TestInfo(
+            test_infos.append(test_info.TestInfo(
                 test_name=suite_name,
                 test_runner=self._SUITE_PLAN_TEST_RUNNER,
                 build_targets=set([suite_name]),
-                suite=suite_name)
-        test_file = test_finder_utils.search_integration_dirs(
-            suite_name, self.suite_plan_dirs)
-        if test_file is None:
-            return None
-        return self._get_test_info_from_path(test_file, suite_name)
+                suite=suite_name))
+        else:
+            test_files = test_finder_utils.search_integration_dirs(
+                suite_name, self.suite_plan_dirs)
+            if not test_files:
+                return None
+            for test_file in test_files:
+                _test_info = self._get_test_info_from_path(test_file, suite_name)
+                if _test_info:
+                    test_infos.append(_test_info)
+        return test_infos
diff --git a/atest/test_finders/suite_plan_finder_unittest.py b/atest/test_finders/suite_plan_finder_unittest.py
index 4e9eaa5..0fed2d2 100755
--- a/atest/test_finders/suite_plan_finder_unittest.py
+++ b/atest/test_finders/suite_plan_finder_unittest.py
@@ -103,7 +103,7 @@
                                        test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
                                        build_targets={suite_name},
                                        suite=suite_name)
-        unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+        unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
 
         suite_name = 'CTS'
         _search.return_value = None
@@ -113,13 +113,13 @@
 
         suite_name = 'cts-common'
         suite = 'cts'
-        _search.return_value = os.path.join(uc.ROOT, uc.CTS_INT_DIR, suite_name + '.xml')
+        _search.return_value = [os.path.join(uc.ROOT, uc.CTS_INT_DIR, suite_name + '.xml')]
         t_info = self.suite_plan_finder.find_test_by_suite_name(suite_name)
         want_info = test_info.TestInfo(test_name=suite_name,
                                        test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
                                        build_targets=set([suite]),
                                        suite=suite)
-        unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+        unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
 
     @mock.patch('os.path.realpath',
                 side_effect=unittest_utils.realpath_side_effect)
@@ -155,7 +155,7 @@
                                        test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
                                        build_targets=set([suite]),
                                        suite=suite)
-        unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+        unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
 
         suite_int_name = 'cts-common'
         suite = 'cts'
@@ -166,7 +166,7 @@
                                        test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
                                        build_targets=set([suite]),
                                        suite=suite)
-        unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+        unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
 
         suite_int_name = 'cts-camera'
         suite = 'cts'
@@ -177,7 +177,7 @@
                                        test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
                                        build_targets=set([suite]),
                                        suite=suite)
-        unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+        unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
 
 
 if __name__ == '__main__':
diff --git a/atest/test_finders/test_finder_utils.py b/atest/test_finders/test_finder_utils.py
index 0fc84b9..63d9014 100644
--- a/atest/test_finders/test_finder_utils.py
+++ b/atest/test_finders/test_finder_utils.py
@@ -35,8 +35,17 @@
 # We want to make sure we don't grab apks with paths in their name since we
 # assume the apk name is the build target.
 _APK_RE = re.compile(r'^[^/]+\.apk$', re.I)
-# RE for check if TEST or TEST_F is in a cc file or not.
-_CC_CLASS_RE = re.compile(r'TEST(_F)?\(', re.I)
+# RE for checking if TEST or TEST_F is in a cc file or not.
+_CC_CLASS_RE = re.compile(r'TEST(_F)?[ ]*\(', re.I)
+# RE for checking if there exists one of the methods in java file.
+_JAVA_METHODS_PATTERN = r'.*[ ]+({0})\(.*'
+# RE for checking if there exists one of the methods in cc file.
+_CC_METHODS_PATTERN = r'[ ]*TEST(_F|_P)?[ ]*\(.*,[ ]*({0})\).*'
+# RE for checking if finding output matches the cc format.
+# e.g. file_path:TEST_F(test_name, method_name){
+_CC_OUTPUT_RE = re.compile(r'(?P<file_path>/.*):[ ]*TEST(_F|_P)?[ ]*\('
+                           r'(?P<test_name>\w+)[ ]*,[ ]*(?P<method_name>\w+)\)'
+                           r'[ ]*\{')
 # Parse package name from the package declaration line of a java or a kotlin file.
 # Group matches "foo.bar" of line "package foo.bar;" or "package foo.bar"
 _PACKAGE_RE = re.compile(r'\s*package\s+(?P<package>[^(;|\s)]+)\s*', re.I)
@@ -46,10 +55,10 @@
 
 # Explanation of FIND_REFERENCE_TYPEs:
 # ----------------------------------
-# 0. CLASS: Name of a java/kotlin class, usually file is named the same (HostTest lives
-#           in HostTest.java or HostTest.kt)
+# 0. CLASS: Name of a java/kotlin class, usually file is named the same
+#    (HostTest lives in HostTest.java or HostTest.kt)
 # 1. QUALIFIED_CLASS: Like CLASS but also contains the package in front like
-#.                    com.android.tradefed.testtype.HostTest.
+#                     com.android.tradefed.testtype.HostTest.
 # 2. PACKAGE: Name of a java package.
 # 3. INTEGRATION: XML file name in one of the 4 integration config directories.
 # 4. CC_CLASS: Name of a cc class.
@@ -57,27 +66,27 @@
 FIND_REFERENCE_TYPE = atest_enum.AtestEnum(['CLASS', 'QUALIFIED_CLASS',
                                             'PACKAGE', 'INTEGRATION', 'CC_CLASS'])
 # Get cpu count.
-_CPU_COUNT = 1
-try:
-    _CPU_COUNT = multiprocessing.cpu_count()
-except NotImplementedError:
-    pass
+_CPU_COUNT = 0 if os.uname()[0] == 'Linux' else multiprocessing.cpu_count()
+
 # Unix find commands for searching for test files based on test type input.
 # Note: Find (unlike grep) exits with status 0 if nothing found.
 FIND_CMDS = {
-    FIND_REFERENCE_TYPE.CLASS: r"find {0} -type d {1} -prune -o -type f "
-                               r"\( -name '*{2}.java' -o -name '*{2}.kt' \) -print",
-    FIND_REFERENCE_TYPE.QUALIFIED_CLASS: r"find {0} -type d {1} -prune -o "
-                                         r"\( -wholename '*{2}.java' "
-                                         r"-o -wholename '*{2}.kt' \) -print",
-    FIND_REFERENCE_TYPE.PACKAGE: r"find {0} -type d {1} -prune -o -wholename "
+    FIND_REFERENCE_TYPE.CLASS: r"find {0} {1} -type f"
+                               r"| egrep '.*/{2}\.(kt|java)$' || true",
+    FIND_REFERENCE_TYPE.QUALIFIED_CLASS: r"find {0} {1} -type f"
+                                         r"| egrep '.*{2}\.(kt|java)$' || true",
+    FIND_REFERENCE_TYPE.PACKAGE: r"find {0} {1} -wholename "
                                  r"'*{2}' -type d -print",
-    FIND_REFERENCE_TYPE.INTEGRATION: r"find {0} -type d {1} -prune -o -wholename "
+    FIND_REFERENCE_TYPE.INTEGRATION: r"find {0} {1} -wholename "
                                      r"'*{2}.xml' -print",
-    FIND_REFERENCE_TYPE.CC_CLASS: r"find {0} -type d {1} -prune -o -type f "
-                                  r"\( -name '*.cpp' -o -name '*.cc' \)"
-                                  r" | xargs -P " + str(_CPU_COUNT) +
-                                  r" grep -s -H -E 'TEST(_F)?\({2},' {{}} + || true"
+    # Searching a test among files where the absolute paths contain *test*.
+    # If users complain atest couldn't find a CC_CLASS, ask them to follow the
+    # convention that the filename or dirname must contain *test*, where *test*
+    # is case-insensitive.
+    FIND_REFERENCE_TYPE.CC_CLASS: r"find {0} {1} -type f -print"
+                                  r"| egrep -i '/*test.*\.(cc|cpp)$'"
+                                  r"| xargs -P" + str(_CPU_COUNT) +
+                                  r" egrep -sH '[ ]*TEST(_F|_P)?[ ]*\({2}' || true"
 }
 
 # XML parsing related constants.
@@ -206,7 +215,38 @@
                 return match.group('package')
 
 
-def extract_test_path(output, is_native_test=False):
+def has_method_in_file(test_path, methods):
+    """Find out if there is at least one method in the file.
+
+    Note: This method doesn't handle if method is in comment sections or not.
+    If the file has any method(even in comment sections), it will return True.
+
+    Args:
+        test_path: A string of absolute path to the test file.
+        methods: A set of method names.
+
+    Returns:
+        Boolean: there is at least one method in test_path.
+    """
+    if not os.path.isfile(test_path):
+        return False
+    methods_re = None
+    if constants.JAVA_EXT_RE.match(test_path):
+        methods_re = re.compile(_JAVA_METHODS_PATTERN.format(
+            '|'.join([r'%s' % x for x in methods])))
+    elif constants.CC_EXT_RE.match(test_path):
+        methods_re = re.compile(_CC_METHODS_PATTERN.format(
+            '|'.join([r'%s' % x for x in methods])))
+    if methods_re:
+        with open(test_path) as test_file:
+            for line in test_file:
+                match = re.match(methods_re, line)
+                if match:
+                    return True
+    return False
+
+
+def extract_test_path(output, methods=None):
     """Extract the test path from the output of a unix 'find' command.
 
     Example of find output for CLASS find cmd:
@@ -214,49 +254,74 @@
 
     Args:
         output: A string output of a unix 'find' command.
-        is_native_test: A boolean variable of whether to search for a native
-        test or not.
+        methods: A set of method names.
 
     Returns:
-        A string of the test path or None if output is '' or None.
+        A list of the test paths or None if output is '' or None.
     """
     if not output:
         return None
-    tests = output.strip('\n').split('\n')
-    if is_native_test:
-        tests = list(set([path.split(":")[0] for path in tests]))
-    return extract_test_from_tests(tests)
+    verified_tests = set()
+    output_lines = output.strip('\n').split('\n')
+    for test in output_lines:
+        # compare _CC_OUTPUT_RE with output
+        match_obj = _CC_OUTPUT_RE.match(test)
+        if match_obj:
+            # cc/cpp
+            fpath = match_obj.group('file_path')
+            if not methods or match_obj.group('method_name') in methods:
+                verified_tests.add(fpath)
+        else:
+            # java/kt
+            if not methods or has_method_in_file(test, methods):
+                verified_tests.add(test)
+    return extract_test_from_tests(list(verified_tests))
 
 
 def extract_test_from_tests(tests):
     """Extract the test path from the tests.
 
     Return the test to run from tests. If more than one option, prompt the user
-    to select one.
+    to select multiple ones. Supporting formats:
+    - An integer. E.g. 0
+    - Comma-separated integers. E.g. 1,3,5
+    - A range of integers denoted by the starting integer separated from
+      the end integer by a dash, '-'. E.g. 1-3
 
     Args:
         tests: A string list which contains multiple test paths.
 
     Returns:
-        A string of the test path or None if tests is out-of-index or ''.
+        A string list of paths.
     """
     count = len(tests)
-    test_index = 0
-    if count == 0:
-        return None
-    elif count > 1:
+    if count <= 1:
+        return tests if count else None
+    mtests = set()
+    try:
         numbered_list = ['%s: %s' % (i, t) for i, t in enumerate(tests)]
+        numbered_list.append('%s: All' % count)
         print('Multiple tests found:\n{0}'.format('\n'.join(numbered_list)))
-        try:
-            test_index = int(raw_input('Please enter number of test to use '
-                                       'or hit return to keep searching: '))
-            if test_index not in range(count):
-                logging.warn('The input %s is out-of-range(%s).',
-                             test_index, (count-1))
-                return None
-        except ValueError:
-            return None
-    return tests[test_index]
+        test_indices = raw_input("Please enter numbers of test to use. "
+                                 "If none of above option matched, keep "
+                                 "searching for other possible tests."
+                                 "\n(multiple selection is supported,"
+                                 " e.g. '1' or '0,1' or '0-2'): ")
+        for idx in re.sub(r'(\s)', '', test_indices).split(','):
+            indices = idx.split('-')
+            len_indices = len(indices)
+            if len_indices > 0:
+                start_index = min(int(indices[0]), int(indices[len_indices-1]))
+                end_index = max(int(indices[0]), int(indices[len_indices-1]))
+                # One of input is 'All', return all options.
+                if start_index == count or end_index == count:
+                    return tests
+                mtests.update(tests[start_index:(end_index+1)])
+    except (ValueError, IndexError, AttributeError, TypeError) as err:
+        logging.debug('%s', err)
+        print('None of above option matched, keep searching for other'
+              ' possible tests...')
+    return list(mtests)
 
 
 def static_var(varname, value):
@@ -321,23 +386,24 @@
         A string of the prune condition of the ignore dirs.
     """
     out_dirs = _get_ignored_dirs()
-    prune_cond = r'\( -name ".*"'
+    prune_cond = r'-type d \( -name ".*"'
     for out_dir in out_dirs:
         prune_cond += r' -o -path %s' % out_dir
-    prune_cond += r' \)'
+    prune_cond += r' \) -prune -o'
     return prune_cond
 
 
-def run_find_cmd(ref_type, search_dir, target):
+def run_find_cmd(ref_type, search_dir, target, methods=None):
     """Find a path to a target given a search dir and a target name.
 
     Args:
         ref_type: An AtestEnum of the reference type.
         search_dir: A string of the dirpath to search in.
         target: A string of what you're trying to find.
+        methods: A set of method names.
 
     Return:
-        A string of the path to the target.
+        A list of the path to the target.
     """
     prune_cond = _get_prune_cond_of_ignored_dirs()
     find_cmd = FIND_CMDS[ref_type].format(search_dir, prune_cond, target)
@@ -347,12 +413,10 @@
     out = subprocess.check_output(find_cmd, shell=True)
     logging.debug('%s find completed in %ss', ref_name, time.time() - start)
     logging.debug('%s find cmd out: %s', ref_name, out)
-    if ref_type == FIND_REFERENCE_TYPE.CC_CLASS:
-        return extract_test_path(out, True)
-    return extract_test_path(out)
+    return extract_test_path(out, methods)
 
 
-def find_class_file(search_dir, class_name, is_native_test=False):
+def find_class_file(search_dir, class_name, is_native_test=False, methods=None):
     """Find a path to a class file given a search dir and a class name.
 
     Args:
@@ -360,21 +424,22 @@
         class_name: A string of the class to search for.
         is_native_test: A boolean variable of whether to search for a native
         test or not.
+        methods: A set of method names.
 
     Return:
-        A string of the path to the java/cc file.
+        A list of the path to the java/cc file.
     """
     if is_native_test:
         find_target = class_name
         ref_type = FIND_REFERENCE_TYPE.CC_CLASS
-        return run_find_cmd(ref_type, search_dir, find_target)
+        return run_find_cmd(ref_type, search_dir, find_target, methods)
     if '.' in class_name:
         find_target = class_name.replace('.', '/')
         ref_type = FIND_REFERENCE_TYPE.QUALIFIED_CLASS
     else:
         find_target = class_name
         ref_type = FIND_REFERENCE_TYPE.CLASS
-    return run_find_cmd(ref_type, search_dir, find_target)
+    return run_find_cmd(ref_type, search_dir, find_target, methods)
 
 
 def is_equal_or_sub_dir(sub_dir, parent_dir):
@@ -762,7 +827,7 @@
         int_dirs: A list of path needed to be searched.
 
     Returns:
-        A string of the test path.
+        A list of the test path.
         Ask user to select if multiple tests are found.
         None if no matched test found.
     """
@@ -770,10 +835,10 @@
     test_files = []
     for integration_dir in int_dirs:
         abs_path = os.path.join(root_dir, integration_dir)
-        test_file = run_find_cmd(FIND_REFERENCE_TYPE.INTEGRATION, abs_path,
-                                 name)
-        if test_file:
-            test_files.append(test_file)
+        test_paths = run_find_cmd(FIND_REFERENCE_TYPE.INTEGRATION, abs_path,
+                                  name)
+        if test_paths:
+            test_files.extend(test_paths)
     return extract_test_from_tests(test_files)
 
 
diff --git a/atest/test_finders/test_finder_utils_unittest.py b/atest/test_finders/test_finder_utils_unittest.py
index 2e77ed6..4c17678 100755
--- a/atest/test_finders/test_finder_utils_unittest.py
+++ b/atest/test_finders/test_finder_utils_unittest.py
@@ -31,10 +31,13 @@
 CLASS_DIR = 'foo/bar/jank/src/android/jank/cts/ui'
 OTHER_DIR = 'other/dir/'
 OTHER_CLASS_NAME = 'test.java'
+CLASS_NAME3 = 'test2'
 INT_DIR1 = os.path.join(uc.TEST_DATA_DIR, 'integration_dir_testing/int_dir1')
 INT_DIR2 = os.path.join(uc.TEST_DATA_DIR, 'integration_dir_testing/int_dir2')
 INT_FILE_NAME = 'int_dir_testing'
 FIND_TWO = uc.ROOT + 'other/dir/test.java\n' + uc.FIND_ONE
+FIND_THREE = '/a/b/c.java\n/d/e/f.java\n/g/h/i.java'
+FIND_THREE_LIST = ['/a/b/c.java', '/d/e/f.java', '/g/h/i.java']
 VTS_XML = 'VtsAndroidTest.xml'
 VTS_BITNESS_XML = 'VtsBitnessAndroidTest.xml'
 VTS_PUSH_DIR = 'vts_push_files'
@@ -114,33 +117,97 @@
             test_finder_utils.split_methods('foo/bar/class.java#Method'),
             ('foo/bar/class.java', {'Method'}))
 
+    @mock.patch.object(test_finder_utils, 'has_method_in_file',
+                       return_value=False)
     @mock.patch('__builtin__.raw_input', return_value='1')
-    def test_extract_test_path(self, _):
+    def test_extract_test_path(self, _, has_method):
         """Test extract_test_dir method."""
-        path = os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')
+        paths = [os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')]
         unittest_utils.assert_strict_equal(
-            self, test_finder_utils.extract_test_path(uc.FIND_ONE), path)
-        path = os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')
+            self, test_finder_utils.extract_test_path(uc.FIND_ONE), paths)
+        paths = [os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')]
         unittest_utils.assert_strict_equal(
-            self, test_finder_utils.extract_test_path(FIND_TWO), path)
+            self, test_finder_utils.extract_test_path(FIND_TWO), paths)
+        paths = None
+        unittest_utils.assert_strict_equal(
+            self, test_finder_utils.extract_test_path(uc.FIND_ONE, 'method'), paths)
+        has_method.return_value = True
+        paths = [os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')]
+        unittest_utils.assert_strict_equal(
+            self, test_finder_utils.extract_test_path(uc.FIND_ONE, 'method'), paths)
+
+    def test_has_method_in_file(self):
+        """Test has_method_in_file method."""
+        test_path = os.path.join(uc.TEST_DATA_DIR, 'class_file_path_testing',
+                                 'hello_world_test.cc')
+        self.assertTrue(test_finder_utils.has_method_in_file(
+            test_path, frozenset(['PrintHelloWorld'])))
+        self.assertFalse(test_finder_utils.has_method_in_file(
+            test_path, frozenset(['PrintHelloWorld1'])))
+        test_path = os.path.join(uc.TEST_DATA_DIR, 'class_file_path_testing',
+                                 'hello_world_test.java')
+        self.assertTrue(test_finder_utils.has_method_in_file(
+            test_path, frozenset(['testMethod1'])))
+        test_path = os.path.join(uc.TEST_DATA_DIR, 'class_file_path_testing',
+                                 'hello_world_test.java')
+        self.assertTrue(test_finder_utils.has_method_in_file(
+            test_path, frozenset(['testMethod', 'testMethod2'])))
+        test_path = os.path.join(uc.TEST_DATA_DIR, 'class_file_path_testing',
+                                 'hello_world_test.java')
+        self.assertFalse(test_finder_utils.has_method_in_file(
+            test_path, frozenset(['testMethod'])))
 
     @mock.patch('__builtin__.raw_input', return_value='1')
     def test_extract_test_from_tests(self, mock_input):
         """Test method extract_test_from_tests method."""
         tests = []
         self.assertEquals(test_finder_utils.extract_test_from_tests(tests), None)
-        path = os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')
+        paths = [os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')]
         unittest_utils.assert_strict_equal(
-            self, test_finder_utils.extract_test_path(uc.FIND_ONE), path)
-        path = os.path.join(uc.ROOT, OTHER_DIR, OTHER_CLASS_NAME)
+            self, test_finder_utils.extract_test_path(uc.FIND_ONE), paths)
+        paths = [os.path.join(uc.ROOT, OTHER_DIR, OTHER_CLASS_NAME)]
         mock_input.return_value = '0'
         unittest_utils.assert_strict_equal(
-            self, test_finder_utils.extract_test_path(FIND_TWO), path)
+            self, test_finder_utils.extract_test_path(FIND_TWO), paths)
         # Test inputing out-of-range integer or a string
         mock_input.return_value = '100'
-        self.assertEquals(test_finder_utils.extract_test_from_tests(uc.CLASS_NAME), None)
+        self.assertEquals(test_finder_utils.extract_test_from_tests(
+            uc.CLASS_NAME), [])
         mock_input.return_value = 'lOO'
-        self.assertEquals(test_finder_utils.extract_test_from_tests(uc.CLASS_NAME), None)
+        self.assertEquals(test_finder_utils.extract_test_from_tests(
+            uc.CLASS_NAME), [])
+
+    @mock.patch('__builtin__.raw_input', return_value='1')
+    def test_extract_test_from_multiselect(self, mock_input):
+        """Test method extract_test_from_tests method."""
+        # selecting 'All'
+        paths = ['/a/b/c.java', '/d/e/f.java', '/g/h/i.java']
+        mock_input.return_value = '3'
+        unittest_utils.assert_strict_equal(
+            self, sorted(test_finder_utils.extract_test_from_tests(
+                FIND_THREE_LIST)), sorted(paths))
+        # multi-select
+        paths = ['/a/b/c.java', '/g/h/i.java']
+        mock_input.return_value = '0,2'
+        unittest_utils.assert_strict_equal(
+            self, sorted(test_finder_utils.extract_test_from_tests(
+                FIND_THREE_LIST)), sorted(paths))
+        # selecting a range
+        paths = ['/d/e/f.java', '/g/h/i.java']
+        mock_input.return_value = '1-2'
+        unittest_utils.assert_strict_equal(
+            self, test_finder_utils.extract_test_from_tests(FIND_THREE_LIST), paths)
+        # mixed formats
+        paths = ['/a/b/c.java', '/d/e/f.java', '/g/h/i.java']
+        mock_input.return_value = '0,1-2'
+        unittest_utils.assert_strict_equal(
+            self, sorted(test_finder_utils.extract_test_from_tests(
+                FIND_THREE_LIST)), sorted(paths))
+        # input unsupported formats, return empty
+        paths = []
+        mock_input.return_value = '?/#'
+        unittest_utils.assert_strict_equal(
+            self, test_finder_utils.extract_test_path(FIND_THREE), paths)
 
     @mock.patch('os.path.isdir')
     def test_is_equal_or_sub_dir(self, mock_isdir):
@@ -358,13 +425,13 @@
     def test_search_integration_dirs(self, mock_input):
         """Test search_integration_dirs."""
         mock_input.return_value = '0'
-        path = os.path.join(uc.ROOT, INT_DIR1, INT_FILE_NAME+'.xml')
+        paths = [os.path.join(uc.ROOT, INT_DIR1, INT_FILE_NAME+'.xml')]
         int_dirs = [INT_DIR1]
         test_result = test_finder_utils.search_integration_dirs(INT_FILE_NAME, int_dirs)
-        unittest_utils.assert_strict_equal(self, test_result, path)
+        unittest_utils.assert_strict_equal(self, test_result, paths)
         int_dirs = [INT_DIR1, INT_DIR2]
         test_result = test_finder_utils.search_integration_dirs(INT_FILE_NAME, int_dirs)
-        unittest_utils.assert_strict_equal(self, test_result, path)
+        unittest_utils.assert_strict_equal(self, test_result, paths)
 
     @mock.patch('os.environ.get', return_value=uc.TEST_CONFIG_DATA_DIR)
     @mock.patch('__builtin__.raw_input', return_value='0')
@@ -373,12 +440,12 @@
         java_tmp_test_result = []
         mock_input.return_value = '0'
         java_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_TESTCASE_JAVA + '.java')
-        java_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+        java_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
                                                                       uc.FIND_PATH_TESTCASE_JAVA))
 
         mock_input.return_value = '1'
         kt_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_TESTCASE_JAVA + '.kt')
-        java_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+        java_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
                                                                       uc.FIND_PATH_TESTCASE_JAVA))
 
         self.assertTrue(java_class in java_tmp_test_result)
@@ -387,10 +454,10 @@
         del java_tmp_test_result[:]
         mock_input.return_value = '0'
         java_qualified_class = '{0}.{1}'.format(uc.FIND_PATH_FOLDER, uc.FIND_PATH_TESTCASE_JAVA)
-        java_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+        java_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
                                                                       java_qualified_class))
         mock_input.return_value = '1'
-        java_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+        java_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
                                                                       java_qualified_class))
         self.assertTrue(java_class in java_tmp_test_result)
         self.assertTrue(kt_class in java_tmp_test_result)
@@ -398,12 +465,12 @@
         cc_tmp_test_result = []
         mock_input.return_value = '0'
         cpp_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_FILENAME_CC + '.cpp')
-        cc_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+        cc_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
                                                                     uc.FIND_PATH_TESTCASE_CC,
                                                                     True))
         mock_input.return_value = '1'
         cc_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_FILENAME_CC + '.cc')
-        cc_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+        cc_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
                                                                     uc.FIND_PATH_TESTCASE_CC,
                                                                     True))
 
diff --git a/atest/test_finders/tf_integration_finder.py b/atest/test_finders/tf_integration_finder.py
index a96ad63..eecb9927 100644
--- a/atest/test_finders/tf_integration_finder.py
+++ b/atest/test_finders/tf_integration_finder.py
@@ -120,8 +120,10 @@
                 if not integration_name:
                     logging.warn('skipping <include> tag with no "name" value')
                     continue
-                full_path = self._search_integration_dirs(integration_name)
-                node = self._load_xml_file(full_path)
+                full_paths = self._search_integration_dirs(integration_name)
+                node = None
+                if full_paths:
+                    node = self._load_xml_file(full_paths[0])
                 if node is None:
                     raise atest_error.FatalIncludeError("can't load %r" %
                                                         integration_name)
@@ -137,16 +139,17 @@
             name: A string of integration name as seen in tf's list configs.
 
         Returns:
-            A string of test path if test found, else None.
+            A list of test path.
         """
+        test_files = []
         for integration_dir in self.integration_dirs:
             abs_path = os.path.join(self.root_dir, integration_dir)
-            test_file = test_finder_utils.run_find_cmd(
+            found_test_files = test_finder_utils.run_find_cmd(
                 test_finder_utils.FIND_REFERENCE_TYPE.INTEGRATION,
                 abs_path, name)
-            if test_file:
-                return test_file
-        return None
+            if found_test_files:
+                test_files.extend(found_test_files)
+        return test_files
 
     def find_test_by_integration_name(self, name):
         """Find the test info matching the given integration name.
@@ -160,13 +163,17 @@
         class_name = None
         if ':' in name:
             name, class_name = name.split(':')
-        test_file = self._search_integration_dirs(name)
-        if test_file is None:
+        test_files = self._search_integration_dirs(name)
+        if test_files is None:
             return None
         # Don't use names that simply match the path,
         # must be the actual name used by TF to run the test.
-        t_info = self._get_test_info(name, test_file, class_name)
-        return t_info
+        t_infos = []
+        for test_file in test_files:
+            t_info = self._get_test_info(name, test_file, class_name)
+            if t_info:
+                t_infos.append(t_info)
+        return t_infos
 
     def _get_test_info(self, name, test_file, class_name):
         """Find the test info matching the given test_file and class_name.
@@ -193,17 +200,24 @@
         filters = frozenset()
         if class_name:
             class_name, methods = test_finder_utils.split_methods(class_name)
-            if '.' not in class_name:
+            test_filters = []
+            if '.' in class_name:
+                test_filters.append(test_info.TestFilter(class_name, methods))
+            else:
                 logging.warn('Looking up fully qualified class name for: %s.'
                              'Improve speed by using fully qualified names.',
                              class_name)
-                path = test_finder_utils.find_class_file(self.root_dir,
-                                                         class_name)
-                if not path:
+                paths = test_finder_utils.find_class_file(self.root_dir,
+                                                          class_name)
+                if not paths:
                     return None
-                class_name = test_finder_utils.get_fully_qualified_class_name(
-                    path)
-            filters = frozenset([test_info.TestFilter(class_name, methods)])
+                for path in paths:
+                    class_name = (
+                        test_finder_utils.get_fully_qualified_class_name(
+                            path))
+                    test_filters.append(test_info.TestFilter(
+                        class_name, methods))
+            filters = frozenset(test_filters)
         return test_info.TestInfo(
             test_name=name,
             test_runner=self._TEST_RUNNER,
@@ -223,7 +237,7 @@
             path: A string of the test's path.
 
         Returns:
-            A populated TestInfo namedtuple if test found, else None
+            A list of populated TestInfo namedtuple if test found, else None
         """
         path, _ = test_finder_utils.split_methods(path)
 
@@ -246,10 +260,10 @@
                               rel_config)
                 return None
             int_name = match.group('int_name')
-            return test_info.TestInfo(
+            return [test_info.TestInfo(
                 test_name=int_name,
                 test_runner=self._TEST_RUNNER,
                 build_targets=self._get_build_targets(rel_config),
                 data={constants.TI_REL_CONFIG: rel_config,
-                      constants.TI_FILTER: frozenset()})
+                      constants.TI_FILTER: frozenset()})]
         return None
diff --git a/atest/test_finders/tf_integration_finder_unittest.py b/atest/test_finders/tf_integration_finder_unittest.py
index 0a2cc84..a8b58cc 100755
--- a/atest/test_finders/tf_integration_finder_unittest.py
+++ b/atest/test_finders/tf_integration_finder_unittest.py
@@ -69,24 +69,25 @@
                                            _fcqn, _build):
         """Test find_test_by_integration_name."""
         mock_find.return_value = os.path.join(uc.ROOT, uc.INT_DIR, uc.INT_NAME + '.xml')
-        t_info = self.tf_finder.find_test_by_integration_name(uc.INT_NAME)
-        unittest_utils.assert_equal_testinfos(self, t_info, uc.INT_INFO)
-        t_info = self.tf_finder.find_test_by_integration_name(INT_NAME_CLASS)
-        unittest_utils.assert_equal_testinfos(self, t_info, INT_CLASS_INFO)
-        t_info = self.tf_finder.find_test_by_integration_name(INT_NAME_METHOD)
-        unittest_utils.assert_equal_testinfos(self, t_info, INT_METHOD_INFO)
+        t_infos = self.tf_finder.find_test_by_integration_name(uc.INT_NAME)
+        unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.INT_INFO)
+        t_infos = self.tf_finder.find_test_by_integration_name(INT_NAME_CLASS)
+        unittest_utils.assert_equal_testinfos(self, t_infos[0], INT_CLASS_INFO)
+        t_infos = self.tf_finder.find_test_by_integration_name(INT_NAME_METHOD)
+        unittest_utils.assert_equal_testinfos(self, t_infos[0], INT_METHOD_INFO)
         not_fully_qual = uc.INT_NAME + ':' + 'someClass'
-        t_info = self.tf_finder.find_test_by_integration_name(not_fully_qual)
-        unittest_utils.assert_equal_testinfos(self, t_info, INT_CLASS_INFO)
+        t_infos = self.tf_finder.find_test_by_integration_name(not_fully_qual)
+        unittest_utils.assert_equal_testinfos(self, t_infos[0], INT_CLASS_INFO)
         mock_find.return_value = os.path.join(uc.ROOT, uc.GTF_INT_DIR,
                                               uc.GTF_INT_NAME + '.xml')
+        t_infos = self.tf_finder.find_test_by_integration_name(uc.GTF_INT_NAME)
         unittest_utils.assert_equal_testinfos(
             self,
-            self.tf_finder.find_test_by_integration_name(uc.GTF_INT_NAME),
+            t_infos[0],
             uc.GTF_INT_INFO)
         mock_find.return_value = ''
-        self.assertIsNone(
-            self.tf_finder.find_test_by_integration_name('NotIntName'))
+        self.assertEqual(
+            self.tf_finder.find_test_by_integration_name('NotIntName'), [])
 
     @mock.patch.object(tf_integration_finder.TFIntegrationFinder,
                        '_get_build_targets', return_value=set())
@@ -100,21 +101,22 @@
                                    _build):
         """Test find_int_test_by_path."""
         path = os.path.join(uc.INT_DIR, uc.INT_NAME + '.xml')
+        t_infos = self.tf_finder.find_int_test_by_path(path)
         unittest_utils.assert_equal_testinfos(
-            self, uc.INT_INFO, self.tf_finder.find_int_test_by_path(path))
+            self, uc.INT_INFO, t_infos[0])
         path = os.path.join(uc.GTF_INT_DIR, uc.GTF_INT_NAME + '.xml')
+        t_infos = self.tf_finder.find_int_test_by_path(path)
         unittest_utils.assert_equal_testinfos(
-            self, uc.GTF_INT_INFO, self.tf_finder.find_int_test_by_path(path))
+            self, uc.GTF_INT_INFO, t_infos[0])
 
     #pylint: disable=protected-access
     @mock.patch.object(tf_integration_finder.TFIntegrationFinder,
                        '_search_integration_dirs')
     def test_load_xml_file(self, search):
         """Test _load_xml_file and _load_include_tags methods."""
-        search.return_value = os.path.join(uc.TEST_DATA_DIR,
-                                           'CtsUiDeviceTestCases.xml')
+        search.return_value = [os.path.join(uc.TEST_DATA_DIR,
+                                            'CtsUiDeviceTestCases.xml')]
         xml_file = os.path.join(uc.TEST_DATA_DIR, constants.MODULE_CONFIG)
-        print 'xml_file: %s' % xml_file
         xml_root = self.tf_finder._load_xml_file(xml_file)
         include_tags = xml_root.findall('.//include')
         self.assertEqual(0, len(include_tags))
diff --git a/atest/test_runners/atest_tf_test_runner.py b/atest/test_runners/atest_tf_test_runner.py
index a4f62d5..ffd1ac9 100644
--- a/atest/test_runners/atest_tf_test_runner.py
+++ b/atest/test_runners/atest_tf_test_runner.py
@@ -500,6 +500,10 @@
         if test_infos[0].from_test_mapping:
             args.extend(constants.TEST_MAPPING_RESULT_SERVER_ARGS)
         test_infos = self._flatten_test_infos(test_infos)
+        # In order to do dry-run verification, sort it to make each run has the
+        # same result
+        test_infos = list(test_infos)
+        test_infos.sort()
 
         for info in test_infos:
             args.extend([constants.TF_INCLUDE_FILTER, info.test_name])
diff --git a/atest/test_runners/robolectric_test_runner_unittest.py b/atest/test_runners/robolectric_test_runner_unittest.py
index 26e30f6..46164f0 100755
--- a/atest/test_runners/robolectric_test_runner_unittest.py
+++ b/atest/test_runners/robolectric_test_runner_unittest.py
@@ -66,11 +66,9 @@
 
         json_event_data = json.dumps(event_data)
         data = '%s %s\n\n' %(event_name, json_event_data)
-        event_file = tempfile.NamedTemporaryFile(mode='w+r', delete=False)
-        robo_proc = subprocess.Popen("echo '%s' >> %s && sleep %s"
-                                     %(data,
-                                       event_file.name,
-                                       str(self.polling_time)), shell=True)
+        event_file = tempfile.NamedTemporaryFile(mode='w+r', delete=True)
+        subprocess.call("echo '%s' -n >> %s" %(data, event_file.name), shell=True)
+        robo_proc = subprocess.Popen("sleep %s" %str(self.polling_time * 2), shell=True)
         self.suite_tr. _exec_with_robo_polling(event_file, robo_proc, mock_pe)
         calls = [mock.call.process_event(event_name, event_data)]
         mock_pe.assert_has_calls(calls)
@@ -105,11 +103,8 @@
                               'at FailureStrategy.fail(FailureStrategy.java:20)\n'}
         data = '%s %s\n\n'%(event_name, json.dumps(event_data))
         event_file = tempfile.NamedTemporaryFile(mode='w+r', delete=True)
-        robo_proc = subprocess.Popen("echo '%s' >> %s && sleep %s"
-                                     %(data,
-                                       event_file.name,
-                                       str(self.polling_time)),
-                                     shell=True)
+        subprocess.call("echo '%s' -n >> %s" %(data, event_file.name), shell=True)
+        robo_proc = subprocess.Popen("sleep %s" %str(self.polling_time * 2), shell=True)
         self.suite_tr. _exec_with_robo_polling(event_file, robo_proc, mock_pe)
         calls = [mock.call.process_event(event_name, event_data)]
         mock_pe.assert_has_calls(calls)
@@ -135,15 +130,12 @@
                             'testName':'someTestName2'}),
             ('TEST_RUN_ENDED', {}),
             ('TEST_MODULE_ENDED', {'foo': 'bar'}),]
-        data_data = ''
+        data = ''
         for event in events:
-            data_data += '%s %s\n\n'%(event[0], json.dumps(event[1]))
+            data += '%s %s\n\n'%(event[0], json.dumps(event[1]))
 
-        robo_proc = subprocess.Popen("echo '%s' >> %s && sleep %s"
-                                     %(data_data,
-                                       event_file.name,
-                                       str(self.polling_time)),
-                                     shell=True)
+        subprocess.call("echo '%s' -n >> %s" %(data, event_file.name), shell=True)
+        robo_proc = subprocess.Popen("sleep %s" %str(self.polling_time * 2), shell=True)
         self.suite_tr. _exec_with_robo_polling(event_file, robo_proc, mock_pe)
         calls = [mock.call.process_event(name, data) for name, data in events]
         mock_pe.assert_has_calls(calls)
diff --git a/atest/tools/__init__.py b/atest/tools/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/tools/__init__.py
diff --git a/atest/tools/atest_updatedb.py b/atest/tools/atest_updatedb.py
new file mode 100755
index 0000000..6b0c8f8
--- /dev/null
+++ b/atest/tools/atest_updatedb.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Atest updatedb functions.
+"""
+
+from __future__ import print_function
+
+import logging
+import os
+import platform
+import shutil
+import subprocess
+
+AND_HOSTOUT_DIR = os.getenv('ANDROID_HOST_OUT', '')
+MAC_UPDB_SRC = os.path.join(os.path.dirname(__file__), 'updatedb_darwin.sh')
+MAC_UPDB_DST = os.path.join(AND_HOSTOUT_DIR, 'bin')
+UPDATEDB = 'updatedb'
+
+# updatedb does not support ".*" so below are excluded explicitly.
+_PRUNENAMES = ['.abc', '.appveyor', '.azure-pipelines',
+               '.bazelci', '.buildscript',
+               '.ci', '.circleci',
+               '.conan',
+               '.externalToolBuilders',
+               '.git', '.github', '.google', '.gradle',
+               '.idea', '.intermediates',
+               '.kokoro',
+               '.mvn',
+               '.prebuilt_info', '.private', '__pycache__',
+               '.repo',
+               '.semaphore', '.settings',
+               '.static', '.svn',
+               '.test', '.travis', '.tx',
+               '.vscode']
+_CACHE = 'locate.database'
+
+def _install_updatedb():
+    """Install a customized updatedb for MacOS."""
+    if platform.system() == 'Darwin':
+        if not os.path.isdir(MAC_UPDB_DST):
+            os.makedirs(MAC_UPDB_DST)
+        shutil.copy2(MAC_UPDB_SRC, os.path.join(MAC_UPDB_DST, UPDATEDB))
+        os.chmod(os.path.join(MAC_UPDB_DST, UPDATEDB), 0755)
+
+
+def run_updatedb(**kwargs):
+    """Run updatedb and generate cache in $ANDROID_HOST_OUT/locate.database
+
+    Args:
+        search_root: The path of the search root(-U).
+        prunepaths: A list of paths unwanted to be searched(-e).
+        prunenames: A list of dirname that won't be cached(-n).
+        output_cache: The filename of the updatedb cache(-o).
+
+    Returns:
+        Boolean of the status of updatedb execution, True if update successfully,
+        False otherwise.
+    """
+    repo_root = os.getenv('ANDROID_BUILD_TOP', '')
+    search_root = kwargs.get('search_root', repo_root)
+    prunepaths = kwargs.get('prunepaths', os.path.join(search_root, 'out'))
+    prunenames = kwargs.get('prunenames', ' '.join(_PRUNENAMES))
+    output_cache = kwargs.get('output_cache',
+                              os.path.join(AND_HOSTOUT_DIR, _CACHE))
+    if not os.path.exists(os.path.dirname(output_cache)):
+        os.makedirs(os.path.dirname(output_cache))
+    updatedb_cmd = [UPDATEDB, '-l0']
+    updatedb_cmd.append('-U%s' % search_root)
+    updatedb_cmd.append('-e%s' % prunepaths)
+    updatedb_cmd.append('-n%s' % prunenames)
+    updatedb_cmd.append('-o%s' % output_cache)
+    try:
+        _install_updatedb()
+    except IOError as e:
+        logging.error('Error installing updatedb: %s', e)
+        return False
+    print('Running updatedb for locate...')
+    try:
+        full_env_vars = os.environ.copy()
+        logging.debug('Executing: %s', updatedb_cmd)
+        subprocess.check_call(updatedb_cmd, stderr=subprocess.STDOUT,
+                              env=full_env_vars)
+        return True
+    except subprocess.CalledProcessError as err:
+        logging.error('Error executing: %s', updatedb_cmd)
+        if err.output:
+            logging.error(err.output)
+        return False
+
+if __name__ == '__main__':
+    run_updatedb()
diff --git a/atest/tools/atest_updatedb_unittest.py b/atest/tools/atest_updatedb_unittest.py
new file mode 100755
index 0000000..923c5dc
--- /dev/null
+++ b/atest/tools/atest_updatedb_unittest.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+#
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unittest for atest_updatedb."""
+
+import os
+import platform
+import subprocess
+import sys
+import unittest
+
+import atest_updatedb
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+# pylint: disable=wrong-import-position
+import unittest_constants
+
+SEARCH_ROOT = unittest_constants.TEST_DATA_DIR
+PRUNEPATH = unittest_constants.TEST_CONFIG_DATA_DIR
+_CACHE = '/tmp/locate.database'
+
+
+class AtestUpdatedbUnittests(unittest.TestCase):
+    """"Unittest Class for atest_updatedb.py."""
+
+    def test_atest_updatedb(self):
+        """Test method run_updatedb."""
+        atest_updatedb.run_updatedb(search_root=SEARCH_ROOT,
+                                    prunepaths=PRUNEPATH,
+                                    output_cache=_CACHE)
+        # test_config/ is excluded so that a.xml won't be found.
+        locate_cmd1 = ['locate', '-d', _CACHE, '/a.xml']
+        # locate always return 0 when not found in Darwin, therefore,
+        # check null return in Darwin and return value in Linux.
+        if platform.system() == 'Darwin':
+            self.assertEqual(subprocess.check_output(locate_cmd1), "")
+        else:
+            self.assertEqual(subprocess.call(locate_cmd1), 1)
+        # module-info.json can be found in the search_root.
+        locate_cmd2 = ['locate', '-d', _CACHE, 'module-info.json']
+        self.assertEqual(subprocess.call(locate_cmd2), 0)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/atest/tools/updatedb_darwin.sh b/atest/tools/updatedb_darwin.sh
new file mode 100755
index 0000000..9d621bc
--- /dev/null
+++ b/atest/tools/updatedb_darwin.sh
@@ -0,0 +1,111 @@
+#!/usr/bin/env bash
+#
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Warning and exit when failed to meet the requirements.
+[ "$(uname -s)" != "Darwin" ] && { echo "This program runs on Darwin only."; exit 0; }
+[ "$UID" -eq 0 ] && { echo "Running with root user is not supported."; exit 0; }
+
+function usage() {
+    echo "###########################################"
+    echo "Usage: $prog [-U|-e|-n|-o||-l|-f|-h]"
+    echo "  -U: The PATH of the search root."
+    echo "  -e: The PATH that unwanted to be searched."
+    echo "  -n: The name of directories that won't be cached."
+    echo "  -o: The PATH of the generated database."
+    echo "  -l: No effect. For compatible with Linux mlocate."
+    echo "  -f: Filesystems which should not search for."
+    echo "  -h: This usage helper."
+    echo
+    echo "################ [EXAMPLE] ################"
+    echo "$prog -U \$ANDROID_BUILD_TOP -n .git -l 0 \\"
+    echo " -e \"\$ANDROID_BUILD_TOP/out \$ANDROID_BUILD_TOP/.repo\" \\"
+    echo " -o \"\$ANDROID_HOST_OUT/locate.database\""
+    echo
+    echo "locate -d \$ANDROID_HOST_OUT/locate.database atest.py"
+    echo "locate -d \$ANDROID_HOST_OUT/locate.database contrib/res/config"
+}
+
+function mktempdir() {
+    TMPDIR=/tmp
+    if ! TMPDIR=`mktemp -d $TMPDIR/locateXXXXXXXXXX`; then
+        exit 1
+    fi
+    temp=$TMPDIR/_updatedb$$
+}
+
+function _updatedb_main() {
+    # 0. Disable default features of bash.
+    set -o noglob   # Disable * expension before passing arguments to find.
+    set -o errtrace # Sub-shells inherit error trap.
+
+    # 1. Get positional arguments and set variables.
+    prog=$(basename $0)
+    while getopts 'U:n:e:o:l:f:h' option; do
+        case $option in
+            U) SEARCHROOT="$OPTARG";; # Search root.
+            e) PRUNEPATHS="$OPTARG";; # Paths to be excluded.
+            n) PRUNENAMES="$OPTARG";; # Dirnames to be pruned.
+            o) DATABASE="$OPTARG";;   # the output of the DB.
+            l) ;;                     # No effect.
+            f) PRUNEFS="$OPTARG";;    # Disallow network filesystems.
+            *) usage; exit 0;;
+        esac
+    done
+
+    : ${SEARCHROOT:="$ANDROID_BUILD_TOP"}
+    if [ -z "$SEARCHROOT" ]; then
+        echo 'Either $SEARCHROOT or $ANDROID_BUILD_TOP is required.'
+        exit 0
+    fi
+
+    if [ -n "$ANDROID_BUILD_TOP" ]; then
+        PRUNEPATHS="$PRUNEPATHS $ANDROID_BUILD_TOP/out"
+    fi
+
+    PRUNENAMES="$PRUNENAMES *.class *.pyc .gitignore"
+    : ${DATABASE:=/tmp/locate.database}
+    : ${PRUNEFS:="nfs afp smb"}
+
+    # 2. Assemble excludes strings.
+    excludes=""
+    or=""
+    sortarg="-presort"
+    for fs in $PRUNEFS; do
+        excludes="$excludes $or -fstype $fs -prune"
+        or="-o"
+    done
+    for path in $PRUNEPATHS; do
+        excludes="$excludes $or -path $path -prune"
+    done
+    for file in $PRUNENAMES; do
+        excludes="$excludes $or -name $file -prune"
+    done
+
+    # 3. Find and create locate database.
+    # Delete $temp when trapping specified return values.
+    mktempdir
+    trap 'rm -rf $temp $TMPDIR; exit' 0 1 2 3 5 10 15
+    if find -s $SEARCHROOT $excludes $or -print 2>/dev/null -true |
+        /usr/libexec/locate.mklocatedb $sortarg > $temp 2>/dev/null; then
+            case x"`find $temp -size 257c -print`" in
+                x) cat $temp > $DATABASE;;
+                *) echo "$prog: database $temp is found empty."
+                   exit 1;;
+            esac
+    fi
+}
+
+_updatedb_main "$@"
diff --git a/atest/unittest_constants.py b/atest/unittest_constants.py
index 0134618..03bf5c0 100644
--- a/atest/unittest_constants.py
+++ b/atest/unittest_constants.py
@@ -54,12 +54,21 @@
 MODULE_BUILD_TARGETS = {'tradefed-core', MODULE_INFO_TARGET,
                         'MODULES-IN-%s' % MODULE_DIR.replace('/', '-'),
                         'module-specific-target'}
+MODULE_BUILD_TARGETS2 = {'build-target2'}
 MODULE_DATA = {constants.TI_REL_CONFIG: CONFIG_FILE,
                constants.TI_FILTER: frozenset()}
+MODULE_DATA2 = {constants.TI_REL_CONFIG: CONFIG_FILE,
+                constants.TI_FILTER: frozenset()}
 MODULE_INFO = test_info.TestInfo(MODULE_NAME,
                                  atf_tr.AtestTradefedTestRunner.NAME,
                                  MODULE_BUILD_TARGETS,
                                  MODULE_DATA)
+MODULE_INFO2 = test_info.TestInfo(MODULE2_NAME,
+                                  atf_tr.AtestTradefedTestRunner.NAME,
+                                  MODULE_BUILD_TARGETS2,
+                                  MODULE_DATA2)
+MODULE_INFOS = [MODULE_INFO]
+MODULE_INFOS2 = [MODULE_INFO, MODULE_INFO2]
 CLASS_FILTER = test_info.TestFilter(FULL_CLASS_NAME, frozenset())
 CLASS_DATA = {constants.TI_REL_CONFIG: CONFIG_FILE,
               constants.TI_FILTER: frozenset([CLASS_FILTER])}
@@ -80,6 +89,17 @@
                                 atf_tr.AtestTradefedTestRunner.NAME,
                                 CLASS_BUILD_TARGETS,
                                 CLASS_DATA)
+CLASS_INFOS = [CLASS_INFO]
+
+CLASS_BUILD_TARGETS2 = {'class-specific-target2'}
+CLASS_DATA2 = {constants.TI_REL_CONFIG: CONFIG_FILE,
+               constants.TI_FILTER: frozenset([CLASS_FILTER])}
+CLASS_INFO2 = test_info.TestInfo(MODULE2_NAME,
+                                 atf_tr.AtestTradefedTestRunner.NAME,
+                                 CLASS_BUILD_TARGETS2,
+                                 CLASS_DATA2)
+CLASS_INFOS = [CLASS_INFO]
+CLASS_INFOS2 = [CLASS_INFO, CLASS_INFO2]
 PACKAGE_INFO = test_info.TestInfo(MODULE_NAME,
                                   atf_tr.AtestTradefedTestRunner.NAME,
                                   CLASS_BUILD_TARGETS,
@@ -160,10 +180,10 @@
 CC_MODULE2_DIR = 'foo/bar/hello'
 CC_MODULE2_NAME = 'hello_world_test'
 CC_PATH = 'pf_test.cc'
-CC_FIND_ONE = ROOT + 'system/bt/hci/test/pf_test.cc:TEST_F(PFTest, test1) {\n' +\
-    ROOT + 'system/bt/hci/test/pf_test.cc:TEST_F(PFTest, test2) {\n'
-CC_FIND_TWO = ROOT + 'other/dir/test.cpp:TEST(PFTest, test_f) {\n' +\
-                        ROOT + 'other/dir/test.cpp:TEST(PFTest, test_p) {\n'
+CC_FIND_ONE = ROOT + 'system/bt/hci/test/pf_test.cc:TEST_F(PFTest, test1) {\n' + \
+              ROOT + 'system/bt/hci/test/pf_test.cc:TEST_F(PFTest, test2) {\n'
+CC_FIND_TWO = ROOT + 'other/dir/test.cpp:TEST(PFTest, test_f) {\n' + \
+              ROOT + 'other/dir/test.cpp:TEST(PFTest, test_p) {\n'
 CC_CONFIG2_FILE = os.path.join(CC_MODULE2_DIR, constants.MODULE_CONFIG)
 CC_CLASS_FILTER = test_info.TestFilter(CC_CLASS_NAME+".*", frozenset())
 CC_CLASS_DATA = {constants.TI_REL_CONFIG: CC_CONFIG_FILE,
@@ -175,7 +195,7 @@
 CC_METHOD2_NAME = 'test2'
 CC_METHOD_FILTER = test_info.TestFilter(CC_CLASS_NAME+"."+CC_METHOD_NAME,
                                         frozenset())
-CC_METHOD2_FILTER = test_info.TestFilter(CC_CLASS_NAME+"."+CC_METHOD_NAME+\
+CC_METHOD2_FILTER = test_info.TestFilter(CC_CLASS_NAME+"."+CC_METHOD_NAME+ \
                                          ":"+CC_CLASS_NAME+"."+CC_METHOD2_NAME,
                                          frozenset())
 CC_METHOD_INFO = test_info.TestInfo(
diff --git a/atest/unittest_data/cache_root/cd66f9f5ad63b42d0d77a9334de6bb73.cache b/atest/unittest_data/cache_root/cd66f9f5ad63b42d0d77a9334de6bb73.cache
new file mode 100644
index 0000000..451a51e
--- /dev/null
+++ b/atest/unittest_data/cache_root/cd66f9f5ad63b42d0d77a9334de6bb73.cache
@@ -0,0 +1,72 @@
+c__builtin__
+set
+p0
+((lp1
+ccopy_reg
+_reconstructor
+p2
+(ctest_finders.test_info
+TestInfo
+p3
+c__builtin__
+object
+p4
+Ntp5
+Rp6
+(dp7
+S'install_locations'
+p8
+g0
+((lp9
+S'device'
+p10
+aS'host'
+p11
+atp12
+Rp13
+sS'test_runner'
+p14
+S'AtestTradefedTestRunner'
+p15
+sS'module_class'
+p16
+(lp17
+VNATIVE_TESTS
+p18
+asS'from_test_mapping'
+p19
+I00
+sS'build_targets'
+p20
+g0
+((lp21
+VMODULES-IN-platform_testing-tests-example-native
+p22
+atp23
+Rp24
+sg11
+I00
+sS'test_name'
+p25
+S'hello_world_test'
+p26
+sS'suite'
+p27
+NsS'data'
+p28
+(dp29
+S'rel_config'
+p30
+Vplatform_testing/tests/example/native/AndroidTest.xml
+p31
+sS'filter'
+p32
+c__builtin__
+frozenset
+p33
+((lp34
+tp35
+Rp36
+ssbatp37
+Rp38
+.
\ No newline at end of file
diff --git a/atest/unittest_data/class_file_path_testing/hello_world_test.java b/atest/unittest_data/class_file_path_testing/hello_world_test.java
index 8715753..8e0a999 100644
--- a/atest/unittest_data/class_file_path_testing/hello_world_test.java
+++ b/atest/unittest_data/class_file_path_testing/hello_world_test.java
@@ -1 +1,9 @@
 package com.test.hello_world_test;
+
+public class HelloWorldTest {
+    @Test
+    public void testMethod1() throws Exception {}
+
+    @Test
+    public void testMethod2() throws Exception {}
+}
diff --git a/atest/unittest_data/test_mapping/folder6/test_mapping_sample_golden b/atest/unittest_data/test_mapping/folder6/test_mapping_sample_golden
new file mode 100644
index 0000000..db3998d
--- /dev/null
+++ b/atest/unittest_data/test_mapping/folder6/test_mapping_sample_golden
@@ -0,0 +1,14 @@
+{
+  "presubmit": [
+    {
+      "name": "test1",
+      "host": true,
+      "include-filter": "testClass#testMethod"
+    }
+  ],
+  "imports": [
+    {
+      "path": "path1//path2//path3"
+    }
+  ]
+}
diff --git a/atest/unittest_data/test_mapping/folder6/test_mapping_sample_with_comments b/atest/unittest_data/test_mapping/folder6/test_mapping_sample_with_comments
new file mode 100644
index 0000000..3f4083f
--- /dev/null
+++ b/atest/unittest_data/test_mapping/folder6/test_mapping_sample_with_comments
@@ -0,0 +1,16 @@
+{#comments1
+  "presubmit": [//comments2 // comments3 # comment4
+  #comments3
+    { #comments4
+      "name": "test1",#comments5
+//comments6
+      "host": true,//comments7
+      "include-filter": "testClass#testMethod" #comment11 // another comments
+    }#comments8
+  ],#comments9 // another comments
+  "imports": [
+    {
+      "path": "path1//path2//path3"#comment12
+    }
+  ]
+}#comments10
diff --git a/clearcut_client/Android.bp b/clearcut_client/Android.bp
new file mode 100644
index 0000000..6b9cbd9
--- /dev/null
+++ b/clearcut_client/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+    name: "tradefed-clearcut-client",
+    defaults: ["tradefed_defaults"],
+    srcs: [
+        "com/**/*.java",
+    ],
+    static_libs: [
+        "protobuf-java-util-prebuilt-jar",
+    ],
+    libs: [
+        "tradefed-protos",
+        "tradefed-common-util",
+        "devtools-annotations-prebuilt",
+    ],
+}
diff --git a/clearcut_client/OWNERS b/clearcut_client/OWNERS
new file mode 100644
index 0000000..01ac8d9
--- /dev/null
+++ b/clearcut_client/OWNERS
@@ -0,0 +1,3 @@
+# Base Owners + extra folks familiar with clearcut and can help reviewing it
+kellyhung@google.com
+yangbill@google.com
diff --git a/clearcut_client/README.md b/clearcut_client/README.md
new file mode 100644
index 0000000..ecac943
--- /dev/null
+++ b/clearcut_client/README.md
@@ -0,0 +1,5 @@
+# Trade Federation Clearcut Client
+
+A Tradefed component for our user metrics collection client.
+
+This directory should only contain classes related to clearcut.
diff --git a/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java b/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java
new file mode 100644
index 0000000..ee1d573
--- /dev/null
+++ b/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.asuite.clearcut.Clientanalytics.ClientInfo;
+import com.android.asuite.clearcut.Clientanalytics.LogEvent;
+import com.android.asuite.clearcut.Clientanalytics.LogRequest;
+import com.android.asuite.clearcut.Clientanalytics.LogResponse;
+import com.android.asuite.clearcut.Common.UserType;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.net.HttpHelper;
+
+import com.google.protobuf.util.JsonFormat;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/** Client that allows reporting usage metrics to clearcut. */
+public class ClearcutClient {
+
+    public static final String DISABLE_CLEARCUT_KEY = "DISABLE_CLEARCUT";
+
+    private static final String CLEARCUT_PROD_URL = "https://play.googleapis.com/log";
+    private static final int CLIENT_TYPE = 1;
+    private static final int INTERNAL_LOG_SOURCE = 971;
+    private static final int EXTERNAL_LOG_SOURCE = 934;
+
+    private static final long SCHEDULER_INITIAL_DELAY_SECONDS = 2;
+    private static final long SCHEDULER_PERDIOC_SECONDS = 30;
+
+    private static final String GOOGLE_EMAIL = "@google.com";
+    private static final String GOOGLE_HOSTNAME = ".google.com";
+
+    private File mCachedUuidFile = new File(System.getProperty("user.home"), ".tradefed");
+    private String mRunId;
+
+    private final int mLogSource;
+    private final String mUrl;
+    private final UserType mUserType;
+
+    // Consider synchronized list
+    private List<LogRequest> mExternalEventQueue;
+    // The pool executor to actually post the metrics
+    private ScheduledThreadPoolExecutor mExecutor;
+    // Whether the clearcut client should be inop
+    private boolean mDisabled = false;
+
+    public ClearcutClient() {
+        this(null);
+    }
+
+    /**
+     * Create Client with customized posting URL and forcing whether it's internal or external user.
+     */
+    @VisibleForTesting
+    protected ClearcutClient(String url) {
+        mDisabled = isClearcutDisabled();
+
+        // We still have to set the 'final' variable so go through the assignments before returning
+        if (!mDisabled && isGoogleUser()) {
+            mLogSource = INTERNAL_LOG_SOURCE;
+            mUserType = UserType.GOOGLE;
+        } else {
+            mLogSource = EXTERNAL_LOG_SOURCE;
+            mUserType = UserType.EXTERNAL;
+        }
+        if (url == null) {
+            mUrl = CLEARCUT_PROD_URL;
+        } else {
+            mUrl = url;
+        }
+        mRunId = UUID.randomUUID().toString();
+        mExternalEventQueue = new ArrayList<>();
+
+        if (mDisabled) {
+            return;
+        }
+
+        // Print the notice
+        System.out.println(NoticeMessageUtil.getNoticeMessage(mUserType));
+
+        // Executor to actually send the events.
+        mExecutor = new ScheduledThreadPoolExecutor(1);
+        Runnable command =
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        flushEvents();
+                    }
+                };
+        mExecutor.scheduleAtFixedRate(
+                command,
+                SCHEDULER_INITIAL_DELAY_SECONDS,
+                SCHEDULER_PERDIOC_SECONDS,
+                TimeUnit.SECONDS);
+    }
+
+    /** Send the first event to notify that Tradefed was started. */
+    public void notifyTradefedStartEvent() {
+        if (mDisabled) {
+            return;
+        }
+        LogRequest.Builder request = createBaseLogRequest();
+        LogEvent.Builder logEvent = LogEvent.newBuilder();
+        logEvent.setEventTimeMs(System.currentTimeMillis());
+        logEvent.setSourceExtension(
+                ClearcutEventHelper.createStartEvent(getGroupingKey(), mRunId, mUserType));
+        request.addLogEvent(logEvent);
+        queueEvent(request.build());
+    }
+
+    /** Stop the periodic sending of clearcut events */
+    public void stop() {
+        if (mExecutor != null) {
+            mExecutor.setRemoveOnCancelPolicy(true);
+            mExecutor.shutdown();
+            mExecutor = null;
+        }
+        // Send all remaining events
+        flushEvents();
+    }
+
+    /** Add an event to the queue of events that needs to be send. */
+    public void queueEvent(LogRequest event) {
+        synchronized (mExternalEventQueue) {
+            mExternalEventQueue.add(event);
+        }
+    }
+
+    /** Returns the current queue size. */
+    public final int getQueueSize() {
+        synchronized (mExternalEventQueue) {
+            return mExternalEventQueue.size();
+        }
+    }
+
+    /** Allows to override the default cached uuid file. */
+    public void setCachedUuidFile(File uuidFile) {
+        mCachedUuidFile = uuidFile;
+    }
+
+    /** Get a new or the cached uuid for the user. */
+    @VisibleForTesting
+    String getGroupingKey() {
+        String uuid = null;
+        if (mCachedUuidFile.exists()) {
+            try {
+                uuid = FileUtil.readStringFromFile(mCachedUuidFile);
+            } catch (IOException e) {
+                CLog.e(e);
+            }
+        }
+        if (uuid == null || uuid.isEmpty()) {
+            uuid = UUID.randomUUID().toString();
+            try {
+                FileUtil.writeToFile(uuid, mCachedUuidFile);
+            } catch (IOException e) {
+                CLog.e(e);
+            }
+        }
+        return uuid;
+    }
+
+    /** Returns True if clearcut is disabled, False otherwise. */
+    @VisibleForTesting
+    boolean isClearcutDisabled() {
+        return "1".equals(System.getenv(DISABLE_CLEARCUT_KEY));
+    }
+
+    /** Returns True if the user is a Googler, False otherwise. */
+    @VisibleForTesting
+    boolean isGoogleUser() {
+        CommandResult gitRes =
+                RunUtil.getDefault()
+                        .runTimedCmdSilently(60000L, "git", "config", "--get", "user.email");
+        if (CommandStatus.SUCCESS.equals(gitRes.getStatus())) {
+            String stdout = gitRes.getStdout();
+            if (stdout != null && stdout.trim().endsWith(GOOGLE_EMAIL)) {
+                return true;
+            }
+        }
+        try {
+            String hostname = InetAddress.getLocalHost().getHostName();
+            if (hostname.contains(GOOGLE_HOSTNAME)) {
+                return true;
+            }
+        } catch (UnknownHostException e) {
+            // Ignore
+        }
+        return false;
+    }
+
+    private LogRequest.Builder createBaseLogRequest() {
+        LogRequest.Builder request = LogRequest.newBuilder();
+        request.setLogSource(mLogSource);
+        request.setClientInfo(ClientInfo.newBuilder().setClientType(CLIENT_TYPE));
+        return request;
+    }
+
+    private void flushEvents() {
+        List<LogRequest> copy = new ArrayList<>();
+        synchronized (mExternalEventQueue) {
+            copy.addAll(mExternalEventQueue);
+            mExternalEventQueue.clear();
+        }
+        while (!copy.isEmpty()) {
+            LogRequest event = copy.remove(0);
+            sendToClearcut(event);
+        }
+    }
+
+    /** Send one event to the configured server. */
+    private void sendToClearcut(LogRequest event) {
+        HttpHelper helper = new HttpHelper();
+
+        InputStream inputStream = null;
+        InputStream errorStream = null;
+        OutputStream outputStream = null;
+        OutputStreamWriter outputStreamWriter = null;
+        try {
+            HttpURLConnection connection = helper.createConnection(new URL(mUrl), "POST", "text");
+            outputStream = connection.getOutputStream();
+            outputStreamWriter = new OutputStreamWriter(outputStream);
+
+            String jsonObject = JsonFormat.printer().preservingProtoFieldNames().print(event);
+            outputStreamWriter.write(jsonObject.toString());
+            outputStreamWriter.flush();
+
+            inputStream = connection.getInputStream();
+            LogResponse response = LogResponse.parseFrom(inputStream);
+
+            errorStream = connection.getErrorStream();
+            if (errorStream != null) {
+                String message = StreamUtil.getStringFromStream(errorStream);
+                CLog.e("Error posting clearcut event: '%s'. LogResponse: '%s'", message, response);
+            }
+        } catch (IOException e) {
+            CLog.e(e);
+        } finally {
+            StreamUtil.close(outputStream);
+            StreamUtil.close(inputStream);
+            StreamUtil.close(outputStreamWriter);
+            StreamUtil.close(errorStream);
+        }
+    }
+}
diff --git a/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java b/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java
new file mode 100644
index 0000000..e8a6ab334
--- /dev/null
+++ b/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+import com.android.asuite.clearcut.Common.UserType;
+import com.android.asuite.clearcut.ExternalUserLog.AtestLogEventExternal;
+import com.android.asuite.clearcut.ExternalUserLog.AtestLogEventExternal.AtestStartEvent;
+import com.android.asuite.clearcut.InternalUserLog.AtestLogEventInternal;
+
+import com.google.protobuf.ByteString;
+
+/** Utility to help populate the event protos */
+public class ClearcutEventHelper {
+
+    private static final String TOOL_NAME = "Tradefed";
+
+    /**
+     * Create the start event for Tradefed.
+     *
+     * @param userKey The unique id representing the user
+     * @param runId The current id for the session.
+     * @param userType The type of the user: internal or external.
+     * @return a ByteString representation of the even proto.
+     */
+    public static ByteString createStartEvent(String userKey, String runId, UserType userType) {
+        if (UserType.GOOGLE.equals(userType)) {
+            AtestLogEventInternal.Builder builder =
+                    createBaseInternalEventBuilder(userKey, runId, userType);
+            AtestLogEventInternal.AtestStartEvent.Builder startEventBuilder =
+                    AtestLogEventInternal.AtestStartEvent.newBuilder();
+            builder.setAtestStartEvent(startEventBuilder.build());
+            return builder.build().toByteString();
+        }
+
+        AtestLogEventExternal.Builder builder =
+                createBaseExternalEventBuilder(userKey, runId, userType);
+        AtestStartEvent.Builder startBuilder = AtestStartEvent.newBuilder();
+        builder.setAtestStartEvent(startBuilder.build());
+        return builder.build().toByteString();
+    }
+
+    /**
+     * Create the basic event builder with all the common informations.
+     *
+     * @param userKey The unique id representing the user
+     * @param runId The current id for the session.
+     * @param userType The type of the user: internal or external.
+     * @return a builder for the event.
+     */
+    private static AtestLogEventExternal.Builder createBaseExternalEventBuilder(
+            String userKey, String runId, UserType userType) {
+        AtestLogEventExternal.Builder builder = AtestLogEventExternal.newBuilder();
+        builder.setUserKey(userKey);
+        builder.setRunId(runId);
+        builder.setUserType(userType);
+        builder.setToolName(TOOL_NAME);
+        return builder;
+    }
+
+    /**
+     * Create the basic event builder with all the common informations.
+     *
+     * @param userKey The unique id representing the user
+     * @param runId The current id for the session.
+     * @param userType The type of the user: internal or external.
+     * @return a builder for the event.
+     */
+    private static AtestLogEventInternal.Builder createBaseInternalEventBuilder(
+            String userKey, String runId, UserType userType) {
+        AtestLogEventInternal.Builder builder = AtestLogEventInternal.newBuilder();
+        builder.setUserKey(userKey);
+        builder.setRunId(runId);
+        builder.setUserType(userType);
+        builder.setToolName(TOOL_NAME);
+        return builder;
+    }
+}
diff --git a/clearcut_client/com/android/tradefed/clearcut/NoticeMessageUtil.java b/clearcut_client/com/android/tradefed/clearcut/NoticeMessageUtil.java
new file mode 100644
index 0000000..75cf601
--- /dev/null
+++ b/clearcut_client/com/android/tradefed/clearcut/NoticeMessageUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+import com.android.asuite.clearcut.Common.UserType;
+
+/** Utility to create the notice message. */
+public class NoticeMessageUtil {
+
+    private static final String INTERNAL_AGREEMENT = "https://cla.developers.google.com/";
+    private static final String EXTERNAL_AGREEMENT = "https://opensource.google.com/docs/cla/";
+    private static final String ANONYMOUS = "anonymous ";
+
+    private static final String NOTICE_MESSAGE =
+            "==================\nNotice:\n"
+                    + "We collect %susage statistics in accordance with our Content Licenses "
+                    + "(https://source.android.com/setup/start/licenses), Contributor License "
+                    + "Agreement (%s), Privacy Policy "
+                    + "(https://policies.google.com/privacy) and Terms of Service "
+                    + "(https://policies.google.com/terms)."
+                    + "\n==================";
+
+    private NoticeMessageUtil() {}
+
+    /** Returns the notice message based on the user type (internal vs external). */
+    public static String getNoticeMessage(UserType type) {
+        if (UserType.EXTERNAL.equals(type)) {
+            return String.format(NOTICE_MESSAGE, ANONYMOUS, EXTERNAL_AGREEMENT);
+        } else {
+            return String.format(NOTICE_MESSAGE, "", INTERNAL_AGREEMENT);
+        }
+    }
+}
diff --git a/clearcut_client/com/android/tradefed/clearcut/TerminateClearcutClient.java b/clearcut_client/com/android/tradefed/clearcut/TerminateClearcutClient.java
new file mode 100644
index 0000000..d5f6204
--- /dev/null
+++ b/clearcut_client/com/android/tradefed/clearcut/TerminateClearcutClient.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+/** Thread that allows to ensure client has been stop via {@link Runtime}. */
+public class TerminateClearcutClient extends Thread {
+
+    private final ClearcutClient mClient;
+
+    public TerminateClearcutClient(ClearcutClient client) {
+        mClient = client;
+    }
+
+    @Override
+    public void run() {
+        // TODO: report the exit event if not already reported
+        mClient.stop();
+    }
+}
diff --git a/common_util/Android.bp b/common_util/Android.bp
new file mode 100644
index 0000000..9457e29
--- /dev/null
+++ b/common_util/Android.bp
@@ -0,0 +1,31 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+    name: "tradefed-common-util",
+    defaults: ["tradefed_defaults"],
+    srcs: [
+        "com/**/*.java",
+    ],
+    static_libs: [
+        "commons-compress-prebuilt",
+    ],
+    libs: [
+        "ddmlib-prebuilt",
+        "guava",
+        "tradefed-protos",
+        "devtools-annotations-prebuilt",
+    ],
+}
+
diff --git a/common_util/OWNERS b/common_util/OWNERS
new file mode 100644
index 0000000..19b5a68
--- /dev/null
+++ b/common_util/OWNERS
@@ -0,0 +1,4 @@
+# Base Owners + extra folks that can review common_util
+allenhair@google.com
+bettyzhou@google.com
+mrosenfeld@google.com
diff --git a/common_util/README.md b/common_util/README.md
new file mode 100644
index 0000000..ac50fd8
--- /dev/null
+++ b/common_util/README.md
@@ -0,0 +1,9 @@
+# Trade Federation Common Util
+
+Set of utilities and classes that are shared across TF components.
+
+This directory should contain classes that are:
+*  Providing a generic service.
+*  A shared data representation.
+
+
diff --git a/src/com/android/tradefed/build/BuildSerializedVersion.java b/common_util/com/android/tradefed/build/BuildSerializedVersion.java
similarity index 100%
rename from src/com/android/tradefed/build/BuildSerializedVersion.java
rename to common_util/com/android/tradefed/build/BuildSerializedVersion.java
diff --git a/src/com/android/tradefed/command/CommandInterrupter.java b/common_util/com/android/tradefed/command/CommandInterrupter.java
similarity index 100%
rename from src/com/android/tradefed/command/CommandInterrupter.java
rename to common_util/com/android/tradefed/command/CommandInterrupter.java
diff --git a/src/com/android/tradefed/command/FatalHostError.java b/common_util/com/android/tradefed/command/FatalHostError.java
similarity index 100%
rename from src/com/android/tradefed/command/FatalHostError.java
rename to common_util/com/android/tradefed/command/FatalHostError.java
diff --git a/src/com/android/tradefed/config/ConfigurationException.java b/common_util/com/android/tradefed/config/ConfigurationException.java
similarity index 100%
rename from src/com/android/tradefed/config/ConfigurationException.java
rename to common_util/com/android/tradefed/config/ConfigurationException.java
diff --git a/src/com/android/tradefed/config/Option.java b/common_util/com/android/tradefed/config/Option.java
similarity index 96%
rename from src/com/android/tradefed/config/Option.java
rename to common_util/com/android/tradefed/config/Option.java
index 7004dfa..736e990 100644
--- a/src/com/android/tradefed/config/Option.java
+++ b/common_util/com/android/tradefed/config/Option.java
@@ -120,4 +120,10 @@
      * ignored completely for options that are {@link Collection}s or {@link Map}s.
      */
     OptionUpdateRule updateRule() default OptionUpdateRule.LAST;
+
+    /**
+     * Internal Only - Do not set. Specify whether the field value was changed from its default
+     * value or not.
+     */
+    boolean isChanged() default false;
 }
diff --git a/src/com/android/tradefed/config/OptionClass.java b/common_util/com/android/tradefed/config/OptionClass.java
similarity index 100%
rename from src/com/android/tradefed/config/OptionClass.java
rename to common_util/com/android/tradefed/config/OptionClass.java
diff --git a/common_util/com/android/tradefed/config/OptionDef.java b/common_util/com/android/tradefed/config/OptionDef.java
new file mode 100644
index 0000000..b6f0a03
--- /dev/null
+++ b/common_util/com/android/tradefed/config/OptionDef.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config;
+
+import com.android.tradefed.build.BuildSerializedVersion;
+
+import java.io.Serializable;
+
+/** Holds the details of an {@link Option}. */
+public final class OptionDef implements Serializable {
+    private static final long serialVersionUID = BuildSerializedVersion.VERSION;
+
+    public final String name;
+    public final String key;
+    public final String value;
+    public final String source;
+    public final String applicableObjectType;
+
+    public OptionDef(String optionName, String optionValue, String source) {
+        this(optionName, null, optionValue, source, null);
+    }
+
+    public OptionDef(String optionName, String optionKey, String optionValue, String source) {
+        this(optionName, optionKey, optionValue, source, null);
+    }
+
+    public OptionDef(
+            String optionName, String optionKey, String optionValue, String source, String type) {
+        this.name = optionName;
+        this.key = optionKey;
+        this.value = optionValue;
+        this.source = source;
+        this.applicableObjectType = type;
+    }
+}
diff --git a/src/com/android/tradefed/config/OptionUpdateRule.java b/common_util/com/android/tradefed/config/OptionUpdateRule.java
similarity index 100%
rename from src/com/android/tradefed/config/OptionUpdateRule.java
rename to common_util/com/android/tradefed/config/OptionUpdateRule.java
diff --git a/src/com/android/tradefed/log/LogUtil.java b/common_util/com/android/tradefed/log/LogUtil.java
similarity index 90%
rename from src/com/android/tradefed/log/LogUtil.java
rename to common_util/com/android/tradefed/log/LogUtil.java
index 1ae13c3..5ae0f84 100644
--- a/src/com/android/tradefed/log/LogUtil.java
+++ b/common_util/com/android/tradefed/log/LogUtil.java
@@ -18,8 +18,6 @@
 
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
-import com.android.tradefed.config.GlobalConfiguration;
-import com.android.tradefed.config.IGlobalConfiguration;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -68,7 +66,6 @@
     public static class CLog {
 
         protected static final String CLASS_NAME = CLog.class.getName();
-        private static IGlobalConfiguration sGlobalConfig = null;
 
         /**
          * The shim version of {@link Log#v(String, String)}.
@@ -264,11 +261,6 @@
          * @param t (Optional) An exception to log. If null, only message will be logged.
          */
         public static void wtf(String message, Throwable t) {
-            ITerribleFailureHandler wtfHandler = getGlobalConfigInstance().getWtfHandler();
-
-            /* since wtf(String, Throwable) can be called directly or through an overloaded
-             * method, ie wtf(String), the stack trace frame of the external class name that
-             * called CLog can vary, so we use findCallerClassName to find it */
             String tag = findCallerClassName();
             String logMessage = "WTF - " + message;
             String stackTrace = getStackTraceString(t);
@@ -277,31 +269,6 @@
             }
 
             Log.logAndDisplay(LogLevel.ASSERT, tag, logMessage);
-            if (wtfHandler != null) {
-                wtfHandler.onTerribleFailure(message, t);
-            }
-        }
-
-        /**
-         * Sets the GlobalConfiguration instance for CLog to use - exposed for unit testing
-         *
-         * @param globalConfig the GlobalConfiguration object for CLog to use
-         */
-        // @VisibleForTesting
-        public static void setGlobalConfigInstance(IGlobalConfiguration globalConfig) {
-            sGlobalConfig = globalConfig;
-        }
-
-        /**
-         * Gets the GlobalConfiguration instance, useful for unit testing
-         *
-         * @return the GlobalConfiguration singleton instance
-         */
-        private static IGlobalConfiguration getGlobalConfigInstance() {
-            if (sGlobalConfig == null) {
-                sGlobalConfig = GlobalConfiguration.getInstance();
-            }
-            return sGlobalConfig;
         }
 
         /**
diff --git a/src/com/android/tradefed/result/ByteArrayInputStreamSource.java b/common_util/com/android/tradefed/result/ByteArrayInputStreamSource.java
similarity index 100%
rename from src/com/android/tradefed/result/ByteArrayInputStreamSource.java
rename to common_util/com/android/tradefed/result/ByteArrayInputStreamSource.java
diff --git a/src/com/android/tradefed/result/FileInputStreamSource.java b/common_util/com/android/tradefed/result/FileInputStreamSource.java
similarity index 100%
rename from src/com/android/tradefed/result/FileInputStreamSource.java
rename to common_util/com/android/tradefed/result/FileInputStreamSource.java
diff --git a/src/com/android/tradefed/result/InputStreamSource.java b/common_util/com/android/tradefed/result/InputStreamSource.java
similarity index 100%
rename from src/com/android/tradefed/result/InputStreamSource.java
rename to common_util/com/android/tradefed/result/InputStreamSource.java
diff --git a/src/com/android/tradefed/result/LogDataType.java b/common_util/com/android/tradefed/result/LogDataType.java
similarity index 96%
rename from src/com/android/tradefed/result/LogDataType.java
rename to common_util/com/android/tradefed/result/LogDataType.java
index 9589c90..2024bf1 100644
--- a/src/com/android/tradefed/result/LogDataType.java
+++ b/common_util/com/android/tradefed/result/LogDataType.java
@@ -27,6 +27,8 @@
     MP4("mp4", "video/mp4", true, false),
     EAR("ear", "application/octet-stream", true, false),
     ZIP("zip", "application/zip", true, false),
+    SEVEN_Z("7z", "application/x-7z-compressed", true, false),
+    BITS("bits", "application/octet-stream", true, false),
     JPEG("jpeg", "image/jpeg", true, false),
     TAR_GZ("tar.gz", "application/gzip", true, false),
     GZIP("gz", "application/gzip", true, false),
diff --git a/src/com/android/tradefed/testtype/Abi.java b/common_util/com/android/tradefed/testtype/Abi.java
similarity index 100%
rename from src/com/android/tradefed/testtype/Abi.java
rename to common_util/com/android/tradefed/testtype/Abi.java
diff --git a/src/com/android/tradefed/testtype/IAbi.java b/common_util/com/android/tradefed/testtype/IAbi.java
similarity index 100%
rename from src/com/android/tradefed/testtype/IAbi.java
rename to common_util/com/android/tradefed/testtype/IAbi.java
diff --git a/src/com/android/tradefed/util/AbiUtils.java b/common_util/com/android/tradefed/util/AbiUtils.java
similarity index 100%
rename from src/com/android/tradefed/util/AbiUtils.java
rename to common_util/com/android/tradefed/util/AbiUtils.java
diff --git a/src/com/android/tradefed/util/ArrayUtil.java b/common_util/com/android/tradefed/util/ArrayUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/ArrayUtil.java
rename to common_util/com/android/tradefed/util/ArrayUtil.java
diff --git a/src/com/android/tradefed/util/ByteArrayList.java b/common_util/com/android/tradefed/util/ByteArrayList.java
similarity index 100%
rename from src/com/android/tradefed/util/ByteArrayList.java
rename to common_util/com/android/tradefed/util/ByteArrayList.java
diff --git a/common_util/com/android/tradefed/util/ByteArrayUtil.java b/common_util/com/android/tradefed/util/ByteArrayUtil.java
new file mode 100644
index 0000000..510321b
--- /dev/null
+++ b/common_util/com/android/tradefed/util/ByteArrayUtil.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Utilities to operate on byte array, e.g., convert bytes to integer.
+ *
+ * <p>Java doesn't have an unsigned value type, so expansion is needed to convert an unsigned
+ * integer stored in 4 bytes to a long value, or unsigned short stored in 2 bytes to an integer
+ * value.
+ */
+public class ByteArrayUtil {
+
+    /**
+     * Get a {@link ByteBuffer} for the given bytes wrapped in a byte array of given size.
+     *
+     * <p>java doesn't have an unsigned value type, so expansion is needed to convert an unsigned
+     * short stored in 2 bytes to an integer value.
+     *
+     * @param bytes an array of bytes.
+     * @param offset the start offset of the integer data.
+     * @param length the length of the integer data.
+     * @param containerSize the size of the array to store the given bytes, append zero to unfilled
+     *     items.
+     * @return a {@link ByteBuffer} for the given bytes wrapped in a byte array of given size.
+     */
+    private static ByteBuffer getByteBuffer(
+            byte[] bytes, int offset, int length, int containerSize) {
+        byte[] data = new byte[containerSize];
+        for (int i = 0; i < length; i++) {
+            data[i] = bytes[offset + i];
+        }
+        return ByteBuffer.wrap(data).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+    }
+
+    /**
+     * Get an integer from the given bytes.
+     *
+     * <p>java doesn't have an unsigned value type, so expansion is needed to convert an unsigned
+     * short stored in 2 bytes to an integer value.
+     *
+     * @param bytes an array of bytes.
+     * @param offset the start offset of the integer data.
+     * @param length the length of the integer data.
+     * @return an int value from the given bytes.
+     */
+    public static int getInt(byte[] bytes, int offset, int length) {
+        return getByteBuffer(bytes, offset, length, 4).getInt();
+    }
+
+    /**
+     * Get a long value from the given bytes.
+     *
+     * <p>java doesn't have an unsigned value type, so expansion is needed to convert an unsigned
+     * integer stored in 4 bytes to a long value.
+     *
+     * @param bytes an array of bytes.
+     * @param offset the start offset of the long value.
+     * @param length the length of the long value.
+     * @return a long value from the given bytes.
+     */
+    public static long getLong(byte[] bytes, int offset, int length) {
+        return getByteBuffer(bytes, offset, length, 8).getLong();
+    }
+
+    /**
+     * Get the string from the given bytes.
+     *
+     * @param bytes an array of bytes.
+     * @param offset the start offset of the string data.
+     * @param length the length of the string data.
+     */
+    public static String getString(byte[] bytes, int offset, int length) {
+        return new String(Arrays.copyOfRange(bytes, offset, offset + length));
+    }
+}
diff --git a/src/com/android/tradefed/util/CommandResult.java b/common_util/com/android/tradefed/util/CommandResult.java
similarity index 100%
rename from src/com/android/tradefed/util/CommandResult.java
rename to common_util/com/android/tradefed/util/CommandResult.java
diff --git a/src/com/android/tradefed/util/CommandStatus.java b/common_util/com/android/tradefed/util/CommandStatus.java
similarity index 100%
rename from src/com/android/tradefed/util/CommandStatus.java
rename to common_util/com/android/tradefed/util/CommandStatus.java
diff --git a/src/com/android/tradefed/util/Email.java b/common_util/com/android/tradefed/util/Email.java
similarity index 100%
rename from src/com/android/tradefed/util/Email.java
rename to common_util/com/android/tradefed/util/Email.java
diff --git a/src/com/android/tradefed/util/FileUtil.java b/common_util/com/android/tradefed/util/FileUtil.java
similarity index 96%
rename from src/com/android/tradefed/util/FileUtil.java
rename to common_util/com/android/tradefed/util/FileUtil.java
index 9d919e9..2034972 100644
--- a/src/com/android/tradefed/util/FileUtil.java
+++ b/common_util/com/android/tradefed/util/FileUtil.java
@@ -1,3 +1,4 @@
+package com.android.tradefed.util;
 /*
  * Copyright (C) 2010 The Android Open Source Project
  *
@@ -13,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tradefed.util;
+
 
 import com.android.ddmlib.Log;
 import com.android.tradefed.command.FatalHostError;
@@ -615,12 +616,29 @@
      */
     public static void writeToFile(
             InputStream input, File destFile, boolean append) throws IOException {
+        // Set size to a negative value to write all content starting at the given offset.
+        writeToFile(input, destFile, append, 0, -1);
+    }
+
+    /**
+     * A helper method for writing stream data to file
+     *
+     * @param input the unbuffered input stream
+     * @param destFile the destination file to write or append to
+     * @param append append to end of file if true, overwrite otherwise
+     * @param startOffset the start offset of the input stream to retrieve data
+     * @param size number of bytes to retrieve from the input stream, set it to a negative value to
+     *     retrieve all content starting at the given offset.
+     */
+    public static void writeToFile(
+            InputStream input, File destFile, boolean append, long startOffset, long size)
+            throws IOException {
         InputStream origStream = null;
         OutputStream destStream = null;
         try {
             origStream = new BufferedInputStream(input);
             destStream = new BufferedOutputStream(new FileOutputStream(destFile, append));
-            StreamUtil.copyStreams(origStream, destStream);
+            StreamUtil.copyStreams(origStream, destStream, startOffset, size);
         } finally {
             StreamUtil.close(origStream);
             StreamUtil.flushAndCloseStream(destStream);
@@ -1023,6 +1041,19 @@
     }
 
     /**
+     * Helper method to calculate CRC-32 for a file.
+     *
+     * @param file
+     * @return CRC-32 of the file
+     * @throws IOException
+     */
+    public static long calculateCrc32(File file) throws IOException {
+        try (BufferedInputStream inputSource = new BufferedInputStream(new FileInputStream(file))) {
+            return StreamUtil.calculateCrc32(inputSource);
+        }
+    }
+
+    /**
      * Helper method to calculate md5 for a file.
      *
      * @param file
diff --git a/src/com/android/tradefed/util/IEmail.java b/common_util/com/android/tradefed/util/IEmail.java
similarity index 100%
rename from src/com/android/tradefed/util/IEmail.java
rename to common_util/com/android/tradefed/util/IEmail.java
diff --git a/src/com/android/tradefed/util/IRunUtil.java b/common_util/com/android/tradefed/util/IRunUtil.java
similarity index 99%
rename from src/com/android/tradefed/util/IRunUtil.java
rename to common_util/com/android/tradefed/util/IRunUtil.java
index 3a36b7d..6b73d8d 100644
--- a/src/com/android/tradefed/util/IRunUtil.java
+++ b/common_util/com/android/tradefed/util/IRunUtil.java
@@ -17,7 +17,6 @@
 package com.android.tradefed.util;
 
 import com.android.annotations.Nullable;
-import com.android.tradefed.command.CommandScheduler;
 
 import java.io.File;
 import java.io.IOException;
diff --git a/src/com/android/tradefed/util/MultiMap.java b/common_util/com/android/tradefed/util/MultiMap.java
similarity index 97%
rename from src/com/android/tradefed/util/MultiMap.java
rename to common_util/com/android/tradefed/util/MultiMap.java
index b592440..51c2f64 100644
--- a/src/com/android/tradefed/util/MultiMap.java
+++ b/common_util/com/android/tradefed/util/MultiMap.java
@@ -48,6 +48,13 @@
         }
     }
 
+    public MultiMap(Map<K, V> map) {
+        this();
+        for (K key : map.keySet()) {
+            put(key, map.get(key));
+        }
+    }
+
     /**
      * Clears the map.
      */
diff --git a/src/com/android/tradefed/util/RunInterruptedException.java b/common_util/com/android/tradefed/util/RunInterruptedException.java
similarity index 100%
rename from src/com/android/tradefed/util/RunInterruptedException.java
rename to common_util/com/android/tradefed/util/RunInterruptedException.java
diff --git a/src/com/android/tradefed/util/RunUtil.java b/common_util/com/android/tradefed/util/RunUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/RunUtil.java
rename to common_util/com/android/tradefed/util/RunUtil.java
diff --git a/src/com/android/tradefed/util/StreamUtil.java b/common_util/com/android/tradefed/util/StreamUtil.java
similarity index 78%
rename from src/com/android/tradefed/util/StreamUtil.java
rename to common_util/com/android/tradefed/util/StreamUtil.java
index 2f1d009..f6fce21 100644
--- a/src/com/android/tradefed/util/StreamUtil.java
+++ b/common_util/com/android/tradefed/util/StreamUtil.java
@@ -23,6 +23,8 @@
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -35,6 +37,7 @@
 import java.security.NoSuchAlgorithmException;
 import java.util.Base64;
 import java.util.Objects;
+import java.util.zip.CRC32;
 import java.util.zip.GZIPOutputStream;
 import java.util.zip.ZipOutputStream;
 
@@ -170,16 +173,51 @@
      *
      * @param inStream the {@link InputStream}
      * @param outStream the {@link OutputStream}
-     * @param offset The offset of when to start copying the data.
+     * @param offset the offset of when to start copying the data.
      * @throws IOException
      */
     public static void copyStreams(InputStream inStream, OutputStream outStream, int offset)
             throws IOException {
+        // Set size to a negative value to copy all content starting at the given offset.
+        copyStreams(inStream, outStream, offset, -1);
+    }
+
+    /**
+     * Copies contents of origStream to destStream starting at a given offset with a specific size.
+     *
+     * <p>Recommended to provide a buffered stream for input and output
+     *
+     * @param inStream the {@link InputStream}
+     * @param outStream the {@link OutputStream}
+     * @param offset the offset of when to start copying the data.
+     * @param size the number of bytes to copy. A negative value means to copy all content.
+     * @throws IOException
+     */
+    public static void copyStreams(
+            InputStream inStream, OutputStream outStream, long offset, long size)
+            throws IOException {
+        assert offset >= 0 : "offset must be greater or equal to zero.";
+        assert size != 0 : "size cannot be zero.";
         inStream.skip(offset);
         byte[] buf = new byte[BUF_SIZE];
-        int size = -1;
-        while ((size = inStream.read(buf)) != -1) {
-            outStream.write(buf, 0, size);
+        long totalRetrievedSize = 0;
+        int retrievedSize = -1;
+        while ((retrievedSize = inStream.read(buf)) != -1) {
+            if (size > 0 && size < totalRetrievedSize + retrievedSize) {
+                retrievedSize = (int) (size - totalRetrievedSize);
+            }
+            outStream.write(buf, 0, retrievedSize);
+            totalRetrievedSize += retrievedSize;
+            if (size == totalRetrievedSize) {
+                break;
+            }
+        }
+        if (size > 0 && size > totalRetrievedSize) {
+            throw new IOException(
+                    String.format(
+                            "Failed to read %d bytes starting at offset %d, only %d bytes "
+                                    + "retrieved.",
+                            size, offset, totalRetrievedSize));
         }
     }
 
@@ -201,6 +239,24 @@
     }
 
     /**
+     * Copies contents of file to outStream. It is recommended to provide a buffered stream.
+     *
+     * @param file the {@link File}
+     * @param outStream the {@link OutputStream}
+     * @throws IOException
+     */
+    public static void copyFileToStream(File file, OutputStream outStream) throws IOException {
+        InputStream inStream = null;
+        try {
+            inStream = new FileInputStream(file);
+            inStream = new BufferedInputStream(inStream);
+            StreamUtil.copyStreams(inStream, outStream);
+        } finally {
+            StreamUtil.close(inStream);
+        }
+    }
+
+    /**
      * Gets the stack trace as a {@link String}.
      *
      * @param throwable the {@link Throwable} to convert.
@@ -314,6 +370,28 @@
     }
 
     /**
+     * Helper method to calculate CRC-32 for an {@link InputStream}. The stream will be consumed and
+     * closed. It is recommended to provide a buffered stream.
+     *
+     * @param inStream the {@link InputStream}
+     * @return CRC-32 of the stream
+     * @throws IOException
+     */
+    public static long calculateCrc32(InputStream inStream) throws IOException {
+        CRC32 crc32 = new CRC32();
+        byte[] buf = new byte[BUF_SIZE];
+        int size = -1;
+        try {
+            while ((size = inStream.read(buf)) >= 0) {
+                crc32.update(buf, 0, size);
+            }
+        } finally {
+            inStream.close();
+        }
+        return crc32.getValue();
+    }
+
+    /**
      * Helper method to calculate md5 for a inputStream. The inputStream will be consumed and
      * closed.
      *
diff --git a/src/com/android/tradefed/util/TimeUtil.java b/common_util/com/android/tradefed/util/TimeUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/TimeUtil.java
rename to common_util/com/android/tradefed/util/TimeUtil.java
diff --git a/src/com/android/tradefed/util/UniqueMultiMap.java b/common_util/com/android/tradefed/util/UniqueMultiMap.java
similarity index 100%
rename from src/com/android/tradefed/util/UniqueMultiMap.java
rename to common_util/com/android/tradefed/util/UniqueMultiMap.java
diff --git a/src/com/android/tradefed/util/VersionParser.java b/common_util/com/android/tradefed/util/VersionParser.java
similarity index 100%
rename from src/com/android/tradefed/util/VersionParser.java
rename to common_util/com/android/tradefed/util/VersionParser.java
diff --git a/common_util/com/android/tradefed/util/ZipUtil.java b/common_util/com/android/tradefed/util/ZipUtil.java
new file mode 100644
index 0000000..6204b8a
--- /dev/null
+++ b/common_util/com/android/tradefed/util/ZipUtil.java
@@ -0,0 +1,575 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.zip.CentralDirectoryInfo;
+import com.android.tradefed.util.zip.EndCentralDirectoryInfo;
+import com.android.tradefed.util.zip.LocalFileHeader;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.zip.DataFormatException;
+import java.util.zip.GZIPOutputStream;
+import java.util.zip.Inflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * A helper class for compression-related operations
+ */
+public class ZipUtil {
+
+    private static final int COMPRESSION_METHOD_STORED = 0;
+    private static final int COMPRESSION_METHOD_DEFLATE = 8;
+    private static final String DEFAULT_DIRNAME = "dir";
+    private static final String DEFAULT_FILENAME = "files";
+    private static final String ZIP_EXTENSION = ".zip";
+    private static final String PARTIAL_ZIP_DATA = "compressed_data";
+
+    private static final boolean IS_UNIX;
+
+    static {
+        String OS = System.getProperty("os.name").toLowerCase();
+        IS_UNIX = (OS.contains("nix") || OS.contains("nux") || OS.contains("aix"));
+    }
+
+    /**
+     * Utility method to verify that a zip file is not corrupt.
+     *
+     * @param zipFile the {@link File} to check
+     * @param thorough Whether to attempt to fully extract the archive.  If {@code false}, this
+     *        method will fail to detect CRC errors in a well-formed archive.
+     * @throws IOException if the file could not be opened or read
+     * @return {@code false} if the file appears to be corrupt; {@code true} otherwise
+     */
+    public static boolean isZipFileValid(File zipFile, boolean thorough) throws IOException {
+        if (zipFile != null && !zipFile.exists()) {
+            CLog.d("Zip file does not exist: %s", zipFile.getAbsolutePath());
+            return false;
+        }
+
+        try (ZipFile z = new ZipFile(zipFile)) {
+            if (thorough) {
+                // Reading the entire file is the only way to detect CRC errors within the archive
+                final File extractDir = FileUtil.createTempDir("extract-" + zipFile.getName());
+                try {
+                    extractZip(z, extractDir);
+                } finally {
+                    FileUtil.recursiveDelete(extractDir);
+                }
+            }
+        } catch (ZipException e) {
+            // File is likely corrupted
+            CLog.d("Detected corrupt zip file %s:", zipFile.getCanonicalPath());
+            CLog.e(e);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Utility method to extract entire contents of zip file into given directory
+     *
+     * @param zipFile the {@link ZipFile} to extract
+     * @param destDir the local dir to extract file to
+     * @throws IOException if failed to extract file
+     */
+    public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
+        Enumeration<? extends ZipEntry> entries = zipFile.entries();
+        while (entries.hasMoreElements()) {
+
+            ZipEntry entry = entries.nextElement();
+            File childFile = new File(destDir, entry.getName());
+            childFile.getParentFile().mkdirs();
+            if (entry.isDirectory()) {
+                continue;
+            } else {
+                FileUtil.writeToFile(zipFile.getInputStream(entry), childFile);
+            }
+        }
+    }
+
+    /**
+     * Utility method to extract one specific file from zip file into a tmp file
+     *
+     * @param zipFile the {@link ZipFile} to extract
+     * @param filePath the filePath of to extract
+     * @throws IOException if failed to extract file
+     * @return the {@link File} or null if not found
+     */
+    public static File extractFileFromZip(ZipFile zipFile, String filePath) throws IOException {
+        ZipEntry entry = zipFile.getEntry(filePath);
+        if (entry == null) {
+            return null;
+        }
+        File createdFile = FileUtil.createTempFile("extracted",
+                FileUtil.getExtension(filePath));
+        FileUtil.writeToFile(zipFile.getInputStream(entry), createdFile);
+        return createdFile;
+    }
+
+    /**
+     * Utility method to create a temporary zip file containing the given directory and
+     * all its contents.
+     *
+     * @param dir the directory to zip
+     * @return a temporary zip {@link File} containing directory contents
+     * @throws IOException if failed to create zip file
+     */
+    public static File createZip(File dir) throws IOException {
+        return createZip(dir, DEFAULT_DIRNAME);
+    }
+
+    /**
+     * Utility method to create a temporary zip file containing the given directory and
+     * all its contents.
+     *
+     * @param dir the directory to zip
+     * @param name the base name of the zip file created without the extension.
+     * @return a temporary zip {@link File} containing directory contents
+     * @throws IOException if failed to create zip file
+     */
+    public static File createZip(File dir, String name) throws IOException {
+        File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
+        createZip(dir, zipFile);
+        return zipFile;
+    }
+
+    /**
+     * Utility method to create a zip file containing the given directory and
+     * all its contents.
+     *
+     * @param dir the directory to zip
+     * @param zipFile the zip file to create - it should not already exist
+     * @throws IOException if failed to create zip file
+     */
+    public static void createZip(File dir, File zipFile) throws IOException {
+        ZipOutputStream out = null;
+        try {
+            FileOutputStream fileStream = new FileOutputStream(zipFile);
+            out = new ZipOutputStream(new BufferedOutputStream(fileStream));
+            addToZip(out, dir, new LinkedList<String>());
+        } catch (IOException e) {
+            zipFile.delete();
+            throw e;
+        } catch (RuntimeException e) {
+            zipFile.delete();
+            throw e;
+        } finally {
+            StreamUtil.close(out);
+        }
+    }
+
+    /**
+     * Utility method to create a temporary zip file containing the given files
+     *
+     * @param files list of files to zip
+     * @return a temporary zip {@link File} containing directory contents
+     * @throws IOException if failed to create zip file
+     */
+    public static File createZip(List<File> files) throws IOException {
+        return createZip(files, DEFAULT_FILENAME);
+    }
+
+    /**
+     * Utility method to create a temporary zip file containing the given files.
+     *
+     * @param files list of files to zip
+     * @param name the base name of the zip file created without the extension.
+     * @return a temporary zip {@link File} containing directory contents
+     * @throws IOException if failed to create zip file
+     */
+    public static File createZip(List<File> files, String name) throws IOException {
+        File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
+        createZip(files, zipFile);
+        return zipFile;
+    }
+
+    /**
+     * Utility method to create a zip file containing the given files
+     *
+     * @param files list of files to zip
+     * @param zipFile the zip file to create - it should not already exist
+     * @throws IOException if failed to create zip file
+     */
+    public static void createZip(List<File> files, File zipFile) throws IOException {
+        ZipOutputStream out = null;
+        try {
+            FileOutputStream fileStream = new FileOutputStream(zipFile);
+            out = new ZipOutputStream(new BufferedOutputStream(fileStream));
+            for (File file : files) {
+                addToZip(out, file, new LinkedList<String>());
+            }
+        } catch (IOException|RuntimeException e) {
+            zipFile.delete();
+            throw e;
+        } finally {
+            StreamUtil.close(out);
+        }
+    }
+
+    /**
+     * Recursively adds given file and its contents to ZipOutputStream
+     *
+     * @param out the {@link ZipOutputStream}
+     * @param file the {@link File} to add to the stream
+     * @param relativePathSegs the relative path of file, including separators
+     * @throws IOException if failed to add file to zip
+     */
+    public static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
+            throws IOException {
+        relativePathSegs.add(file.getName());
+        if (file.isDirectory()) {
+            // note: it appears even on windows, ZipEntry expects '/' as a path separator
+            relativePathSegs.add("/");
+        }
+        ZipEntry zipEntry = new ZipEntry(buildPath(relativePathSegs));
+        out.putNextEntry(zipEntry);
+        if (file.isFile()) {
+            writeToStream(file, out);
+        }
+        out.closeEntry();
+        if (file.isDirectory()) {
+            // recursively add contents
+            File[] subFiles = file.listFiles();
+            if (subFiles == null) {
+                throw new IOException(String.format("Could not read directory %s",
+                        file.getAbsolutePath()));
+            }
+            for (File subFile : subFiles) {
+                addToZip(out, subFile, relativePathSegs);
+            }
+            // remove the path separator
+            relativePathSegs.remove(relativePathSegs.size()-1);
+        }
+        // remove the last segment, added at beginning of method
+        relativePathSegs.remove(relativePathSegs.size()-1);
+    }
+
+    /**
+     * Close an open {@link ZipFile}, ignoring any exceptions.
+     *
+     * @param zipFile the file to close
+     */
+    public static void closeZip(ZipFile zipFile) {
+        if (zipFile != null) {
+            try {
+                zipFile.close();
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+    }
+
+    /**
+     * Helper method to create a gzipped version of a single file.
+     *
+     * @param file the original file
+     * @param gzipFile the file to place compressed contents in
+     * @throws IOException
+     */
+    public static void gzipFile(File file, File gzipFile) throws IOException {
+        GZIPOutputStream out = null;
+        try {
+            FileOutputStream fileStream = new FileOutputStream(gzipFile);
+            out = new GZIPOutputStream(new BufferedOutputStream(fileStream, 64 * 1024));
+            writeToStream(file, out);
+        } catch (IOException e) {
+            gzipFile.delete();
+            throw e;
+        } catch (RuntimeException e) {
+            gzipFile.delete();
+            throw e;
+        } finally {
+            StreamUtil.close(out);
+        }
+    }
+
+    /**
+     * Helper method to write input file contents to output stream.
+     *
+     * @param file the input {@link File}
+     * @param out the {@link OutputStream}
+     *
+     * @throws IOException
+     */
+    private static void writeToStream(File file, OutputStream out) throws IOException {
+        InputStream inputStream = null;
+        try {
+            inputStream = new BufferedInputStream(new FileInputStream(file));
+            StreamUtil.copyStreams(inputStream, out);
+        } finally {
+            StreamUtil.close(inputStream);
+        }
+    }
+
+    /**
+     * Builds a file system path from a stack of relative path segments
+     *
+     * @param relativePathSegs the list of relative paths
+     * @return a {@link String} containing all relativePathSegs
+     */
+    private static String buildPath(List<String> relativePathSegs) {
+        StringBuilder pathBuilder = new StringBuilder();
+        for (String segment : relativePathSegs) {
+            pathBuilder.append(segment);
+        }
+        return pathBuilder.toString();
+    }
+
+    /**
+     * Extract a zip file to a temp directory prepended with a string
+     *
+     * @param zipFile the zip file to extract
+     * @param nameHint a prefix for the temp directory
+     * @return a {@link File} pointing to the temp directory
+     */
+    public static File extractZipToTemp(File zipFile, String nameHint)
+            throws IOException, ZipException {
+        File localRootDir = FileUtil.createTempDir(nameHint);
+        try (ZipFile zip = new ZipFile(zipFile)) {
+            extractZip(zip, localRootDir);
+            return localRootDir;
+        } catch (IOException e) {
+            // clean tmp file since we couldn't extract.
+            FileUtil.recursiveDelete(localRootDir);
+            throw e;
+        }
+    }
+
+    /**
+     * Get a list of {link CentralDirectoryInfo} for files in a zip file.
+     *
+     * @param partialZipFile a {@link File} object of the partial zip file that contains central
+     *     directory entries.
+     * @param endCentralDirInfo a {@link EndCentralDirectoryInfo} object of the zip file.
+     * @return A list of {@link CentralDirectoryInfo} of the zip file
+     * @throws IOException
+     */
+    public static List<CentralDirectoryInfo> getZipCentralDirectoryInfos(
+            File partialZipFile, EndCentralDirectoryInfo endCentralDirInfo) throws IOException {
+        return getZipCentralDirectoryInfos(partialZipFile, endCentralDirInfo, 0);
+    }
+
+    /**
+     * Get a list of {link CentralDirectoryInfo} for files in a zip file.
+     *
+     * @param partialZipFile a {@link File} object of the partial zip file that contains central
+     *     directory entries.
+     * @param endCentralDirInfo a {@link EndCentralDirectoryInfo} object of the zip file.
+     * @param offset the offset in the partial zip file where the content of central directory
+     *     entries starts.
+     * @return A list of {@link CentralDirectoryInfo} of the zip file
+     * @throws IOException
+     */
+    public static List<CentralDirectoryInfo> getZipCentralDirectoryInfos(
+            File partialZipFile, EndCentralDirectoryInfo endCentralDirInfo, long offset)
+            throws IOException {
+        List<CentralDirectoryInfo> infos = new ArrayList<>();
+        byte[] data;
+        try (FileInputStream stream = new FileInputStream(partialZipFile)) {
+            // Read in the entire central directory block for a zip file till the end. The block
+            // should be small even for a large zip file.
+            long totalSize = stream.getChannel().size();
+            stream.skip(offset);
+            data = new byte[(int) (totalSize - offset)];
+            stream.read(data);
+        }
+        int startOffset = 0;
+        for (int i = 0; i < endCentralDirInfo.getEntryNumber(); i++) {
+            CentralDirectoryInfo info = new CentralDirectoryInfo(data, startOffset);
+            infos.add(info);
+            startOffset += info.getInfoSize();
+        }
+
+        return infos;
+    }
+
+    /**
+     * Apply the file permission configured in the central directory entry.
+     *
+     * @param targetFile the {@link File} to set permission to.
+     * @param zipEntry a {@link CentralDirectoryInfo} object that contains the file permissions.
+     * @throws IOException if fail to access the file.
+     */
+    public static void applyPermission(File targetFile, CentralDirectoryInfo zipEntry)
+            throws IOException {
+        if (!IS_UNIX) {
+            CLog.w("Permission setting is only supported in Unix/Linux system.");
+            return;
+        }
+
+        if (zipEntry.getFilePermission() != 0) {
+            Files.setPosixFilePermissions(
+                    targetFile.toPath(), FileUtil.unixModeToPosix(zipEntry.getFilePermission()));
+        }
+    }
+
+    /**
+     * Extract the requested folder from a partial zip file and apply proper permission.
+     *
+     * @param targetFile the {@link File} to save the extracted file to.
+     * @param zipEntry a {@link CentralDirectoryInfo} object of the file to extract from the partial
+     *     zip file.
+     * @throws IOException
+     */
+    public static void unzipPartialZipFolder(File targetFile, CentralDirectoryInfo zipEntry)
+            throws IOException {
+        unzipPartialZipFile(null, targetFile, zipEntry, null, -1);
+    }
+
+    /**
+     * Extract the requested file from a partial zip file.
+     *
+     * <p>This method assumes all files are on the same disk when compressed. It doesn't support
+     * following features yet:
+     *
+     * <p>Zip file larger than 4GB
+     *
+     * <p>ZIP64(require ZipLocalFileHeader update on compressed size)
+     *
+     * <p>Encrypted zip file
+     *
+     * <p>Symlink
+     *
+     * @param partialZip a {@link File} that's a partial of the zip file.
+     * @param targetFile the {@link File} to save the extracted file to.
+     * @param zipEntry a {@link CentralDirectoryInfo} object of the file to extract from the partial
+     *     zip file.
+     * @param localFileHeader a {@link LocalFileHeader} object of the file to extract from the
+     *     partial zip file.
+     * @param startOffset start offset of the file to extract.
+     * @throws IOException
+     */
+    public static void unzipPartialZipFile(
+            File partialZip,
+            File targetFile,
+            CentralDirectoryInfo zipEntry,
+            LocalFileHeader localFileHeader,
+            long startOffset)
+            throws IOException {
+        try {
+            if (zipEntry.getFileName().endsWith("/")) {
+                // Create a folder.
+                targetFile.mkdir();
+                return;
+            } else if (zipEntry.getCompressedSize() == 0) {
+                // The file is empty, just create an empty file.
+                targetFile.createNewFile();
+                return;
+            }
+
+            File zipFile = targetFile;
+            if (zipEntry.getCompressionMethod() != COMPRESSION_METHOD_STORED)
+                // Create a temp file to store the compressed data, then unzip it.
+                zipFile = FileUtil.createTempFile(PARTIAL_ZIP_DATA, ZIP_EXTENSION);
+            else {
+                // The file is not compressed, stream it directly to the target.
+                zipFile.getParentFile().mkdirs();
+                zipFile.createNewFile();
+            }
+
+            // Save compressed data to zipFile
+            try (FileInputStream stream = new FileInputStream(partialZip)) {
+                FileUtil.writeToFile(
+                        stream,
+                        zipFile,
+                        false,
+                        startOffset + localFileHeader.getHeaderSize(),
+                        zipEntry.getCompressedSize());
+            }
+
+            if (zipEntry.getCompressionMethod() == COMPRESSION_METHOD_STORED) {
+                return;
+            } else if (zipEntry.getCompressionMethod() == COMPRESSION_METHOD_DEFLATE) {
+                boolean success = false;
+                try {
+                    unzipRawZip(zipFile, targetFile, zipEntry);
+                    success = true;
+                } catch (DataFormatException e) {
+                    throw new IOException(e);
+                } finally {
+                    zipFile.delete();
+                    if (!success) {
+                        CLog.e("Failed to unzip %s", zipEntry.getFileName());
+                        targetFile.delete();
+                    }
+                }
+            } else {
+                throw new RuntimeException(
+                        String.format(
+                                "Compression method %d is not supported.",
+                                localFileHeader.getCompressionMethod()));
+            }
+        } finally {
+            if (targetFile.exists()) {
+                applyPermission(targetFile, zipEntry);
+            }
+        }
+    }
+
+    /**
+     * Unzip the raw compressed content without wrapper (local file header).
+     *
+     * @param zipFile the {@link File} that contains the compressed data of the target file.
+     * @param targetFile {@link File} to same the decompressed data to.
+     * @throws DataFormatException if decompression failed due to zip format issue.
+     * @throws IOException if failed to access the compressed data or the decompressed file has
+     *     mismatched CRC.
+     */
+    private static void unzipRawZip(File zipFile, File targetFile, CentralDirectoryInfo zipEntry)
+            throws IOException, DataFormatException {
+        Inflater decompresser = new Inflater(true);
+
+        targetFile.getParentFile().mkdirs();
+        targetFile.createNewFile();
+
+        try (FileInputStream inputStream = new FileInputStream(zipFile);
+                FileOutputStream outputStream = new FileOutputStream(targetFile)) {
+            byte[] data = new byte[32768];
+            byte[] buffer = new byte[65536];
+            while (inputStream.read(data) > 0) {
+                decompresser.setInput(data);
+                while (!decompresser.finished() && !decompresser.needsInput()) {
+                    int size = decompresser.inflate(buffer);
+                    outputStream.write(buffer, 0, size);
+                }
+            }
+        } finally {
+            decompresser.end();
+        }
+
+        // Validate CRC
+        if (FileUtil.calculateCrc32(targetFile) != zipEntry.getCrc()) {
+            throw new IOException(String.format("Failed to match CRC for file %s", targetFile));
+        }
+    }
+}
diff --git a/src/com/android/tradefed/util/ZipUtil2.java b/common_util/com/android/tradefed/util/ZipUtil2.java
similarity index 100%
rename from src/com/android/tradefed/util/ZipUtil2.java
rename to common_util/com/android/tradefed/util/ZipUtil2.java
diff --git a/src/com/android/tradefed/util/net/HttpHelper.java b/common_util/com/android/tradefed/util/net/HttpHelper.java
similarity index 100%
rename from src/com/android/tradefed/util/net/HttpHelper.java
rename to common_util/com/android/tradefed/util/net/HttpHelper.java
diff --git a/src/com/android/tradefed/util/net/HttpMultipartPost.java b/common_util/com/android/tradefed/util/net/HttpMultipartPost.java
similarity index 100%
rename from src/com/android/tradefed/util/net/HttpMultipartPost.java
rename to common_util/com/android/tradefed/util/net/HttpMultipartPost.java
diff --git a/src/com/android/tradefed/util/net/IHttpHelper.java b/common_util/com/android/tradefed/util/net/IHttpHelper.java
similarity index 100%
rename from src/com/android/tradefed/util/net/IHttpHelper.java
rename to common_util/com/android/tradefed/util/net/IHttpHelper.java
diff --git a/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
new file mode 100644
index 0000000..49ec5a7
--- /dev/null
+++ b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util.zip;
+
+import com.android.tradefed.util.ByteArrayUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * CentralDirectoryInfo is a class containing the information of a file/folder inside a zip file.
+ *
+ * <p>Overall zipfile format: [Local file header + Compressed data [+ Extended local header]?]*
+ * [Central directory]* [End of central directory record]
+ *
+ * <p>Refer to following link for more details: https://en.wikipedia.org/wiki/Zip_(file_format)
+ */
+public final class CentralDirectoryInfo {
+
+    private static final byte[] CENTRAL_DIRECTORY_SIGNATURE = {0x50, 0x4b, 0x01, 0x02};
+
+    private int mCompressionMethod;
+    private long mCrc;
+    private long mCompressedSize;
+    private long mUncompressedSize;
+    private long mLocalHeaderOffset;
+    private int mInternalFileAttributes;
+    private long mExternalFileAttributes;
+    private String mFileName;
+    private int mFileNameLength;
+    private int mExtraFieldLength;
+    private int mFileCommentLength;
+
+    /** Get the compression method. */
+    public int getCompressionMethod() {
+        return mCompressionMethod;
+    }
+
+    /** Set the compression method. */
+    public void setCompressionMethod(int compressionMethod) {
+        mCompressionMethod = compressionMethod;
+    }
+
+    /** Get the CRC of the file. */
+    public long getCrc() {
+        return mCrc;
+    }
+
+    /** Set the CRC of the file. */
+    public void setCrc(long crc) {
+        mCrc = crc;
+    }
+
+    /** Get the compressed size. */
+    public int getCompressedSize() {
+        return (int) mCompressedSize;
+    }
+
+    /** Set the compressed size. */
+    public void setCompressedSize(long compressionSize) {
+        mCompressedSize = compressionSize;
+    }
+
+    /** Get the uncompressed size. */
+    public long getUncompressedSize() {
+        return mUncompressedSize;
+    }
+
+    /** Set the uncompressed size. */
+    public void setUncompressedSize(long uncompressedSize) {
+        mUncompressedSize = uncompressedSize;
+    }
+
+    /** Get the offset of local file header entry. */
+    public long getLocalHeaderOffset() {
+        return mLocalHeaderOffset;
+    }
+
+    /** Set the offset of local file header entry. */
+    public void setLocalHeaderOffset(long localHeaderOffset) {
+        mLocalHeaderOffset = localHeaderOffset;
+    }
+
+    /** Get the internal file attributes. */
+    public int getInternalFileAttributes() {
+        return mInternalFileAttributes;
+    }
+
+    /** Set the internal file attributes. */
+    public void setInternalFileAttributes(int internalFileAttributes) {
+        mInternalFileAttributes = internalFileAttributes;
+    }
+
+    /** Get the external file attributes. */
+    public long getExternalFileAttributes() {
+        return mExternalFileAttributes;
+    }
+
+    /** Set the external file attributes. */
+    public void setExternalFileAttributes(long externalFileAttributes) {
+        mExternalFileAttributes = externalFileAttributes;
+    }
+
+    /** Get the Linux file permission, stored in the last 9 bits of external file attributes. */
+    public int getFilePermission() {
+        return ((int) mExternalFileAttributes & (0777 << 16L)) >> 16L;
+    }
+
+    /** Get the file name including the relative path. */
+    public String getFileName() {
+        return mFileName;
+    }
+
+    /** Set the file name including the relative path. */
+    public void setFileName(String fileName) {
+        mFileName = fileName;
+    }
+
+    /** Get the file name length. */
+    public int getFileNameLength() {
+        return mFileNameLength;
+    }
+
+    /** Set the file name length. */
+    public void setFileNameLength(int fileNameLength) {
+        mFileNameLength = fileNameLength;
+    }
+
+    /** Get the extra field length. */
+    public int getExtraFieldLength() {
+        return mExtraFieldLength;
+    }
+
+    /** Set the extra field length. */
+    public void setExtraFieldLength(int extraFieldLength) {
+        mExtraFieldLength = extraFieldLength;
+    }
+
+    /** Get the file comment length. */
+    public int getFileCommentLength() {
+        return mFileCommentLength;
+    }
+
+    /** Set the file comment length. */
+    public void setFileCommentLength(int fileCommentLength) {
+        mFileCommentLength = fileCommentLength;
+    }
+
+    /** Get the size of the central directory entry. */
+    public int getInfoSize() {
+        return 46 + mFileNameLength + mExtraFieldLength + mFileCommentLength;
+    }
+
+    /** Default constructor used for unit test. */
+    @VisibleForTesting
+    protected CentralDirectoryInfo() {}
+
+    /**
+     * Constructor to collect the information of a file entry inside zip file.
+     *
+     * @param data {@code byte[]} of data that contains the information of a file entry.
+     * @param startOffset start offset of the information block.
+     * @throws IOException
+     */
+    public CentralDirectoryInfo(byte[] data, int startOffset) throws IOException {
+        // Central directory:
+        //    Offset   Length   Contents
+        //      0      4 bytes  Central file header signature (0x02014b50)
+        //      4      2 bytes  Version made by
+        //      6      2 bytes  Version needed to extract
+        //      8      2 bytes  General purpose bit flag
+        //     10      2 bytes  Compression method
+        //     12      2 bytes  Last mod file time
+        //     14      2 bytes  Last mod file date
+        //     16      4 bytes  CRC-32
+        //     20      4 bytes  Compressed size
+        //     24      4 bytes  Uncompressed size
+        //     28      2 bytes  Filename length (f)
+        //     30      2 bytes  Extra field length (e)
+        //     32      2 bytes  File comment length (c)
+        //     34      2 bytes  Disk number start
+        //     36      2 bytes  Internal file attributes
+        //     38      4 bytes  External file attributes (file permission stored in the last 9 bits)
+        //     42      4 bytes  Relative offset of local header
+        //     46     (f)bytes  Filename
+        //            (e)bytes  Extra field
+        //            (c)bytes  File comment
+
+        // Check signature
+        if (!Arrays.equals(
+                CENTRAL_DIRECTORY_SIGNATURE,
+                Arrays.copyOfRange(data, startOffset, startOffset + 4))) {
+            throw new IOException("Invalid central directory info for zip file is found.");
+        }
+        mCompressionMethod = ByteArrayUtil.getInt(data, startOffset + 10, 2);
+        mCrc = ByteArrayUtil.getLong(data, startOffset + 16, 4);
+        mCompressedSize = ByteArrayUtil.getLong(data, startOffset + 20, 4);
+        mUncompressedSize = ByteArrayUtil.getLong(data, startOffset + 24, 4);
+        mInternalFileAttributes = ByteArrayUtil.getInt(data, startOffset + 36, 2);
+        mExternalFileAttributes = ByteArrayUtil.getLong(data, startOffset + 38, 4);
+        mLocalHeaderOffset = ByteArrayUtil.getLong(data, startOffset + 42, 4);
+        mFileNameLength = ByteArrayUtil.getInt(data, startOffset + 28, 2);
+        mFileName = ByteArrayUtil.getString(data, startOffset + 46, mFileNameLength);
+        mExtraFieldLength = ByteArrayUtil.getInt(data, startOffset + 30, 2);
+        mFileCommentLength = ByteArrayUtil.getInt(data, startOffset + 32, 2);
+    }
+}
diff --git a/common_util/com/android/tradefed/util/zip/EndCentralDirectoryInfo.java b/common_util/com/android/tradefed/util/zip/EndCentralDirectoryInfo.java
new file mode 100644
index 0000000..b675ebb
--- /dev/null
+++ b/common_util/com/android/tradefed/util/zip/EndCentralDirectoryInfo.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util.zip;
+
+import com.android.tradefed.util.ByteArrayUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * EndCentralDirectoryInfo is a class containing the overall information of a zip file. It's at the
+ * end of the zip file.
+ *
+ * <p>Overall zipfile format: [Local file header + Compressed data [+ Extended local header]?]*
+ * [Central directory]* [End of central directory record]
+ *
+ * <p>Refer to following link for more details: https://en.wikipedia.org/wiki/Zip_(file_format)
+ */
+public final class EndCentralDirectoryInfo {
+
+    // End central directory of a zip file is at the end of the file, and its size shouldn't be
+    // larger than 64k.
+    public static final int MAX_LOOKBACK = 64 * 1024;
+
+    private static final byte[] END_CENTRAL_DIRECTORY_SIGNATURE = {0x50, 0x4b, 0x05, 0x06};
+    // Central directory signature is always 4 bytes.
+    private static final int CENTRAL_DIRECTORY_MAGIC_LENGTH = 4;
+
+    private int mEntryNumber;
+    private long mCentralDirSize;
+    private long mCentralDirOffset;
+
+    public int getEntryNumber() {
+        return mEntryNumber;
+    }
+
+    public long getCentralDirSize() {
+        return mCentralDirSize;
+    }
+
+    public long getCentralDirOffset() {
+        return mCentralDirOffset;
+    }
+
+    /**
+     * Constructor to collect end central directory information of a zip file.
+     *
+     * @param zipFile a {@link File} contains the end central directory information. It's likely the
+     *     ending part of the zip file.
+     * @throws IOException
+     */
+    public EndCentralDirectoryInfo(File zipFile) throws IOException {
+        // End of central directory record:
+        //    Offset   Length   Contents
+        //      0      4 bytes  End of central dir signature (0x06054b50)
+        //      4      2 bytes  Number of this disk
+        //      6      2 bytes  Number of the disk with the start of the central directory
+        //      8      2 bytes  Total number of entries in the central dir on this disk
+        //     10      2 bytes  Total number of entries in the central dir
+        //     12      4 bytes  Size of the central directory
+        //     16      4 bytes  Offset of start of central directory with respect to the starting
+        //                      disk number
+        //     20      2 bytes  zipfile comment length (c)
+        //     22     (c)bytes  zipfile comment
+
+        try (FileInputStream stream = new FileInputStream(zipFile)) {
+            long size = stream.getChannel().size();
+            if (size > MAX_LOOKBACK) {
+                stream.skip(size - MAX_LOOKBACK);
+                size = MAX_LOOKBACK;
+            }
+            byte[] endCentralDir = new byte[(int) size];
+            stream.read(endCentralDir);
+            int offset = (int) size - CENTRAL_DIRECTORY_MAGIC_LENGTH - 1;
+            // Seek from the end of the file, searching for the end central directory signature.
+            while (offset >= 0) {
+                if (!java.util.Arrays.equals(
+                        END_CENTRAL_DIRECTORY_SIGNATURE,
+                        Arrays.copyOfRange(endCentralDir, offset, offset + 4))) {
+                    offset--;
+                    continue;
+                }
+                // Get the total number of entries in the central directory
+                mEntryNumber = ByteArrayUtil.getInt(endCentralDir, offset + 10, 2);
+                // Get the size of the central directory block
+                mCentralDirSize = ByteArrayUtil.getLong(endCentralDir, offset + 12, 4);
+                // Get the offset of start of central directory
+                mCentralDirOffset = ByteArrayUtil.getLong(endCentralDir, offset + 16, 4);
+
+                if (mCentralDirOffset < 0) {
+                    throw new IOException(
+                            "Failed to get offset of EndCentralDirectoryInfo. Partial unzip doesn't support zip files larger than 4GB.");
+                }
+                break;
+            }
+            if (offset < 0) {
+                throw new RuntimeException(
+                        "Failed to find end central directory info for zip file: "
+                                + zipFile.getPath());
+            }
+        }
+    }
+}
diff --git a/common_util/com/android/tradefed/util/zip/LocalFileHeader.java b/common_util/com/android/tradefed/util/zip/LocalFileHeader.java
new file mode 100644
index 0000000..2012189
--- /dev/null
+++ b/common_util/com/android/tradefed/util/zip/LocalFileHeader.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util.zip;
+
+import com.android.tradefed.util.ByteArrayUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * LocalFileHeader is a class containing the information of a file/folder inside a zip file. The
+ * block of data is at the beginning part of each file entry.
+ *
+ * <p>Overall zipfile format: [Local file header + Compressed data [+ Extended local header]?]*
+ * [Central directory]* [End of central directory record]
+ *
+ * <p>Refer to following link for more details: https://en.wikipedia.org/wiki/Zip_(file_format)
+ */
+public final class LocalFileHeader {
+
+    public static final int LOCAL_FILE_HEADER_SIZE = 30;
+    private static final byte[] LOCAL_FILE_HEADER_SIGNATURE = {0x50, 0x4b, 0x03, 0x04};
+
+    private int mCompressionMethod;
+    private long mCrc;
+    private long mCompressedSize;
+    private long mUncompressedSize;
+    private int mFileNameLength;
+    private int mExtraFieldLength;
+
+    public int getCompressionMethod() {
+        return mCompressionMethod;
+    }
+
+    public long getCrc() {
+        return mCrc;
+    }
+
+    public long getCompressedSize() {
+        return mCompressedSize;
+    }
+
+    public long getUncompressedSize() {
+        return mUncompressedSize;
+    }
+
+    public int getFileNameLength() {
+        return mFileNameLength;
+    }
+
+    public int getExtraFieldLength() {
+        return mExtraFieldLength;
+    }
+
+    public int getHeaderSize() {
+        return LOCAL_FILE_HEADER_SIZE + mFileNameLength + mExtraFieldLength;
+    }
+
+    public LocalFileHeader(File partialZipFile) throws IOException {
+        this(partialZipFile, 0);
+    }
+
+    /**
+     * Constructor to collect local file header information of a file entry in a zip file.
+     *
+     * @param partialZipFile a {@link File} contains the local file header information.
+     * @param startOffset the start offset of the block of data for a local file header.
+     * @throws IOException
+     */
+    public LocalFileHeader(File partialZipFile, int startOffset) throws IOException {
+        // Local file header:
+        //    Offset   Length   Contents
+        //      0      4 bytes  Local file header signature (0x04034b50)
+        //      4      2 bytes  Version needed to extract
+        //      6      2 bytes  General purpose bit flag
+        //      8      2 bytes  Compression method
+        //     10      2 bytes  Last mod file time
+        //     12      2 bytes  Last mod file date
+        //     14      4 bytes  CRC-32
+        //     18      4 bytes  Compressed size (n)
+        //     22      4 bytes  Uncompressed size
+        //     26      2 bytes  Filename length (f)
+        //     28      2 bytes  Extra field length (e)
+        //            (f)bytes  Filename
+        //            (e)bytes  Extra field
+        //            (n)bytes  Compressed data
+        byte[] data;
+        try (FileInputStream stream = new FileInputStream(partialZipFile)) {
+            stream.skip(startOffset);
+            data = new byte[LOCAL_FILE_HEADER_SIZE];
+            stream.read(data);
+        }
+
+        // Check signature
+        if (!Arrays.equals(LOCAL_FILE_HEADER_SIGNATURE, Arrays.copyOfRange(data, 0, 4))) {
+            throw new IOException("Invalid local file header for zip file is found.");
+        }
+        mCompressionMethod = ByteArrayUtil.getInt(data, 8, 2);
+        mCrc = ByteArrayUtil.getLong(data, 14, 4);
+        mCompressedSize = ByteArrayUtil.getLong(data, 18, 2);
+        mUncompressedSize = ByteArrayUtil.getLong(data, 22, 2);
+        mFileNameLength = ByteArrayUtil.getInt(data, 26, 2);
+        mExtraFieldLength = ByteArrayUtil.getInt(data, 28, 2);
+    }
+}
diff --git a/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java b/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java
new file mode 100644
index 0000000..ed1e8fc
--- /dev/null
+++ b/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util.zip;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Merge individual zip entries in a large zip file into blocks to minimize the download attempts.
+ */
+public class MergedZipEntryCollection {
+
+    // The maximum number of bytes between each section of merged zip entries.
+    public static final int MAX_GAP = 4096;
+
+    // The maximum percentage of gaps between zip entries over the total size of the download block.
+    public static final double MAX_GAP_PERCENTAGE = 0.15;
+
+    // Best guess of header size. 2k Should be more than enough for file path and extra attributes.
+    public static final int HEADER_SIZE = LocalFileHeader.LOCAL_FILE_HEADER_SIZE + 2048;
+
+    private List<CentralDirectoryInfo> mZipEntries;
+
+    public MergedZipEntryCollection(List<CentralDirectoryInfo> zipEntries) {
+        mZipEntries = zipEntries;
+    }
+
+    public long getStartOffset() {
+        return mZipEntries.get(0).getLocalHeaderOffset();
+    }
+
+    public long getEndOffset() {
+        CentralDirectoryInfo lastEntry = mZipEntries.get(mZipEntries.size() - 1);
+        return lastEntry.getLocalHeaderOffset() + lastEntry.getCompressedSize() + HEADER_SIZE;
+    }
+
+    public List<CentralDirectoryInfo> getZipEntries() {
+        return mZipEntries;
+    }
+
+    /*
+     * Merge a list of zip entries into groups to minimize download attempts.
+     *
+     *  @param zipEntries a list of {@link CentralDirectoryInfo} for a zip file.
+     *
+     *  @return a list of {@link MergedZipEntryCollection}, each contains a list of
+     *    {@link CentralDirectoryInfo} that are stored closely inside the zip file.
+     */
+    public static List<MergedZipEntryCollection> CreateCollections(
+            List<CentralDirectoryInfo> zipEntries) {
+        if (zipEntries.size() == 0) {
+            return new ArrayList<MergedZipEntryCollection>();
+        }
+
+        // Sort the entries by start offset.
+        List<CentralDirectoryInfo> entries =
+                zipEntries
+                        .stream()
+                        .sorted(Comparator.comparing(CentralDirectoryInfo::getLocalHeaderOffset))
+                        .collect(Collectors.toList());
+        long endOffset = -1;
+        long totalGap = 0;
+        List<MergedZipEntryCollection> collections = new ArrayList<>();
+        List<CentralDirectoryInfo> group = new ArrayList<>();
+        for (CentralDirectoryInfo entry : entries) {
+            if (endOffset >= 0) {
+                long newGap = entry.getLocalHeaderOffset() - endOffset + totalGap;
+                long totalSize =
+                        entry.getLocalHeaderOffset()
+                                + HEADER_SIZE
+                                + entry.getCompressedSize()
+                                - group.get(0).getLocalHeaderOffset();
+                double gapPercentage = (double) (newGap) / totalSize;
+                if (endOffset < entry.getLocalHeaderOffset() - MAX_GAP
+                        && MAX_GAP_PERCENTAGE < gapPercentage) {
+                    collections.add(new MergedZipEntryCollection(group));
+                    group = new ArrayList<>();
+                    totalGap = 0;
+                }
+            }
+            group.add(entry);
+            if (group.size() > 1 && entry.getLocalHeaderOffset() > endOffset) {
+                totalGap += entry.getLocalHeaderOffset() - endOffset;
+            }
+            endOffset = entry.getLocalHeaderOffset() + HEADER_SIZE + entry.getCompressedSize();
+        }
+        collections.add(new MergedZipEntryCollection(group));
+
+        return collections;
+    }
+}
diff --git a/device_build_interfaces/Android.bp b/device_build_interfaces/Android.bp
new file mode 100644
index 0000000..3d8a287
--- /dev/null
+++ b/device_build_interfaces/Android.bp
@@ -0,0 +1,32 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+    name: "tradefed-device-build-interfaces",
+    defaults: ["tradefed_defaults"],
+    srcs: [
+        "com/**/*.java",
+    ],
+    libs: [
+        "ddmlib-prebuilt",
+        "error_prone_annotations-2.0.18",
+        "guava",
+        "tradefed-protos",
+        "devtools-annotations-prebuilt",
+        "tradefed-common-util",
+        "tf-remote-client",
+        "tradefed-result-interfaces",
+    ],
+}
+
diff --git a/src/com/android/tradefed/build/BuildInfoKey.java b/device_build_interfaces/com/android/tradefed/build/BuildInfoKey.java
similarity index 100%
rename from src/com/android/tradefed/build/BuildInfoKey.java
rename to device_build_interfaces/com/android/tradefed/build/BuildInfoKey.java
diff --git a/src/com/android/tradefed/build/IBuildInfo.java b/device_build_interfaces/com/android/tradefed/build/IBuildInfo.java
similarity index 87%
rename from src/com/android/tradefed/build/IBuildInfo.java
rename to device_build_interfaces/com/android/tradefed/build/IBuildInfo.java
index d50b8bd..919bc31 100644
--- a/src/com/android/tradefed/build/IBuildInfo.java
+++ b/device_build_interfaces/com/android/tradefed/build/IBuildInfo.java
@@ -21,6 +21,7 @@
 
 import java.io.File;
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -40,14 +41,18 @@
         DO_NOT_COPY_IMAGE_FILE,
     }
 
-    /**
-     * Default value when build ID is unknown.
-     */
-    public final static String UNKNOWN_BUILD_ID = "-1";
+    /** Default value when build ID is unknown. */
+    public static final String UNKNOWN_BUILD_ID = "-1";
+
+    /** Prefix used in name to indicate the file is set to be delayed download. */
+    public static final String REMOTE_FILE_PREFIX = "remote_file:";
+
+    /** Remote file is not versioned. */
+    public static final String REMOTE_FILE_VERSION = "";
 
     /**
-     * Returns the unique identifier of build under test. Should never be null. Defaults to
-     * {@link #UNKNOWN_BUILD_ID}.
+     * Returns the unique identifier of build under test. Should never be null. Defaults to {@link
+     * #UNKNOWN_BUILD_ID}.
      */
     public String getBuildId();
 
@@ -254,6 +259,22 @@
     }
 
     /**
+     * Gets a copy of the set of local app apk file(s) and their versions. The returned order
+     * matches the order in which the apks were added to the {@code IAppBuildInfo}.
+     */
+    public default List<VersionedFile> getAppPackageFiles() {
+        return new ArrayList<>();
+    }
+
+    /**
+     * Adds the local apk file and its associated version. Note that apks will be returned from
+     * {@link #getAppPackageFiles()} in the order in which they were added by this method.
+     */
+    public default void addAppPackageFile(File appPackageFile, String version) {
+        // Default implementation for projects that don't extend BuildInfo class.
+    }
+
+    /**
      * Clean up any temporary build files
      */
     public void cleanUp();
@@ -279,4 +300,9 @@
 
     /** Set the build as test resource build. */
     public default void setTestResourceBuild(boolean testResourceBuild) {}
+
+    /** Get the paths for build artifacts that are delayed download. */
+    public default Set<File> getRemoteFiles() {
+        return null;
+    }
 }
diff --git a/src/com/android/tradefed/build/VersionedFile.java b/device_build_interfaces/com/android/tradefed/build/VersionedFile.java
similarity index 100%
rename from src/com/android/tradefed/build/VersionedFile.java
rename to device_build_interfaces/com/android/tradefed/build/VersionedFile.java
diff --git a/src/com/android/tradefed/device/AndroidDebugBridgeWrapper.java b/device_build_interfaces/com/android/tradefed/device/AndroidDebugBridgeWrapper.java
similarity index 100%
rename from src/com/android/tradefed/device/AndroidDebugBridgeWrapper.java
rename to device_build_interfaces/com/android/tradefed/device/AndroidDebugBridgeWrapper.java
diff --git a/src/com/android/tradefed/device/DeviceNotAvailableException.java b/device_build_interfaces/com/android/tradefed/device/DeviceNotAvailableException.java
similarity index 100%
rename from src/com/android/tradefed/device/DeviceNotAvailableException.java
rename to device_build_interfaces/com/android/tradefed/device/DeviceNotAvailableException.java
diff --git a/src/com/android/tradefed/device/DeviceRuntimeException.java b/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
similarity index 100%
rename from src/com/android/tradefed/device/DeviceRuntimeException.java
rename to device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
diff --git a/src/com/android/tradefed/device/IAndroidDebugBridge.java b/device_build_interfaces/com/android/tradefed/device/IAndroidDebugBridge.java
similarity index 100%
rename from src/com/android/tradefed/device/IAndroidDebugBridge.java
rename to device_build_interfaces/com/android/tradefed/device/IAndroidDebugBridge.java
diff --git a/src/com/android/tradefed/device/IDeviceRecovery.java b/device_build_interfaces/com/android/tradefed/device/IDeviceRecovery.java
similarity index 100%
rename from src/com/android/tradefed/device/IDeviceRecovery.java
rename to device_build_interfaces/com/android/tradefed/device/IDeviceRecovery.java
diff --git a/src/com/android/tradefed/device/IDeviceStateMonitor.java b/device_build_interfaces/com/android/tradefed/device/IDeviceStateMonitor.java
similarity index 100%
rename from src/com/android/tradefed/device/IDeviceStateMonitor.java
rename to device_build_interfaces/com/android/tradefed/device/IDeviceStateMonitor.java
diff --git a/src/com/android/tradefed/device/IFileEntry.java b/device_build_interfaces/com/android/tradefed/device/IFileEntry.java
similarity index 100%
rename from src/com/android/tradefed/device/IFileEntry.java
rename to device_build_interfaces/com/android/tradefed/device/IFileEntry.java
diff --git a/src/com/android/tradefed/device/INativeDevice.java b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
similarity index 96%
rename from src/com/android/tradefed/device/INativeDevice.java
rename to device_build_interfaces/com/android/tradefed/device/INativeDevice.java
index 172f0bd..df7420a 100644
--- a/src/com/android/tradefed/device/INativeDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
@@ -40,6 +40,7 @@
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
@@ -1275,26 +1276,46 @@
     public String getDeviceClass();
 
     /**
-     * Extra steps for device specific required setup that will be executed on the device prior
-     * to the invocation flow.
+     * Extra steps for device specific required setup that will be executed on the device prior to
+     * the invocation flow.
      */
-    public void preInvocationSetup(IBuildInfo info)
-            throws TargetSetupError, DeviceNotAvailableException;
+    public default void preInvocationSetup(IBuildInfo info)
+            throws TargetSetupError, DeviceNotAvailableException {
+        preInvocationSetup(info, null);
+    }
 
     /**
      * Extra steps for device specific required setup that will be executed on the device prior to
      * the invocation flow.
+     *
+     * @param info The {@link IBuildInfo} of the device.
+     * @param testResourceBuildInfos The list of test resources.
+     * @throws TargetSetupError
+     * @throws DeviceNotAvailableException
      */
     public default void preInvocationSetup(IBuildInfo info, List<IBuildInfo> testResourceBuildInfos)
             throws TargetSetupError, DeviceNotAvailableException {
-        preInvocationSetup(info);
+        // Empty default implementation.
     }
 
     /**
      * Extra steps for device specific required clean up that will be executed after the invocation
      * is done.
+     *
+     * @deprecated Use {@link #postInvocationTearDown(Throwable)} instead.
      */
-    public void postInvocationTearDown();
+    @Deprecated
+    public default void postInvocationTearDown() {
+        postInvocationTearDown(null);
+    }
+
+    /**
+     * Extra steps for device specific required clean up that will be executed after the invocation
+     * is done.
+     *
+     * @param invocationException if any, the final exception raised by the invocation failure.
+     */
+    public void postInvocationTearDown(Throwable invocationException);
 
     /**
      * Return true if the device is headless (no screen), false otherwise.
@@ -1308,20 +1329,32 @@
     public DeviceDescriptor getDeviceDescriptor();
 
     /**
-     * Helper method runs the "ps" command and returns list of USER, PID and NAME of all the
-     * processes.
-     *
-     * @return List of ProcessInfo objects
-     */
-    public List<ProcessInfo> getProcesses() throws DeviceNotAvailableException;
-
-    /**
-     * Helper method runs the "ps" command and returns USER, PID and NAME of the given process name.
+     * Helper method runs the "pidof" and "stat" command and returns {@link ProcessInfo} object with
+     * PID and process start time of the given process.
      *
      * @return ProcessInfo of given processName
      */
     public ProcessInfo getProcessByName(String processName) throws DeviceNotAvailableException;
 
+    /**
+     * Helper method collects the boot history map with boot time and boot reason.
+     *
+     * @return Map of boot time (UTC time in second since Epoch) and boot reason
+     */
+    public Map<Long, String> getBootHistory() throws DeviceNotAvailableException;
+
+    /**
+     * Helper method collects the boot history map with boot time and boot reason since the given
+     * time in second since epoch from device. The current device utcEpochTime in second can be
+     * obtained by adb shell command "date +%s". Method {@link #getDeviceDate} uses adb shell
+     * command "date +%s" to get device UTC Time since Epoch and return the value in scale of
+     * millisecond.
+     *
+     * @return Map of boot time (UTC time in second since Epoch) and boot reason
+     */
+    public Map<Long, String> getBootHistorySince(long utcEpochTime)
+            throws DeviceNotAvailableException;
+
     /** Returns the pid of the service or null if something went wrong. */
     public String getProcessPid(String process) throws DeviceNotAvailableException;
 
@@ -1359,4 +1392,6 @@
      * @see <a href="https://source.android.com/devices/tech/debug">Tombstones documentation</a>
      */
     public List<File> getTombstones() throws DeviceNotAvailableException;
+
+
 }
diff --git a/src/com/android/tradefed/device/ITestDevice.java b/device_build_interfaces/com/android/tradefed/device/ITestDevice.java
similarity index 98%
rename from src/com/android/tradefed/device/ITestDevice.java
rename to device_build_interfaces/com/android/tradefed/device/ITestDevice.java
index 10ecc30..6510852 100644
--- a/src/com/android/tradefed/device/ITestDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/ITestDevice.java
@@ -665,6 +665,16 @@
     ArrayList<Integer> listUsers() throws DeviceNotAvailableException;
 
     /**
+     * Gets the Map of useId to {@link UserInfo} on the device. Will throw {@link
+     * DeviceRuntimeException} if output from device is not as expected.
+     *
+     * @return the list of UserInfo objects.
+     * @throws DeviceNotAvailableException
+     * @throws DeviceRuntimeException
+     */
+    public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException;
+
+    /**
      * Get the maximum number of supported users. Defaults to 0.
      *
      * @return an integer indicating the number of supported users
diff --git a/src/com/android/tradefed/device/NullDevice.java b/device_build_interfaces/com/android/tradefed/device/NullDevice.java
similarity index 91%
rename from src/com/android/tradefed/device/NullDevice.java
rename to device_build_interfaces/com/android/tradefed/device/NullDevice.java
index d429264..9973370 100644
--- a/src/com/android/tradefed/device/NullDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/NullDevice.java
@@ -24,8 +24,7 @@
 public class NullDevice extends StubDevice {
 
     /** Naming pattern for auto-created null devices */
-    public static final String TEMP_NULL_DEVICE_PREFIX =
-            DeviceManager.NULL_DEVICE_SERIAL_PREFIX + "-temp-";
+    public static final String TEMP_NULL_DEVICE_PREFIX = "null-device-temp-";
 
     private boolean mTemporaryDevice = false;
 
diff --git a/src/com/android/tradefed/device/PackageInfo.java b/device_build_interfaces/com/android/tradefed/device/PackageInfo.java
similarity index 88%
rename from src/com/android/tradefed/device/PackageInfo.java
rename to device_build_interfaces/com/android/tradefed/device/PackageInfo.java
index 1f114d4..fe55ff8 100644
--- a/src/com/android/tradefed/device/PackageInfo.java
+++ b/device_build_interfaces/com/android/tradefed/device/PackageInfo.java
@@ -27,14 +27,17 @@
     // frameworks/base/core/java/android/content/pm/ApplicationInfo.java
     private static final int FLAG_UPDATED_SYSTEM_APP = 1 << 7;
     private static final int FLAG_SYSTEM = 1 << 0;
+    public static final int FLAG_PERSISTENT = 1 << 3;
 
     // string flag constants. Used for newer platforms
     private static final String FLAG_UPDATED_SYSTEM_APP_TEXT = " UPDATED_SYSTEM_APP ";
     private static final String FLAG_SYSTEM_TEXT = " SYSTEM ";
+    private static final String FLAG_PERSISTENT_TEXT = " PERSISTENT ";
 
     private final String mPackageName;
     private boolean mIsSystemApp;
     private boolean mIsUpdatedSystemApp;
+    private boolean mIsPersistentApp;
     private Map<String, String> mAttributes = new HashMap<String, String>();
 
     PackageInfo(String pkgName) {
@@ -56,6 +59,13 @@
     }
 
     /**
+     * Returns <code>true</code> if this is a persistent app.
+     */
+    public boolean isPersistentApp() {
+        return mIsPersistentApp;
+    }
+
+    /**
      * Returns the package name of the application.
      */
     public String getPackageName() {
@@ -89,6 +99,7 @@
     private void parseFlagsAsString(String flagString) {
         mIsSystemApp = flagString.contains(FLAG_SYSTEM_TEXT);
         mIsUpdatedSystemApp = flagString.contains(FLAG_UPDATED_SYSTEM_APP_TEXT);
+        mIsPersistentApp = flagString.contains(FLAG_PERSISTENT_TEXT);
     }
 
     private boolean parseFlagsAsInt(String value) {
@@ -98,6 +109,7 @@
             // note: FLAG_UPDATED_SYSTEM_APP never seems to be set. Rely on parsing hidden system
             // packages
             mIsUpdatedSystemApp = (flags & FLAG_UPDATED_SYSTEM_APP) != 0;
+            mIsPersistentApp = (flags & FLAG_PERSISTENT) != 0;
             return true;
         } catch (NumberFormatException e) {
             // not an int, fall through
@@ -105,3 +117,4 @@
         return false;
     }
 }
+
diff --git a/src/com/android/tradefed/device/StubDevice.java b/device_build_interfaces/com/android/tradefed/device/StubDevice.java
similarity index 100%
rename from src/com/android/tradefed/device/StubDevice.java
rename to device_build_interfaces/com/android/tradefed/device/StubDevice.java
diff --git a/src/com/android/tradefed/device/TcpDevice.java b/device_build_interfaces/com/android/tradefed/device/TcpDevice.java
similarity index 100%
rename from src/com/android/tradefed/device/TcpDevice.java
rename to device_build_interfaces/com/android/tradefed/device/TcpDevice.java
diff --git a/src/com/android/tradefed/device/TestDeviceOptions.java b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
similarity index 96%
rename from src/com/android/tradefed/device/TestDeviceOptions.java
rename to device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
index be94963..eb62635 100644
--- a/src/com/android/tradefed/device/TestDeviceOptions.java
+++ b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
@@ -247,6 +247,17 @@
     )
     private Set<String> mRemoteFetchFilePattern = new HashSet<>();
 
+    @Option(name = "cros-user", description = "(CHEEPS ONLY) Account to log in to Chrome OS with.")
+    private String mCrosUser = null;
+
+    @Option(
+        name = "cros-password",
+        description =
+                "(CHEEPS ONLY) Password to log in to Chrome OS with. Only used if cros-user "
+                        + "is specified."
+    )
+    private String mCrosPassword = null;
+
     // END ====================== Options Related to Virtual Devices ======================
 
     /** Check whether adb root should be enabled on boot for this device */
@@ -616,6 +627,16 @@
         return mRemoteFetchFilePattern;
     }
 
+    /** Returns the Chrome OS User to log in as. */
+    public String getCrosUser() {
+        return mCrosUser;
+    }
+
+    /** Returns the password to log in to Chrome OS with. */
+    public String getCrosPassword() {
+        return mCrosPassword;
+    }
+
     public static String getCreateCommandByInstanceType(InstanceType type) {
         switch (type) {
             case CHEEPS:
diff --git a/src/com/android/tradefed/device/TestDeviceState.java b/device_build_interfaces/com/android/tradefed/device/TestDeviceState.java
similarity index 100%
rename from src/com/android/tradefed/device/TestDeviceState.java
rename to device_build_interfaces/com/android/tradefed/device/TestDeviceState.java
diff --git a/device_build_interfaces/com/android/tradefed/device/UserInfo.java b/device_build_interfaces/com/android/tradefed/device/UserInfo.java
new file mode 100644
index 0000000..4cccbbb
--- /dev/null
+++ b/device_build_interfaces/com/android/tradefed/device/UserInfo.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device;
+
+/**
+ * Similar to UserInfo class from platform.
+ *
+ * <p>This is intended to be similar to android.content.pm.UserInfo.
+ *
+ * <p>Stores data and basic logic around the information for one user.
+ */
+public final class UserInfo {
+    // From android.content.pm.UserInfo
+    public static final int FLAG_PRIMARY = 0x00000001;
+    public static final int FLAG_GUEST = 0x00000004;
+    public static final int FLAG_RESTRICTED = 0x00000008;
+    public static final int FLAG_MANAGED_PROFILE = 0x00000020;
+    public static final int USER_SYSTEM = 0;
+
+    public static final int FLAGS_NOT_SECONDARY =
+            FLAG_PRIMARY | FLAG_MANAGED_PROFILE | FLAG_GUEST | FLAG_RESTRICTED;
+
+    private final int mUserId;
+    private final String mUserName;
+    private final int mFlag;
+    private final boolean mIsRunning;
+
+    /** Supported variants of a user's type in external APIs. */
+    public enum UserType {
+        /** current foreground user of the device */
+        CURRENT,
+        /**
+         * guest user. Only one can exist at a time, may be ephemeral and have more restrictions.
+         */
+        GUEST,
+        /** user flagged as primary on the device; most often primary = system user = user 0 */
+        PRIMARY,
+        /** system user = user 0 */
+        SYSTEM,
+        /** secondary user, i.e. non-primary and non-system. */
+        SECONDARY;
+
+        public boolean isCurrent() {
+            return this == CURRENT;
+        }
+
+        public boolean isGuest() {
+            return this == GUEST;
+        }
+
+        public boolean isPrimary() {
+            return this == PRIMARY;
+        }
+
+        public boolean isSystem() {
+            return this == SYSTEM;
+        }
+
+        public boolean isSecondary() {
+            return this == SECONDARY;
+        }
+    }
+
+    public UserInfo(int userId, String userName, int flag, boolean isRunning) {
+        mUserId = userId;
+        mUserName = userName;
+        mFlag = flag;
+        mIsRunning = isRunning;
+    }
+
+    public int userId() {
+        return mUserId;
+    }
+
+    public String userName() {
+        return mUserName;
+    }
+
+    public int flag() {
+        return mFlag;
+    }
+
+    public boolean isRunning() {
+        return mIsRunning;
+    }
+
+    public boolean isGuest() {
+        return (mFlag & FLAG_GUEST) == FLAG_GUEST;
+    }
+
+    public boolean isPrimary() {
+        return (mFlag & FLAG_PRIMARY) == FLAG_PRIMARY;
+    }
+
+    public boolean isSecondary() {
+        return !isSystem() && (mFlag & FLAGS_NOT_SECONDARY) == 0;
+    }
+
+    public boolean isSystem() {
+        return mUserId == USER_SYSTEM;
+    }
+
+    /** Return whether this instance is of the specified type. */
+    public boolean isUserType(UserType userType, int currentUserId) {
+        switch (userType) {
+            case CURRENT:
+                return mUserId == currentUserId;
+            case GUEST:
+                return isGuest();
+            case PRIMARY:
+                return isPrimary();
+            case SYSTEM:
+                return isSystem();
+            case SECONDARY:
+                return isSecondary();
+            default:
+                throw new RuntimeException("Variant not covered: " + userType);
+        }
+    }
+}
diff --git a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
similarity index 100%
rename from src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
rename to device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
diff --git a/src/com/android/tradefed/targetprep/TargetSetupError.java b/device_build_interfaces/com/android/tradefed/targetprep/TargetSetupError.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/TargetSetupError.java
rename to device_build_interfaces/com/android/tradefed/targetprep/TargetSetupError.java
diff --git a/src/com/android/tradefed/util/Bugreport.java b/device_build_interfaces/com/android/tradefed/util/Bugreport.java
similarity index 100%
rename from src/com/android/tradefed/util/Bugreport.java
rename to device_build_interfaces/com/android/tradefed/util/Bugreport.java
diff --git a/src/com/android/tradefed/util/KeyguardControllerState.java b/device_build_interfaces/com/android/tradefed/util/KeyguardControllerState.java
similarity index 100%
rename from src/com/android/tradefed/util/KeyguardControllerState.java
rename to device_build_interfaces/com/android/tradefed/util/KeyguardControllerState.java
diff --git a/device_build_interfaces/com/android/tradefed/util/ProcessInfo.java b/device_build_interfaces/com/android/tradefed/util/ProcessInfo.java
new file mode 100644
index 0000000..158af60
--- /dev/null
+++ b/device_build_interfaces/com/android/tradefed/util/ProcessInfo.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+/** Used to store process related(USER, PID, NAME, START TIME IN SECOND SINCE EPOCH) information. */
+public class ProcessInfo {
+
+    private String mUser;
+    private int mPid;
+    private String mName;
+    private long mStartTime;
+
+    /**
+     * Constructs the process info object based on the user, process id and name of the process.
+     *
+     * @param user username of process owner
+     * @param pid process id number
+     * @param name process name
+     */
+    public ProcessInfo(String user, int pid, String name) {
+        mUser = user;
+        mPid = pid;
+        mName = name;
+    }
+
+    /**
+     * Constructs the process info object based on the user, process id, name of the process,
+     * process start time.
+     *
+     * @param user username of process owner
+     * @param pid process id number
+     * @param name process name
+     * @param startTime process start time in second since epoch
+     */
+    public ProcessInfo(String user, int pid, String name, long startTime) {
+        mUser = user;
+        mPid = pid;
+        mName = name;
+        mStartTime = startTime;
+    }
+
+    /** Returns the username of the process's owner. */
+    public String getUser() {
+        return mUser;
+    }
+
+    /** Returns the process ID number. */
+    public int getPid() {
+        return mPid;
+    }
+
+    /** Returns the process name. */
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Returns the process start time in second since epoch. For device process, the start time
+     * would use device time
+     */
+    public long getStartTime() {
+        return mStartTime;
+    }
+}
+
diff --git a/global_configuration/OWNERS b/global_configuration/OWNERS
new file mode 100644
index 0000000..6802673
--- /dev/null
+++ b/global_configuration/OWNERS
@@ -0,0 +1,4 @@
+# host/ drives host related setup or configuration: base OWNERS +
+fangk@google.com
+jeffreylu@google.com
+xingdai@google.com
diff --git a/src/com/android/tradefed/config/GlobalConfiguration.java b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
similarity index 99%
rename from src/com/android/tradefed/config/GlobalConfiguration.java
rename to global_configuration/com/android/tradefed/config/GlobalConfiguration.java
index daf6ebc..e3f0142 100644
--- a/src/com/android/tradefed/config/GlobalConfiguration.java
+++ b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
@@ -95,6 +95,7 @@
                 DEVICE_MANAGER_TYPE_NAME,
                 KEY_STORE_TYPE_NAME,
                 HOST_OPTIONS_TYPE_NAME,
+                DynamicRemoteFileResolver.DYNAMIC_RESOLVER,
                 "android-build"
             };
 
@@ -802,7 +803,7 @@
                 isGenericObject = true;
             }
             ConfigurationUtil.dumpClassToXml(
-                    serializer, config, configObj, isGenericObject, new ArrayList<>(), true);
+                    serializer, config, configObj, isGenericObject, new ArrayList<>(), true, true);
         }
         serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
         serializer.endDocument();
diff --git a/src/com/android/tradefed/config/IConfigurationServer.java b/global_configuration/com/android/tradefed/config/IConfigurationServer.java
similarity index 100%
rename from src/com/android/tradefed/config/IConfigurationServer.java
rename to global_configuration/com/android/tradefed/config/IConfigurationServer.java
diff --git a/src/com/android/tradefed/config/IGlobalConfiguration.java b/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
similarity index 100%
rename from src/com/android/tradefed/config/IGlobalConfiguration.java
rename to global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
diff --git a/src/com/android/tradefed/config/gcs/GCSConfigurationFactory.java b/global_configuration/com/android/tradefed/config/gcs/GCSConfigurationFactory.java
similarity index 100%
rename from src/com/android/tradefed/config/gcs/GCSConfigurationFactory.java
rename to global_configuration/com/android/tradefed/config/gcs/GCSConfigurationFactory.java
diff --git a/src/com/android/tradefed/config/gcs/GCSConfigurationServer.java b/global_configuration/com/android/tradefed/config/gcs/GCSConfigurationServer.java
similarity index 100%
rename from src/com/android/tradefed/config/gcs/GCSConfigurationServer.java
rename to global_configuration/com/android/tradefed/config/gcs/GCSConfigurationServer.java
diff --git a/src/com/android/tradefed/host/HostOptions.java b/global_configuration/com/android/tradefed/host/HostOptions.java
similarity index 100%
rename from src/com/android/tradefed/host/HostOptions.java
rename to global_configuration/com/android/tradefed/host/HostOptions.java
diff --git a/src/com/android/tradefed/host/IHostOptions.java b/global_configuration/com/android/tradefed/host/IHostOptions.java
similarity index 100%
rename from src/com/android/tradefed/host/IHostOptions.java
rename to global_configuration/com/android/tradefed/host/IHostOptions.java
diff --git a/src/com/android/tradefed/host/IHostResourceManager.java b/global_configuration/com/android/tradefed/host/IHostResourceManager.java
similarity index 100%
rename from src/com/android/tradefed/host/IHostResourceManager.java
rename to global_configuration/com/android/tradefed/host/IHostResourceManager.java
diff --git a/src/com/android/tradefed/host/LocalHostResourceManager.java b/global_configuration/com/android/tradefed/host/LocalHostResourceManager.java
similarity index 100%
rename from src/com/android/tradefed/host/LocalHostResourceManager.java
rename to global_configuration/com/android/tradefed/host/LocalHostResourceManager.java
diff --git a/src/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java b/global_configuration/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java
rename to global_configuration/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java
diff --git a/src/com/android/tradefed/util/hostmetric/EmailHostHealthAgent.java b/global_configuration/com/android/tradefed/util/hostmetric/EmailHostHealthAgent.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/EmailHostHealthAgent.java
rename to global_configuration/com/android/tradefed/util/hostmetric/EmailHostHealthAgent.java
diff --git a/src/com/android/tradefed/util/hostmetric/HeapHostMonitor.java b/global_configuration/com/android/tradefed/util/hostmetric/HeapHostMonitor.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/HeapHostMonitor.java
rename to global_configuration/com/android/tradefed/util/hostmetric/HeapHostMonitor.java
diff --git a/src/com/android/tradefed/util/hostmetric/HostMetric.java b/global_configuration/com/android/tradefed/util/hostmetric/HostMetric.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/HostMetric.java
rename to global_configuration/com/android/tradefed/util/hostmetric/HostMetric.java
diff --git a/src/com/android/tradefed/util/hostmetric/IHostHealthAgent.java b/global_configuration/com/android/tradefed/util/hostmetric/IHostHealthAgent.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/IHostHealthAgent.java
rename to global_configuration/com/android/tradefed/util/hostmetric/IHostHealthAgent.java
diff --git a/src/com/android/tradefed/util/hostmetric/IHostMonitor.java b/global_configuration/com/android/tradefed/util/hostmetric/IHostMonitor.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/IHostMonitor.java
rename to global_configuration/com/android/tradefed/util/hostmetric/IHostMonitor.java
diff --git a/src/com/android/tradefed/util/keystore/DryRunKeyStore.java b/global_configuration/com/android/tradefed/util/keystore/DryRunKeyStore.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/DryRunKeyStore.java
rename to global_configuration/com/android/tradefed/util/keystore/DryRunKeyStore.java
diff --git a/src/com/android/tradefed/util/keystore/IKeyStoreClient.java b/global_configuration/com/android/tradefed/util/keystore/IKeyStoreClient.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/IKeyStoreClient.java
rename to global_configuration/com/android/tradefed/util/keystore/IKeyStoreClient.java
diff --git a/src/com/android/tradefed/util/keystore/IKeyStoreFactory.java b/global_configuration/com/android/tradefed/util/keystore/IKeyStoreFactory.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/IKeyStoreFactory.java
rename to global_configuration/com/android/tradefed/util/keystore/IKeyStoreFactory.java
diff --git a/src/com/android/tradefed/util/keystore/JSONFileKeyStoreClient.java b/global_configuration/com/android/tradefed/util/keystore/JSONFileKeyStoreClient.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/JSONFileKeyStoreClient.java
rename to global_configuration/com/android/tradefed/util/keystore/JSONFileKeyStoreClient.java
diff --git a/src/com/android/tradefed/util/keystore/JSONFileKeyStoreFactory.java b/global_configuration/com/android/tradefed/util/keystore/JSONFileKeyStoreFactory.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/JSONFileKeyStoreFactory.java
rename to global_configuration/com/android/tradefed/util/keystore/JSONFileKeyStoreFactory.java
diff --git a/src/com/android/tradefed/util/keystore/KeyStoreException.java b/global_configuration/com/android/tradefed/util/keystore/KeyStoreException.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/KeyStoreException.java
rename to global_configuration/com/android/tradefed/util/keystore/KeyStoreException.java
diff --git a/src/com/android/tradefed/util/keystore/StubKeyStoreClient.java b/global_configuration/com/android/tradefed/util/keystore/StubKeyStoreClient.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/StubKeyStoreClient.java
rename to global_configuration/com/android/tradefed/util/keystore/StubKeyStoreClient.java
diff --git a/src/com/android/tradefed/util/keystore/StubKeyStoreFactory.java b/global_configuration/com/android/tradefed/util/keystore/StubKeyStoreFactory.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/StubKeyStoreFactory.java
rename to global_configuration/com/android/tradefed/util/keystore/StubKeyStoreFactory.java
diff --git a/invocation_interfaces/Android.bp b/invocation_interfaces/Android.bp
new file mode 100644
index 0000000..5208e51
--- /dev/null
+++ b/invocation_interfaces/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+    name: "tradefed-invocation-interfaces",
+    defaults: ["tradefed_defaults"],
+    srcs: [
+        "com/**/*.java",
+    ],
+    libs: [
+        "guava",
+        "tradefed-common-util",
+        "tradefed-protos",
+        "tradefed-result-interfaces",
+        "tradefed-device-build-interfaces",
+    ],
+}
+
diff --git a/src/com/android/tradefed/config/ConfigurationDescriptor.java b/invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java
similarity index 98%
rename from src/com/android/tradefed/config/ConfigurationDescriptor.java
rename to invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java
index 8d948d2..bc6d30c 100644
--- a/src/com/android/tradefed/config/ConfigurationDescriptor.java
+++ b/invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java
@@ -16,7 +16,6 @@
 package com.android.tradefed.config;
 
 import com.android.tradefed.build.BuildSerializedVersion;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
 import com.android.tradefed.config.proto.ConfigurationDescription;
 import com.android.tradefed.config.proto.ConfigurationDescription.Descriptor;
 import com.android.tradefed.config.proto.ConfigurationDescription.Metadata;
@@ -48,6 +47,8 @@
 
     /** Metadata key for a config to specify that it was sharded. */
     public static final String LOCAL_SHARDED_KEY = "sharded";
+    /** Metadata key for a config parameterization, optional. */
+    public static final String PARAMETER_KEY = "parameter";
 
     @Option(name = "test-suite-tag", description = "A membership tag to suite. Can be repeated.")
     private List<String> mSuiteTags = new ArrayList<>();
diff --git a/src/com/android/tradefed/invoker/IInvocationContext.java b/invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java
similarity index 95%
rename from src/com/android/tradefed/invoker/IInvocationContext.java
rename to invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java
index 9175550..537daf4 100644
--- a/src/com/android/tradefed/invoker/IInvocationContext.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java
@@ -19,7 +19,6 @@
 import com.android.tradefed.config.ConfigurationDescriptor;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
-import com.android.tradefed.testtype.suite.ITestSuite;
 import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.UniqueMultiMap;
 
@@ -159,14 +158,10 @@
      */
     public ConfigurationDescriptor getConfigurationDescriptor();
 
-    /**
-     * Sets the invocation context of module while being executed as part of a {@link ITestSuite}
-     */
+    /** Sets the invocation context of module while being executed as part of a suite. */
     public void setModuleInvocationContext(IInvocationContext invocationContext);
 
-    /**
-     * Returns the invocation context of module while being executed as part of a {@link ITestSuite}
-     */
+    /** Returns the invocation context of module while being executed as part of a suite. */
     public IInvocationContext getModuleInvocationContext();
 
     /** Returns the invocation test-tag. */
diff --git a/src/com/android/tradefed/result/ILogSaver.java b/invocation_interfaces/com/android/tradefed/result/ILogSaver.java
similarity index 100%
rename from src/com/android/tradefed/result/ILogSaver.java
rename to invocation_interfaces/com/android/tradefed/result/ILogSaver.java
diff --git a/src/com/android/tradefed/result/ILogSaverListener.java b/invocation_interfaces/com/android/tradefed/result/ILogSaverListener.java
similarity index 100%
rename from src/com/android/tradefed/result/ILogSaverListener.java
rename to invocation_interfaces/com/android/tradefed/result/ILogSaverListener.java
diff --git a/src/com/android/tradefed/result/ITestInvocationListener.java b/invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java
similarity index 89%
rename from src/com/android/tradefed/result/ITestInvocationListener.java
rename to invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java
index 72ff5ad..e6f5357 100644
--- a/src/com/android/tradefed/result/ITestInvocationListener.java
+++ b/invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java
@@ -15,10 +15,8 @@
  */
 package com.android.tradefed.result;
 
-import com.android.tradefed.command.ICommandScheduler;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.log.ITestLogger;
-import com.android.tradefed.testtype.suite.ITestSuite;
 
 /**
  * Listener for test results from the test invocation.
@@ -82,17 +80,17 @@
     default public TestSummary getSummary() { return null; }
 
     /**
-     * Called on {@link ICommandScheduler#shutdown()}, gives the invocation the opportunity to do
-     * something before terminating.
+     * Called on scheduler shutdown, gives the invocation the opportunity to do something before
+     * terminating.
      */
-    default public void invocationInterrupted() {
+    public default void invocationInterrupted() {
         // do nothing in default implementation.
     }
 
     /**
      * Reports the beginning of a module running. This callback is associated with {@link
      * #testModuleEnded()} and is optional in the sequence. It is only used during a run that uses
-     * modules: {@link ITestSuite} based runners.
+     * modules: suite based runners.
      *
      * @param moduleContext the {@link IInvocationContext} of the module.
      */
diff --git a/src/com/android/tradefed/result/LogFile.java b/invocation_interfaces/com/android/tradefed/result/LogFile.java
similarity index 100%
rename from src/com/android/tradefed/result/LogFile.java
rename to invocation_interfaces/com/android/tradefed/result/LogFile.java
diff --git a/src/com/android/tradefed/result/TestSummary.java b/invocation_interfaces/com/android/tradefed/result/TestSummary.java
similarity index 100%
rename from src/com/android/tradefed/result/TestSummary.java
rename to invocation_interfaces/com/android/tradefed/result/TestSummary.java
diff --git a/remote/.classpath b/remote/.classpath
index 74e2efb..f7e5ee7 100644
--- a/remote/.classpath
+++ b/remote/.classpath
@@ -1,15 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
 	<classpathentry kind="src" path="src"/>
-	<classpathentry kind="src" path="guava-failureaccess"/>
-	<classpathentry kind="src" path="guava-annot"/>
-	<classpathentry kind="src" path="jsr305"/>
-	<classpathentry kind="src" path="guava"/>
 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/sdklib/sdklib-prebuilt.jar"/>
 	<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/tools-common/tools-common-prebuilt.jar"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
 	<classpathentry kind="var" path="TRADEFED_ROOT/external/error_prone/error_prone/error_prone_annotations-2.3.2.jar"/>
+	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/external/guava/guava-jre/linux_glibc_common/combined/guava-jre.jar"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/remote/.project b/remote/.project
index d5451e0..39f7e18 100644
--- a/remote/.project
+++ b/remote/.project
@@ -14,26 +14,4 @@
 	<natures>
 		<nature>org.eclipse.jdt.core.javanature</nature>
 	</natures>
-	<linkedResources>
-		<link>
-			<name>guava</name>
-			<type>2</type>
-			<locationURI>TRADEFED_ROOT/external/guava/guava/src</locationURI>
-		</link>
-		<link>
-			<name>guava-annot</name>
-			<type>2</type>
-			<locationURI>TRADEFED_ROOT/external/guava/android-annotation-stubs/src</locationURI>
-		</link>
-		<link>
-			<name>guava-failureaccess</name>
-			<type>2</type>
-			<location>/usr/local/google/home/jdesprez/aosp/external/guava/futures/failureaccess/src</location>
-		</link>
-		<link>
-			<name>jsr305</name>
-			<type>2</type>
-			<locationURI>TRADEFED_ROOT/external/jsr305/ri/src/main/java</locationURI>
-		</link>
-	</linkedResources>
 </projectDescription>
diff --git a/res/apks/wifiutil/PREBUILT b/res/apks/wifiutil/PREBUILT
index 935017a..6475d73 100644
--- a/res/apks/wifiutil/PREBUILT
+++ b/res/apks/wifiutil/PREBUILT
@@ -1,4 +1,4 @@
 This apk can be rebuilt from
         platform/tools/tradefederation/core
 
-By running `m WifiUtil` on revision 2514302f05212870a8fef077bd378a3328571d69
+By running `m WifiUtil` on revision d6279aed8f7ea57530dea2c72a6e86b3bd11462a
diff --git a/res/apks/wifiutil/WifiUtil.apk b/res/apks/wifiutil/WifiUtil.apk
index e5e6fc0..7a98f1c 100644
--- a/res/apks/wifiutil/WifiUtil.apk
+++ b/res/apks/wifiutil/WifiUtil.apk
Binary files differ
diff --git a/res/config/testdef.xml b/res/config/instrumentations.xml
similarity index 60%
rename from res/config/testdef.xml
rename to res/config/instrumentations.xml
index d2e0fe3..88c7d50 100644
--- a/res/config/testdef.xml
+++ b/res/config/instrumentations.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2010 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -13,10 +13,12 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<configuration
-    description="Runs tests contained in test_def.xml files on an existing device">
+<configuration description="Runs all the Android instrumentation tests on an existing device">
 
-    <test class="com.android.tradefed.testtype.testdefs.XmlDefsTest" />
-    <logger class="com.android.tradefed.log.FileLogger" />
-    <result_reporter class="com.android.tradefed.result.XmlResultReporter" />
+    <target_preparer class="com.android.tradefed.targetprep.InstallApkSetup">
+        <!-- Use "apk-path" option to specify which apk to install -->
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.InstalledInstrumentationsTest" />
+
 </configuration>
diff --git a/src/com/android/tradefed/build/AppBuildInfo.java b/src/com/android/tradefed/build/AppBuildInfo.java
index b421f2e..402674f 100644
--- a/src/com/android/tradefed/build/AppBuildInfo.java
+++ b/src/com/android/tradefed/build/AppBuildInfo.java
@@ -16,12 +16,6 @@
 
 package com.android.tradefed.build;
 
-import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  * A {@link IBuildInfo} that represents an Android application and its test package(s).
  */
@@ -45,25 +39,4 @@
     public AppBuildInfo(BuildInfo buildToCopy) {
         super(buildToCopy);
     }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public List<VersionedFile> getAppPackageFiles() {
-        List<VersionedFile> origList = getVersionedFiles(BuildInfoFileKey.PACKAGE_FILES);
-        List<VersionedFile> listCopy = new ArrayList<VersionedFile>();
-        if (origList != null) {
-            listCopy.addAll(origList);
-        }
-        return listCopy;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void addAppPackageFile(File appPackageFile, String version) {
-        setFile(BuildInfoFileKey.PACKAGE_FILES, appPackageFile, version);
-    }
 }
diff --git a/src/com/android/tradefed/build/AppDeviceBuildInfo.java b/src/com/android/tradefed/build/AppDeviceBuildInfo.java
index 5ccef4e..78d6f72 100644
--- a/src/com/android/tradefed/build/AppDeviceBuildInfo.java
+++ b/src/com/android/tradefed/build/AppDeviceBuildInfo.java
@@ -16,13 +16,12 @@
 
 package com.android.tradefed.build;
 
-import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-/** A {@link IDeviceBuildInfo} that also contains a {@link IAppBuildInfo}. */
+/**
+ * A {@link IDeviceBuildInfo} that also contains a {@link IAppBuildInfo}.
+ *
+ * @deprecated Use {@link IDeviceBuildInfo} directly.
+ */
+@Deprecated
 public class AppDeviceBuildInfo extends DeviceBuildInfo implements IAppBuildInfo {
 
     private static final long serialVersionUID = BuildSerializedVersion.VERSION;
@@ -34,27 +33,6 @@
         super(buildId, buildName);
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void addAppPackageFile(File appPackageFile, String version) {
-        setFile(BuildInfoFileKey.PACKAGE_FILES, appPackageFile, version);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public List<VersionedFile> getAppPackageFiles() {
-        List<VersionedFile> origList = getVersionedFiles(BuildInfoFileKey.PACKAGE_FILES);
-        List<VersionedFile> listCopy = new ArrayList<VersionedFile>();
-        if (origList != null) {
-            listCopy.addAll(origList);
-        }
-        return listCopy;
-    }
-
     /** Copy all the files from the {@link IAppBuildInfo}. */
     public void setAppBuild(IAppBuildInfo appBuild) {
         copyAllFileFrom((BuildInfo) appBuild);
diff --git a/src/com/android/tradefed/build/BootstrapBuildProvider.java b/src/com/android/tradefed/build/BootstrapBuildProvider.java
index c6cf792..5da8557 100644
--- a/src/com/android/tradefed/build/BootstrapBuildProvider.java
+++ b/src/com/android/tradefed/build/BootstrapBuildProvider.java
@@ -22,6 +22,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.BuildInfoUtil;
 import com.android.tradefed.util.FileUtil;
 
 import java.io.File;
@@ -97,12 +98,7 @@
     @Override
     public IBuildInfo getBuild(ITestDevice device) throws BuildRetrievalError,
             DeviceNotAvailableException {
-        String buildId = mBuildId;
-        // If mBuildId is set, do not use the device build-id
-        if (buildId == null) {
-            buildId = device.getBuildId();
-        }
-        IBuildInfo info = new DeviceBuildInfo(buildId, mBuildTargetName);
+        IBuildInfo info = new DeviceBuildInfo(mBuildId, mBuildTargetName);
         if (!(device.getIDevice() instanceof StubDevice)) {
             if (!device.waitForDeviceShell(mShellAvailableTimeout * 1000)) {
                 throw new DeviceNotAvailableException(
@@ -111,24 +107,19 @@
                                 mShellAvailableTimeout),
                         device.getSerialNumber());
             }
-            if (mBranch == null) {
-                mBranch =
-                        String.format(
-                                "%s-%s-%s-%s",
-                                device.getProperty("ro.product.brand"),
-                                device.getProperty("ro.product.name"),
-                                device.getProductVariant(),
-                                device.getProperty("ro.build.version.release"));
-            }
         } else {
             // In order to avoid issue with a null branch, use a placeholder stub for StubDevice.
             mBranch = "stub";
         }
-        info.setBuildBranch(mBranch);
-        info.setBuildFlavor(device.getBuildFlavor());
-        info.addBuildAttribute("build_alias", device.getBuildAlias());
+        BuildInfoUtil.bootstrapDeviceBuildAttributes(
+                info,
+                device,
+                mBuildId,
+                null /* override build flavor */,
+                mBranch,
+                null /* override build alias */);
         if (mTestsDir != null && mTestsDir.isDirectory()) {
-            info.setFile("testsdir", mTestsDir, buildId);
+            info.setFile("testsdir", mTestsDir, info.getBuildId());
         }
         // Avoid tests dir being null, by creating a temporary dir.
         if (mTestsDir == null) {
diff --git a/src/com/android/tradefed/build/BuildInfo.java b/src/com/android/tradefed/build/BuildInfo.java
index 729e6ca..f1a92fd 100644
--- a/src/com/android/tradefed/build/BuildInfo.java
+++ b/src/com/android/tradefed/build/BuildInfo.java
@@ -33,6 +33,7 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
@@ -390,6 +391,23 @@
         setFile(key.getFileKey(), file, version);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public List<VersionedFile> getAppPackageFiles() {
+        List<VersionedFile> origList = getVersionedFiles(BuildInfoFileKey.PACKAGE_FILES);
+        List<VersionedFile> listCopy = new ArrayList<VersionedFile>();
+        if (origList != null) {
+            listCopy.addAll(origList);
+        }
+        return listCopy;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void addAppPackageFile(File appPackageFile, String version) {
+        setFile(BuildInfoFileKey.PACKAGE_FILES, appPackageFile, version);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -526,13 +544,13 @@
             return false;
         }
         BuildInfo other = (BuildInfo) obj;
-        return Objects.equal(mBuildAttributes, other.mBuildAttributes) &&
-                Objects.equal(mBuildBranch, other.mBuildBranch) &&
-                Objects.equal(mBuildFlavor, other.mBuildFlavor) &&
-                Objects.equal(mBuildId, other.mBuildId) &&
-                Objects.equal(mBuildTargetName, other.mBuildTargetName) &&
-                Objects.equal(mTestTag, other.mTestTag) &&
-                Objects.equal(mDeviceSerial, other.mDeviceSerial);
+        return Objects.equal(mBuildAttributes, other.mBuildAttributes)
+                && Objects.equal(mBuildBranch, other.mBuildBranch)
+                && Objects.equal(mBuildFlavor, other.mBuildFlavor)
+                && Objects.equal(mBuildId, other.mBuildId)
+                && Objects.equal(mBuildTargetName, other.mBuildTargetName)
+                && Objects.equal(mTestTag, other.mTestTag)
+                && Objects.equal(mDeviceSerial, other.mDeviceSerial);
     }
 
     /**
@@ -573,7 +591,12 @@
             for (VersionedFile vFile : mVersionedFileMultiMap.get(fileKey)) {
                 BuildFile.Builder fileInformation = BuildFile.newBuilder();
                 fileInformation.setVersion(vFile.getVersion());
-                fileInformation.setLocalPath(vFile.getFile().getAbsolutePath());
+                if (fileKey.startsWith(IBuildInfo.REMOTE_FILE_PREFIX)) {
+                    // Remote file doesn't exist on local cache, so don't save absolute path.
+                    fileInformation.setLocalPath(vFile.getFile().toString());
+                } else {
+                    fileInformation.setLocalPath(vFile.getFile().getAbsolutePath());
+                }
                 buildFile.addFile(fileInformation);
             }
             protoBuilder.addVersionedFile(buildFile);
@@ -674,4 +697,18 @@
         }
         return null;
     }
+
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<File> getRemoteFiles() {
+        Set<File> remoteFiles = new HashSet<>();
+        for (String fileKey : mVersionedFileMultiMap.keySet()) {
+            if (fileKey.startsWith(IBuildInfo.REMOTE_FILE_PREFIX)) {
+                // Remote file is not versioned, there should be only one entry.
+                remoteFiles.add(mVersionedFileMultiMap.get(fileKey).get(0).getFile());
+            }
+        }
+        return remoteFiles;
+    }
 }
diff --git a/src/com/android/tradefed/build/FileDownloadCacheWrapper.java b/src/com/android/tradefed/build/FileDownloadCacheWrapper.java
index 606b2a4..ecf3f44 100644
--- a/src/com/android/tradefed/build/FileDownloadCacheWrapper.java
+++ b/src/com/android/tradefed/build/FileDownloadCacheWrapper.java
@@ -16,6 +16,8 @@
 package com.android.tradefed.build;
 
 import java.io.File;
+import java.io.IOException;
+import java.util.List;
 
 /**
  * A wrapper class that provides {@link FileDownloadCache} facilities while implementing the
@@ -54,4 +56,16 @@
     public boolean isFresh(File localFile, String remoteFilePath) throws BuildRetrievalError {
         return mDelegateDownloader.isFresh(localFile, remoteFilePath);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public void downloadPartialFiles(
+            File destDir,
+            String remoteFilePath,
+            List<String> includeFilters,
+            List<String> excludeFilters)
+            throws BuildRetrievalError, IOException {
+        mDelegateDownloader.downloadPartialFiles(
+                destDir, remoteFilePath, includeFilters, excludeFilters);
+    }
 }
diff --git a/src/com/android/tradefed/build/IAppBuildInfo.java b/src/com/android/tradefed/build/IAppBuildInfo.java
index fead9d7..71e7bf1 100644
--- a/src/com/android/tradefed/build/IAppBuildInfo.java
+++ b/src/com/android/tradefed/build/IAppBuildInfo.java
@@ -16,24 +16,10 @@
 
 package com.android.tradefed.build;
 
-import java.io.File;
-import java.util.List;
-
 /**
- *  * A {@link IBuildInfo} that represents an Android application and its test package(s).
+ * A {@link IBuildInfo} that represents an Android application and its test package(s).
+ *
+ * @deprecated Use {@link IBuildInfo} directly.
  */
-public interface IAppBuildInfo extends IBuildInfo {
-
-    /**
-     * Gets a copy of the set of local app apk file(s) and their versions.  The returned order
-     * matches the order in which the apks were added to the {@code IAppBuildInfo}.
-     */
-    public List<VersionedFile> getAppPackageFiles();
-
-    /**
-     * Adds the local apk file and its associated version.  Note that apks will be returned from
-     * {@link #getAppPackageFiles()} in the order in which they were added by this method.
-     */
-    public void addAppPackageFile(File appPackageFile, String version);
-
-}
+@Deprecated
+public interface IAppBuildInfo extends IBuildInfo {}
diff --git a/src/com/android/tradefed/build/IFileDownloader.java b/src/com/android/tradefed/build/IFileDownloader.java
index 3b3543f..b3196dd 100644
--- a/src/com/android/tradefed/build/IFileDownloader.java
+++ b/src/com/android/tradefed/build/IFileDownloader.java
@@ -16,6 +16,8 @@
 package com.android.tradefed.build;
 
 import java.io.File;
+import java.io.IOException;
+import java.util.List;
 
 /**
  * Interface for downloading a remote file.
@@ -57,4 +59,26 @@
             throws BuildRetrievalError {
         return true;
     }
+
+    /**
+     * Download the files matching given filters in a remote zip file.
+     *
+     * <p>A file inside the remote zip file is only downloaded to its path matches any of the
+     * include filters but not the exclude filters.
+     *
+     * @param destDir the file to place the downloaded contents into.
+     * @param remoteFilePath the remote path to the file to download, relative to an implementation
+     *     specific root.
+     * @param includeFilters a list of filters to download matching files.
+     * @param excludeFilters a list of filters to skip downloading matching files.
+     * @throws BuildRetrievalError if files could not be downloaded.
+     */
+    public default void downloadPartialFiles(
+            File destDir,
+            String remoteFilePath,
+            List<String> includeFilters,
+            List<String> excludeFilters)
+            throws BuildRetrievalError, IOException {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/src/com/android/tradefed/build/LocalAppBuildProvider.java b/src/com/android/tradefed/build/LocalAppBuildProvider.java
index 9219be3..a4ba19b 100644
--- a/src/com/android/tradefed/build/LocalAppBuildProvider.java
+++ b/src/com/android/tradefed/build/LocalAppBuildProvider.java
@@ -23,10 +23,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 
-/**
- * A {@link IBuildProvider} that constructs a {@link IAppBuildInfo} based on a provided local
- * path
- */
+/** A {@link IBuildProvider} that constructs a {@link IBuildInfo} based on a provided local path */
 @OptionClass(alias = "local-app")
 public class LocalAppBuildProvider extends StubBuildProvider {
 
@@ -45,16 +42,15 @@
         // utilize parent build provider to set build id, test target name etc attributes if
         // desired
         IBuildInfo parentBuild = super.getBuild();
-        IAppBuildInfo appBuild = new AppBuildInfo((BuildInfo)parentBuild);
         for (File apkPath : mApkPaths) {
             if (!apkPath.exists()) {
                 throw new IllegalArgumentException(String.format("path '%s' does not exist. "
                         + "Please provide a valid file via --%s", apkPath.getAbsolutePath(),
                         APP_OPTION_NAME));
             }
-            appBuild.addAppPackageFile(apkPath, parentBuild.getBuildId());
+            parentBuild.addAppPackageFile(apkPath, parentBuild.getBuildId());
         }
-        return appBuild;
+        return parentBuild;
     }
 
     /**
diff --git a/src/com/android/tradefed/command/CommandOptions.java b/src/com/android/tradefed/command/CommandOptions.java
index d558e5f..64ef014 100644
--- a/src/com/android/tradefed/command/CommandOptions.java
+++ b/src/com/android/tradefed/command/CommandOptions.java
@@ -18,10 +18,11 @@
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
-import com.android.tradefed.device.metric.AutoLogCollector;
 import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.config.OptionUpdateRule;
+import com.android.tradefed.device.metric.AutoLogCollector;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.retry.RetryStrategy;
 import com.android.tradefed.util.UniqueMultiMap;
 
 import java.util.LinkedHashSet;
@@ -122,14 +123,6 @@
     private boolean mTokenSharding = false;
 
     @Option(
-        name = "skip-pre-device-setup",
-        description =
-                "allow TestInvocation to skip calling device.preInvocationSetup. This is for "
-                        + "delaying device setup when the test runs with VersionedTfLauncher."
-    )
-    private boolean mSkipPreDeviceSetup = false;
-
-    @Option(
         name = "dynamic-sharding",
         description =
                 "Allow to dynamically move IRemoteTest from one shard to another. Only for local "
@@ -177,6 +170,12 @@
     private boolean mUseParallelRemoteSetup = false;
 
     @Option(
+        name = "report-module-progression",
+        description = "For remote invocation, whether or not to report progress at module level."
+    )
+    private boolean mReportModuleProgression = false;
+
+    @Option(
         name = "auto-collect",
         description =
                 "Specify a set of collectors that will be automatically managed by the harness "
@@ -201,6 +200,30 @@
     )
     private String mHostLogSuffix = null;
 
+    // [Options related to auto-retry]
+    @Option(
+        name = "max-testcase-run-count",
+        description =
+                "If the IRemoteTest can have its testcases run multiple times, "
+                        + "the max number of runs for each testcase."
+    )
+    private int mMaxRunLimit = 1;
+
+    @Option(
+        name = "retry-strategy",
+        description =
+                "The retry strategy to be used when re-running some tests with "
+                        + "--max-testcase-run-count"
+    )
+    private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
+
+    @Option(
+        name = "auto-retry",
+        description =
+                "Whether or not to enable the new auto-retry. This is a feature flag for testing."
+    )
+    private boolean mEnableAutoRetry = false;
+
     /**
      * Set the help mode for the config.
      * <p/>
@@ -448,13 +471,6 @@
 
     /** {@inheritDoc} */
     @Override
-
-    public boolean shouldSkipPreDeviceSetup() {
-        return mSkipPreDeviceSetup;
-    }
-
-    /** {@inheritDoc} */
-    @Override
     public boolean shouldUseDynamicSharding() {
         return mDynamicSharding;
     }
@@ -536,4 +552,34 @@
     public boolean shouldUseParallelRemoteSetup() {
         return mUseParallelRemoteSetup;
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean shouldReportModuleProgression() {
+        return mReportModuleProgression;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getMaxRetryCount() {
+        return mMaxRunLimit;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setMaxRetryCount(int maxRetryCount) {
+        mMaxRunLimit = maxRetryCount;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public RetryStrategy getRetryStrategy() {
+        return mRetryStrategy;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isAutoRetryEnabled() {
+        return mEnableAutoRetry;
+    }
 }
diff --git a/src/com/android/tradefed/command/CommandRunner.java b/src/com/android/tradefed/command/CommandRunner.java
index e1bfb1f..4abd3d9 100644
--- a/src/com/android/tradefed/command/CommandRunner.java
+++ b/src/com/android/tradefed/command/CommandRunner.java
@@ -16,6 +16,8 @@
 
 package com.android.tradefed.command;
 
+import com.android.tradefed.clearcut.ClearcutClient;
+import com.android.tradefed.clearcut.TerminateClearcutClient;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.device.NoDeviceException;
@@ -77,7 +79,13 @@
     public void run(String[] args) {
         try {
             initGlobalConfig(args);
+
+            ClearcutClient client = new ClearcutClient();
+            Runtime.getRuntime().addShutdownHook(new TerminateClearcutClient(client));
+            client.notifyTradefedStartEvent();
+
             mScheduler = getCommandScheduler();
+            mScheduler.setClearcutClient(client);
             mScheduler.start();
             mScheduler.addCommand(args);
         } catch (ConfigurationException e) {
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 5a8458f..35c8228 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -19,6 +19,7 @@
 import com.android.ddmlib.DdmPreferences;
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.clearcut.ClearcutClient;
 import com.android.tradefed.command.CommandFileParser.CommandLine;
 import com.android.tradefed.command.CommandFileWatcher.ICommandFileListener;
 import com.android.tradefed.command.CommandRunner.ExitCode;
@@ -165,6 +166,9 @@
     private ExitCode mLastInvocationExitCode = ExitCode.NO_ERROR;
     private Throwable mLastInvocationThrowable = null;
 
+    /** Client to report metric data of the harness. */
+    private ClearcutClient mClient = null;
+
     @Option(name = "reload-cmdfiles", description =
             "Whether to enable the command file autoreload mechanism")
     // FIXME: enable this to be enabled or disabled on a per-cmdfile basis
@@ -731,7 +735,7 @@
                 // state during the interruption we at least do minimal tear down of devices with
                 // their built-in clean up.
                 CLog.d("Attempting postInvocationTearDown in stopInvocation");
-                device.postInvocationTearDown();
+                device.postInvocationTearDown(null);
             }
             // If invocation is not currently in an interruptible state we provide a timer
             // after which it will become interruptible.
@@ -997,6 +1001,9 @@
             exit(manager);
             cleanUp();
             CLog.logAndDisplay(LogLevel.INFO, "All done");
+            if (mClient != null) {
+                mClient.stop();
+            }
         } finally {
             // Make sure that we don't quit with messages still in the buffers
             System.err.flush();
@@ -1204,7 +1211,7 @@
                 CLog.logAndDisplay(LogLevel.ERROR, "Failed to get json command usage: %s", e);
             }
         } else if (config.getCommandOptions().isDryRunMode()) {
-            config.validateOptions(false);
+            config.validateOptions();
             String cmdLine = QuotationAwareTokenizer.combineTokens(args);
             CLog.d("Dry run mode; skipping adding command: %s", cmdLine);
             if (config.getCommandOptions().isNoisyDryRunMode()) {
@@ -2247,4 +2254,9 @@
     public synchronized int getReadyCommandCount() {
         return mReadyCommands.size();
     }
+
+    @Override
+    public void setClearcutClient(ClearcutClient client) {
+        mClient = client;
+    }
 }
diff --git a/src/com/android/tradefed/command/Console.java b/src/com/android/tradefed/command/Console.java
index a908246..49055c3 100644
--- a/src/com/android/tradefed/command/Console.java
+++ b/src/com/android/tradefed/command/Console.java
@@ -17,6 +17,8 @@
 package com.android.tradefed.command;
 
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.clearcut.ClearcutClient;
+import com.android.tradefed.clearcut.TerminateClearcutClient;
 import com.android.tradefed.config.ArgsOptionParser;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
@@ -1143,12 +1145,19 @@
      */
     public static void startConsole(Console console, String[] args) throws InterruptedException,
             ConfigurationException {
+        ClearcutClient client = new ClearcutClient();
+        Runtime.getRuntime().addShutdownHook(new TerminateClearcutClient(client));
+        client.notifyTradefedStartEvent();
+
         List<String> nonGlobalArgs = GlobalConfiguration.createGlobalConfiguration(args);
         GlobalConfiguration.getInstance().setup();
         console.setArgs(nonGlobalArgs);
         console.setCommandScheduler(GlobalConfiguration.getInstance().getCommandScheduler());
         console.setKeyStoreFactory(GlobalConfiguration.getInstance().getKeyStoreFactory());
         console.setDaemon(true);
+
+        GlobalConfiguration.getInstance().getCommandScheduler().setClearcutClient(client);
+
         console.start();
 
         // Wait for the CommandScheduler to get started before we exit the main thread.  See full
diff --git a/src/com/android/tradefed/command/ICommandOptions.java b/src/com/android/tradefed/command/ICommandOptions.java
index b2fefd2..9e5061d 100644
--- a/src/com/android/tradefed/command/ICommandOptions.java
+++ b/src/com/android/tradefed/command/ICommandOptions.java
@@ -17,6 +17,7 @@
 package com.android.tradefed.command;
 
 import com.android.tradefed.device.metric.AutoLogCollector;
+import com.android.tradefed.testtype.retry.RetryStrategy;
 import com.android.tradefed.util.UniqueMultiMap;
 
 import java.util.Set;
@@ -155,9 +156,6 @@
     /** Whether or not sharding should use the token support. */
     public boolean shouldUseTokenSharding();
 
-    /** Return true if the test should skip device setup during TestInvocation setup. */
-    public boolean shouldSkipPreDeviceSetup();
-
     /** Returns if we should use dynamic sharding or not */
     public boolean shouldUseDynamicSharding();
 
@@ -199,4 +197,19 @@
 
     /** Whether or not to attempt parallel setup of the remote devices. */
     public boolean shouldUseParallelRemoteSetup();
+
+    /** Whether or not to report progression of remote invocation at module level. */
+    public boolean shouldReportModuleProgression();
+
+    /** The maximum number of attempts during auto-retry. */
+    public int getMaxRetryCount();
+
+    /** Set the max retry count allowed during auto-retry. */
+    public void setMaxRetryCount(int maxRetryCount);
+
+    /** The {@link RetryStrategy} used during auto-retry. */
+    public RetryStrategy getRetryStrategy();
+
+    /** Whether or not to enable auto-retry. */
+    public boolean isAutoRetryEnabled();
 }
diff --git a/src/com/android/tradefed/command/ICommandScheduler.java b/src/com/android/tradefed/command/ICommandScheduler.java
index aec4f8b..7a3ea3f 100644
--- a/src/com/android/tradefed/command/ICommandScheduler.java
+++ b/src/com/android/tradefed/command/ICommandScheduler.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.command;
 
+import com.android.tradefed.clearcut.ClearcutClient;
 import com.android.tradefed.command.CommandRunner.ExitCode;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.IConfigurationFactory;
@@ -311,4 +312,7 @@
 
     /** Returns the number of Commands in ready state in the queue. */
     public int getReadyCommandCount();
+
+    /** Set the client to report harness data */
+    public void setClearcutClient(ClearcutClient client);
 }
diff --git a/src/com/android/tradefed/command/Verify.java b/src/com/android/tradefed/command/Verify.java
deleted file mode 100644
index b72e8c0..0000000
--- a/src/com/android/tradefed/command/Verify.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.command;
-
-import com.android.tradefed.config.ArgsOptionParser;
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.Option;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Alternate Trade Federation entrypoint to validate command files
- */
-public class Verify {
-
-    private static final int EXIT_STATUS_OKAY = 0x0;
-    private static final int EXIT_STATUS_FAILED = 0x1;
-
-    @Option(name = "cmdfile", description = "command file to verify")
-    private List<File> mCmdFiles = new ArrayList<>();
-
-    @Option(name = "show-commands", description = "Whether to print all generated commands")
-    private boolean mShowCommands = false;
-
-    @Option(name = "quiet", shortName = 'q', description = "Whether to silence all output. " +
-            "Overrides all other output-related settings.")
-    private boolean mQuiet = false;
-
-    @Option(name = "help", shortName = 'h', description = "Print help")
-    private boolean mHelp = false;
-
-    /**
-     * Returns whether the "--help"/"-h" option was passed to the instance
-     */
-    public boolean isHelpMode() {
-        return mHelp;
-    }
-
-    /**
-     * Program main entrypoint
-     */
-    public static void main(final String[] mainArgs) throws ConfigurationException {
-        try {
-            Verify verify = new Verify();
-            ArgsOptionParser optionSetter = new ArgsOptionParser(verify);
-            optionSetter.parse(mainArgs);
-            if (verify.isHelpMode()) {
-                // Print help, then exit
-                System.err.println(ArgsOptionParser.getOptionHelp(false, verify));
-                System.exit(EXIT_STATUS_OKAY);
-            }
-
-            if (verify.run()) {
-                // true == everything's good
-                System.exit(EXIT_STATUS_OKAY);
-            } else {
-                // false == whoopsie!
-                System.exit(EXIT_STATUS_FAILED);
-            }
-
-        } finally {
-            System.err.flush();
-            System.out.flush();
-        }
-    }
-
-    /**
-     * Start validating all specified cmdfiles
-     */
-    public boolean run() {
-        boolean anyFailures = false;
-
-        for (File cmdFile : mCmdFiles) {
-            try {
-                // if verify returns false, then we set anyFailures to true
-                anyFailures |= !runVerify(cmdFile);
-
-            } catch (Throwable t) {
-                if (!mQuiet) {
-                    System.err.format("Caught exception while parsing \"%s\"\n", cmdFile);
-                    System.err.println(t);
-                }
-                anyFailures = true;
-            }
-        }
-
-        return !anyFailures;
-    }
-
-    /**
-     * Validate the specified cmdfile
-     */
-    public boolean runVerify(File cmdFile) {
-        final CommandFileParser parser = new CommandFileParser();
-        try {
-            List<CommandFileParser.CommandLine> commands = parser.parseFile(cmdFile);
-            if (!mQuiet) {
-                System.out.format("Successfully parsed %d commands from cmdfile %s\n",
-                        commands.size(), cmdFile);
-
-                if (mShowCommands) {
-                    int i = 1;
-                    int digits = (int) Math.ceil(Math.log10(commands.size()));
-                    // Create a format string that will leave enough space for an index prefix
-                    // without mucking up alignment
-                    String format = String.format("%%%dd: %%s\n", digits);
-                    for (CommandFileParser.CommandLine cmd : commands) {
-                        System.out.format(format, i++, cmd);
-                    }
-                }
-                System.out.println();
-            }
-        } catch (ConfigurationException | IOException e) {
-            if (!mQuiet) {
-                System.err.format("Failed to parse %s:\n", cmdFile);
-                System.err.println(e);
-            }
-
-            return false;
-        }
-
-        return true;
-    }
-}
diff --git a/src/com/android/tradefed/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index 9982d59..242c566 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -19,7 +19,6 @@
 import com.android.tradefed.build.IBuildProvider;
 import com.android.tradefed.command.CommandOptions;
 import com.android.tradefed.command.ICommandOptions;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
 import com.android.tradefed.config.OptionSetter.FieldDef;
 import com.android.tradefed.device.IDeviceRecovery;
 import com.android.tradefed.device.IDeviceSelection;
@@ -42,6 +41,7 @@
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.StubTest;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IDisableable;
 import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.QuotationAwareTokenizer;
 import com.android.tradefed.util.keystore.IKeyStoreClient;
@@ -518,23 +518,34 @@
      * Return a copy of all config objects
      */
     private Collection<Object> getAllConfigurationObjects() {
-        return getAllConfigurationObjects(null);
+        return getAllConfigurationObjects(null, true);
+    }
+
+    /** Return a copy of all config objects that are not disabled via {@link IDisableable}. */
+    private Collection<Object> getAllNonDisabledConfigurationObjects() {
+        return getAllConfigurationObjects(null, false);
     }
 
     /**
      * Return a copy of all config objects, minus the object configuration of the type specified.
      * Returns all the config objects if param is null.
      */
-    private Collection<Object> getAllConfigurationObjects(String excludedConfigName) {
+    private Collection<Object> getAllConfigurationObjects(
+            String excludedConfigName, boolean includeDisabled) {
         Collection<Object> objectsCopy = new ArrayList<Object>();
         for (Entry<String, List<Object>> entryList : mConfigMap.entrySet()) {
-            if (excludedConfigName != null) {
-                // Only add if not a descriptor config object type.
-                if (!excludedConfigName.equals(entryList.getKey())) {
-                    objectsCopy.addAll(entryList.getValue());
-                }
-            } else {
+            if (excludedConfigName != null && excludedConfigName.equals(entryList.getKey())) {
+                continue;
+            }
+            if (includeDisabled) {
                 objectsCopy.addAll(entryList.getValue());
+            } else {
+                for (Object o : entryList.getValue()) {
+                    if (o instanceof IDisableable && ((IDisableable) o).isDisabled()) {
+                        continue;
+                    }
+                    objectsCopy.add(o);
+                }
             }
         }
         return objectsCopy;
@@ -1029,7 +1040,7 @@
         // allow passing its option via command line.
         ArgsOptionParser parser =
                 new ArgsOptionParser(
-                        getAllConfigurationObjects(CONFIGURATION_DESCRIPTION_TYPE_NAME));
+                        getAllConfigurationObjects(CONFIGURATION_DESCRIPTION_TYPE_NAME, true));
         if (keyStoreClient != null) {
             parser.setKeyStore(keyStoreClient);
         }
@@ -1272,13 +1283,7 @@
      */
     @Override
     public void validateOptions() throws ConfigurationException {
-        validateOptions(true);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void validateOptions(boolean download) throws ConfigurationException {
-        ArgsOptionParser argsParser = new ArgsOptionParser(getAllConfigurationObjects());
+        ArgsOptionParser argsParser = new ArgsOptionParser(getAllNonDisabledConfigurationObjects());
         argsParser.validateMandatoryOptions();
         ICommandOptions options = getCommandOptions();
         if (options.getShardCount() != null && options.getShardCount() < 1) {
@@ -1289,16 +1294,21 @@
                         || options.getShardIndex() >= options.getShardCount())) {
             throw new ConfigurationException("a shard index must be in range [0, shard count)");
         }
-        // Parent invocation for local sharding should not resolved the dynamic @option yet.
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void resolveDynamicOptions() throws ConfigurationException {
+        ICommandOptions options = getCommandOptions();
         if (options.getShardCount() != null && options.getShardIndex() == null) {
-            download = false;
             CLog.w("Skipping download due to local sharding detected.");
+            return;
         }
-        if (download) {
-            CLog.d("Resolve and download remote files from @Option");
-            // Setup and validate the GCS File paths
-            mRemoteFiles.addAll(argsParser.validateRemoteFilePath());
-        }
+
+        ArgsOptionParser argsParser = new ArgsOptionParser(getAllConfigurationObjects());
+        CLog.d("Resolve and download remote files from @Option");
+        // Setup and validate the GCS File paths
+        mRemoteFiles.addAll(argsParser.validateRemoteFilePath());
     }
 
     /** {@inheritDoc} */
@@ -1320,13 +1330,16 @@
     /** {@inheritDoc} */
     @Override
     public void dumpXml(PrintWriter output, List<String> excludeFilters) throws IOException {
-        dumpXml(output, excludeFilters, true);
+        dumpXml(output, excludeFilters, true, true);
     }
 
     /** {@inheritDoc} */
     @Override
     public void dumpXml(
-            PrintWriter output, List<String> excludeFilters, boolean printDeprecatedOptions)
+            PrintWriter output,
+            List<String> excludeFilters,
+            boolean printDeprecatedOptions,
+            boolean printUnchangedOptions)
             throws IOException {
         KXmlSerializer serializer = new KXmlSerializer();
         serializer.setOutput(output);
@@ -1340,7 +1353,8 @@
                     MULTI_PRE_TARGET_PREPARER_TYPE_NAME,
                     multiPreTargerPrep,
                     excludeFilters,
-                    printDeprecatedOptions);
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
         }
 
         for (IMultiTargetPreparer multipreparer : getMultiTargetPreparers()) {
@@ -1349,7 +1363,8 @@
                     MULTI_PREPARER_TYPE_NAME,
                     multipreparer,
                     excludeFilters,
-                    printDeprecatedOptions);
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
         }
 
         if (getDeviceConfig().size() > 1) {
@@ -1365,33 +1380,38 @@
                         BUILD_PROVIDER_TYPE_NAME,
                         deviceConfig.getBuildProvider(),
                         excludeFilters,
-                        printDeprecatedOptions);
+                        printDeprecatedOptions,
+                        printUnchangedOptions);
                 for (ITargetPreparer preparer : deviceConfig.getTargetPreparers()) {
                     ConfigurationUtil.dumpClassToXml(
                             serializer,
                             TARGET_PREPARER_TYPE_NAME,
                             preparer,
                             excludeFilters,
-                            printDeprecatedOptions);
+                            printDeprecatedOptions,
+                            printUnchangedOptions);
                 }
                 ConfigurationUtil.dumpClassToXml(
                         serializer,
                         DEVICE_RECOVERY_TYPE_NAME,
                         deviceConfig.getDeviceRecovery(),
                         excludeFilters,
-                        printDeprecatedOptions);
+                        printDeprecatedOptions,
+                        printUnchangedOptions);
                 ConfigurationUtil.dumpClassToXml(
                         serializer,
                         DEVICE_REQUIREMENTS_TYPE_NAME,
                         deviceConfig.getDeviceRequirements(),
                         excludeFilters,
-                        printDeprecatedOptions);
+                        printDeprecatedOptions,
+                        printUnchangedOptions);
                 ConfigurationUtil.dumpClassToXml(
                         serializer,
                         DEVICE_OPTIONS_TYPE_NAME,
                         deviceConfig.getDeviceOptions(),
                         excludeFilters,
-                        printDeprecatedOptions);
+                        printDeprecatedOptions,
+                        printUnchangedOptions);
                 serializer.endTag(null, Configuration.DEVICE_NAME);
             }
         } else {
@@ -1401,70 +1421,85 @@
                     BUILD_PROVIDER_TYPE_NAME,
                     getBuildProvider(),
                     excludeFilters,
-                    printDeprecatedOptions);
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
             for (ITargetPreparer preparer : getTargetPreparers()) {
                 ConfigurationUtil.dumpClassToXml(
                         serializer,
                         TARGET_PREPARER_TYPE_NAME,
                         preparer,
                         excludeFilters,
-                        printDeprecatedOptions);
+                        printDeprecatedOptions,
+                        printUnchangedOptions);
             }
             ConfigurationUtil.dumpClassToXml(
                     serializer,
                     DEVICE_RECOVERY_TYPE_NAME,
                     getDeviceRecovery(),
                     excludeFilters,
-                    printDeprecatedOptions);
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
             ConfigurationUtil.dumpClassToXml(
                     serializer,
                     DEVICE_REQUIREMENTS_TYPE_NAME,
                     getDeviceRequirements(),
                     excludeFilters,
-                    printDeprecatedOptions);
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
             ConfigurationUtil.dumpClassToXml(
                     serializer,
                     DEVICE_OPTIONS_TYPE_NAME,
                     getDeviceOptions(),
                     excludeFilters,
-                    printDeprecatedOptions);
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
         }
         for (IRemoteTest test : getTests()) {
             ConfigurationUtil.dumpClassToXml(
-                    serializer, TEST_TYPE_NAME, test, excludeFilters, printDeprecatedOptions);
+                    serializer,
+                    TEST_TYPE_NAME,
+                    test,
+                    excludeFilters,
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
         }
         ConfigurationUtil.dumpClassToXml(
                 serializer,
                 CONFIGURATION_DESCRIPTION_TYPE_NAME,
                 getConfigurationDescription(),
                 excludeFilters,
-                printDeprecatedOptions);
+                printDeprecatedOptions,
+                printUnchangedOptions);
         ConfigurationUtil.dumpClassToXml(
                 serializer,
                 LOGGER_TYPE_NAME,
                 getLogOutput(),
                 excludeFilters,
-                printDeprecatedOptions);
+                printDeprecatedOptions,
+                printUnchangedOptions);
         ConfigurationUtil.dumpClassToXml(
                 serializer,
                 LOG_SAVER_TYPE_NAME,
                 getLogSaver(),
                 excludeFilters,
-                printDeprecatedOptions);
+                printDeprecatedOptions,
+                printUnchangedOptions);
         for (ITestInvocationListener listener : getTestInvocationListeners()) {
             ConfigurationUtil.dumpClassToXml(
                     serializer,
                     RESULT_REPORTER_TYPE_NAME,
                     listener,
                     excludeFilters,
-                    printDeprecatedOptions);
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
         }
         ConfigurationUtil.dumpClassToXml(
                 serializer,
                 CMD_OPTIONS_TYPE_NAME,
                 getCommandOptions(),
                 excludeFilters,
-                printDeprecatedOptions);
+                printDeprecatedOptions,
+                printUnchangedOptions);
 
         for (IMetricCollector collector : getMetricCollectors()) {
             ConfigurationUtil.dumpClassToXml(
@@ -1472,7 +1507,8 @@
                     DEVICE_METRICS_COLLECTOR_TYPE_NAME,
                     collector,
                     excludeFilters,
-                    printDeprecatedOptions);
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
         }
 
         for (ISystemStatusChecker checker : getSystemStatusCheckers()) {
@@ -1481,7 +1517,8 @@
                     SYSTEM_STATUS_CHECKER_TYPE_NAME,
                     checker,
                     excludeFilters,
-                    printDeprecatedOptions);
+                    printDeprecatedOptions,
+                    printUnchangedOptions);
         }
 
         ConfigurationUtil.dumpClassToXml(
@@ -1489,7 +1526,8 @@
                 SANBOX_OPTIONS_TYPE_NAME,
                 getConfigurationObject(SANBOX_OPTIONS_TYPE_NAME),
                 excludeFilters,
-                printDeprecatedOptions);
+                printDeprecatedOptions,
+                printUnchangedOptions);
 
         serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
         serializer.endDocument();
diff --git a/src/com/android/tradefed/config/ConfigurationDef.java b/src/com/android/tradefed/config/ConfigurationDef.java
index c99b79f..4024356 100644
--- a/src/com/android/tradefed/config/ConfigurationDef.java
+++ b/src/com/android/tradefed/config/ConfigurationDef.java
@@ -16,12 +16,10 @@
 
 package com.android.tradefed.config;
 
-import com.android.tradefed.build.BuildSerializedVersion;
 import com.android.tradefed.device.metric.IMetricCollector;
 import com.android.tradefed.log.LogUtil.CLog;
 
 import java.io.File;
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -51,38 +49,6 @@
     /** The set of files (and modification times) that were used to load this config */
     private final Map<File, Long> mSourceFiles = new HashMap<>();
 
-    /** Holds the details of an option. */
-    public static final class OptionDef implements Serializable {
-        private static final long serialVersionUID = BuildSerializedVersion.VERSION;
-
-        public final String name;
-        public final String key;
-        public final String value;
-        public final String source;
-        public final String applicableObjectType;
-
-        public OptionDef(String optionName, String optionValue, String source) {
-            this(optionName, null, optionValue, source, null);
-        }
-
-        public OptionDef(String optionName, String optionKey, String optionValue, String source) {
-            this(optionName, optionKey, optionValue, source, null);
-        }
-
-        public OptionDef(
-                String optionName,
-                String optionKey,
-                String optionValue,
-                String source,
-                String type) {
-            this.name = optionName;
-            this.key = optionKey;
-            this.value = optionValue;
-            this.source = source;
-            this.applicableObjectType = type;
-        }
-    }
-
     /**
      * Object to hold info for a className and the appearance number it has (e.g. if a config has
      * the same object twice, the first one will have the first appearance number).
diff --git a/src/com/android/tradefed/config/ConfigurationUtil.java b/src/com/android/tradefed/config/ConfigurationUtil.java
index f162181..e1c539b 100644
--- a/src/com/android/tradefed/config/ConfigurationUtil.java
+++ b/src/com/android/tradefed/config/ConfigurationUtil.java
@@ -72,16 +72,24 @@
      *     be excluded from the dump. for example: {@link Configuration#TARGET_PREPARER_TYPE_NAME}.
      *     com.android.tradefed.testtype.StubTest
      * @param printDeprecatedOptions whether or not to print deprecated options
+     * @param printUnchangedOptions whether or not to print options that haven't been changed
      */
     static void dumpClassToXml(
             KXmlSerializer serializer,
             String classTypeName,
             Object obj,
             List<String> excludeClassFilter,
-            boolean printDeprecatedOptions)
+            boolean printDeprecatedOptions,
+            boolean printUnchangedOptions)
             throws IOException {
         dumpClassToXml(
-                serializer, classTypeName, obj, false, excludeClassFilter, printDeprecatedOptions);
+                serializer,
+                classTypeName,
+                obj,
+                false,
+                excludeClassFilter,
+                printDeprecatedOptions,
+                printUnchangedOptions);
     }
 
     /**
@@ -95,6 +103,7 @@
      *     be excluded from the dump. for example: {@link Configuration#TARGET_PREPARER_TYPE_NAME}.
      *     com.android.tradefed.testtype.StubTest
      * @param printDeprecatedOptions whether or not to print deprecated options
+     * @param printUnchangedOptions whether or not to print options that haven't been changed
      */
     static void dumpClassToXml(
             KXmlSerializer serializer,
@@ -102,7 +111,8 @@
             Object obj,
             boolean isGenericObject,
             List<String> excludeClassFilter,
-            boolean printDeprecatedOptions)
+            boolean printDeprecatedOptions,
+            boolean printUnchangedOptions)
             throws IOException {
         if (excludeClassFilter.contains(classTypeName)) {
             return;
@@ -114,12 +124,12 @@
             serializer.startTag(null, "object");
             serializer.attribute(null, "type", classTypeName);
             serializer.attribute(null, CLASS_NAME, obj.getClass().getName());
-            dumpOptionsToXml(serializer, obj, printDeprecatedOptions);
+            dumpOptionsToXml(serializer, obj, printDeprecatedOptions, printUnchangedOptions);
             serializer.endTag(null, "object");
         } else {
             serializer.startTag(null, classTypeName);
             serializer.attribute(null, CLASS_NAME, obj.getClass().getName());
-            dumpOptionsToXml(serializer, obj, printDeprecatedOptions);
+            dumpOptionsToXml(serializer, obj, printDeprecatedOptions, printUnchangedOptions);
             serializer.endTag(null, classTypeName);
         }
     }
@@ -130,13 +140,20 @@
      * @param serializer a {@link KXmlSerializer} to create the XML dump
      * @param obj {@link Object} to be added to the XML dump
      * @param printDeprecatedOptions whether or not to skip the deprecated options
+     * @param printUnchangedOptions whether or not to print options that haven't been changed
      */
     @SuppressWarnings({"rawtypes", "unchecked"})
     private static void dumpOptionsToXml(
-            KXmlSerializer serializer, Object obj, boolean printDeprecatedOptions)
+            KXmlSerializer serializer,
+            Object obj,
+            boolean printDeprecatedOptions,
+            boolean printUnchangedOptions)
             throws IOException {
         for (Field field : OptionSetter.getOptionFieldsForClass(obj.getClass())) {
             Option option = field.getAnnotation(Option.class);
+            if (!printUnchangedOptions && !option.isChanged()) {
+                continue;
+            }
             Deprecated deprecatedAnnotation = field.getAnnotation(Deprecated.class);
             // If enabled, skip @Deprecated options
             if (!printDeprecatedOptions && deprecatedAnnotation != null) {
diff --git a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
index b25f556..2585069 100644
--- a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
@@ -18,13 +18,21 @@
 import com.android.annotations.VisibleForTesting;
 import com.android.tradefed.config.OptionSetter.OptionFieldsForName;
 import com.android.tradefed.config.remote.GcsRemoteFileResolver;
+import com.android.tradefed.config.remote.HttpRemoteFileResolver;
+import com.android.tradefed.config.remote.HttpsRemoteFileResolver;
 import com.android.tradefed.config.remote.IRemoteFileResolver;
+import com.android.tradefed.config.remote.LocalFileResolver;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.ZipUtil;
+import com.android.tradefed.util.ZipUtil2;
 
 import java.io.File;
+import java.io.IOException;
 import java.lang.reflect.Field;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -49,9 +57,16 @@
 
     static {
         PROTOCOL_SUPPORT.put(GcsRemoteFileResolver.PROTOCOL, new GcsRemoteFileResolver());
+        PROTOCOL_SUPPORT.put(LocalFileResolver.PROTOCOL, new LocalFileResolver());
+        PROTOCOL_SUPPORT.put(HttpRemoteFileResolver.PROTOCOL_HTTP, new HttpRemoteFileResolver());
+        PROTOCOL_SUPPORT.put(HttpsRemoteFileResolver.PROTOCOL_HTTPS, new HttpsRemoteFileResolver());
     }
     // The configuration map being static, we only need to update it once per TF instance.
     private static AtomicBoolean sIsUpdateDone = new AtomicBoolean(false);
+    // Query key for requesting to unzip a downloaded file automatically.
+    public static final String UNZIP_KEY = "unzip";
+    // Query key for requesting a download to be optional, so if it fails we don't replace it.
+    public static final String OPTIONAL_KEY = "optional";
 
     private Map<String, OptionFieldsForName> mOptionMap;
 
@@ -69,9 +84,11 @@
     public final Set<File> validateRemoteFilePath() throws ConfigurationException {
         Set<File> downloadedFiles = new HashSet<>();
         try {
+            Map<Object, Field> fieldSeen = new HashMap<>();
             for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
                 final OptionFieldsForName optionFields = optionPair.getValue();
                 for (Map.Entry<Object, Field> fieldEntry : optionFields) {
+
                     final Object obj = fieldEntry.getKey();
                     final Field field = fieldEntry.getValue();
                     final Option option = field.getAnnotation(Option.class);
@@ -83,14 +100,21 @@
                     final Object value;
                     try {
                         value = field.get(obj);
+                        if (value == null) {
+                            continue;
+                        }
                     } catch (IllegalAccessException e) {
                         throw new ConfigurationException(
                                 String.format("internal error: %s", e.getMessage()));
                     }
 
-                    if (value == null) {
+                    if (fieldSeen.get(value) != null && fieldSeen.get(value).equals(field)) {
                         continue;
-                    } else if (value instanceof File) {
+                    }
+                    // Keep track of the field set on each object
+                    fieldSeen.put(value, field);
+
+                    if (value instanceof File) {
                         File consideredFile = (File) value;
                         File downloadedFile = resolveRemoteFiles(consideredFile, option);
                         if (downloadedFile != null) {
@@ -188,6 +212,62 @@
         return downloadedFiles;
     }
 
+    /**
+     * Download the files matching given filters in a remote zip file.
+     *
+     * <p>A file inside the remote zip file is only downloaded if its path matches any of the
+     * include filters but not the exclude filters.
+     *
+     * @param destDir the file to place the downloaded contents into.
+     * @param remoteZipFilePath the remote path to the zip file to download, relative to an
+     *     implementation specific root.
+     * @param includeFilters a list of regex strings to download matching files. A file's path
+     *     matching any filter will be downloaded.
+     * @param excludeFilters a list of regex strings to skip downloading matching files. A file's
+     *     path matching any filter will not be downloaded.
+     * @throws ConfigurationException if files could not be downloaded.
+     */
+    public void resolvePartialDownloadZip(
+            File destDir,
+            String remoteZipFilePath,
+            List<String> includeFilters,
+            List<String> excludeFilters)
+            throws ConfigurationException {
+        Map<String, String> queryArgs;
+        String protocol;
+        try {
+            URI uri = new URI(remoteZipFilePath);
+            protocol = uri.getScheme();
+            queryArgs = parseQuery(uri.getQuery());
+        } catch (URISyntaxException e) {
+            throw new ConfigurationException(
+                    String.format(
+                            "Failed to parse the remote zip file path: %s", remoteZipFilePath),
+                    e);
+        }
+        IRemoteFileResolver resolver = getResolver(protocol);
+
+        queryArgs.put("partial_download_dir", destDir.getAbsolutePath());
+        if (includeFilters != null) {
+            queryArgs.put("include_filters", String.join(";", includeFilters));
+        }
+        if (excludeFilters != null) {
+            queryArgs.put("exclude_filters", String.join(";", excludeFilters));
+        }
+        // Downloaded individual files should be saved to destDir, return value is not needed.
+        try {
+            resolver.resolveRemoteFiles(new File(remoteZipFilePath), null, queryArgs);
+        } catch (ConfigurationException e) {
+            if (isOptional(queryArgs)) {
+                CLog.d(
+                        "Failed to partially download '%s' but marked optional so skipping: %s",
+                        remoteZipFilePath, e.getMessage());
+            } else {
+                throw e;
+            }
+        }
+    }
+
     @VisibleForTesting
     protected IRemoteFileResolver getResolver(String protocol) {
         if (updateProtocols()) {
@@ -216,27 +296,83 @@
         return GlobalConfiguration.getInstance();
     }
 
+    /**
+     * Utility that allows to check whether or not a file should be unzip and unzip it if required.
+     */
+    public static final File unzipIfRequired(File downloadedFile, Map<String, String> query)
+            throws IOException {
+        String unzipValue = query.get(UNZIP_KEY);
+        if (unzipValue != null && "true".equals(unzipValue.toLowerCase())) {
+            // File was requested to be unzipped.
+            if (ZipUtil.isZipFileValid(downloadedFile, false)) {
+                File unzipped =
+                        ZipUtil2.extractZipToTemp(
+                                downloadedFile, FileUtil.getBaseName(downloadedFile.getName()));
+                FileUtil.deleteFile(downloadedFile);
+                return unzipped;
+            } else {
+                CLog.w("%s was requested to be unzipped but is not a valid zip.", downloadedFile);
+            }
+        }
+        // Return the original file untouched
+        return downloadedFile;
+    }
+
     private File resolveRemoteFiles(File consideredFile, Option option)
             throws ConfigurationException {
+        File fileToResolve;
         String path = consideredFile.getPath();
-        String protocol = getProtocol(path);
+        String protocol;
+        Map<String, String> query;
+        try {
+            URI uri = new URI(path);
+            protocol = uri.getScheme();
+            query = parseQuery(uri.getQuery());
+            fileToResolve = new File(protocol + ":" + uri.getPath());
+        } catch (URISyntaxException e) {
+            CLog.e(e);
+            return null;
+        }
         IRemoteFileResolver resolver = getResolver(protocol);
         if (resolver != null) {
-            return resolver.resolveRemoteFiles(consideredFile, option);
+            try {
+                return resolver.resolveRemoteFiles(fileToResolve, option, query);
+            } catch (ConfigurationException e) {
+                if (isOptional(query)) {
+                    CLog.d(
+                            "Failed to resolve '%s' but marked optional so skipping: %s",
+                            fileToResolve, e.getMessage());
+                } else {
+                    throw e;
+                }
+            }
         }
         // Not a remote file
         return null;
     }
 
     /**
-     * Java URL doesn't recognize 'gs' as a protocol and throws an exception so we do the protocol
-     * extraction ourselves.
+     * Parse a URL query style. Delimited by &, and map values represented by =. Example:
+     * ?key=value&key2=value2
      */
-    private String getProtocol(String path) {
-        int index = path.indexOf(":/");
-        if (index == -1) {
-            return "";
+    private Map<String, String> parseQuery(String query) {
+        Map<String, String> values = new HashMap<>();
+        if (query == null) {
+            return values;
         }
-        return path.substring(0, index);
+        for (String maps : query.split("&")) {
+            String[] keyVal = maps.split("=");
+            values.put(keyVal[0], keyVal[1]);
+        }
+        return values;
+    }
+
+    /** Whether or not a link was requested as optional. */
+    private boolean isOptional(Map<String, String> query) {
+        String value = query.get(OPTIONAL_KEY);
+        if (value == null) {
+            return false;
+        }
+        return "true".equals(value.toLowerCase());
     }
 }
diff --git a/src/com/android/tradefed/config/IConfiguration.java b/src/com/android/tradefed/config/IConfiguration.java
index bc8d1dd..53751cc 100644
--- a/src/com/android/tradefed/config/IConfiguration.java
+++ b/src/com/android/tradefed/config/IConfiguration.java
@@ -18,7 +18,6 @@
 
 import com.android.tradefed.build.IBuildProvider;
 import com.android.tradefed.command.ICommandOptions;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
 import com.android.tradefed.device.IDeviceRecovery;
 import com.android.tradefed.device.IDeviceSelection;
 import com.android.tradefed.device.TestDeviceOptions;
@@ -37,6 +36,7 @@
 import org.json.JSONArray;
 import org.json.JSONException;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
 import java.io.PrintWriter;
@@ -533,14 +533,12 @@
     public void validateOptions() throws ConfigurationException;
 
     /**
-     * Validate option values.
+     * Resolve options of {@link File} pointing to a remote location. This requires {@link
+     * #cleanDynamicOptionFiles()} to be called to clean up the files.
      *
-     * <p>Currently this will just validate that all mandatory options have been set
-     *
-     * @param download Whether or not to download the files associated to a remote path
-     * @throws ConfigurationException if config is not valid
+     * @throws ConfigurationException
      */
-    public void validateOptions(boolean download) throws ConfigurationException;
+    public void resolveDynamicOptions() throws ConfigurationException;
 
     /** Delete any files that was downloaded to resolved Option fields of remote files. */
     public void cleanDynamicOptionFiles();
@@ -594,6 +592,9 @@
      * @throws IOException
      */
     public void dumpXml(
-            PrintWriter output, List<String> excludeFilters, boolean printDeprecatedOptions)
+            PrintWriter output,
+            List<String> excludeFilters,
+            boolean printDeprecatedOptions,
+            boolean printUnchangedOptions)
             throws IOException;
 }
diff --git a/src/com/android/tradefed/config/OptionSetter.java b/src/com/android/tradefed/config/OptionSetter.java
index 9e28d8b..1697667 100644
--- a/src/com/android/tradefed/config/OptionSetter.java
+++ b/src/com/android/tradefed/config/OptionSetter.java
@@ -29,6 +29,7 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -533,10 +534,35 @@
                     "internal error when setting option '%s'", optionName), e);
 
         }
-
+        Option option = field.getAnnotation(Option.class);
+        updateOptionChangedField(option);
         return fieldWasSet;
     }
 
+    /**
+     * Change the {@link Option#isChanged()} field to True if we are setting a value on that option.
+     * This will help to track which options are used.
+     */
+    @SuppressWarnings("unchecked")
+    private static void updateOptionChangedField(Option option) {
+        Object handler = Proxy.getInvocationHandler(option);
+        Field f;
+        try {
+            // "memberValues" is a special field for annotation and their method current values.
+            f = handler.getClass().getDeclaredField("memberValues");
+        } catch (NoSuchFieldException | SecurityException e) {
+            throw new IllegalStateException(e);
+        }
+        f.setAccessible(true);
+        Map<String, Object> memberValues;
+        try {
+            memberValues = (Map<String, Object>) f.get(handler);
+            // Set the #isChanged() method return to true.
+            memberValues.put("isChanged", true);
+        } catch (IllegalArgumentException | IllegalAccessException e) {
+            throw new IllegalStateException(e);
+        }
+    }
 
     /**
      * Sets the given {@link Option} fields value.
diff --git a/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java b/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java
index afaa5be..8d1ecbd 100644
--- a/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java
@@ -19,10 +19,13 @@
 import com.android.tradefed.build.BuildRetrievalError;
 import com.android.tradefed.build.gcs.GCSDownloaderHelper;
 import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.log.LogUtil.CLog;
 
 import java.io.File;
+import java.io.IOException;
+import java.util.Map;
 
 import javax.annotation.Nonnull;
 
@@ -34,15 +37,17 @@
     private GCSDownloaderHelper mHelper = null;
 
     @Override
-    public File resolveRemoteFiles(File consideredFile, Option option)
+    public File resolveRemoteFiles(File consideredFile, Option option, Map<String, String> query)
             throws ConfigurationException {
         // Don't use absolute path as it would not start with gs:
         String path = consideredFile.getPath();
         CLog.d("Considering option '%s' with path: '%s' for download.", option.name(), path);
-        // We need to download the file from the bucket
         try {
-            return getDownloader().fetchTestResource(path);
-        } catch (BuildRetrievalError e) {
+            // We need to download the file from the bucket
+            File downloadedFile = getDownloader().fetchTestResource(path);
+            // Unzip it if required
+            return DynamicRemoteFileResolver.unzipIfRequired(downloadedFile, query);
+        } catch (BuildRetrievalError | IOException e) {
             CLog.e(e);
             throw new ConfigurationException(
                     String.format("Failed to download %s due to: %s", path, e.getMessage()), e);
diff --git a/src/com/android/tradefed/config/remote/HttpRemoteFileResolver.java b/src/com/android/tradefed/config/remote/HttpRemoteFileResolver.java
new file mode 100644
index 0000000..b92c6c4
--- /dev/null
+++ b/src/com/android/tradefed/config/remote/HttpRemoteFileResolver.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.net.HttpHelper;
+import com.android.tradefed.util.net.IHttpHelper;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+/** Implementation of {@link IRemoteFileResolver} that allows downloading remote file via http */
+public class HttpRemoteFileResolver implements IRemoteFileResolver {
+
+    public static final String PROTOCOL_HTTP = "http";
+
+    @Override
+    public File resolveRemoteFiles(
+            File consideredFile, Option option, Map<String, String> queryArgs)
+            throws ConfigurationException {
+        // Don't use absolute path as it would not start with gs:
+        String path = consideredFile.getPath();
+        CLog.d("Considering option '%s' with path: '%s' for download.", option.name(), path);
+        // Replace the very first / by // to be http:// again.
+        path = path.replaceFirst(":/", "://");
+
+        IHttpHelper downloader = getDownloader();
+        File downloadedFile = null;
+        try {
+            downloadedFile =
+                    FileUtil.createTempFile(
+                            FileUtil.getBaseName(consideredFile.getName()),
+                            FileUtil.getExtension(consideredFile.getName()));
+            downloader.doGet(path, new FileOutputStream(downloadedFile));
+        } catch (IOException | RuntimeException e) {
+            FileUtil.deleteFile(downloadedFile);
+            throw new ConfigurationException(
+                    String.format("Failed to download %s due to: %s", path, e.getMessage()), e);
+        }
+        return downloadedFile;
+    }
+
+    @Override
+    public @Nonnull String getSupportedProtocol() {
+        return PROTOCOL_HTTP;
+    }
+
+    @VisibleForTesting
+    protected IHttpHelper getDownloader() {
+        return new HttpHelper();
+    }
+}
diff --git a/src/com/android/tradefed/config/remote/HttpsRemoteFileResolver.java b/src/com/android/tradefed/config/remote/HttpsRemoteFileResolver.java
new file mode 100644
index 0000000..acb5c03
--- /dev/null
+++ b/src/com/android/tradefed/config/remote/HttpsRemoteFileResolver.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import javax.annotation.Nonnull;
+
+/** Implementation of {@link IRemoteFileResolver} that allows downloading remote file via https */
+public class HttpsRemoteFileResolver extends HttpRemoteFileResolver {
+
+    public static final String PROTOCOL_HTTPS = "https";
+
+    @Override
+    public @Nonnull String getSupportedProtocol() {
+        return PROTOCOL_HTTPS;
+    }
+}
diff --git a/src/com/android/tradefed/config/remote/IRemoteFileResolver.java b/src/com/android/tradefed/config/remote/IRemoteFileResolver.java
index 3d54ecb..f1413bd 100644
--- a/src/com/android/tradefed/config/remote/IRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/remote/IRemoteFileResolver.java
@@ -19,6 +19,7 @@
 import com.android.tradefed.config.Option;
 
 import java.io.File;
+import java.util.Map;
 
 import javax.annotation.Nonnull;
 
@@ -36,8 +37,25 @@
      * @return The resolved local file.
      * @throws ConfigurationException if something goes wrong.
      */
-    public @Nonnull File resolveRemoteFiles(File consideredFile, Option option)
-            throws ConfigurationException;
+    public default @Nonnull File resolveRemoteFiles(File consideredFile, Option option)
+            throws ConfigurationException {
+        throw new ConfigurationException("Should not have been called");
+    }
+
+    /**
+     * Resolve the remote file.
+     *
+     * @param consideredFile {@link File} evaluated as remote.
+     * @param option The original option configuring the file.
+     * @param queryArgs The arguments passed as a query to the URL.
+     * @return The resolved local file.
+     * @throws ConfigurationException if something goes wrong.
+     */
+    public default @Nonnull File resolveRemoteFiles(
+            File consideredFile, Option option, Map<String, String> queryArgs)
+            throws ConfigurationException {
+        return resolveRemoteFiles(consideredFile, option);
+    }
 
     /** Returns the associated protocol supported for download. */
     public @Nonnull String getSupportedProtocol();
diff --git a/src/com/android/tradefed/config/remote/LocalFileResolver.java b/src/com/android/tradefed/config/remote/LocalFileResolver.java
new file mode 100644
index 0000000..432098f
--- /dev/null
+++ b/src/com/android/tradefed/config/remote/LocalFileResolver.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+
+import javax.annotation.Nonnull;
+
+/** Implementation of {@link IRemoteFileResolver} that allows linking local files */
+public class LocalFileResolver implements IRemoteFileResolver {
+
+    public static final String PROTOCOL = "file";
+
+    @Override
+    public File resolveRemoteFiles(File consideredFile, Option option)
+            throws ConfigurationException {
+        // Don't use absolute path as it would not start with gs:
+        String path = consideredFile.getPath();
+        CLog.d("Considering option '%s' with path: '%s' for download.", option.name(), path);
+        String pathWithoutProtocol = path.replaceFirst(PROTOCOL + ":", "");
+        File localFile = new File(pathWithoutProtocol);
+        if (localFile.exists()) {
+            return localFile;
+        }
+        throw new ConfigurationException(String.format("Failed to find local file %s.", localFile));
+    }
+
+    @Override
+    public @Nonnull String getSupportedProtocol() {
+        return PROTOCOL;
+    }
+}
diff --git a/src/com/android/tradefed/device/CpuStatsCollector.java b/src/com/android/tradefed/device/CpuStatsCollector.java
deleted file mode 100644
index 18695ce..0000000
--- a/src/com/android/tradefed/device/CpuStatsCollector.java
+++ /dev/null
@@ -1,501 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.ddmlib.MultiLineReceiver;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.SimpleStats;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Helper class which runs {@code cpustats} continuously on an {@link ITestDevice} and parses the
- * output.
- * <p>
- * Provides a method to record the output of {@code cpustats} and get all the cpu usage measurements
- * as well methods for performing calculations on that data to find the mean of the cpu workload,
- * the approximate cpu frequency, and the percentage of used cpu frequency.
- * </p><p>
- * This is meant to be a replacement for {@link TopHelper}, which does not provide stats about cpu
- * frequency and has a higher overhead due to measuring processes/threads.
- * </p><p>
- * The {@code cpustats} command was added in the Jellybean release, so this collector should only be
- * used for new tests.
- * </p>
- * @see TopHelper
- */
-public class CpuStatsCollector extends Thread {
-    private static final String CPU_STATS_CMD = "cpustats -m -d %s";
-
-    private final ITestDevice mTestDevice;
-    private long mDelay;
-
-    /**
-     * Used to distinguish between the different CPU time categories.
-     */
-    enum TimeCategory {
-        USER,
-        NICE,
-        SYS,
-        IDLE,
-        IOW,
-        IRQ,
-        SIRQ
-    }
-
-    /**
-     * Class for holding parsed output data for a single {@code cpustats} output.
-     * <p>
-     * This class holds parsed output, and also performs simple calculations on that data. The
-     * methods which perform these calucations should only be called after the object has been
-     * populated.
-     * </p>
-     */
-    public static class CpuStats {
-        public Map<TimeCategory, Integer> mTimeStats = new HashMap<TimeCategory, Integer>();
-        public Map<Integer, Integer> mFreqStats = new HashMap<Integer, Integer>();
-        private Map<TimeCategory, Double> mPercentageStats = new HashMap<TimeCategory, Double>();
-        private Integer mTotalTime = null;
-        private Double mAverageMhz = null;
-
-        /**
-         * Get the percentage of cycles used on a given category.
-         */
-        public Double getPercentage(TimeCategory category) {
-            if (!mPercentageStats.containsKey(category)) {
-                mPercentageStats.put(category, 100.0 * mTimeStats.get(category) / getTotalTime());
-            }
-            return mPercentageStats.get(category);
-        }
-
-        /**
-         * Estimate the MHz used by the cpu during the duration.
-         * <p>
-         * This is calculated by:
-         * </p><code>
-         * ((sum(c_time) - idle) / sum(c_time)) * (sum(freq * f_time) / sum(f_time))
-         * </code><p>
-         * where {@code c_time} is the time for a given category, {@code idle} is the time in the
-         * idle state, {@code freq} is a frequency and {@code f_time} is the time spent in that
-         * frequency.
-         * </p>
-         */
-        public Double getEstimatedMhz() {
-            if (mFreqStats.isEmpty()) {
-                return null;
-            }
-            return getTotalUsage() * getAverageMhz();
-        }
-
-        /**
-         * Get the amount of MHz as a percentage of available MHz used by the cpu during the
-         * duration.
-         * <p>
-         * This is calculated by:
-         * </p><code>
-         * 100 * sum(freq * f_time) / (max_freq * sum(f_time))
-         * </code><p>
-         * where {@code freq} is a frequency, {@code f_time} is the time spent in that frequency,
-         * and {@code max_freq} is the maximum frequency the cpu is capable of.
-         * </p>
-         */
-        public Double getUsedMhzPercentage() {
-            if (mFreqStats.isEmpty()) {
-                return null;
-            }
-            return 100.0 * getAverageMhz() / getMaxMhz();
-        }
-
-        /**
-         * Get the total usage, or the sum of all the times except idle over the sum of all the
-         * times.
-         */
-        private Double getTotalUsage() {
-            return (double) (getTotalTime() - mTimeStats.get(TimeCategory.IDLE)) / getTotalTime();
-        }
-
-        /**
-         * Get the average MHz.
-         * <p>
-         * This is calculated by:
-         * </p><code>
-         * sum(freq * f_time) / sum(f_time))
-         * </code><p>
-         * where {@code freq} is a frequency and {@code f_time} is the time spent in that frequency.
-         * </p>
-         */
-        private Double getAverageMhz() {
-            if (mFreqStats.isEmpty()) {
-                return null;
-            }
-            if (mAverageMhz == null) {
-                double sumFreqTime = 0.0;
-                long sumTime = 0;
-                for (Map.Entry<Integer, Integer> e : mFreqStats.entrySet()) {
-                    sumFreqTime += e.getKey() * e.getValue() / 1000.0;
-                    sumTime += e.getValue();
-                }
-                mAverageMhz = sumFreqTime / sumTime;
-            }
-            return mAverageMhz;
-        }
-
-        /**
-         * Get the maximum possible MHz.
-         */
-        private Double getMaxMhz() {
-            if (mFreqStats.isEmpty()) {
-                return null;
-            }
-            int max = 0;
-            for (int freq : mFreqStats.keySet()) {
-                max = Math.max(max, freq);
-            }
-            return max / 1000.0;
-        }
-
-        /**
-         * Get the total amount of time cycles.
-         */
-        private Integer getTotalTime() {
-            if (mTotalTime == null) {
-                int sum = 0;
-                for (int time : mTimeStats.values()) {
-                    sum += time;
-                }
-                mTotalTime = sum;
-            }
-            return mTotalTime;
-        }
-    }
-
-    /**
-     * Receiver which parses the output from {@code cpustats} and optionally logs to a file.
-     */
-    public static class CpuStatsReceiver extends MultiLineReceiver {
-        private Map<String, List<CpuStats>> mCpuStats = new HashMap<String, List<CpuStats>>(4);
-
-        private boolean mIsCancelled = false;
-        private File mLogFile = null;
-        private BufferedWriter mLogWriter = null;
-
-        public CpuStatsReceiver() {
-            setTrimLine(false);
-        }
-
-        /**
-         * Specify a file to log the output to.
-         * <p>
-         * This can be called at any time in the receivers life cycle, but only new output will be
-         * logged to the file.
-         * </p>
-         */
-        public synchronized void logToFile(File logFile) {
-            try {
-                mLogFile = logFile;
-                mLogWriter = new BufferedWriter(new FileWriter(mLogFile));
-            } catch (IOException e) {
-                CLog.e("IOException when creating a fileWriter:");
-                CLog.e(e);
-                mLogWriter = null;
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void processNewLines(String[] lines) {
-            if (mIsCancelled) {
-                return;
-            }
-            synchronized (this) {
-                if (mLogWriter != null) {
-                    try {
-                        for (String line : lines) {
-                            mLogWriter.write(line + "\n");
-                        }
-                    } catch (IOException e) {
-                        CLog.e("Error writing to file");
-                        CLog.e(e);
-                    }
-                }
-            }
-            for (String line : lines) {
-                String[] args = line.trim().split(",");
-                if (args.length >= 8) {
-                    try {
-                        CpuStats s = new CpuStats();
-                        s.mTimeStats.put(TimeCategory.USER, Integer.parseInt(args[1]));
-                        s.mTimeStats.put(TimeCategory.NICE, Integer.parseInt(args[2]));
-                        s.mTimeStats.put(TimeCategory.SYS, Integer.parseInt(args[3]));
-                        s.mTimeStats.put(TimeCategory.IDLE, Integer.parseInt(args[4]));
-                        s.mTimeStats.put(TimeCategory.IOW, Integer.parseInt(args[5]));
-                        s.mTimeStats.put(TimeCategory.IRQ, Integer.parseInt(args[6]));
-                        s.mTimeStats.put(TimeCategory.SIRQ, Integer.parseInt(args[7]));
-                        for (int i = 0; i + 8 < args.length; i += 2) {
-                            s.mFreqStats.put(Integer.parseInt(args[8 + i]),
-                                    Integer.parseInt(args[9 + i]));
-                        }
-                        synchronized(this) {
-                            if (!mCpuStats.containsKey(args[0])) {
-                                mCpuStats.put(args[0], new LinkedList<CpuStats>());
-                            }
-                            mCpuStats.get(args[0]).add(s);
-                        }
-                    } catch (NumberFormatException e) {
-                        CLog.w("Unexpected input: %s", line.trim());
-                    } catch (IndexOutOfBoundsException e) {
-                        CLog.w("Unexpected input: %s", line.trim());
-                    }
-                } else if (args.length > 1 || !"".equals(args[0])) {
-                    CLog.w("Unexpected input: %s", line.trim());
-                }
-            }
-        }
-
-        /**
-         * Cancels the {@code cpustats} command.
-         */
-        public synchronized void cancel() {
-            if (mIsCancelled) {
-                return;
-            }
-            mIsCancelled = true;
-            if (mLogWriter != null) {
-                try {
-                    mLogWriter.flush();
-                    mLogWriter.close();
-                } catch (IOException e) {
-                    CLog.e("Error closing writer");
-                    CLog.e(e);
-                } finally {
-                    mLogWriter = null;
-                }
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public synchronized boolean isCancelled() {
-            return mIsCancelled;
-        }
-
-        /**
-         * Get all the parsed data as a map from label to lists of {@link CpuStats} objects.
-         */
-        public synchronized Map<String, List<CpuStats>> getCpuStats() {
-            Map<String, List<CpuStats>> copy = new HashMap<String, List<CpuStats>>(
-                    mCpuStats.size());
-            for (String k  : mCpuStats.keySet()) {
-                copy.put(k, new ArrayList<CpuStats>(mCpuStats.get(k)));
-            }
-            return copy;
-        }
-    }
-
-    private CpuStatsReceiver mReceiver = new CpuStatsReceiver();
-
-    /**
-     * Create a {@link CpuStatsCollector}.
-     *
-     * @param testDevice The test device
-     */
-    public CpuStatsCollector(ITestDevice testDevice) {
-        this(testDevice, 1);
-    }
-
-    /**
-     * Create a {@link CpuStatsCollector} with a delay specified.
-     *
-     * @param testDevice The test device
-     * @param delay The delay time in seconds
-     */
-    public CpuStatsCollector(ITestDevice testDevice, int delay) {
-        super("CpuStatsCollector");
-        mTestDevice = testDevice;
-        mDelay = delay;
-    }
-
-    /**
-     * Specify a file to log output to.
-     *
-     * @param logFile the file to log output to.
-     */
-    public void logToFile(File logFile) {
-        mReceiver.logToFile(logFile);
-    }
-
-    /**
-     * Cancels the {@code cpustats} command.
-     */
-    public synchronized void cancel() {
-        mReceiver.cancel();
-    }
-
-    /**
-     * Gets whether the {@code cpustats} command is canceled.
-     *
-     * @return if the {@code cpustats} command is canceled.
-     */
-    public synchronized boolean isCancelled() {
-        return mReceiver.isCancelled();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void run() {
-        try {
-            mTestDevice.executeShellCommand(String.format(CPU_STATS_CMD, mDelay), mReceiver);
-        } catch (DeviceNotAvailableException e) {
-            CLog.e("Device %s not available:", mTestDevice.getSerialNumber());
-            CLog.e(e);
-        }
-    }
-
-    /**
-     * Get the mapping of labels to lists of {@link CpuStats} instances.
-     *
-     * @return a mapping of labels to lists of {@link CpuStats} instances. The labels will include
-     * "Total" and "cpu0"..."cpuN" for each CPU on the device.
-     */
-    public Map<String, List<CpuStats>> getCpuStats() {
-        return mReceiver.getCpuStats();
-    }
-
-    /**
-     * Get the mean of the total CPU usage for a list of {@link CpuStats}.
-     *
-     * @param cpuStats the list of {@link CpuStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getTotalPercentageMean(List<CpuStats> cpuStats) {
-        SimpleStats stats = new SimpleStats();
-        for (CpuStats s : cpuStats) {
-            if (s.getTotalUsage() != null) {
-                stats.add(s.getTotalUsage());
-            }
-        }
-        return 100 * stats.mean();
-    }
-
-    /**
-     * Get the mean of the user and nice CPU usage for a list of {@link CpuStats}.
-     *
-     * @param cpuStats the list of {@link CpuStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getUserPercentageMean(List<CpuStats> cpuStats) {
-        return (getPercentageMean(cpuStats, TimeCategory.USER) +
-                getPercentageMean(cpuStats, TimeCategory.NICE));
-    }
-
-    /**
-     * Get the mean of the system CPU usage for a list of {@link CpuStats}.
-     *
-     * @param cpuStats the list of {@link CpuStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getSystemPercentageMean(List<CpuStats> cpuStats) {
-        return getPercentageMean(cpuStats, TimeCategory.SYS);
-    }
-
-    /**
-     * Get the mean of the iow CPU usage for a list of {@link CpuStats}.
-     *
-     * @param cpuStats the list of {@link CpuStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getIowPercentageMean(List<CpuStats> cpuStats) {
-        return getPercentageMean(cpuStats, TimeCategory.IOW);
-    }
-
-    /**
-     * Get the mean of the IRQ and SIRQ CPU usage for a list of {@link CpuStats}.
-     *
-     * @param cpuStats the list of {@link CpuStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getIrqPercentageMean(List<CpuStats> cpuStats) {
-        return (getPercentageMean(cpuStats, TimeCategory.IRQ) +
-                getPercentageMean(cpuStats, TimeCategory.SIRQ));
-    }
-
-    /**
-     * Get the mean of the estimated MHz for a list of {@link CpuStats}.
-     *
-     * @param cpuStats the list of {@link CpuStats}
-     * @return The average estimated MHz in MHz.
-     * @see CpuStats#getEstimatedMhz()
-     */
-    public static Double getEstimatedMhzMean(List<CpuStats> cpuStats) {
-        SimpleStats stats = new SimpleStats();
-        for (CpuStats s : cpuStats) {
-            if (!s.mFreqStats.isEmpty()) {
-                stats.add(s.getEstimatedMhz());
-            }
-        }
-        return stats.mean();
-    }
-
-    /**
-     * Get the mean of the used MHz for a list of {@link CpuStats}.
-     *
-     * @param cpuStats the list of {@link CpuStats}
-     * @return The average used MHz as a percentage (0 to 100).
-     * @see CpuStats#getUsedMhzPercentage()
-     */
-    public static Double getUsedMhzPercentageMean(List<CpuStats> cpuStats) {
-        SimpleStats stats = new SimpleStats();
-        for (CpuStats s : cpuStats) {
-            if (!s.mFreqStats.isEmpty()) {
-                stats.add(s.getUsedMhzPercentage());
-            }
-        }
-        return stats.mean();
-    }
-
-    /**
-     * Helper method for calculating the percentage mean for a {@link TimeCategory}.
-     */
-    private static Double getPercentageMean(List<CpuStats> cpuStats, TimeCategory category) {
-        SimpleStats stats = new SimpleStats();
-        for (CpuStats s : cpuStats) {
-            stats.add(s.getPercentage(category));
-        }
-        return stats.mean();
-    }
-
-    /**
-     * Method to access the receiver used to parse the cpu stats. Used for unit testing.
-     */
-    CpuStatsReceiver getReceiver() {
-        return mReceiver;
-    }
-}
diff --git a/src/com/android/tradefed/device/DeviceFatalError.java b/src/com/android/tradefed/device/DeviceFatalError.java
deleted file mode 100644
index 41bb8c4..0000000
--- a/src/com/android/tradefed/device/DeviceFatalError.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-/**
- * Thrown when a fatal error has occurred with device, and it should no longer be used for testing.
- *
- * This should typically be used when the device is still visible through adb, but it is in some
- * known-to-be unrecoverable state. Or if an "interesting" exception condition happened, that
- * requires human inspection while device is in the current state.
- */
-public class DeviceFatalError extends Exception {
-
-    private static final long serialVersionUID = -7928528651742852301L;
-
-    /**
-     * Creates a {@link DeviceFatalError}.
-     *
-     * @param msg a descriptive error message of the error
-     */
-    public DeviceFatalError(String msg) {
-        super(msg);
-    }
-}
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index dc31b9a..41a5096 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -92,7 +92,7 @@
 
     /** a {@link DeviceSelectionOptions} that matches any device. Visible for testing. */
     static final IDeviceSelection ANY_DEVICE_OPTIONS = new DeviceSelectionOptions();
-    static final String NULL_DEVICE_SERIAL_PREFIX = "null-device";
+    private static final String NULL_DEVICE_SERIAL_PREFIX = "null-device";
     private static final String EMULATOR_SERIAL_PREFIX = "emulator";
     private static final String TCP_DEVICE_SERIAL_PREFIX = "tcp-device";
     private static final String GCE_DEVICE_SERIAL_PREFIX = "gce-device";
diff --git a/src/com/android/tradefed/device/DeviceProperties.java b/src/com/android/tradefed/device/DeviceProperties.java
index 0024272..b4fc512 100644
--- a/src/com/android/tradefed/device/DeviceProperties.java
+++ b/src/com/android/tradefed/device/DeviceProperties.java
@@ -32,4 +32,12 @@
     public static final String VARIANT_LEGACY_LESS_EQUAL_O = "ro.product.device";
     /** proprty name to indicate SDK version */
     public static final String SDK_VERSION = "ro.build.version.sdk";
+    /** property name for device brand */
+    public static final String BRAND = "ro.product.brand";
+    /** property name for device product name */
+    public static final String PRODUCT = "ro.product.name";
+    /** property name for device release version, e.g. version 9 for Android Pie */
+    public static final String RELEASE_VERSION = "ro.build.version.release";
+    /** property name for device boot reason history */
+    public static final String BOOT_REASON_HISTORY = "persist.sys.boot.reason.history";
 }
diff --git a/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java b/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java
deleted file mode 100644
index c15394e..0000000
--- a/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java
+++ /dev/null
@@ -1,320 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.command.remote.DeviceDescriptor;
-import com.android.tradefed.config.GlobalConfiguration;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.CircularByteArray;
-
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-
-/**
- * A {@link IDeviceMonitor} that calculates device utilization stats.
- * <p/>
- * Currently measures simple moving average of allocation time % over a 24 hour window.
- */
-public class DeviceUtilStatsMonitor implements IDeviceMonitor {
-
-    private static final int INITIAL_DELAY_MS = 5000;
-
-    /**
-     * Enum for configuring treatment of stub devices when calculating average host utilization
-     */
-    public enum StubDeviceUtil {
-        /** never include stub device data */
-        IGNORE,
-        /**
-         * include stub device data only if any stub device of same type is allocated at least
-         * once
-         */
-        INCLUDE_IF_USED,
-        /** always include stub device data */
-        ALWAYS_INCLUDE
-    }
-
-    @Option(name = "collect-null-device", description =
-            "controls if null device data should be used when calculating avg host utilization")
-    private StubDeviceUtil mCollectNullDevice = StubDeviceUtil.INCLUDE_IF_USED;
-
-    @Option(name = "collect-emulator", description =
-            "controls if emulator data should be used when calculating avg host utilization")
-    private StubDeviceUtil mCollectEmulator = StubDeviceUtil.INCLUDE_IF_USED;
-
-    @Option(name = "sample-window-hours", description =
-            "the moving average window size to use, in hours")
-    private int mSampleWindowHours = 8;
-
-    @Option(name = "sample-interval-secs", description =
-            "the time period between samples, in seconds")
-    private int mSamplingIntervalSec = 60;
-
-    private boolean mNullDeviceAllocated = false;
-    private boolean mEmulatorAllocated = false;
-
-    /**
-     * Container for utilization stats.
-     */
-    public static class UtilizationDesc {
-        final int mTotalUtil;
-        final Map<String, Integer> mDeviceUtil;
-
-        public UtilizationDesc(int totalUtil, Map<String, Integer> deviceUtil) {
-            mTotalUtil = totalUtil;
-            mDeviceUtil = deviceUtil;
-        }
-
-        /**
-         * Return the total utilization for all devices in TF process, measured as total allocation
-         * time for all devices vs total available time.
-         *
-         * @return percentage utilization
-         */
-        public int getTotalUtil() {
-            return mTotalUtil;
-        }
-
-        /**
-         * Helper method to return percent utilization for a device. Returns 0 if no utilization
-         * data exists for device
-         */
-        public Integer getUtilForDevice(String serial) {
-            Integer util = mDeviceUtil.get(serial);
-            if (util == null) {
-                return 0;
-            }
-            return util;
-        }
-    }
-
-    private class DeviceUtilRecord {
-        // store samples of device util, where 0 = avail, 1 = allocated
-        // TODO: save memory by using CircularBitArray
-        private CircularByteArray mData;
-        private int mConsecutiveMissedSamples = 0;
-
-        DeviceUtilRecord() {
-            mData = new CircularByteArray(mMaxSamples);
-        }
-
-        public void addSample(DeviceAllocationState state) {
-            if (DeviceAllocationState.Allocated.equals(state)) {
-                mData.add((byte)1);
-            } else {
-                mData.add((byte)0);
-            }
-            mConsecutiveMissedSamples = 0;
-        }
-
-        public long getNumAllocations() {
-            return mData.getSum();
-        }
-
-        public long getTotalSamples() {
-            return mData.size();
-        }
-
-        /**
-         * Record sample for missing device.
-         *
-         * @param serial device serial number
-         * @return true if sample was added, false if device has been missing for more than max
-         * samples
-         */
-        public boolean addMissingSample(String serial) {
-            mConsecutiveMissedSamples++;
-            if (mConsecutiveMissedSamples > mMaxSamples) {
-                return false;
-            }
-            mData.add((byte)0);
-            return true;
-        }
-    }
-
-    private class SamplingTask extends TimerTask {
-        @Override
-        public void run() {
-            CLog.d("Collecting utilization");
-            // track devices that we have records for, but are not reported by device lister
-            Map<String, DeviceUtilRecord> goneDevices = new HashMap<>(mDeviceUtilMap);
-
-            for (DeviceDescriptor deviceDesc : mDeviceLister.listDevices()) {
-                DeviceUtilRecord record = getDeviceRecord(deviceDesc.getSerial());
-                record.addSample(deviceDesc.getState());
-                goneDevices.remove(deviceDesc.getSerial());
-            }
-
-            // now record samples for gone devices
-            for (Map.Entry<String, DeviceUtilRecord> goneSerialEntry : goneDevices.entrySet()) {
-                String serial = goneSerialEntry.getKey();
-                if (!goneSerialEntry.getValue().addMissingSample(serial)) {
-                    CLog.d("Forgetting device %s", serial);
-                    mDeviceUtilMap.remove(serial);
-                }
-            }
-        }
-    }
-
-    private int mMaxSamples;
-
-    /** a map of device serial to device records */
-    private Map<String, DeviceUtilRecord> mDeviceUtilMap = new Hashtable<>();
-
-    private DeviceLister mDeviceLister;
-
-    private Timer mTimer;
-    private SamplingTask mSamplingTask = new SamplingTask();
-
-    /**
-     * Get the device utilization up to the last 24 hours
-     */
-    public synchronized UtilizationDesc getUtilizationStats() {
-        CLog.d("Calculating device util");
-
-        long totalAllocSamples = 0;
-        long totalSamples = 0;
-        Map<String, Integer> deviceUtilMap = new HashMap<>();
-        for (Map.Entry<String, DeviceUtilRecord> deviceRecordEntry : mDeviceUtilMap.entrySet()) {
-            if (shouldTrackDevice(deviceRecordEntry.getKey())) {
-                long allocSamples = deviceRecordEntry.getValue().getNumAllocations();
-                long numSamples = deviceRecordEntry.getValue().getTotalSamples();
-                totalAllocSamples += allocSamples;
-                totalSamples += numSamples;
-                deviceUtilMap.put(deviceRecordEntry.getKey(), getUtil(allocSamples, numSamples));
-            }
-        }
-        return new UtilizationDesc(getUtil(totalAllocSamples, totalSamples), deviceUtilMap);
-    }
-
-    /**
-     * Get device utilization as a percent
-     */
-    private static int getUtil(long allocSamples, long numSamples) {
-        if (numSamples <= 0) {
-            return 0;
-        }
-        return (int)((allocSamples * 100) / numSamples);
-    }
-
-    @Override
-    public void run() {
-        calculateMaxSamples();
-        mTimer  = new Timer();
-        mTimer.scheduleAtFixedRate(mSamplingTask, INITIAL_DELAY_MS, mSamplingIntervalSec * 1000);
-    }
-
-    @Override
-    public void stop() {
-        if (mTimer != null) {
-            mTimer.cancel();
-            mTimer.purge();
-        }
-    }
-
-    @Override
-    public void setDeviceLister(DeviceLister lister) {
-        mDeviceLister = lister;
-    }
-
-    /**
-     * Listens to device state changes and records time that device transitions from or to
-     * available or allocated state.
-     */
-    @Override
-    public synchronized void notifyDeviceStateChange(String serial, DeviceAllocationState oldState,
-            DeviceAllocationState newState) {
-        if (mNullDeviceAllocated && mEmulatorAllocated) {
-            // optimization, don't enter calculation below unless needed
-            return;
-        }
-        if (DeviceAllocationState.Allocated.equals(newState)) {
-            IDeviceManager dvcMgr = getDeviceManager();
-            if (dvcMgr.isNullDevice(serial)) {
-                mNullDeviceAllocated = true;
-            } else if (dvcMgr.isEmulator(serial)) {
-                mEmulatorAllocated = true;
-            }
-        }
-    }
-
-    /**
-     * Get the device util records for given serial, creating if necessary.
-     */
-    private DeviceUtilRecord getDeviceRecord(String serial) {
-        DeviceUtilRecord r = mDeviceUtilMap.get(serial);
-        if (r == null) {
-            r = new DeviceUtilRecord();
-            mDeviceUtilMap.put(serial, r);
-        }
-        return r;
-    }
-
-    private boolean shouldTrackDevice(String serial) {
-        IDeviceManager dvcMgr = getDeviceManager();
-        if (dvcMgr.isNullDevice(serial)) {
-            switch (mCollectNullDevice) {
-                case ALWAYS_INCLUDE:
-                    return true;
-                case IGNORE:
-                    return false;
-                case INCLUDE_IF_USED:
-                    return mNullDeviceAllocated;
-            }
-        } else if (dvcMgr.isEmulator(serial)) {
-            switch (mCollectEmulator) {
-                case ALWAYS_INCLUDE:
-                    return true;
-                case IGNORE:
-                    return false;
-                case INCLUDE_IF_USED:
-                    return mEmulatorAllocated;
-            }
-        }
-        return true;
-    }
-
-    IDeviceManager getDeviceManager() {
-        return GlobalConfiguration.getDeviceManagerInstance();
-    }
-
-    TimerTask getSamplingTask() {
-        return mSamplingTask;
-    }
-
-    // @VisibleForTesting
-    void calculateMaxSamples() {
-        // find max samples to collect by converting sample window to seconds then divide by
-        // sampling interval
-        mMaxSamples = mSampleWindowHours * 60 * 60 / mSamplingIntervalSec;
-        assert(mMaxSamples > 0);
-    }
-
-    // @VisibleForTesting
-    void setMaxSamples(int maxSamples) {
-        mMaxSamples = maxSamples;
-    }
-
-    // @VisibleForTesting
-    int getMaxSamples() {
-        return mMaxSamples;
-    }
-}
diff --git a/src/com/android/tradefed/device/ITestDeviceMutator.java b/src/com/android/tradefed/device/ITestDeviceMutator.java
deleted file mode 100644
index fa53850..0000000
--- a/src/com/android/tradefed/device/ITestDeviceMutator.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.ddmlib.IDevice;
-
-/**
- * An interface allowing manipulation of the {@link IManagedTestDevice}.
- * Note that {@link IManagedTestDevice} is not public, {@link ITestDevice} is cast to
- * {@link IManagedTestDevice}.
- * <b>This is a rather dangerous manipulation and not recommended if you have no need for this.</b>
- */
-public interface ITestDeviceMutator {
-
-
-   /**
-    * Changes the {@link IDevice} held by {@link ITestDevice}
-    * @param testDevice
-    * @param device
-    */
-    public void setIDevice(ITestDevice testDevice, IDevice device);
-
-    /**
-     * Sets if Fastboot is enabled or not for a given {@link ITestDevice}.
-     * @param testDevice
-     * @param fastbootEnabled
-     */
-    public void setFastbootEnabled(ITestDevice testDevice, boolean fastbootEnabled);
-}
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index f4c8f37..2ae8e43 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -56,7 +56,6 @@
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.KeyguardControllerState;
 import com.android.tradefed.util.ProcessInfo;
-import com.android.tradefed.util.PsParser;
 import com.android.tradefed.util.QuotationAwareTokenizer;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.SizeLimitedOutputStream;
@@ -82,6 +81,7 @@
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
@@ -102,7 +102,7 @@
  */
 public class NativeDevice implements IManagedTestDevice {
 
-    private static final String SD_CARD = "/sdcard/";
+    protected static final String SD_CARD = "/sdcard/";
     /**
      * Allow pauses of up to 2 minutes while receiving bugreport.
      * <p/>
@@ -261,17 +261,26 @@
             mCmd = cmd;
         }
 
+        private void logExceptionAndOutput(CommandResult result) {
+            CLog.w("Command exited with status: %s", result.getStatus().toString());
+            CLog.w("Command stdout:\n%s\n", result.getStdout());
+            CLog.w("Command stderr:\n%s\n", result.getStderr());
+        }
+
         @Override
         public boolean run() throws TimeoutException, IOException {
             CommandResult result = getRunUtil().runTimedCmd(mTimeout, mCmd);
             // TODO: how to determine device not present with command failing for other reasons
             if (result.getStatus() == CommandStatus.EXCEPTION) {
-                throw new IOException();
+                logExceptionAndOutput(result);
+                throw new IOException("CommandStatus was EXCEPTION, details in host log");
             } else if (result.getStatus() == CommandStatus.TIMED_OUT) {
-                throw new TimeoutException();
+                logExceptionAndOutput(result);
+                throw new TimeoutException("CommandStatus was TIMED_OUT, details in host log");
             } else if (result.getStatus() == CommandStatus.FAILED) {
                 // interpret as communication failure
-                throw new IOException();
+                logExceptionAndOutput(result);
+                throw new IOException("CommandStatus was FAILED, details in host log");
             }
             mOutput = result.getStdout();
             return true;
@@ -3337,7 +3346,7 @@
             CLog.w("Property ro.crypto.state is null on device %s", getSerialNumber());
         }
 
-        return "encrypted".equals(output);
+        return "encrypted".equals(output.trim());
     }
 
     /**
@@ -3354,11 +3363,13 @@
             return mIsEncryptionSupported.booleanValue();
         }
         enableAdbRoot();
-        String output = executeShellCommand("vdc cryptfs enablecrypto").trim();
 
-        mIsEncryptionSupported =
-                (output != null
-                        && Pattern.matches("(500)(\\s+)(\\d+)(\\s+)(Usage)(.*)(:)(.*)", output));
+        String output = getProperty("ro.crypto.state");
+        if (output == null || "unsupported".equals(output.trim())) {
+            mIsEncryptionSupported = false;
+            return mIsEncryptionSupported;
+        }
+        mIsEncryptionSupported = true;
         return mIsEncryptionSupported;
     }
 
@@ -3850,6 +3861,12 @@
         throw new UnsupportedOperationException("No support for user's feature.");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException {
+        throw new UnsupportedOperationException("No support for user's feature.");
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -4135,11 +4152,9 @@
         return device.getClass().getSimpleName();
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public void preInvocationSetup(IBuildInfo info)
+    public void preInvocationSetup(IBuildInfo info, List<IBuildInfo> testResourceBuildInfos)
             throws TargetSetupError, DeviceNotAvailableException {
         // Default implementation
         mContentProvider = null;
@@ -4153,11 +4168,10 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public void postInvocationTearDown() {
+    public void postInvocationTearDown(Throwable exception) {
+        mIsEncryptionSupported = null;
         FileUtil.deleteFile(mExecuteShellCommandLogs);
         mExecuteShellCommandLogs = null;
         // Default implementation
@@ -4171,6 +4185,11 @@
             if (mContentProvider == null) {
                 return;
             }
+            if (exception instanceof DeviceNotAvailableException) {
+                CLog.e(
+                        "Skip Tradefed Content Provider teardown due to DeviceNotAvailableException.");
+                return;
+            }
             if (TestDeviceState.ONLINE.equals(getDeviceState())) {
                 mContentProvider.tearDown();
             }
@@ -4243,28 +4262,86 @@
         return o == null ? "unknown" : o.toString();
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public List<ProcessInfo> getProcesses() throws DeviceNotAvailableException {
-        return PsParser.getProcesses(executeShellCommand(PS_COMMAND));
-    }
-
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
     public ProcessInfo getProcessByName(String processName) throws DeviceNotAvailableException {
-        List<ProcessInfo> processList = getProcesses();
-        for (ProcessInfo processInfo : processList) {
-            if (processName.equals(processInfo.getName())) {
-                return processInfo;
+        String pidString = getProcessPid(processName);
+        if (pidString == null) {
+            return null;
+        }
+        return new ProcessInfo(
+                getProcessUserByPid(pidString),
+                Integer.parseInt(pidString),
+                processName,
+                getProcessStartTimeByPid(pidString));
+    }
+
+    /** Return the process start time since epoch for the given pid string */
+    private long getProcessStartTimeByPid(String pidString) throws DeviceNotAvailableException {
+        String output = executeShellCommand("stat -c%Z /proc/" + pidString);
+        if (output != null && !output.trim().isEmpty()) {
+            try {
+                return Long.parseLong(output.trim());
+            } catch (NumberFormatException e) {
+                return -1L;
+            }
+        }
+        return -1L;
+    }
+
+    /** Return the process user for the given pid string */
+    private String getProcessUserByPid(String pidString) throws DeviceNotAvailableException {
+        String output = executeShellCommand("stat -c%U /proc/" + pidString);
+        if (output != null && !output.trim().isEmpty()) {
+            try {
+                return output.trim();
+            } catch (NumberFormatException e) {
+                return null;
             }
         }
         return null;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public Map<Long, String> getBootHistory() throws DeviceNotAvailableException {
+        String output = getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+        /* Sample output:
+        kernel_panic,1556587278
+        reboot,,1556238008
+        reboot,,1556237796
+        reboot,,1556237725
+        */
+        Map<Long, String> bootHistory = new LinkedHashMap<Long, String>();
+        if (Strings.isNullOrEmpty(output)) {
+            return bootHistory;
+        }
+        for (String line : output.split("\\n")) {
+            String infoStr[] = line.split(",");
+            String startStr = infoStr[infoStr.length - 1];
+            try {
+                long startTime = Long.parseLong(startStr.trim());
+                bootHistory.put(startTime, infoStr[0].trim());
+            } catch (NumberFormatException e) {
+                CLog.e("Fail to parse boot time from line %s", line);
+            }
+        }
+        return bootHistory;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Map<Long, String> getBootHistorySince(long utcEpochTime)
+            throws DeviceNotAvailableException {
+        Map<Long, String> bootHistory = new LinkedHashMap<Long, String>();
+        for (Map.Entry<Long, String> entry : getBootHistory().entrySet()) {
+            if (entry.getKey() > utcEpochTime) {
+                bootHistory.put(entry.getKey(), entry.getValue());
+            }
+        }
+        return bootHistory;
+    }
+
     /**
      * Validates that the given input is a valid MAC address
      *
diff --git a/src/com/android/tradefed/device/ReconnectingRecovery.java b/src/com/android/tradefed/device/ReconnectingRecovery.java
deleted file mode 100644
index 56a62c1..0000000
--- a/src/com/android/tradefed/device/ReconnectingRecovery.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.IRunUtil;
-import com.android.tradefed.util.RunUtil;
-
-/**
- * Recovers a device by re-establishing a TCP connection via the adb server on
- * the host.
- */
-public class ReconnectingRecovery implements IDeviceRecovery {
-
-    private static final int ADB_TIMEOUT = 2 * 60 * 1000;
-    private static final int CONNECTION_ATTEMPTS = 5;
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline)
-            throws DeviceNotAvailableException {
-        String serial = monitor.getSerialNumber();
-
-        // disconnect - many versions of adb client have stale TCP connection
-        // status
-        getRunUtil().runTimedCmd(ADB_TIMEOUT, "adb", "disconnect", serial);
-
-        // try to reconnect
-        int attempt = 1;
-        do {
-            CLog.i("Trying to reconnect with device " + serial + " / attempt " + attempt);
-            getRunUtil().runTimedCmd(ADB_TIMEOUT, "adb", "connect", serial);
-        } while (monitor.waitForDeviceOnline() == null && ++attempt <= CONNECTION_ATTEMPTS);
-
-        String errMsg = "Could not recover device " + serial + " after " + --attempt + " attempts";
-
-        // occasionally device is erroneously reported as online - double check
-        // that we can shell into device
-        if (!monitor.waitForDeviceShell(10 * 1000)) {
-            throw new DeviceUnresponsiveException(errMsg, serial);
-        }
-
-        if (!recoverUntilOnline) {
-            if (monitor.waitForDeviceAvailable() == null) {
-                throw new DeviceUnresponsiveException(errMsg, serial);
-            }
-        }
-
-        CLog.v("Successfully reconnected with device " + serial);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void recoverDeviceBootloader(IDeviceStateMonitor monitor)
-            throws DeviceNotAvailableException {
-        throw new java.lang.UnsupportedOperationException(
-                "This implementation can't recover a device in bootloader mode.");
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p>
-     * This implementation assumes devices in recovery mode can't be talked to
-     * at all, so it will try to recover a device and leave it in fully booted
-     * mode.
-     */
-    @Override
-    public void recoverDeviceRecovery(IDeviceStateMonitor monitor)
-            throws DeviceNotAvailableException {
-        recoverDevice(monitor, false);
-    }
-
-    /**
-     * Get the {@link RunUtil} instance to use.
-     * <p/>
-     * Exposed for unit testing.
-     */
-    IRunUtil getRunUtil() {
-        return RunUtil.getDefault();
-    }
-}
diff --git a/src/com/android/tradefed/device/RemoteAndroidDevice.java b/src/com/android/tradefed/device/RemoteAndroidDevice.java
index 77b0c05..4b70e28 100644
--- a/src/com/android/tradefed/device/RemoteAndroidDevice.java
+++ b/src/com/android/tradefed/device/RemoteAndroidDevice.java
@@ -63,8 +63,8 @@
     }
 
     @Override
-    public void postInvocationTearDown() {
-        super.postInvocationTearDown();
+    public void postInvocationTearDown(Throwable exception) {
+        super.postInvocationTearDown(exception);
         FileUtil.deleteFile(mAdbConnectLogs);
     }
 
diff --git a/src/com/android/tradefed/device/RetryingWaitDeviceRecovery.java b/src/com/android/tradefed/device/RetryingWaitDeviceRecovery.java
deleted file mode 100644
index 313fed5..0000000
--- a/src/com/android/tradefed/device/RetryingWaitDeviceRecovery.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.tradefed.config.Option;
-import com.android.tradefed.log.LogUtil.CLog;
-
-/**
- * A {@link WaitDeviceRecovery} which retries its recovery step either indefinitely
- * or for a certain number of iterations.
- */
-public class RetryingWaitDeviceRecovery extends WaitDeviceRecovery {
-
-    @Option(name = "max-wait-iter",
-            description = "maximum number of retries for device recovery, 0 for unlimited")
-    private int mMaxIters = 0;
-
-    @Override
-    public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) {
-        int iter = 0;
-        while (iter < mMaxIters || mMaxIters == 0) {
-            try {
-                super.recoverDevice(monitor, recoverUntilOnline);
-                return;
-            } catch (DeviceNotAvailableException e) {
-                CLog.i("Wait attempt %d failed, trying again. Max iterations: %d",
-                        ++iter, mMaxIters);
-            }
-        }
-    }
-}
-
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index 8df4860..a7dfa4c 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -32,7 +32,6 @@
 import com.android.tradefed.util.KeyguardControllerState;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
-import com.android.tradefed.util.UserUtil;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
@@ -1025,6 +1024,17 @@
         return packages;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
+        if (deviceFilePath.startsWith(SD_CARD)) {
+            deviceFilePath =
+                    deviceFilePath.replaceFirst(
+                            SD_CARD, String.format("/storage/emulated/%s/", getCurrentUser()));
+        }
+        return super.doesFileExist(deviceFilePath);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -1038,6 +1048,25 @@
         return userIds;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException {
+        ArrayList<String[]> lines = tokenizeListUsers();
+        Map<Integer, UserInfo> result = new HashMap<Integer, UserInfo>(lines.size());
+        for (String[] tokens : lines) {
+            UserInfo userInfo =
+                    new UserInfo(
+                            /* userId= */ Integer.parseInt(tokens[1]),
+                            /* userName= */ tokens[2],
+                            /* flag= */ Integer.parseInt(tokens[3], 16),
+                            /* isRunning= */ tokens.length >= 5
+                                    ? tokens[4].contains("running")
+                                    : false);
+            result.put(userInfo.userId(), userInfo);
+        }
+        return result;
+    }
+
     /**
      * Tokenizes the output of 'pm list users'.
      * The returned tokens for each user have the form: {"\tUserInfo", Integer.toString(id), name,
@@ -1299,14 +1328,14 @@
     /** {@inheritDoc} */
     @Override
     public boolean isUserSecondary(int userId) throws DeviceNotAvailableException {
-        if (userId == UserUtil.USER_SYSTEM) {
+        if (userId == UserInfo.USER_SYSTEM) {
             return false;
         }
         int flags = getUserFlags(userId);
         if (flags == INVALID_USER_ID) {
             return false;
         }
-        return (flags & UserUtil.FLAGS_NOT_SECONDARY) == 0;
+        return (flags & UserInfo.FLAGS_NOT_SECONDARY) == 0;
     }
 
     /**
@@ -1374,9 +1403,9 @@
                 // disable keyguard if option is true
                 prePostBootSetup();
                 return true;
-            } else {
-                RunUtil.getDefault().sleep(getCheckNewUserSleep());
             }
+            RunUtil.getDefault().sleep(getCheckNewUserSleep());
+            executeShellCommand(String.format("am switch-user %d", userId));
         }
         CLog.e("User did not switch in the given %d timeout", timeout);
         return false;
@@ -1569,8 +1598,8 @@
 
     /** {@inheritDoc} */
     @Override
-    public void postInvocationTearDown() {
-        super.postInvocationTearDown();
+    public void postInvocationTearDown(Throwable exception) {
+        super.postInvocationTearDown(exception);
         // If wifi was installed and it's a real device, attempt to clean it.
         if (mWasWifiHelperInstalled) {
             mWasWifiHelperInstalled = false;
@@ -1580,6 +1609,10 @@
             if (!TestDeviceState.ONLINE.equals(getDeviceState())) {
                 return;
             }
+            if (exception instanceof DeviceNotAvailableException) {
+                CLog.e("Skip WifiHelper teardown due to DeviceNotAvailableException.");
+                return;
+            }
             try {
                 // Uninstall the wifi utility if it was installed.
                 IWifiHelper wifi = createWifiHelper(false);
diff --git a/src/com/android/tradefed/device/TestDeviceMutator.java b/src/com/android/tradefed/device/TestDeviceMutator.java
deleted file mode 100644
index 2044e8d..0000000
--- a/src/com/android/tradefed/device/TestDeviceMutator.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.ddmlib.IDevice;
-
-/**
- * Default implementation of {@link ITestDeviceMutator}
- */
-public class TestDeviceMutator implements ITestDeviceMutator {
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setIDevice(ITestDevice testDevice, IDevice device) {
-       ((IManagedTestDevice)testDevice).setIDevice(device);
-    }
-
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setFastbootEnabled(ITestDevice testDevice, boolean fastbootEnabled) {
-        ((IManagedTestDevice)testDevice).setFastbootEnabled(fastbootEnabled);
-    }
-
-}
diff --git a/src/com/android/tradefed/device/TopHelper.java b/src/com/android/tradefed/device/TopHelper.java
deleted file mode 100644
index c77b5a4..0000000
--- a/src/com/android/tradefed/device/TopHelper.java
+++ /dev/null
@@ -1,346 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.ddmlib.MultiLineReceiver;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.SimpleStats;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Helper class which runs top continuously on an {@link ITestDevice} and parses the output.
- * <p>
- * Provides a method to record the output of top and get all recorded CPU usage measurements or an
- * average of a specified range of measurements.  Note that top can cause approximately a 10%
- * overhead to the CPU usage while running, so results will not be entirely accurate.
- * </p>
- */
-public class TopHelper extends Thread {
-    /** The top command to run during the actions. */
-    private static final String TOP_CMD = "top -d %d -m 10 -t";
-    /** The pattern to match for the top output. */
-    private static final Pattern TOP_PERCENT_PATTERN =
-            Pattern.compile("User (\\d+)%, System (\\d+)%, IOW (\\d+)%, IRQ (\\d+)%");
-
-    private ITestDevice mTestDevice;
-    private int mDelay;
-
-    /**
-     * Enum used for distinguishing between the various percentages in the top output.
-     */
-    enum PercentCategory {
-        TOTAL,
-        USER,
-        SYSTEM,
-        IOW,
-        IRQ
-    }
-
-    /**
-     * Class for holding the parsed output for a single top output.
-     * <p>
-     * Currently, this only holds the percentage info from top but can be extended to contain the
-     * process information in the top output.
-     * </p>
-     */
-    public static class TopStats {
-        public Double mTotalPercent = null;
-        public Double mUserPercent = null;
-        public Double mSystemPercent = null;
-        public Double mIowPercent = null;
-        public Double mIrqPercent = null;
-    }
-
-    /**
-     * Receiver which parses the output from top.
-     */
-    static class TopReceiver extends MultiLineReceiver {
-        private List<TopStats> mTopStats = new LinkedList<TopStats>();
-        private boolean mIsCancelled = false;
-        private File mLogFile = null;
-        private BufferedWriter mLogWriter = null;
-
-        public TopReceiver() {
-            setTrimLine(false);
-        }
-
-        /**
-         * Specify a file to log the top output to.
-         *
-         * @param logFile the file to lot output to.
-         */
-        public synchronized void logToFile(File logFile) {
-            try {
-                mLogFile = logFile;
-                mLogWriter = new BufferedWriter(new FileWriter(mLogFile));
-            } catch (IOException e) {
-                CLog.e("Error creating fileWriter:");
-                CLog.e(e);
-                mLogWriter = null;
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void processNewLines(String[] lines) {
-            if (mIsCancelled) {
-                return;
-            }
-            synchronized (this) {
-                if (mLogWriter != null) {
-                    try {
-                        for (String line : lines) {
-                            mLogWriter.write(line + "\n");
-                        }
-                    } catch (IOException e) {
-                        CLog.e("Error writing to file:");
-                        CLog.e(e);
-                    }
-                }
-            }
-            for (String line : lines) {
-                line = line.trim();
-                Matcher m = TOP_PERCENT_PATTERN.matcher(line);
-                if (m.matches()) {
-                    TopStats s = new TopStats();
-
-                    // Will not trigger NumberFormatException due to TOP_PATTERN matching.
-                    s.mUserPercent = Double.parseDouble(m.group(1));
-                    s.mSystemPercent = Double.parseDouble(m.group(2));
-                    s.mIowPercent = Double.parseDouble(m.group(3));
-                    s.mIrqPercent = Double.parseDouble(m.group(4));
-                    s.mTotalPercent = (s.mUserPercent + s.mSystemPercent + s.mIowPercent +
-                            s.mIrqPercent);
-                    synchronized(this) {
-                        mTopStats.add(s);
-                    }
-                }
-            }
-        }
-
-        /**
-         * Cancels the top command.
-         */
-        public synchronized void cancel() {
-            if (mIsCancelled) {
-                return;
-            }
-            mIsCancelled = true;
-            if (mLogWriter != null) {
-                try {
-                    mLogWriter.flush();
-                    mLogWriter.close();
-                } catch (IOException e) {
-                    CLog.e("Error closing writer:");
-                    CLog.e(e);
-                } finally {
-                    mLogWriter = null;
-                }
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public synchronized boolean isCancelled() {
-            return mIsCancelled;
-        }
-
-        /**
-         * Gets a list of {@link TopStats} instances.
-         *
-         * @return a list of {@link TopStats} instances ordered from oldest to newest.
-         */
-        public synchronized List<TopStats> getTopStats() {
-            return new ArrayList<TopStats>(mTopStats);
-        }
-    }
-
-    private TopReceiver mReceiver = new TopReceiver();
-
-    /**
-     * Create a {@link TopHelper} instance with a delay specified.
-     *
-     * @param testDevice The device.
-     * @param delay The delay time interval for the top command in seconds.
-     */
-    public TopHelper(ITestDevice testDevice, int delay) {
-        super("TopHelper");
-        mTestDevice = testDevice;
-        mDelay = delay;
-    }
-
-    /**
-     * Create a {@link TopHelper} instance with a default delay of 1 second.
-     *
-     * @param testDevice The device.
-     */
-    public TopHelper(ITestDevice testDevice) {
-        this(testDevice, 1);
-    }
-
-    /**
-     * Specify a file to log the top output to.
-     *
-     * @param logFile the file to lot output to.
-     */
-    public void logToFile(File logFile) {
-        mReceiver.logToFile(logFile);
-    }
-
-    /**
-     * Cancels the top command.
-     */
-    public synchronized void cancel() {
-        mReceiver.cancel();
-    }
-
-    /**
-     * Gets whether the top command is canceled.
-     *
-     * @return if the top command is canceled.
-     */
-    public synchronized boolean isCancelled() {
-        return mReceiver.isCancelled();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void run() {
-        try {
-            mTestDevice.executeShellCommand(String.format(TOP_CMD, mDelay), mReceiver);
-        } catch (DeviceNotAvailableException e) {
-            CLog.e("Device %s not available:", mTestDevice.getSerialNumber());
-            CLog.e(e);
-        }
-    }
-
-    /**
-     * Gets a list of {@link TopStats} instances.
-     *
-     * @return a list of {@link TopStats} instances ordered from oldest to newest.
-     */
-    public List<TopStats> getTopStats() {
-        return mReceiver.getTopStats();
-    }
-
-    /**
-     * Get the average total CPU usage for a list of {@link TopStats}.
-     *
-     * @param topStats the list of {@link TopStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getTotalAverage(List<TopStats> topStats) {
-        return getAveragePercentage(topStats, PercentCategory.TOTAL);
-    }
-
-    /**
-     * Get the average user CPU usage for a list of {@link TopStats}.
-     *
-     * @param topStats the list of {@link TopStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getUserAverage(List<TopStats> topStats) {
-        return getAveragePercentage(topStats, PercentCategory.USER);
-    }
-
-    /**
-     * Get the average system CPU usage for a list of {@link TopStats}.
-     *
-     * @param topStats the list of {@link TopStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getSystemAverage(List<TopStats> topStats) {
-        return getAveragePercentage(topStats, PercentCategory.SYSTEM);
-    }
-
-    /**
-     * Get the average IOW CPU usage for a list of {@link TopStats}.
-     *
-     * @param topStats the list of {@link TopStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getIowAverage(List<TopStats> topStats) {
-        return getAveragePercentage(topStats, PercentCategory.IOW);
-    }
-
-    /**
-     * Get the average IRQ CPU usage for a list of {@link TopStats}.
-     *
-     * @param topStats the list of {@link TopStats}
-     * @return The average usage as a percentage (0 to 100).
-     */
-    public static Double getIrqAverage(List<TopStats> topStats) {
-        return getAveragePercentage(topStats, PercentCategory.IRQ);
-    }
-
-
-    /**
-     * Get the average CPU usage for a list of {@link TopStats} and a given category.
-     *
-     * @param topStats the list of {@link TopStats}
-     * @param category the percentage category
-     * @return The average usage as a percentage (0 to 100).
-     */
-    private static Double getAveragePercentage(List<TopStats> topStats, PercentCategory category)
-            throws IndexOutOfBoundsException {
-        SimpleStats stats = new SimpleStats();
-        for (TopStats s : topStats) {
-            switch(category) {
-                case TOTAL:
-                    stats.add(s.mTotalPercent);
-                    break;
-                case USER:
-                    stats.add(s.mUserPercent);
-                    break;
-                case SYSTEM:
-                    stats.add(s.mSystemPercent);
-                    break;
-                case IOW:
-                    stats.add(s.mIowPercent);
-                    break;
-                case IRQ:
-                    stats.add(s.mIrqPercent);
-                    break;
-            }
-        }
-        return stats.mean();
-    }
-
-    /**
-     * Package protected method used for testing.
-     *
-     * @return the TopReceiver
-     */
-    TopReceiver getReceiver() {
-        return mReceiver;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/tradefed/device/WifiHelper.java b/src/com/android/tradefed/device/WifiHelper.java
index d2122b7..e98e28d 100644
--- a/src/com/android/tradefed/device/WifiHelper.java
+++ b/src/com/android/tradefed/device/WifiHelper.java
@@ -461,7 +461,6 @@
         }
         if (!asBool(runWifiUtil("connectToNetwork", "ssid", ssid, "psk", psk, "urlToCheck",
                 urlToCheck, "scan_ssid", Boolean.toString(scanSsid)))) {
-            CLog.e("Failed to connect to " + ssid);
             return false;
         }
         return true;
@@ -473,7 +472,6 @@
     @Override
     public boolean disconnectFromNetwork() throws DeviceNotAvailableException {
         if (!asBool(runWifiUtil("disconnectFromNetwork"))) {
-            CLog.e("Failed to disconnect");
             return false;
         }
         if (!disableWifi()) {
@@ -524,7 +522,11 @@
         WifiUtilOutput parser = new WifiUtilOutput();
         mDevice.executeShellCommand(cmd, parser, WIFIUTIL_CMD_TIMEOUT_MINUTES, TimeUnit.MINUTES, 0);
         if (parser.getError() != null) {
-            CLog.e(parser.getError());
+            String errorMessage =
+                    String.format(
+                            "Failed to %s due to: '%s'. See logcat for details.",
+                            method, parser.getError());
+            CLog.e(errorMessage);
         }
         return parser.getResult();
     }
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index ccf9403..508170f 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -222,6 +222,14 @@
                 getTestDeviceOptions().getInstanceType())) {
             gceArgs.add("--avd-type");
             gceArgs.add("cheeps");
+
+            if (getTestDeviceOptions().getCrosUser() != null
+                    && getTestDeviceOptions().getCrosPassword() != null) {
+                gceArgs.add("--user");
+                gceArgs.add(getTestDeviceOptions().getCrosUser());
+                gceArgs.add("--password");
+                gceArgs.add(getTestDeviceOptions().getCrosPassword());
+            }
         }
 
         // If args passed by gce-driver-param do not contain build_id or branch,
diff --git a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
index 9e00ac1..8e2b8f0 100644
--- a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
@@ -17,11 +17,16 @@
 
 import com.android.ddmlib.IDevice;
 import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.IDeviceMonitor;
 import com.android.tradefed.device.IDeviceStateMonitor;
 import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.device.TestDeviceOptions;
 import com.android.tradefed.device.cloud.GceAvdInfo.GceStatus;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -49,6 +54,9 @@
     private GceAvdInfo mGceAvd;
     private ITestLogger mTestLogger;
 
+    private TestDeviceOptions mCopiedOptions;
+    private IConfiguration mValidationConfig;
+
     /**
      * Creates a {@link ManagedRemoteDevice}.
      *
@@ -66,28 +74,27 @@
             throws TargetSetupError, DeviceNotAvailableException {
         super.preInvocationSetup(info, testResourceBuildInfos);
         mGceAvd = null;
-
+        // First get the options
+        TestDeviceOptions options = getOptions();
         // We create a brand new GceManager each time to ensure clean state.
-        mGceHandler =
-                new GceManager(getDeviceDescriptor(), getOptions(), info, testResourceBuildInfos);
+        mGceHandler = new GceManager(getDeviceDescriptor(), options, info, testResourceBuildInfos);
         getGceHandler().logStableHostImageInfos(info);
         setFastbootEnabled(false);
 
         // Launch GCE helper script.
         long startTime = getCurrentTime();
         launchGce();
-        long remainingTime = getOptions().getGceCmdTimeout() - (getCurrentTime() - startTime);
+        long remainingTime = options.getGceCmdTimeout() - (getCurrentTime() - startTime);
         if (remainingTime < 0) {
             throw new DeviceNotAvailableException(
-                    String.format(
-                            "Failed to launch GCE after %sms", getOptions().getGceCmdTimeout()),
+                    String.format("Failed to launch GCE after %sms", options.getGceCmdTimeout()),
                     getSerialNumber());
         }
     }
 
     /** {@inheritDoc} */
     @Override
-    public void postInvocationTearDown() {
+    public void postInvocationTearDown(Throwable exception) {
         try {
             CLog.i("Shutting down GCE device %s", getSerialNumber());
             // Log the last part of the logcat from the tear down.
@@ -124,8 +131,11 @@
                 getGceHandler().cleanUp();
             }
         } finally {
+            if (mValidationConfig != null) {
+                mValidationConfig.cleanDynamicOptionFiles();
+            }
             // Ensure parent postInvocationTearDown is always called.
-            super.postInvocationTearDown();
+            super.postInvocationTearDown(exception);
         }
     }
 
@@ -192,4 +202,25 @@
     GceManager getGceHandler() {
         return mGceHandler;
     }
+
+    /**
+     * Override the base getter to be able to resolve dynamic options before attempting to do the
+     * remote setup.
+     */
+    @Override
+    public TestDeviceOptions getOptions() {
+        if (mCopiedOptions == null) {
+            mCopiedOptions = new TestDeviceOptions();
+            TestDeviceOptions options = super.getOptions();
+            OptionCopier.copyOptionsNoThrow(options, mCopiedOptions);
+            mValidationConfig = new Configuration("validation", "validation");
+            mValidationConfig.setDeviceOptions(mCopiedOptions);
+            try {
+                mValidationConfig.resolveDynamicOptions();
+            } catch (ConfigurationException e) {
+                throw new RuntimeException(e);
+            }
+        }
+        return mCopiedOptions;
+    }
 }
diff --git a/src/com/android/tradefed/device/cloud/MultiUserSetupUtil.java b/src/com/android/tradefed/device/cloud/MultiUserSetupUtil.java
index 3a69667..abbcbfe 100644
--- a/src/com/android/tradefed/device/cloud/MultiUserSetupUtil.java
+++ b/src/com/android/tradefed/device/cloud/MultiUserSetupUtil.java
@@ -118,7 +118,7 @@
             TestDeviceOptions options,
             IRunUtil runUtil,
             long timeoutMs) {
-        StringBuilder copyCommandBuilder = new StringBuilder("sudo cp ");
+        StringBuilder copyCommandBuilder = new StringBuilder("sudo cp --reflink=auto ");
         for (String file : FILE_TO_BE_COPIED) {
             copyCommandBuilder.append(" /home/" + mainRootUser + "/" + file);
         }
diff --git a/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java b/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
index ebf577e..70d319e 100644
--- a/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
@@ -164,7 +164,7 @@
         // Reset recovery since it's a new device
         setRecoveryMode(RecoveryMode.AVAILABLE);
         try {
-            preInvocationSetup(info);
+            preInvocationSetup(info, null);
         } catch (TargetSetupError e) {
             CLog.e("Failed to re-init the device %s", getSerialNumber());
             CLog.e(e);
diff --git a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
index 4ba6cfc..d68a353 100644
--- a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
@@ -82,15 +82,9 @@
 
     /** {@inheritDoc} */
     @Override
-    public void preInvocationSetup(IBuildInfo info)
-            throws TargetSetupError, DeviceNotAvailableException {
-        preInvocationSetup(info, null);
-    }
-
-    /** {@inheritDoc} */
-    @Override
     public void preInvocationSetup(IBuildInfo info, List<IBuildInfo> testResourceBuildInfos)
             throws TargetSetupError, DeviceNotAvailableException {
+        super.preInvocationSetup(info, testResourceBuildInfos);
         try {
             mGceAvd = null;
             mGceSshMonitor = null;
@@ -155,7 +149,7 @@
 
     /** {@inheritDoc} */
     @Override
-    public void postInvocationTearDown() {
+    public void postInvocationTearDown(Throwable exception) {
         try {
             CLog.i("Invocation tear down for device %s", getSerialNumber());
             // Log the last part of the logcat from the tear down.
@@ -214,7 +208,7 @@
             }
         } finally {
             // Ensure parent postInvocationTearDown is always called.
-            super.postInvocationTearDown();
+            super.postInvocationTearDown(exception);
         }
     }
 
@@ -358,19 +352,20 @@
                     String.format(
                                     CommonLogRemoteFileUtil.NESTED_REMOTE_LOG_DIR,
                                     getOptions().getInstanceUser())
-                            + "tombstones";
-            File tombstonesDir =
-                    RemoteFileUtil.fetchRemoteDir(
-                            mGceAvd,
-                            getOptions(),
-                            getRunUtil(),
-                            FETCH_TOMBSTONES_TIMEOUT_MS,
-                            remoteRuntimePath);
-            if (tombstonesDir == null) {
-                CLog.e("Pulled tombstone dir was not valid. Path: %s", tombstonesDir);
+                            + "tombstones/*";
+            File localDir = null;
+            try {
+                localDir = FileUtil.createTempDir("tombstones");
+            } catch (IOException e) {
+                CLog.e(e);
+                return tombs;
+            }
+            if (!fetchRemoteDir(localDir, remoteRuntimePath)) {
+                CLog.e("Failed to pull %s", remoteRuntimePath);
+                FileUtil.recursiveDelete(localDir);
             } else {
-                // TODO: If possible delete the tombstones parent temp dir.
-                tombs.addAll(Arrays.asList(tombstonesDir.listFiles()));
+                tombs.addAll(Arrays.asList(localDir.listFiles()));
+                localDir.deleteOnExit();
             }
             return tombs;
         }
@@ -378,6 +373,17 @@
         return super.getTombstones();
     }
 
+    @VisibleForTesting
+    boolean fetchRemoteDir(File localDir, String remotePath) {
+        return RemoteFileUtil.fetchRemoteDir(
+                mGceAvd,
+                getOptions(),
+                getRunUtil(),
+                FETCH_TOMBSTONES_TIMEOUT_MS,
+                remotePath,
+                localDir);
+    }
+
     /**
      * Returns the {@link com.android.tradefed.device.cloud.GceSshTunnelMonitor} of the device.
      * Exposed for testing.
diff --git a/src/com/android/tradefed/device/cloud/RemoteFileUtil.java b/src/com/android/tradefed/device/cloud/RemoteFileUtil.java
index 5330992..0443c43 100644
--- a/src/com/android/tradefed/device/cloud/RemoteFileUtil.java
+++ b/src/com/android/tradefed/device/cloud/RemoteFileUtil.java
@@ -104,6 +104,36 @@
      * @param runUtil a {@link IRunUtil} to execute commands.
      * @param timeout in millisecond for the fetch to complete
      * @param remoteDirPath The remote path where to find the directory.
+     * @param localDir The local directory where to put the pulled files.
+     * @return True if successful, False otherwise
+     */
+    public static boolean fetchRemoteDir(
+            GceAvdInfo remoteInstance,
+            TestDeviceOptions options,
+            IRunUtil runUtil,
+            long timeout,
+            String remoteDirPath,
+            File localDir) {
+        return internalScpExec(
+                remoteInstance,
+                options,
+                Arrays.asList("-r"),
+                runUtil,
+                timeout,
+                remoteDirPath,
+                localDir,
+                ScpMode.PULL);
+    }
+
+    /**
+     * Fetch a remote directory from the remote host.
+     *
+     * @param remoteInstance The {@link GceAvdInfo} that describe the device.
+     * @param options a {@link TestDeviceOptions} describing the device options to be used for the
+     *     GCE device.
+     * @param runUtil a {@link IRunUtil} to execute commands.
+     * @param timeout in millisecond for the fetch to complete
+     * @param remoteDirPath The remote path where to find the directory.
      * @return The pulled directory {@link File} if successful, null otherwise
      */
     public static File fetchRemoteDir(
diff --git a/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java b/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
index 1e4d16c..3a64b07 100644
--- a/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
@@ -18,6 +18,7 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
@@ -33,6 +34,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Base implementation of {@link IMetricCollector} that allows to start and stop collection on
@@ -65,6 +67,7 @@
     private List<String> mTestCaseExcludeAnnotationGroup = new ArrayList<>();
 
     private IInvocationContext mContext;
+    private List<ITestDevice> mRealDeviceList;
     private ITestInvocationListener mForwarder;
     private DeviceMetricData mRunData;
     private DeviceMetricData mTestData;
@@ -74,12 +77,19 @@
      * Variable for whether or not to skip the collection of one test case because it was filtered.
      */
     private boolean mSkipTestCase = false;
+    /** Whether or not the collector was initialized already or not. */
+    private boolean mWasInitDone = false;
 
     @Override
     public ITestInvocationListener init(
             IInvocationContext context, ITestInvocationListener listener) {
         mContext = context;
         mForwarder = listener;
+        if (mWasInitDone) {
+            throw new IllegalStateException(
+                    String.format("init was called a second time on %s", this));
+        }
+        mWasInitDone = true;
         return this;
     }
 
@@ -88,6 +98,18 @@
         return mContext.getDevices();
     }
 
+    /** Returns all the non-stub devices from the {@link #getDevices()} list. */
+    public final List<ITestDevice> getRealDevices() {
+        if (mRealDeviceList == null) {
+            mRealDeviceList =
+                    mContext.getDevices()
+                            .stream()
+                            .filter(d -> (!(d.getIDevice() instanceof StubDevice)))
+                            .collect(Collectors.toList());
+        }
+        return mRealDeviceList;
+    }
+
     @Override
     public final List<IBuildInfo> getBuildInfos() {
         return mContext.getBuildInfos();
@@ -130,6 +152,15 @@
         // Does nothing
     }
 
+    @Override
+    public void onTestEnd(
+            DeviceMetricData testData,
+            final Map<String, Metric> currentTestCaseMetrics,
+            TestDescription test) {
+        // Call the default implementation of onTestEnd if not overridden
+        onTestEnd(testData, currentTestCaseMetrics);
+    }
+
     /** =================================== */
     /** Invocation Listeners for forwarding */
     @Override
@@ -233,7 +264,7 @@
             TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
         if (!mSkipTestCase) {
             try {
-                onTestEnd(mTestData, testMetrics);
+                onTestEnd(mTestData, testMetrics, test);
                 mTestData.addToMetrics(testMetrics);
             } catch (Throwable t) {
                 // Prevent exception from messing up the status reporting.
diff --git a/src/com/android/tradefed/device/metric/IMetricCollector.java b/src/com/android/tradefed/device/metric/IMetricCollector.java
index 387e536..1c8b84f 100644
--- a/src/com/android/tradefed/device/metric/IMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/IMetricCollector.java
@@ -113,4 +113,18 @@
      */
     public void onTestEnd(
             DeviceMetricData testData, final Map<String, Metric> currentTestCaseMetrics);
+
+    /**
+     * Callback when a test case is ended. This should be the time for clean up.
+     *
+     * @param testData the {@link DeviceMetricData} holding the data for the test case. Will be the
+     *     same object as during {@link #onTestStart(DeviceMetricData)}.
+     * @param currentTestCaseMetrics the current map of metrics passed to {@link
+     *     #testEnded(TestDescription, Map)}.
+     * @param test the {@link TestDescription} of the test case in progress.
+     */
+    public void onTestEnd(
+            DeviceMetricData testData,
+            final Map<String, Metric> currentTestCaseMetrics,
+            TestDescription test);
 }
diff --git a/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java b/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java
index 3dcc717..066b425 100644
--- a/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java
+++ b/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java
@@ -43,7 +43,7 @@
 
     @Override
     public void onTestRunStart(DeviceMetricData runData) {
-        for (ITestDevice device : getDevices()) {
+        for (ITestDevice device : getRealDevices()) {
             // In case of multiple runs for the same test runner, re-init the receiver.
             initReceiver(device);
             // Get the current offset of the buffer to be able to query later
@@ -62,7 +62,7 @@
 
     @Override
     public void onTestFail(DeviceMetricData testData, TestDescription test) {
-        for (ITestDevice device : getDevices()) {
+        for (ITestDevice device : getRealDevices()) {
             // Delay slightly for the error to get in the logcat
             getRunUtil().sleep(100);
             try (InputStreamSource logcatSource =
diff --git a/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java b/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
index 5b3af5f..d382cfb 100644
--- a/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
@@ -27,6 +27,7 @@
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.Pair;
 import com.android.tradefed.util.RunUtil;
 
@@ -47,11 +48,15 @@
 
     private static final String LINE_SEPARATOR = "\\r?\\n";
     private static final char KEY_VALUE_SEPARATOR = ':';
+    private static final String EXTRACTOR_STATUS = "trace_extractor_status";
+    private static final String EXTRACTOR_SUCCESS = "1";
+    private static final String EXTRACTOR_FAILURE = "0";
+    private static final String EXTRACTOR_RUNTIME = "trace_extractor_runtime";
 
     @Option(
             name = "perfetto-binary-path",
             description = "Path to the script files used to analyze the trace files.")
-    private List<String> mScriptPaths = new ArrayList<>();
+    private List<File> mScriptFiles = new ArrayList<>();
 
     @Option(
             name = "perfetto-metric-prefix",
@@ -87,9 +92,12 @@
     public void processMetricFile(String key, File metricFile,
             DeviceMetricData data) {
         // Extract the metrics from the trace file.
-        for (String scriptPath : mScriptPaths) {
+        for (File scriptFile : mScriptFiles) {
+            // Apply necessary execute permissions to the script.
+            FileUtil.chmodGroupRWX(scriptFile);
+
             List<String> commandArgsList = new ArrayList<String>();
-            commandArgsList.add(scriptPath);
+            commandArgsList.add(scriptFile.getAbsolutePath());
             commandArgsList.add("-trace_file");
             commandArgsList.add(metricFile.getAbsolutePath());
 
@@ -98,12 +106,26 @@
                 commandArgsList.add(Joiner.on(",").join(mProcessNames));
             }
 
+            String traceExtractorStatus = EXTRACTOR_SUCCESS;
+
+            double scriptDuration = 0;
+            double scriptStartTime = System.currentTimeMillis();
             CommandResult cr = runHostCommand(commandArgsList.toArray(new String[commandArgsList
                     .size()]));
+            scriptDuration = System.currentTimeMillis() - scriptStartTime;
+
+            // Update the script duration metrics.
+            Metric.Builder metricDurationBuilder = Metric.newBuilder();
+            metricDurationBuilder.getMeasurementsBuilder().setSingleDouble(scriptDuration);
+            data.addMetric(
+                    String.format("%s_%s", mMetricPrefix, EXTRACTOR_RUNTIME),
+                    metricDurationBuilder.setType(DataType.RAW));
+
             if (CommandStatus.SUCCESS.equals(cr.getStatus())) {
                 String[] metrics = cr.getStdout().split(LINE_SEPARATOR);
                 for (String metric : metrics) {
                     Pair<String, String> kv = splitKeyValue(metric);
+
                     if (kv != null) {
                         Metric.Builder metricBuilder = Metric.newBuilder();
                         metricBuilder.getMeasurementsBuilder().setSingleString(kv.second);
@@ -116,9 +138,16 @@
                 }
                 CLog.i(cr.getStdout());
             } else {
+                traceExtractorStatus = EXTRACTOR_FAILURE;
                 CLog.e("Unable to parse the trace file %s due to %s - Status - %s ",
                         metricFile.getName(), cr.getStderr(), cr.getStatus());
             }
+
+            Metric.Builder metricStatusBuilder = Metric.newBuilder();
+            metricStatusBuilder.getMeasurementsBuilder().setSingleString(traceExtractorStatus);
+            data.addMetric(
+                    String.format("%s_%s", mMetricPrefix, EXTRACTOR_STATUS),
+                    metricStatusBuilder.setType(DataType.RAW));
         }
 
         // Upload and delete the host trace file.
diff --git a/src/com/android/tradefed/device/metric/RebootReasonCollector.java b/src/com/android/tradefed/device/metric/RebootReasonCollector.java
index faf849c..ae2c2f7 100644
--- a/src/com/android/tradefed/device/metric/RebootReasonCollector.java
+++ b/src/com/android/tradefed/device/metric/RebootReasonCollector.java
@@ -43,6 +43,7 @@
 public class RebootReasonCollector extends BaseDeviceMetricCollector {
     private static final String METRIC_SEP = "-";
     public static final String METRIC_PREFIX = "rebooted" + METRIC_SEP;
+    public static final String COUNT_KEY = String.join(METRIC_SEP, "reboot", "count");
 
     private List<ITestDevice> mTestDevices;
     // Map to store statsd config ids for each device, keyed by the device serial number.
@@ -84,9 +85,12 @@
                         "Failed to pull metric data from device %s. Exception: %s.",
                         device.getSerialNumber(), e.toString());
             }
+            Map<String, Integer> metricsForDevice = new HashMap<>();
+            int rebootCount = 0;
             for (EventMetricData eventMetricEntry : metricData) {
                 Atom eventAtom = eventMetricEntry.getAtom();
                 if (eventAtom.hasBootSequenceReported()) {
+                    rebootCount += 1;
                     BootSequenceReported bootAtom = eventAtom.getBootSequenceReported();
                     String bootReasonKey =
                             METRIC_PREFIX
@@ -94,24 +98,20 @@
                                             METRIC_SEP,
                                             bootAtom.getBootloaderReason(),
                                             bootAtom.getSystemReason());
-                    // Append the device serial number only if there are more than one device.
-                    if (mTestDevices.size() > 1) {
-                        bootReasonKey =
-                                String.join(METRIC_SEP, bootReasonKey, device.getSerialNumber());
-                    }
-                    currentRunMetrics.computeIfPresent(
-                            bootReasonKey,
-                            (key, metric) ->
-                                    stringToMetric(
-                                            String.valueOf(
-                                                    Integer.valueOf(
-                                                                    metric.getMeasurements()
-                                                                            .getSingleString())
-                                                            + 1)));
-                    currentRunMetrics.computeIfAbsent(
-                            bootReasonKey, key -> stringToMetric(String.valueOf(1)));
+                    // Update the counts for the specific boot reason in the current atom.
+                    metricsForDevice.computeIfPresent(bootReasonKey, (k, v) -> v + 1);
+                    metricsForDevice.computeIfAbsent(bootReasonKey, k -> 1);
                 }
             }
+            for (String key : metricsForDevice.keySet()) {
+                runData.addMetricForDevice(
+                        device,
+                        key,
+                        stringToMetric(String.valueOf(metricsForDevice.get(key))).toBuilder());
+            }
+            // Add the count regardless of whether reboots occurred or not.
+            runData.addMetricForDevice(
+                    device, COUNT_KEY, stringToMetric(String.valueOf(rebootCount)).toBuilder());
             try {
                 removeConfig(device, configId);
             } catch (DeviceNotAvailableException e) {
diff --git a/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollector.java b/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollector.java
index 877a153..761034b 100644
--- a/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollector.java
+++ b/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollector.java
@@ -29,7 +29,7 @@
 
     @Override
     public void onTestFail(DeviceMetricData testData, TestDescription test) {
-        for (ITestDevice device : getDevices()) {
+        for (ITestDevice device : getRealDevices()) {
             try (InputStreamSource screenSource = device.getScreenshot()) {
                 super.testLog(
                         String.format(NAME_FORMAT, test.toString(), device.getSerialNumber()),
diff --git a/src/com/android/tradefed/host/OWNERS b/src/com/android/tradefed/host/OWNERS
deleted file mode 100644
index a07eedf..0000000
--- a/src/com/android/tradefed/host/OWNERS
+++ /dev/null
@@ -1,4 +0,0 @@
-# host/ drives host related setup or configuration
-fangk@google.com
-jeffreylu@google.com
-xingdai@google.com
diff --git a/src/com/android/tradefed/invoker/IInvocationExecution.java b/src/com/android/tradefed/invoker/IInvocationExecution.java
index 2107a21..da9d0e4 100644
--- a/src/com/android/tradefed/invoker/IInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/IInvocationExecution.java
@@ -94,14 +94,15 @@
             throws DeviceNotAvailableException, TargetSetupError {}
 
     /**
-     * Invoke the {@link ITestDevice#postInvocationTearDown()} for each device part of the
+     * Invoke the {@link ITestDevice#postInvocationTearDown(Throwable)} for each device part of the
      * invocation.
      *
      * @param context the {@link IInvocationContext} of the invocation.
      * @param config the {@link IConfiguration} of this test run.
+     * @param exception the original exception thrown by the test running if any.
      */
     public default void runDevicePostInvocationTearDown(
-            IInvocationContext context, IConfiguration config) {}
+            IInvocationContext context, IConfiguration config, Throwable exception) {}
 
     /**
      * Execute the target_preparer and multi_target_preparer teardown step. Does the devices tear
@@ -141,11 +142,15 @@
      *
      * @param config the current {@link IConfiguration}.
      * @param context the {@link IInvocationContext} holding the info of the tests.
-     * @param rescheduler the {@link IRescheduler}
+     * @param rescheduler the {@link IRescheduler}.
+     * @param logger {@link ITestLogger} used to log file during sharding.
      * @return true if test was sharded. Otherwise return <code>false</code>
      */
     public default boolean shardConfig(
-            IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
+            IConfiguration config,
+            IInvocationContext context,
+            IRescheduler rescheduler,
+            ITestLogger logger) {
         return false;
     }
 
diff --git a/src/com/android/tradefed/invoker/InvocationContext.java b/src/com/android/tradefed/invoker/InvocationContext.java
index 3293fbe..18816a3 100644
--- a/src/com/android/tradefed/invoker/InvocationContext.java
+++ b/src/com/android/tradefed/invoker/InvocationContext.java
@@ -23,6 +23,7 @@
 import com.android.tradefed.config.proto.ConfigurationDescription.Metadata;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.proto.InvocationContext.Context;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.suite.ITestSuite;
@@ -343,6 +344,14 @@
         mLocked = false;
     }
 
+    /** Log the {@link InvocationMetricLogger} attributes to the invocation. */
+    public void logInvocationMetrics() {
+        Map<String, String> metrics = InvocationMetricLogger.getInvocationMetrics();
+        if (!metrics.isEmpty()) {
+            mInvocationAttributes.putAll(new MultiMap<>(metrics));
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public void addSerialsFromShard(Integer index, List<String> serials) {
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index abf19dc..f52555b 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -161,8 +161,11 @@
 
     @Override
     public boolean shardConfig(
-            IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
-        return createShardHelper().shardConfig(config, context, rescheduler);
+            IConfiguration config,
+            IInvocationContext context,
+            IRescheduler rescheduler,
+            ITestLogger logger) {
+        return createShardHelper().shardConfig(config, context, rescheduler, logger);
     }
 
     /** Create an return the {@link IShardHelper} to be used. */
@@ -244,27 +247,23 @@
             if (device instanceof ITestLoggerReceiver) {
                 ((ITestLoggerReceiver) context.getDevice(deviceName)).setTestLogger(logger);
             }
-            if (!config.getCommandOptions().shouldSkipPreDeviceSetup()) {
-                device.preInvocationSetup(
-                        context.getBuildInfo(deviceName),
-                        context.getBuildInfos()
-                                .stream()
-                                .filter(buildInfo -> buildInfo.isTestResourceBuild())
-                                .collect(Collectors.toList()));
-            }
+            device.preInvocationSetup(
+                    context.getBuildInfo(deviceName),
+                    context.getBuildInfos()
+                            .stream()
+                            .filter(buildInfo -> buildInfo.isTestResourceBuild())
+                            .collect(Collectors.toList()));
         }
     }
 
     /** {@inheritDoc} */
     @Override
     public final void runDevicePostInvocationTearDown(
-            IInvocationContext context, IConfiguration config) {
+            IInvocationContext context, IConfiguration config, Throwable exception) {
         // Extra tear down step for the device
         for (String deviceName : context.getDeviceConfigNames()) {
             ITestDevice device = context.getDevice(deviceName);
-            if (!config.getCommandOptions().shouldSkipPreDeviceSetup()) {
-                device.postInvocationTearDown();
-            }
+            device.postInvocationTearDown(exception);
         }
     }
 
@@ -390,7 +389,7 @@
         }
 
         // Extra tear down step for the device
-        runDevicePostInvocationTearDown(context, config);
+        runDevicePostInvocationTearDown(context, config, exception);
 
         // After all, run the multi_pre_target_preparer tearDown.
         List<IMultiTargetPreparer> multiPrePreparers = config.getMultiPreTargetPreparers();
diff --git a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
index f615387..f82442c 100644
--- a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
@@ -19,6 +19,7 @@
 import com.android.tradefed.build.BuildRetrievalError;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.StubBuildProvider;
+import com.android.tradefed.clearcut.ClearcutClient;
 import com.android.tradefed.command.CommandOptions;
 import com.android.tradefed.command.CommandRunner;
 import com.android.tradefed.config.GlobalConfiguration;
@@ -295,7 +296,7 @@
             Throwable exception)
             throws Throwable {
         // Only run device post invocation teardown
-        super.runDevicePostInvocationTearDown(context, config);
+        super.runDevicePostInvocationTearDown(context, config, exception);
     }
 
     @Override
@@ -329,6 +330,8 @@
                 new StringBuilder("TF_GLOBAL_CONFIG=" + globalConfig.getName());
         // Set an env variable to notify that this a remote environment.
         tfCmdBuilder.append(" " + REMOTE_VM_VARIABLE + "=1");
+        // Disable clearcut in the remote
+        tfCmdBuilder.append(" " + ClearcutClient.DISABLE_CLEARCUT_KEY + "=1");
         tfCmdBuilder.append(" ENTRY_CLASS=" + CommandRunner.class.getCanonicalName());
         tfCmdBuilder.append(" ./tradefed.sh " + mRemoteTradefedDir + configFile.getName());
         if (config.getCommandOptions().shouldUseRemoteSandboxMode()) {
@@ -351,45 +354,57 @@
 
         // Monitor the remote invocation to ensure it's completing. Block until timeout or stops
         // running.
-        boolean stillRunning =
-                isStillRunning(
-                        currentInvocationListener, configFile, info, options, runUtil, config);
+        boolean stillRunning = true;
+        try {
+            stillRunning =
+                    isStillRunning(
+                            currentInvocationListener,
+                            configFile,
+                            info,
+                            options,
+                            runUtil,
+                            config,
+                            context);
+        } finally {
+            // Fetch the logs for debugging
+            File stdoutFile =
+                    RemoteFileUtil.fetchRemoteFile(
+                            info,
+                            options,
+                            runUtil,
+                            PULL_RESULT_TIMEOUT,
+                            mRemoteTradefedDir + STDOUT_FILE);
+            if (stdoutFile != null) {
+                try (InputStreamSource source = new FileInputStreamSource(stdoutFile, true)) {
+                    currentInvocationListener.testLog(STDOUT_FILE, LogDataType.TEXT, source);
+                }
+            }
 
-        // Fetch the logs
-        File stdoutFile =
-                RemoteFileUtil.fetchRemoteFile(
-                        info,
-                        options,
-                        runUtil,
-                        PULL_RESULT_TIMEOUT,
-                        mRemoteTradefedDir + STDOUT_FILE);
-        if (stdoutFile != null) {
-            try (InputStreamSource source = new FileInputStreamSource(stdoutFile, true)) {
-                currentInvocationListener.testLog(STDOUT_FILE, LogDataType.TEXT, source);
+            File stderrFile =
+                    RemoteFileUtil.fetchRemoteFile(
+                            info,
+                            options,
+                            runUtil,
+                            PULL_RESULT_TIMEOUT,
+                            mRemoteTradefedDir + STDERR_FILE);
+            if (stderrFile != null) {
+                try (InputStreamSource source = new FileInputStreamSource(stderrFile, true)) {
+                    currentInvocationListener.testLog(STDERR_FILE, LogDataType.TEXT, source);
+                }
             }
         }
 
-        File stderrFile =
-                RemoteFileUtil.fetchRemoteFile(
-                        info,
-                        options,
-                        runUtil,
-                        PULL_RESULT_TIMEOUT,
-                        mRemoteTradefedDir + STDERR_FILE);
-        if (stderrFile != null) {
-            try (InputStreamSource source = new FileInputStreamSource(stderrFile, true)) {
-                currentInvocationListener.testLog(STDERR_FILE, LogDataType.TEXT, source);
-            }
+        // If not result in progress are reported, parse the full results at the end.
+        if (!config.getCommandOptions().shouldReportModuleProgression()) {
+            fetchAndProcessResults(
+                    stillRunning,
+                    currentInvocationListener,
+                    context,
+                    info,
+                    options,
+                    runUtil,
+                    mRemoteTradefedDir);
         }
-
-        fetchAndProcessResults(
-                stillRunning,
-                currentInvocationListener,
-                context,
-                info,
-                options,
-                runUtil,
-                mRemoteTradefedDir);
     }
 
     private boolean isStillRunning(
@@ -398,7 +413,9 @@
             GceAvdInfo info,
             TestDeviceOptions options,
             IRunUtil runUtil,
-            IConfiguration config) {
+            IConfiguration config,
+            IInvocationContext context)
+            throws IOException {
         long maxTimeout = config.getCommandOptions().getInvocationTimeout();
         Long endTime = null;
         if (maxTimeout > 0L) {
@@ -406,7 +423,31 @@
         }
         boolean stillRunning = true;
         int errorConnectCount = 0;
+        int currentIndex = 0;
+        ProtoResultParser parser =
+                new ProtoResultParser(
+                        currentInvocationListener,
+                        context, /* Don't report invocation level */
+                        false,
+                        "remote-");
         while (stillRunning) {
+            if (config.getCommandOptions().shouldReportModuleProgression()) {
+                File resultFile =
+                        RemoteFileUtil.fetchRemoteFile(
+                                info,
+                                options,
+                                runUtil,
+                                PULL_RESULT_TIMEOUT,
+                                mRemoteTradefedDir + PROTO_RESULT_NAME + currentIndex);
+                if (resultFile != null) {
+                    currentIndex++;
+                    parser.processFileProto(resultFile);
+                    // Don't sleep in that case since we might have more file to process, this will
+                    // sleep next time we don't find a file to process on the remote.
+                    continue;
+                }
+            }
+
             CommandResult psRes =
                     GceManager.remoteSshCommandExecution(
                             info,
@@ -443,6 +484,24 @@
                 RunUtil.getDefault().sleep(REMOTE_PROCESS_RUNNING_WAIT);
             }
         }
+
+        File resultFile = null;
+        if (config.getCommandOptions().shouldReportModuleProgression()) {
+            // Process all remaining proto files available
+            do {
+                resultFile =
+                        RemoteFileUtil.fetchRemoteFile(
+                                info,
+                                options,
+                                runUtil,
+                                PULL_RESULT_TIMEOUT,
+                                mRemoteTradefedDir + PROTO_RESULT_NAME + currentIndex);
+                if (resultFile != null) {
+                    currentIndex++;
+                    parser.processFileProto(resultFile);
+                }
+            } while (resultFile != null);
+        }
         return stillRunning;
     }
 
@@ -520,6 +579,9 @@
         // Setup the remote reporting to a proto file
         List<ITestInvocationListener> reporters = new ArrayList<>();
         FileProtoResultReporter protoReporter = new FileProtoResultReporter();
+        if (config.getCommandOptions().shouldReportModuleProgression()) {
+            protoReporter.setPeriodicWriting(true);
+        }
         protoReporter.setFileOutput(new File(resultDirPath + PROTO_RESULT_NAME));
         reporters.add(protoReporter);
 
@@ -535,7 +597,11 @@
 
         // Dump and log the configuration
         File configFile = FileUtil.createTempFile(config.getName(), ".xml");
-        config.dumpXml(new PrintWriter(configFile));
+        config.dumpXml(
+                new PrintWriter(configFile),
+                new ArrayList<String>(),
+                /* print deprecated */ true,
+                /* print unchanged*/ false);
         try (InputStreamSource source = new FileInputStreamSource(configFile)) {
             logger.testLog(REMOTE_CONFIG, LogDataType.XML, source);
         }
diff --git a/src/com/android/tradefed/invoker/ShardListener.java b/src/com/android/tradefed/invoker/ShardListener.java
index b3d8348..5805a3b 100644
--- a/src/com/android/tradefed/invoker/ShardListener.java
+++ b/src/com/android/tradefed/invoker/ShardListener.java
@@ -43,6 +43,7 @@
 public class ShardListener extends CollectingTestListener {
 
     private ITestInvocationListener mMasterListener;
+    private IInvocationContext mModuleContext = null;
 
     /**
      * Create a {@link ShardListener}.
@@ -112,10 +113,9 @@
 
     /** {@inheritDoc} */
     @Override
-    public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
-        super.testRunEnded(elapsedTime, runMetrics);
-        CLog.logAndDisplay(LogLevel.INFO, "Sharded test completed: %s",
-                getCurrentRunResults().getName());
+    public void testModuleStarted(IInvocationContext moduleContext) {
+        super.testModuleStarted(moduleContext);
+        mModuleContext = moduleContext;
     }
 
     /**
@@ -128,16 +128,34 @@
                 getCurrentRunResults().getName(), failureMessage);
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public void invocationEnded(long elapsedTime) {
-        super.invocationEnded(elapsedTime);
+    public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
+        super.testRunEnded(elapsedTime, runMetrics);
+        CLog.logAndDisplay(
+                LogLevel.INFO, "Sharded test completed: %s", getCurrentRunResults().getName());
+        if (mModuleContext == null) {
+            // testRunEnded only forwards if it's not part of a module. If it's a module
+            // testModuleEnded is in charge of forwarding all run results.
+            synchronized (mMasterListener) {
+                forwardRunResults(getCurrentRunResults());
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void testModuleEnded() {
+        super.testModuleEnded();
+
         synchronized (mMasterListener) {
-            logShardContent(getMergedTestRunResults());
             IInvocationContext moduleContext = null;
+            // TODO: Support attempts and retries
             for (TestRunResult runResult : getMergedTestRunResults()) {
+                // Only consider run results of the module in progress
+                if (getModuleContextForRunResult(runResult.getName()) != mModuleContext) {
+                    continue;
+                }
 
                 // Stop or start the module
                 if (moduleContext != null
@@ -151,33 +169,44 @@
                     moduleContext = getModuleContextForRunResult(runResult.getName());
                     mMasterListener.testModuleStarted(moduleContext);
                 }
-
-                mMasterListener.testRunStarted(
-                        runResult.getName(), runResult.getExpectedTestCount());
-                forwardTestResults(runResult.getTestResults());
-                if (runResult.isRunFailure()) {
-                    mMasterListener.testRunFailed(runResult.getRunFailureMessage());
-                }
-
-                // Provide a strong association of the run to its logs.
-                forwardLogAssociation(runResult.getRunLoggedFiles(), mMasterListener);
-
-                mMasterListener.testRunEnded(
-                        runResult.getElapsedTime(), runResult.getRunProtoMetrics());
+                // Forward the run level results
+                forwardRunResults(runResult);
             }
             // Close the last module
             if (moduleContext != null) {
                 mMasterListener.testModuleEnded();
                 moduleContext = null;
             }
-            // In case there was no run, we still want to report the logs we received.
-            if (getMergedTestRunResults().isEmpty()) {
-                forwardLogAssociation(getCurrentRunResults().getRunLoggedFiles(), mMasterListener);
-            }
+        }
+        mModuleContext = null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void invocationEnded(long elapsedTime) {
+        super.invocationEnded(elapsedTime);
+        synchronized (mMasterListener) {
+            logShardContent(getMergedTestRunResults());
+            // Report all logs not associated with test runs
+            forwardLogAssociation(getNonAssociatedLogFiles(), mMasterListener);
             mMasterListener.invocationEnded(elapsedTime);
         }
     }
 
+    private void forwardRunResults(TestRunResult runResult) {
+        // TODO: Support attempts and retries
+        mMasterListener.testRunStarted(runResult.getName(), runResult.getExpectedTestCount());
+        forwardTestResults(runResult.getTestResults());
+        if (runResult.isRunFailure()) {
+            mMasterListener.testRunFailed(runResult.getRunFailureMessage());
+        }
+
+        // Provide a strong association of the run to its logs.
+        forwardLogAssociation(runResult.getRunLoggedFiles(), mMasterListener);
+
+        mMasterListener.testRunEnded(runResult.getElapsedTime(), runResult.getRunProtoMetrics());
+    }
+
     private void forwardTestResults(Map<TestDescription, TestResult> testResults) {
         for (Map.Entry<TestDescription, TestResult> testEntry : testResults.entrySet()) {
             mMasterListener.testStarted(testEntry.getKey(), testEntry.getValue().getStartTime());
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index b7b2599..8f18c26 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -32,6 +32,7 @@
 import com.android.tradefed.device.cloud.NestedRemoteDevice;
 import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.guice.InvocationScope;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecution;
 import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
 import com.android.tradefed.invoker.shard.ShardBuildCloner;
@@ -55,6 +56,7 @@
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.IResumableTest;
 import com.android.tradefed.testtype.IRetriableTest;
+import com.android.tradefed.testtype.retry.ResultAggregator;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.PrettyPrintDelimiter;
@@ -91,7 +93,8 @@
      */
     private static final String BATTERY_ATTRIBUTE_FORMAT_KEY = "%s-battery-%s";
 
-    static final String TRADEFED_LOG_NAME = "host_log";
+    public static final String TRADEFED_LOG_NAME = "host_log";
+    public static final String TRADEFED_END_HOST_LOG = "end_host_log";
     /** Suffix used on host_log for the part before sharding occurs. */
     static final String BEFORE_SHARDING_SUFFIX = "_before_sharding";
     static final String DEVICE_LOG_NAME_PREFIX = "device_logcat_";
@@ -339,20 +342,30 @@
                     invocationPath.reportLogs(device, listener, Stage.TEARDOWN);
                 }
                 if (mStopRequested) {
-                    CLog.e(
-                            "====================================================================="
-                                    + "====");
-                    CLog.e(
+                    String message =
                             "Invocation was interrupted due to TradeFed stop, results will be "
-                                    + "affected.");
-                    CLog.e(
-                            "====================================================================="
-                                    + "====");
+                                    + "affected.";
+                    listener.invocationFailed(new RuntimeException(message));
+                    PrettyPrintDelimiter.printStageDelimiter(message);
                 }
                 reportHostLog(listener, config);
+
                 elapsedTime = System.currentTimeMillis() - startTime;
                 if (!resumed) {
-                    listener.invocationEnded(elapsedTime);
+                    // Init a log for the end of the host_log.
+                    ILeveledLogOutput endHostLog = config.getLogOutput();
+                    endHostLog.init();
+                    getLogRegistry().registerLogger(endHostLog);
+                    PrettyPrintDelimiter.printStageDelimiter("===== Result Reporters =====");
+                    try {
+                        // Copy the invocation metrics to the context
+                        ((InvocationContext) context).logInvocationMetrics();
+                        listener.invocationEnded(elapsedTime);
+                    } finally {
+                        InvocationMetricLogger.clearInvocationMetrics();
+                        endHostLog.closeLog();
+                        getLogRegistry().unregisterLogger();
+                    }
                 }
             } finally {
                 invocationPath.cleanUpBuilds(context, config);
@@ -620,6 +633,17 @@
         allListeners.addAll(config.getTestInvocationListeners());
         allListeners.addAll(Arrays.asList(extraListeners));
         ITestInvocationListener listener = null;
+
+        // Auto retry feature
+        if (config.getCommandOptions().isAutoRetryEnabled()
+                && config.getCommandOptions().getMaxRetryCount() > 1) {
+            CLog.d("Auto-retry enabled, using the ResultAggregator to handle multiple retries.");
+            ResultAggregator aggregator =
+                    new ResultAggregator(
+                            allListeners, config.getCommandOptions().getRetryStrategy());
+            allListeners = Arrays.asList(aggregator);
+        }
+
         if (!config.getPostProcessors().isEmpty()) {
             ITestInvocationListener forwarder = new ResultAndLogForwarder(allListeners);
             // Post-processors are the first layer around the final reporters.
@@ -654,13 +678,16 @@
         scope.seed(IRescheduler.class, rescheduler);
         scope.seedConfiguration(config);
         try {
-            mStatus = "fetching build";
             ILeveledLogOutput leveledLogOutput = config.getLogOutput();
             leveledLogOutput.init();
             if (leveledLogOutput instanceof BaseLeveledLogOutput) {
                 ((BaseLeveledLogOutput) leveledLogOutput).initFilters(config);
             }
-            getLogRegistry().registerLogger(config.getLogOutput());
+            getLogRegistry().registerLogger(leveledLogOutput);
+            mStatus = "resolving dynamic options";
+            config.resolveDynamicOptions();
+
+            mStatus = "fetching build";
             for (String deviceName : context.getDeviceConfigNames()) {
                 context.getDevice(deviceName).clearLastConnectedWifiNetwork();
                 context.getDevice(deviceName)
@@ -712,7 +739,7 @@
                         CLog.e(e);
                         setExitCode(ExitCode.THROWABLE_EXCEPTION, e);
                         try {
-                            invocationPath.runDevicePostInvocationTearDown(context, config);
+                            invocationPath.runDevicePostInvocationTearDown(context, config, e);
                         } finally {
                             listener.invocationFailed(e);
                             // Reports the logs
@@ -726,7 +753,8 @@
                     }
                 }
 
-                boolean sharding = invocationPath.shardConfig(config, context, rescheduler);
+                boolean sharding =
+                        invocationPath.shardConfig(config, context, rescheduler, listener);
                 if (sharding) {
                     CLog.i(
                             "Invocation for %s has been sharded, rescheduling",
@@ -745,7 +773,7 @@
                 CLog.e("No tests to run");
                 if (deviceInit) {
                     // If we did an early setup, do the tear down.
-                    invocationPath.runDevicePostInvocationTearDown(context, config);
+                    invocationPath.runDevicePostInvocationTearDown(context, config, null);
                 }
                 listener.invocationEnded(0L);
                 return;
@@ -838,19 +866,20 @@
                 return;
             }
             try {
-                if (log != null && !FileUtil.readStringFromFile(log).isEmpty()) {
-                    try (InputStreamSource source = new FileInputStreamSource(log)) {
-                        logger.testLog(
-                                String.format(
-                                        "executeShellCommandLog_%s", device.getSerialNumber()),
-                                LogDataType.TEXT,
-                                source);
-                    }
+                if (FileUtil.readStringFromFile(log).isEmpty()) {
+                    CLog.d("executeShellCommandLog file was empty, skip logging.");
+                    return;
                 }
             } catch (IOException e) {
                 // Ignored
                 CLog.e(e);
             }
+            try (InputStreamSource source = new FileInputStreamSource(log)) {
+                logger.testLog(
+                        String.format("executeShellCommandLog_%s", device.getSerialNumber()),
+                        LogDataType.TEXT,
+                        source);
+            }
         }
     }
 }
diff --git a/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
new file mode 100644
index 0000000..bf2eb87
--- /dev/null
+++ b/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.invoker.logger;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/** A utility class for an invocation to log some metrics. */
+public class InvocationMetricLogger {
+
+    /** Some special named key that we will always populate for the invocation. */
+    public enum InvocationMetricKey {
+        FETCH_BUILD("fetch_build_time_ms"),
+        SETUP("setup_time_ms");
+
+        private final String mKeyName;
+
+        private InvocationMetricKey(String key) {
+            mKeyName = key;
+        }
+
+        @Override
+        public String toString() {
+            return mKeyName;
+        }
+    }
+
+    private InvocationMetricLogger() {}
+
+    /**
+     * Track metrics per ThreadGroup as a proxy to invocation since an invocation run within one
+     * threadgroup.
+     */
+    private static final Map<ThreadGroup, Map<String, String>> mPerGroupMetrics =
+            Collections.synchronizedMap(new HashMap<ThreadGroup, Map<String, String>>());
+
+    /**
+     * Add one key-value to be tracked at the invocation level.
+     *
+     * @param key The key under which the invocation metric will be tracked.
+     * @param value The value of the invocation metric.
+     */
+    public static void addInvocationMetrics(InvocationMetricKey key, String value) {
+        addInvocationMetrics(key.toString(), value);
+    }
+
+    /**
+     * Add one key-value to be tracked at the invocation level. Don't expose the String key yet to
+     * avoid abuse, stick to the official {@link InvocationMetricKey} to start with.
+     *
+     * @param key The key under which the invocation metric will be tracked.
+     * @param value The value of the invocation metric.
+     */
+    private static void addInvocationMetrics(String key, String value) {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        synchronized (mPerGroupMetrics) {
+            if (mPerGroupMetrics.get(group) == null) {
+                mPerGroupMetrics.put(group, new HashMap<>());
+            }
+            mPerGroupMetrics.get(group).put(key, value);
+        }
+    }
+
+    /** Returns the Map of invocation metrics for the invocation in progress. */
+    public static Map<String, String> getInvocationMetrics() {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        synchronized (mPerGroupMetrics) {
+            if (mPerGroupMetrics.get(group) == null) {
+                mPerGroupMetrics.put(group, new HashMap<>());
+            }
+        }
+        return new HashMap<>(mPerGroupMetrics.get(group));
+    }
+
+    /** Clear the invocation metrics for an invocation. */
+    public static void clearInvocationMetrics() {
+        ThreadGroup group = Thread.currentThread().getThreadGroup();
+        mPerGroupMetrics.remove(group);
+    }
+}
diff --git a/src/com/android/tradefed/invoker/shard/IShardHelper.java b/src/com/android/tradefed/invoker/shard/IShardHelper.java
index 37d0928..7eb3079 100644
--- a/src/com/android/tradefed/invoker/shard/IShardHelper.java
+++ b/src/com/android/tradefed/invoker/shard/IShardHelper.java
@@ -18,6 +18,7 @@
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.IRescheduler;
+import com.android.tradefed.log.ITestLogger;
 
 /** Interface of an object that describes the sharding strategy to adopt for a configuration. */
 public interface IShardHelper {
@@ -31,5 +32,8 @@
      * @return True if the configuration was sharded. false otherwise.
      */
     public boolean shardConfig(
-            IConfiguration config, IInvocationContext context, IRescheduler rescheduler);
+            IConfiguration config,
+            IInvocationContext context,
+            IRescheduler rescheduler,
+            ITestLogger logger);
 }
diff --git a/src/com/android/tradefed/invoker/shard/ShardHelper.java b/src/com/android/tradefed/invoker/shard/ShardHelper.java
index 4aa1c06..b0935ee 100644
--- a/src/com/android/tradefed/invoker/shard/ShardHelper.java
+++ b/src/com/android/tradefed/invoker/shard/ShardHelper.java
@@ -28,9 +28,11 @@
 import com.android.tradefed.invoker.ShardListener;
 import com.android.tradefed.invoker.ShardMasterResultForwarder;
 import com.android.tradefed.invoker.shard.token.ITokenRequest;
+import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.IShardableListener;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.ITestLoggerReceiver;
 import com.android.tradefed.suite.checker.ISystemStatusChecker;
 import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IDeviceTest;
@@ -85,12 +87,15 @@
      */
     @Override
     public boolean shardConfig(
-            IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
+            IConfiguration config,
+            IInvocationContext context,
+            IRescheduler rescheduler,
+            ITestLogger logger) {
         List<IRemoteTest> shardableTests = new ArrayList<IRemoteTest>();
         boolean isSharded = false;
         Integer shardCount = config.getCommandOptions().getShardCount();
         for (IRemoteTest test : config.getTests()) {
-            isSharded |= shardTest(shardableTests, test, shardCount, context);
+            isSharded |= shardTest(shardableTests, test, shardCount, context, logger);
         }
         if (!isSharded) {
             return false;
@@ -193,10 +198,11 @@
         return GlobalConfiguration.getInstance();
     }
 
-    /** Runs the {@link IConfiguration#validateOptions(boolean)} on the config. */
+    /** Runs the {@link IConfiguration#validateOptions()} on the config. */
     @VisibleForTesting
     protected void validateOptions(IConfiguration config) throws ConfigurationException {
-        config.validateOptions(true);
+        config.validateOptions();
+        config.resolveDynamicOptions();
     }
 
     /**
@@ -251,7 +257,8 @@
             List<IRemoteTest> shardableTests,
             IRemoteTest test,
             Integer shardCount,
-            IInvocationContext context) {
+            IInvocationContext context,
+            ITestLogger logger) {
         boolean isSharded = false;
         if (test instanceof IShardableTest) {
             // inject device and build since they might be required to shard.
@@ -267,6 +274,9 @@
             if (test instanceof IInvocationContextReceiver) {
                 ((IInvocationContextReceiver) test).setInvocationContext(context);
             }
+            if (test instanceof ITestLoggerReceiver) {
+                ((ITestLoggerReceiver) test).setTestLogger(logger);
+            }
 
             IShardableTest shardableTest = (IShardableTest) test;
             Collection<IRemoteTest> shards = null;
diff --git a/src/com/android/tradefed/invoker/shard/StrictShardHelper.java b/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
index 23021b0..4be515c 100644
--- a/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
+++ b/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
@@ -19,7 +19,9 @@
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.IRescheduler;
+import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestLoggerReceiver;
 import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IInvocationContextReceiver;
@@ -42,19 +44,22 @@
     /** {@inheritDoc} */
     @Override
     public boolean shardConfig(
-            IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
+            IConfiguration config,
+            IInvocationContext context,
+            IRescheduler rescheduler,
+            ITestLogger logger) {
         Integer shardCount = config.getCommandOptions().getShardCount();
         Integer shardIndex = config.getCommandOptions().getShardIndex();
 
         if (shardIndex == null) {
-            return super.shardConfig(config, context, rescheduler);
+            return super.shardConfig(config, context, rescheduler, logger);
         }
         if (shardCount == null) {
             throw new RuntimeException("shard-count is null while shard-index is " + shardIndex);
         }
 
         // Split tests in place, without actually sharding.
-        List<IRemoteTest> listAllTests = getAllTests(config, shardCount, context);
+        List<IRemoteTest> listAllTests = getAllTests(config, shardCount, context, logger);
         // We cannot shuffle to get better average results
         normalizeDistribution(listAllTests, shardCount);
         List<IRemoteTest> splitList;
@@ -78,7 +83,10 @@
      * @return the list of all {@link IRemoteTest}.
      */
     private List<IRemoteTest> getAllTests(
-            IConfiguration config, Integer shardCount, IInvocationContext context) {
+            IConfiguration config,
+            Integer shardCount,
+            IInvocationContext context,
+            ITestLogger logger) {
         List<IRemoteTest> allTests = new ArrayList<>();
         for (IRemoteTest test : config.getTests()) {
             if (test instanceof IShardableTest) {
@@ -95,6 +103,9 @@
                 if (test instanceof IInvocationContextReceiver) {
                     ((IInvocationContextReceiver) test).setInvocationContext(context);
                 }
+                if (test instanceof ITestLoggerReceiver) {
+                    ((ITestLoggerReceiver) test).setTestLogger(logger);
+                }
 
                 // Handling of the ITestSuite is a special case, we do not allow pool of tests
                 // since each shard needs to be independent.
diff --git a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
index b341d87..85c0c28 100644
--- a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
+++ b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
@@ -214,7 +214,8 @@
                     // At this point only the <test> object needs to be validated for options, this
                     // ensures that the object is fine before running it.
                     validationConfig.setTest(test);
-                    validationConfig.validateOptions(true);
+                    validationConfig.validateOptions();
+                    validationConfig.resolveDynamicOptions();
                     // Run the test itself and prevent random exception from stopping the poller.
                     if (test instanceof IMetricCollectorReceiver) {
                         ((IMetricCollectorReceiver) test).setMetricCollectors(mCollectors);
diff --git a/src/com/android/tradefed/log/BaseStreamLogger.java b/src/com/android/tradefed/log/BaseStreamLogger.java
new file mode 100644
index 0000000..5027386
--- /dev/null
+++ b/src/com/android/tradefed/log/BaseStreamLogger.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.log;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.util.StreamUtil;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/** A {@link ILeveledLogOutput} that directs log messages to an output stream and to stdout. */
+public abstract class BaseStreamLogger<OS extends OutputStream> extends BaseLeveledLogOutput {
+
+    /**
+     * Map of log tag to a level they are forced at for writing to log file purpose. This ensure
+     * that some logs we have less control over can still be regulated.
+     */
+    private static final Map<String, LogLevel> FORCED_LOG_LEVEL = new HashMap<>();
+
+    static {
+        FORCED_LOG_LEVEL.put("ddms", LogLevel.WARN);
+    }
+
+    @Option(name = "log-level", description = "the minimum log level to log.")
+    private LogLevel mLogLevel = LogLevel.DEBUG;
+
+    @Option(
+        name = "log-level-display",
+        shortName = 'l',
+        description = "the minimum log level to display on stdout.",
+        importance = Importance.ALWAYS
+    )
+    private LogLevel mLogLevelDisplay = LogLevel.ERROR;
+
+    // output stream to print logs to, exposed to subclasses
+    protected OS mOutputStream;
+
+    @Override
+    public LogLevel getLogLevel() {
+        return mLogLevel;
+    }
+
+    @Override
+    public void setLogLevel(LogLevel logLevel) {
+        mLogLevel = logLevel;
+    }
+
+    /** Sets the minimum {@link LogLevel} to display on stdout. */
+    public void setLogLevelDisplay(LogLevel logLevel) {
+        mLogLevelDisplay = logLevel;
+    }
+
+    /** @return current minimum {@link LogLevel} to display on stdout. */
+    LogLevel getLogLevelDisplay() {
+        return mLogLevelDisplay;
+    }
+
+    @Override
+    public void closeLog() {
+        StreamUtil.flushAndCloseStream(mOutputStream);
+        mOutputStream = null;
+    }
+
+    @Override
+    public void printAndPromptLog(LogLevel logLevel, String tag, String message) {
+        internalPrintLog(logLevel, tag, message, true /* force print to stdout */);
+    }
+
+    @Override
+    public void printLog(LogLevel logLevel, String tag, String message) {
+        internalPrintLog(logLevel, tag, message, false /* don't force stdout */);
+    }
+
+    /**
+     * A version of printLog(...) which can be forced to print to stdout, even if the log level
+     * isn't above the urgency threshold.
+     */
+    private void internalPrintLog(
+            LogLevel logLevel, String tag, String message, boolean forceStdout) {
+        String outMessage = LogUtil.getLogFormatString(logLevel, tag, message);
+        if (shouldDisplay(forceStdout, mLogLevelDisplay, logLevel, tag)) {
+            System.out.print(outMessage);
+        }
+        if (shouldWrite(tag, logLevel, mLogLevel)) {
+            try {
+                writeToLog(outMessage);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    // Determines whether a message should be written to the output stream.
+    private boolean shouldWrite(String tag, LogLevel messageLogLevel, LogLevel invocationLogLevel) {
+        LogLevel forcedLevel = FORCED_LOG_LEVEL.get(tag);
+        if (forcedLevel == null) {
+            return true;
+        }
+        // Use the highest level of our forced and invocation to decide if we should log the
+        // particular tag.
+        int minWriteLevel = Math.max(forcedLevel.getPriority(), invocationLogLevel.getPriority());
+        return messageLogLevel.getPriority() >= minWriteLevel;
+    }
+
+    /**
+     * Writes a message to the output stream.
+     *
+     * @param message the entry to write to log
+     * @throws IOException if an I/O error occurs
+     */
+    protected void writeToLog(String message) throws IOException {
+        if (mOutputStream != null) {
+            mOutputStream.write(message.getBytes());
+        }
+    }
+}
diff --git a/src/com/android/tradefed/log/FileLogger.java b/src/com/android/tradefed/log/FileLogger.java
index 100f5ca..c985a0d 100644
--- a/src/com/android/tradefed/log/FileLogger.java
+++ b/src/com/android/tradefed/log/FileLogger.java
@@ -15,9 +15,7 @@
  */
 package com.android.tradefed.log;
 
-import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.config.Option;
-import com.android.tradefed.config.Option.Importance;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.result.ByteArrayInputStreamSource;
@@ -26,46 +24,20 @@
 import com.android.tradefed.util.SizeLimitedOutputStream;
 import com.android.tradefed.util.StreamUtil;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
 
 /** A {@link ILeveledLogOutput} that directs log messages to a file and to stdout. */
 @OptionClass(alias = "file")
-public class FileLogger extends BaseLeveledLogOutput {
+public class FileLogger extends BaseStreamLogger<SizeLimitedOutputStream> {
     private static final String TEMP_FILE_PREFIX = "tradefed_log_";
     private static final String TEMP_FILE_SUFFIX = ".txt";
 
-    /**
-     * Map of log tag to a level they are forced at for writing to log file purpose. This ensure
-     * that some logs we have less control over can still be regulated.
-     */
-    private static final Map<String, LogLevel> FORCED_LOG_LEVEL = new HashMap<>();
-
-    static {
-        FORCED_LOG_LEVEL.put("ddms", LogLevel.WARN);
-    }
-
-    @Option(name = "log-level", description = "the minimum log level to log.")
-    private LogLevel mLogLevel = LogLevel.DEBUG;
-
-    @Option(name = "log-level-display", shortName = 'l',
-            description = "the minimum log level to display on stdout.",
-            importance = Importance.ALWAYS)
-    private LogLevel mLogLevelDisplay = LogLevel.ERROR;
-
     @Option(name = "max-log-size", description = "maximum allowable size of tmp log data in mB.")
     private long mMaxLogSizeMbytes = 20;
 
-    private SizeLimitedOutputStream mLogStream;
-
-    public FileLogger() {
-    }
-
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void init() throws IOException {
         init(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
@@ -78,7 +50,7 @@
      * @param fileSuffix the extension of the file where to log.
      */
     protected void init(String logPrefix, String fileSuffix) {
-        mLogStream =
+        mOutputStream =
                 new SizeLimitedOutputStream(mMaxLogSizeMbytes * 1024 * 1024, logPrefix, fileSuffix);
     }
 
@@ -95,104 +67,18 @@
         return logger;
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void printAndPromptLog(LogLevel logLevel, String tag, String message) {
-        internalPrintLog(logLevel, tag, message, true /* force print to stdout */);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void printLog(LogLevel logLevel, String tag, String message) {
-        internalPrintLog(logLevel, tag, message, false /* don't force stdout */);
-    }
-
-    /**
-     * A version of printLog(...) which can be forced to print to stdout, even if the log level
-     * isn't above the urgency threshold.
-     */
-    private void internalPrintLog(LogLevel logLevel, String tag, String message,
-            boolean forceStdout) {
-        String outMessage = LogUtil.getLogFormatString(logLevel, tag, message);
-        if (shouldDisplay(forceStdout, mLogLevelDisplay, logLevel, tag)) {
-            System.out.print(outMessage);
-        }
-        try {
-            if (shouldWrite(tag, logLevel, mLogLevel)) {
-                writeToLog(outMessage);
-            }
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-    }
-
-    /**
-     * Writes given message to log.
-     * <p/>
-     * Exposed for unit testing.
-     *
-     * @param outMessage the entry to write to log
-     * @throws IOException
-     */
-    void writeToLog(String outMessage) throws IOException {
-        if (mLogStream != null) {
-            mLogStream.write(outMessage.getBytes());
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public LogLevel getLogLevel() {
-        return mLogLevel;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setLogLevel(LogLevel logLevel) {
-        mLogLevel = logLevel;
-    }
-
-    /**
-     * Sets the log level filtering for stdout.
-     *
-     * @param logLevel the minimum {@link LogLevel} to display
-     */
-    public void setLogLevelDisplay(LogLevel logLevel) {
-        mLogLevelDisplay = logLevel;
-    }
-
-    /**
-     * Gets the log level filtering for stdout.
-     *
-     * @return the current {@link LogLevel}
-     */
-    LogLevel getLogLevelDisplay() {
-        return mLogLevelDisplay;
-    }
-
     /** Returns the max log size of the log in MBytes. */
     public long getMaxLogSizeMbytes() {
         return mMaxLogSizeMbytes;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public InputStreamSource getLog() {
-        if (mLogStream != null) {
+        if (mOutputStream != null) {
             try {
                 // create a InputStream from log file
-                mLogStream.flush();
-                return new SnapshotInputStreamSource("FileLogger", mLogStream.getData());
+                mOutputStream.flush();
+                return new SnapshotInputStreamSource("FileLogger", mOutputStream.getData());
             } catch (IOException e) {
                 System.err.println("Failed to get log");
                 e.printStackTrace();
@@ -201,22 +87,16 @@
         return new ByteArrayInputStreamSource(new byte[0]);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public void closeLog() {
         doCloseLog();
     }
 
-    /**
-     * Flushes stream and closes log file.
-     * <p/>
-     * Exposed for unit testing.
-     */
+    /** Flushes stream and closes log file. */
+    @VisibleForTesting
     void doCloseLog() {
-        SizeLimitedOutputStream stream = mLogStream;
-        mLogStream = null;
+        SizeLimitedOutputStream stream = mOutputStream;
+        mOutputStream = null;
         StreamUtil.flushAndCloseStream(stream);
         if (stream != null) {
             stream.delete();
@@ -226,26 +106,12 @@
     /**
      * Dump the contents of the input stream to this log
      *
-     * @param inputStream
-     * @throws IOException
+     * @param inputStream input stream to dump
+     * @throws IOException if an I/O error occurs
      */
     void dumpToLog(InputStream inputStream) throws IOException {
-        if (mLogStream != null) {
-            StreamUtil.copyStreams(inputStream, mLogStream);
+        if (mOutputStream != null) {
+            StreamUtil.copyStreams(inputStream, mOutputStream);
         }
     }
-
-    private boolean shouldWrite(String tag, LogLevel messageLogLevel, LogLevel invocationLogLevel) {
-        LogLevel forcedLevel = FORCED_LOG_LEVEL.get(tag);
-        if (forcedLevel == null) {
-            return true;
-        }
-        // Use the highest level of our forced and invocation to decide if we should log the
-        // particular tag.
-        int minWriteLevel = Math.max(forcedLevel.getPriority(), invocationLogLevel.getPriority());
-        if (messageLogLevel.getPriority() >= minWriteLevel) {
-            return true;
-        }
-        return false;
-    }
 }
diff --git a/src/com/android/tradefed/log/HistoryLogger.java b/src/com/android/tradefed/log/HistoryLogger.java
index be983c7..d713163 100644
--- a/src/com/android/tradefed/log/HistoryLogger.java
+++ b/src/com/android/tradefed/log/HistoryLogger.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.log;
 
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.log.ILogRegistry.EventType;
 
 import org.json.JSONObject;
@@ -37,14 +38,12 @@
         init(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
     }
 
-    /** {@inheritDoc} */
     @Override
     public void printAndPromptLog(LogLevel logLevel, String tag, String message) {
         throw new UnsupportedOperationException(
                 "printAndPromptLog is not supported by HistoryLogger");
     }
 
-    /** {@inheritDoc} */
     @Override
     public void printLog(LogLevel logLevel, String tag, String message) {
         throw new UnsupportedOperationException("printLog is not supported by HistoryLogger");
@@ -84,9 +83,8 @@
 
     @Override
     public ILeveledLogOutput clone() {
-        FileLogger logger = new HistoryLogger();
-        logger.setLogLevelDisplay(getLogLevelDisplay());
-        logger.setLogLevel(getLogLevel());
+        HistoryLogger logger = new HistoryLogger();
+        OptionCopier.copyOptionsNoThrow(this, logger);
         return logger;
     }
 }
diff --git a/src/com/android/tradefed/log/ILogRegistry.java b/src/com/android/tradefed/log/ILogRegistry.java
index 4c2cd55..860890a 100644
--- a/src/com/android/tradefed/log/ILogRegistry.java
+++ b/src/com/android/tradefed/log/ILogRegistry.java
@@ -19,7 +19,6 @@
 import com.android.ddmlib.Log.ILogOutput;
 import com.android.ddmlib.Log.LogLevel;
 
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -97,11 +96,4 @@
     /** Diagnosis method to dump all logs to files. */
     public void dumpLogs();
 
-    /**
-     * Get log entries after the given logEntry.
-     *
-     * @param logEntry a {@link LogEntry} as a pivot.
-     * @return log entries logged after the given logEntry.
-     */
-    public List<LogEntry> getLogEntriesAfter(LogEntry logEntry);
 }
diff --git a/src/com/android/tradefed/log/LogEntry.java b/src/com/android/tradefed/log/LogEntry.java
deleted file mode 100644
index 99aed8c..0000000
--- a/src/com/android/tradefed/log/LogEntry.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.log;
-
-import com.android.ddmlib.Log.LogLevel;
-
-import java.util.concurrent.atomic.AtomicLong;
-
-/** A log entry store's information for one log. */
-public class LogEntry implements Comparable<LogEntry> {
-
-    private static final AtomicLong sLogIndex = new AtomicLong(0);
-    private final long mTimestamp; // time in milliseconds.
-    private final LogLevel mLogLevel;
-    private final String mTag;
-    private final String mMessage;
-    private final long mLogIndex;
-
-    /**
-     * Constructor for LogEntry.
-     *
-     * @param timestamp the currentTimeMillis when the log happen.
-     * @param logLevel log level of the log.
-     * @param tag log's tag.
-     * @param message the log's actual message.
-     */
-    public LogEntry(long timestamp, LogLevel logLevel, String tag, String message) {
-        mTimestamp = timestamp;
-        mLogLevel = logLevel;
-        mTag = tag;
-        mMessage = message;
-        mLogIndex = sLogIndex.incrementAndGet();
-    }
-
-    public LogEntry(LogLevel logLevel, String tag, String message) {
-        this(System.currentTimeMillis(), logLevel, tag, message);
-    }
-
-    public long getTimestamp() {
-        return mTimestamp;
-    }
-
-    public LogLevel getLogLevel() {
-        return mLogLevel;
-    }
-
-    public String getTag() {
-        return mTag;
-    }
-
-    public String getMessage() {
-        return mMessage;
-    }
-
-    @Override
-    public int compareTo(LogEntry o) {
-        if (this.mLogIndex > o.mLogIndex) {
-            return 1;
-        }
-        if (this.mLogIndex == o.mLogIndex) {
-            return 0;
-        }
-        return -1;
-    }
-}
diff --git a/src/com/android/tradefed/log/LogRegistry.java b/src/com/android/tradefed/log/LogRegistry.java
index b6c04a1..2acb8eb 100644
--- a/src/com/android/tradefed/log/LogRegistry.java
+++ b/src/com/android/tradefed/log/LogRegistry.java
@@ -20,20 +20,15 @@
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.util.FileUtil;
 
-import com.google.common.annotations.VisibleForTesting;
-
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.Hashtable;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ConcurrentLinkedQueue;
 
 /**
  * A {@link ILogRegistry} implementation that multiplexes and manages different loggers,
@@ -48,12 +43,10 @@
     private static final String LOG_TAG = "LogRegistry";
     private static final String GLOBAL_LOG_PREFIX = "tradefed_global_log_";
     private static final String HISTORY_LOG_PREFIX = "tradefed_history_log_";
-    private static final long MAX_HISTORY_SIZE = 10000;
     private static LogRegistry mLogRegistry = null;
     private Map<ThreadGroup, ILeveledLogOutput> mLogTable = new Hashtable<>();
     private FileLogger mGlobalLogger;
     private HistoryLogger mHistoryLogger;
-    private ConcurrentLinkedQueue<LogEntry> mLogEntryHistory = new ConcurrentLinkedQueue<>();
 
     /**
      * Package-private constructor; callers should use {@link #getLogRegistry} to get an instance of
@@ -162,32 +155,6 @@
         return Thread.currentThread().getThreadGroup();
     }
 
-    private void addToLogEntryHistory(LogLevel logLevel, String tag, String message) {
-        mLogEntryHistory.add(new LogEntry(logLevel, tag, message));
-        while (mLogEntryHistory.size() > getMaxHistorySize()) {
-            mLogEntryHistory.poll();
-        }
-    }
-
-    @VisibleForTesting
-    long getMaxHistorySize() {
-        return MAX_HISTORY_SIZE;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public List<LogEntry> getLogEntriesAfter(LogEntry logEntry) {
-        List<LogEntry> logs = new ArrayList<LogEntry>(mLogEntryHistory);
-        if (logs.isEmpty() || logEntry == null) {
-            return logs;
-        }
-        int ind = logs.indexOf(logEntry);
-        if (ind < 0) {
-            return logs;
-        }
-        return logs.subList(ind + 1, logs.size());
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -198,7 +165,6 @@
         if (logLevel.getPriority() >= currentLogLevel.getPriority()) {
             log.printLog(logLevel, tag, message);
         }
-        addToLogEntryHistory(logLevel, tag, message);
     }
 
     /**
@@ -207,7 +173,6 @@
     @Override
     public void printAndPromptLog(LogLevel logLevel, String tag, String message) {
         getLogger().printAndPromptLog(logLevel, tag, message);
-        addToLogEntryHistory(logLevel, tag, message);
     }
 
     /**
diff --git a/src/com/android/tradefed/log/SimpleFileLogger.java b/src/com/android/tradefed/log/SimpleFileLogger.java
new file mode 100644
index 0000000..5322b43
--- /dev/null
+++ b/src/com/android/tradefed/log/SimpleFileLogger.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.log;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.config.OptionCopier;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.InputStreamSource;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/** A {@link ILeveledLogOutput} that directs log messages to stdout and to a single log file. */
+@OptionClass(alias = "simple-file")
+public class SimpleFileLogger extends BaseStreamLogger<FileOutputStream> {
+
+    @Option(name = "path", description = "File path to log to.", mandatory = true)
+    private File mFile;
+
+    @Override
+    public void init() throws IOException {
+        mFile.getParentFile().mkdirs();
+        mOutputStream = new FileOutputStream(mFile, true);
+    }
+
+    @Override
+    public InputStreamSource getLog() {
+        if (mFile != null) {
+            return new FileInputStreamSource(mFile);
+        }
+        return new ByteArrayInputStreamSource(new byte[0]);
+    }
+
+    @Override
+    public SimpleFileLogger clone() {
+        SimpleFileLogger logger = new SimpleFileLogger();
+        OptionCopier.copyOptionsNoThrow(this, logger);
+        return logger;
+    }
+}
diff --git a/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java b/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java
index 8ffafe8..f6e4100 100644
--- a/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java
+++ b/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java
@@ -44,6 +44,7 @@
     private static final String STATS_KEY_VAR = "var";
     private static final String STATS_KEY_STDEV = "stdev";
     private static final String STATS_KEY_MEDIAN = "median";
+    private static final String STATS_KEY_TOTAL = "total";
     // Separator for final upload
     private static final String STATS_KEY_SEPARATOR = "-";
 
@@ -186,6 +187,7 @@
         stats.put(STATS_KEY_VAR, variance);
         stats.put(STATS_KEY_STDEV, Math.sqrt(variance));
         stats.put(STATS_KEY_MEDIAN, median);
+        stats.put(STATS_KEY_TOTAL, sum);
         return stats;
     }
 }
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index cd32cbd..aa8a4c2 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.testtype.retry.MergeStrategy;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -64,6 +65,10 @@
     private TestRunResult mCurrentTestRunResult = new TestRunResult();
     /** True if the default initialized mCurrentTestRunResult has its original value. */
     private boolean mDefaultRun = true;
+    /** Track whether or not a test run is currently in progress */
+    private boolean mRunInProgress = false;
+
+    private Map<String, LogFile> mNonAssociatedLogFiles = new LinkedHashMap<>();
 
     // Tracks if mStatusCounts are accurate, or if they need to be recalculated
     private AtomicBoolean mIsCountDirty = new AtomicBoolean(true);
@@ -176,6 +181,12 @@
     /** {@inheritDoc} */
     @Override
     public void testRunStarted(String name, int numTests, int attemptNumber) {
+        testRunStarted(name, numTests, attemptNumber, System.currentTimeMillis());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void testRunStarted(String name, int numTests, int attemptNumber, long startTime) {
         setCountDirty();
         // Only testRunStarted can affect the expected count.
         mIsExpectedCountDirty.set(true);
@@ -205,7 +216,7 @@
             int size = results.size();
             for (int i = size; i < attemptNumber; i++) {
                 TestRunResult result = getNewRunResult();
-                result.testRunStarted(name, numTests);
+                result.testRunStarted(name, numTests, startTime);
                 String errorMessage =
                         String.format(
                                 "Run attempt %s of %s did not exists, but got attempt %s. This is a placeholder for the missing attempt.",
@@ -220,7 +231,8 @@
         }
         mCurrentTestRunResult = results.get(attemptNumber);
 
-        mCurrentTestRunResult.testRunStarted(name, numTests);
+        mCurrentTestRunResult.testRunStarted(name, numTests, startTime);
+        mRunInProgress = true;
     }
 
     /** {@inheritDoc} */
@@ -228,6 +240,7 @@
     public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
         setCountDirty();
         mCurrentTestRunResult.testRunEnded(elapsedTime, runMetrics);
+        mRunInProgress = false;
     }
 
     /** {@inheritDoc} */
@@ -292,7 +305,11 @@
     /** {@inheritDoc} */
     @Override
     public void logAssociation(String dataName, LogFile logFile) {
-        mCurrentTestRunResult.testLogSaved(dataName, logFile);
+        if (mRunInProgress) {
+            mCurrentTestRunResult.testLogSaved(dataName, logFile);
+        } else {
+            mNonAssociatedLogFiles.put(dataName, logFile);
+        }
     }
 
     /**
@@ -459,6 +476,23 @@
     }
 
     /**
+     * Gets all the results for a given attempt.
+     *
+     * @param attempt The attempt we want results for.
+     * @return All {@link TestRunResult} for a given attempt.
+     */
+    public List<TestRunResult> getTestRunForAttempts(int attempt) {
+        List<TestRunResult> allResultForAttempts = new ArrayList<>();
+        for (Entry<String, List<TestRunResult>> runInfo : mTestRunResultMap.entrySet()) {
+            if (attempt < runInfo.getValue().size()) {
+                TestRunResult attemptRes = runInfo.getValue().get(attempt);
+                allResultForAttempts.add(attemptRes);
+            }
+        }
+        return allResultForAttempts;
+    }
+
+    /**
      * Returns whether a given test run name has any results.
      *
      * @param testRunName The name given by {{@link #testRunStarted(String, int)}.
@@ -507,4 +541,18 @@
     public IInvocationContext getModuleContextForRunResult(String testRunName) {
         return mModuleContextMap.get(testRunName);
     }
+
+    /** Returns a copy of the map containing all the logged file not associated with a test run. */
+    public Map<String, LogFile> getNonAssociatedLogFiles() {
+        return new LinkedHashMap<>(mNonAssociatedLogFiles);
+    }
+
+    /**
+     * Allows to clear the results for a given run name. Should only be used in some cases like the
+     * aggregator of results.
+     */
+    protected final synchronized void clearResultsForName(String testRunName) {
+        setCountDirty();
+        mTestRunResultMap.remove(testRunName);
+    }
 }
diff --git a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
index 301c5eb..41776e9 100644
--- a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
+++ b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
@@ -18,6 +18,7 @@
 import com.android.ddmlib.Log;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.util.TimeUtil;
 
 import junit.framework.AssertionFailedError;
 import junit.framework.Test;
@@ -70,9 +71,9 @@
     /** {@inheritDoc} */
     @Override
     public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
-       // TODO: no run ended method on TestListener - would be good to propagate the elaspedTime
-       // info up
-       Log.i(LOG_TAG, String.format("run ended %d ms", elapsedTime));
+        // TODO: no run ended method on TestListener - would be good to propagate the elapsedTime
+        // info up
+        Log.i(LOG_TAG, String.format("Run ended in %s", TimeUtil.formatElapsedTime(elapsedTime)));
     }
 
     /**
@@ -90,7 +91,7 @@
     @Override
     public void testRunStarted(String runName, int testCount) {
         // TODO: no run started method on TestResult - would be good to propagate this up
-        Log.i(LOG_TAG, String.format("run %s started: %d tests", runName, testCount));
+        Log.i(LOG_TAG, String.format("Running %s: %d tests", runName, testCount));
     }
 
     /**
@@ -98,7 +99,9 @@
      */
     @Override
     public void testRunStopped(long elapsedTime) {
-        Log.i(LOG_TAG, String.format("run stopped: %d ms", elapsedTime));
+        Log.i(
+                LOG_TAG,
+                String.format("run stopped after %s", TimeUtil.formatElapsedTime(elapsedTime)));
     }
 
     /** {@inheritDoc} */
@@ -159,8 +162,7 @@
          */
         @Override
         public String toString() {
-            // TODO: use ':' or '#' as separator? The eternal debate rages on!
-            return String.format("%s:%s", mTestId.getClassName(), mTestId.getTestName());
+            return mTestId.toString();
         }
     }
 
diff --git a/src/com/android/tradefed/result/LogSaverResultForwarder.java b/src/com/android/tradefed/result/LogSaverResultForwarder.java
index 6e379a2..fc937e7 100644
--- a/src/com/android/tradefed/result/LogSaverResultForwarder.java
+++ b/src/com/android/tradefed/result/LogSaverResultForwarder.java
@@ -17,9 +17,12 @@
 package com.android.tradefed.result;
 
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.TestInvocation;
+import com.android.tradefed.log.LogRegistry;
 import com.android.tradefed.log.LogUtil.CLog;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.List;
 
 /** A {@link ResultForwarder} for saving logs with the global file saver. */
@@ -54,6 +57,7 @@
     @Override
     public void invocationEnded(long elapsedTime) {
         InvocationSummaryHelper.reportInvocationEnded(getListeners(), elapsedTime);
+        reportEndHostLog(mLogSaver);
         // Intentionally call invocationEnded for the log saver last.
         try {
             mLogSaver.invocationEnded(elapsedTime);
@@ -63,6 +67,16 @@
         }
     }
 
+    private void reportEndHostLog(ILogSaver saver) {
+        LogRegistry registry = (LogRegistry) LogRegistry.getLogRegistry();
+        try (InputStreamSource source = registry.getLogger().getLog();
+                InputStream stream = source.createInputStream()) {
+            saver.saveLogData(TestInvocation.TRADEFED_END_HOST_LOG, LogDataType.TEXT, stream);
+        } catch (IOException e) {
+            CLog.e(e);
+        }
+    }
+
     /**
      * {@inheritDoc}
      * <p/>
diff --git a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
index 18e8dde..4a0b738 100644
--- a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
+++ b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
@@ -38,6 +38,7 @@
 
     /** Special error message from the instrumentation when something goes wrong on device side. */
     public static final String ERROR_MESSAGE = "Process crashed.";
+    public static final String SYSTEM_CRASH_MESSAGE = "System has crashed.";
 
     public static final int MAX_NUMBER_CRASH = 3;
 
@@ -94,7 +95,8 @@
 
     /** Attempt to extract the crash from the logcat if the test was seen as started. */
     private String extractCrashAndAddToMessage(String errorMessage, Long startTime) {
-        if (errorMessage.contains(ERROR_MESSAGE) && startTime != null) {
+        if ((errorMessage.contains(ERROR_MESSAGE) || errorMessage.contains(SYSTEM_CRASH_MESSAGE))
+                && startTime != null) {
             mLogcatItem = extractLogcat(mDevice, startTime);
             errorMessage = addJavaCrashToString(mLogcatItem, errorMessage);
         }
diff --git a/src/com/android/tradefed/result/MergeStrategy.java b/src/com/android/tradefed/result/MergeStrategy.java
deleted file mode 100644
index e64c7fb..0000000
--- a/src/com/android/tradefed/result/MergeStrategy.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.result;
-
-/** Describes how the results should be aggregated when multiple attempts are present. */
-public enum MergeStrategy {
-    /** Merging should not be applied and will throw an exception. */
-    NO_MERGE,
-    /** If a single test case pass then we will consider the merged result passed. */
-    ONE_TESTCASE_PASS_IS_PASS,
-    /** If a single test run pass then we will consider the merged run result passed. */
-    ONE_TESTRUN_PASS_IS_PASS,
-    /** If a single run or test cases is a pass we will consider the merged results passed. */
-    ANY_PASS_IS_PASS,
-    /** If a single run or test cases is failed, status will be failed no matter what. */
-    ANY_FAIL_IS_FAIL,
-}
diff --git a/src/com/android/tradefed/result/ResultForwarder.java b/src/com/android/tradefed/result/ResultForwarder.java
index 5034663..fa64e72 100644
--- a/src/com/android/tradefed/result/ResultForwarder.java
+++ b/src/com/android/tradefed/result/ResultForwarder.java
@@ -178,6 +178,19 @@
         }
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void testRunStarted(String runName, int testCount, int attemptNumber, long startTime) {
+        for (ITestInvocationListener listener : mListeners) {
+            try {
+                listener.testRunStarted(runName, testCount, attemptNumber, startTime);
+            } catch (RuntimeException e) {
+                CLog.e("Exception while invoking %s#testRunStarted", listener.getClass().getName());
+                CLog.e(e);
+            }
+        }
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/result/SubprocessResultsReporter.java b/src/com/android/tradefed/result/SubprocessResultsReporter.java
index 713fea7..9f9075d 100644
--- a/src/com/android/tradefed/result/SubprocessResultsReporter.java
+++ b/src/com/android/tradefed/result/SubprocessResultsReporter.java
@@ -67,9 +67,11 @@
 
     private IBuildInfo mPrimaryBuildInfo = null;
     private Socket mReportSocket = null;
+    private Object mLock = new Object();
     private PrintWriter mPrintWriter = null;
 
     private boolean mPrintWarning = true;
+    private boolean mCancelled = false;
 
     /** {@inheritDoc} */
     @Override
@@ -138,8 +140,14 @@
     /** {@inheritDoc} */
     @Override
     public void testRunStarted(String runName, int testCount, int attemptNumber) {
+        testRunStarted(runName, testCount, attemptNumber, System.currentTimeMillis());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void testRunStarted(String runName, int testCount, int attemptNumber, long startTime) {
         TestRunStartedEventInfo info =
-                new TestRunStartedEventInfo(runName, testCount, attemptNumber);
+                new TestRunStartedEventInfo(runName, testCount, attemptNumber, startTime);
         printEvent(SubprocessTestResultsParser.StatusKeys.TEST_RUN_STARTED, info);
     }
 
@@ -205,8 +213,7 @@
     /** {@inheritDoc} */
     @Override
     public void logAssociation(String dataName, LogFile logFile) {
-        LogAssociationEventInfo info =
-                new LogAssociationEventInfo("subprocess-a-" + dataName, logFile);
+        LogAssociationEventInfo info = new LogAssociationEventInfo(dataName, logFile);
         printEvent(SubprocessTestResultsParser.StatusKeys.LOG_ASSOCIATION, info);
     }
 
@@ -221,6 +228,9 @@
         InvocationEndedEventInfo eventEnd =
                 new InvocationEndedEventInfo(mPrimaryBuildInfo.getBuildAttributes());
         printEvent(SubprocessTestResultsParser.StatusKeys.INVOCATION_ENDED, eventEnd);
+        // Upon invocation ended, trigger the end of the socket when the process finishes
+        SocketFinisher thread = new SocketFinisher();
+        Runtime.getRuntime().addShutdownHook(thread);
     }
 
     /**
@@ -276,16 +286,21 @@
         }
         if(mReportPort != null) {
             try {
-                if (mReportSocket == null) {
-                    mReportSocket = new Socket("localhost", mReportPort.intValue());
-                    mPrintWriter = new PrintWriter(mReportSocket.getOutputStream(), true);
+                if (mCancelled) {
+                    return;
                 }
-                if (!mReportSocket.isConnected()) {
-                    throw new RuntimeException("Reporter Socket is not connected");
+                synchronized (mLock) {
+                    if (mReportSocket == null) {
+                        mReportSocket = new Socket("localhost", mReportPort.intValue());
+                        mPrintWriter = new PrintWriter(mReportSocket.getOutputStream(), true);
+                    }
+                    if (!mReportSocket.isConnected()) {
+                        throw new RuntimeException("Reporter Socket is not connected");
+                    }
+                    String eventLog = String.format("%s %s\n", key, event.toString());
+                    mPrintWriter.print(eventLog);
+                    mPrintWriter.flush();
                 }
-                String eventLog = String.format("%s %s\n", key, event.toString());
-                mPrintWriter.print(eventLog);
-                mPrintWriter.flush();
             } catch (IOException e) {
                 throw new RuntimeException(e);
             }
@@ -302,12 +317,34 @@
     /** {@inheritDoc} */
     @Override
     public void close() {
-        StreamUtil.close(mReportSocket);
-        StreamUtil.close(mPrintWriter);
+        mCancelled = true;
+        synchronized (mLock) {
+            if (mPrintWriter != null) {
+                mPrintWriter.flush();
+            }
+            StreamUtil.close(mPrintWriter);
+            mPrintWriter = null;
+            StreamUtil.close(mReportSocket);
+            mReportSocket = null;
+        }
     }
 
     /** Sets whether or not we should output the test logged or not. */
     public void setOutputTestLog(boolean outputTestLog) {
         mOutputTestlog = outputTestLog;
     }
+
+    /** Threads that help terminating the socket. */
+    private class SocketFinisher extends Thread {
+
+        public SocketFinisher() {
+            super();
+            setName("SubprocessResultsReporter-socket-finisher");
+        }
+
+        @Override
+        public void run() {
+            close();
+        }
+    }
 }
diff --git a/src/com/android/tradefed/result/TestResult.java b/src/com/android/tradefed/result/TestResult.java
index 426db04..3d33df3 100644
--- a/src/com/android/tradefed/result/TestResult.java
+++ b/src/com/android/tradefed/result/TestResult.java
@@ -17,6 +17,7 @@
 
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.testtype.retry.MergeStrategy;
 
 import com.google.common.base.Joiner;
 
diff --git a/src/com/android/tradefed/result/TestRunResult.java b/src/com/android/tradefed/result/TestRunResult.java
index ebfcfb1..6be64fc 100644
--- a/src/com/android/tradefed/result/TestRunResult.java
+++ b/src/com/android/tradefed/result/TestRunResult.java
@@ -18,6 +18,7 @@
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.testtype.retry.MergeStrategy;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import com.google.common.base.Joiner;
@@ -51,7 +52,8 @@
     // Log files associated with the test run itself (testRunStart / testRunEnd).
     private Map<String, LogFile> mRunLoggedFiles;
     private boolean mIsRunComplete = false;
-    private long mElapsedTime = 0;
+    private long mElapsedTime = 0L;
+    private long mStartTime = 0L;
 
     private TestResult mCurrentTestResult;
 
@@ -163,6 +165,17 @@
         return mStatusCounts[status.ordinal()];
     }
 
+    /** Returns all the {@link TestResult} in a particular state. */
+    public List<TestResult> getTestsResultsInState(TestStatus status) {
+        List<TestResult> results = new ArrayList<>();
+        for (TestResult r : mTestResults.values()) {
+            if (r.getStatus().equals(status)) {
+                results.add(r);
+            }
+        }
+        return results;
+    }
+
     /** Gets the number of tests in this run. */
     public int getNumTests() {
         return mTestResults.size();
@@ -188,6 +201,11 @@
         return mElapsedTime;
     }
 
+    /** Returns the start time of the first testRunStart call. */
+    public long getStartTime() {
+        return mStartTime;
+    }
+
     /** Return the run failure error message, <code>null</code> if run did not fail. */
     public String getRunFailureMessage() {
         return mRunFailureError;
@@ -210,6 +228,16 @@
      * @param testCount the number of expected test cases associated with the test run.
      */
     public void testRunStarted(String runName, int testCount) {
+        testRunStarted(runName, testCount, System.currentTimeMillis());
+    }
+
+    /**
+     * Notify that a test run started.
+     *
+     * @param runName the name associated to the test run for tracking purpose.
+     * @param testCount the number of expected test cases associated with the test run.
+     */
+    public void testRunStarted(String runName, int testCount, long startTime) {
         // A run may be started multiple times due to crashes or other reasons. Normally the first
         // run reflect the expected number of test "testCount". To avoid latter TestRunStarted
         // overrides the expected count, only the first testCount will be recorded.
@@ -225,6 +253,9 @@
         }
         mTestRunName = runName;
         mIsRunComplete = false;
+        if (mStartTime == 0L) {
+            mStartTime = startTime;
+        }
         // Do not reset mRunFailureError since for re-run we want to preserve previous failures.
     }
 
diff --git a/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParser.java b/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParser.java
new file mode 100644
index 0000000..ad2f208
--- /dev/null
+++ b/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParser.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.result.ddmlib;
+
+import com.android.commands.am.InstrumentationData.ResultsBundle;
+import com.android.commands.am.InstrumentationData.ResultsBundleEntry;
+import com.android.commands.am.InstrumentationData.Session;
+import com.android.commands.am.InstrumentationData.SessionStatus;
+import com.android.commands.am.InstrumentationData.TestStatus;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.InstrumentationResultParser;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Parses the instrumentation result proto collected during instrumentation test run
+ * and informs ITestRunListener of the results.
+ */
+public class InstrumentationResultProtoParser implements IShellOutputReceiver {
+
+    /** Error message supplied when no test result file is found. */
+    public static final String NO_TEST_RESULTS_FILE = "No instrumentation proto test"
+            + " results file found";
+
+    /** Error message supplied when no test results are received from test run. */
+    public static final String NO_TEST_RESULTS_MSG = "No test results";
+
+    /** Error message supplied when no test result file is found. */
+    public static final String INVALID_TEST_RESULTS_FILE = "Invalid instrumentation proto"
+            + " test results file";
+
+    private static final String INSTRUMENTATION_STATUS_FORMAT = "INSTRUMENTATION_STATUS: %s=%s";
+    private static final String INSTRUMENTATION_STATUS_CODE_FORMAT =
+            "INSTRUMENTATION_STATUS_CODE: %d";
+    private static final String INSTRUMENTATION_RESULT_FORMAT = "INSTRUMENTATION_RESULT: %s=%s";
+    private static final String INSTRUMENTATION_CODE_FORMAT = "INSTRUMENTATION_CODE: %d";
+
+    private InstrumentationResultParser parser;
+
+    public InstrumentationResultProtoParser(String runName,
+            Collection<ITestRunListener> listeners) {
+        parser = new InstrumentationResultParser(runName, listeners);
+    }
+
+    /**
+     * Process the instrumentation result proto file collected during the instrumentation test run.
+     * Instrumentation proto file consist of test status and instrumentation session status. This
+     * method will be used only when complete instrumentation results proto file is available for
+     * parsing.
+     *
+     * @param protoFile that contains the test status and instrumentation session results.
+     * @throws IOException
+     */
+    public void processProtoFile(File protoFile) throws IOException {
+
+        // Report tes run failures in case of null and empty proto file.
+        if (protoFile == null) {
+            parser.handleTestRunFailed(NO_TEST_RESULTS_FILE);
+            return;
+        }
+        if (protoFile.length() == 0) {
+            parser.handleTestRunFailed(NO_TEST_RESULTS_MSG);
+            return;
+        }
+
+        // Read the input proto file
+        byte[] bytesArray = new byte[(int) protoFile.length()];
+        FileInputStream fis = new FileInputStream(protoFile);
+        fis.read(bytesArray);
+        fis.close();
+
+        try {
+            // Parse the proto file.
+            Session instrumentSession = Session.parseFrom(bytesArray);
+
+            // Process multiple test status.
+            List<TestStatus> multipleTestStatus = instrumentSession.getTestStatusList();
+            for (TestStatus teststatus : multipleTestStatus) {
+                processTestStatus(teststatus);
+            }
+
+            // Process instrumentation session status.
+            SessionStatus sessionStatus = instrumentSession.getSessionStatus();
+            if (sessionStatus.isInitialized()) {
+                processSessionStatus(sessionStatus);
+            }
+        } catch (InvalidProtocolBufferException ex) {
+            parser.handleTestRunFailed(INVALID_TEST_RESULTS_FILE);
+        }
+        parser.done();
+    }
+
+    /**
+     * Preprocess the single TestStatus proto message which includes the test info or test
+     * results and result code in to shell output format for further processing by
+     * InstrumentationResultParser.
+     *
+     * @param testStatus The {@link TestStatus} holding the current test info collected during the
+     *            test.
+     */
+    public void processTestStatus(TestStatus testStatus) {
+        // Process the test results.
+        ResultsBundle results = testStatus.getResults();
+        List<String> preProcessedLines = new LinkedList<>();
+        for (ResultsBundleEntry entry : results.getEntriesList()) {
+            String currentKey = entry.getKey();
+            String currentValue = null;
+            if (entry.hasValueString()) {
+                currentValue = entry.getValueString().trim();
+            } else if (entry.hasValueInt()) {
+                currentValue = String.valueOf(entry.getValueInt());
+            }
+            preProcessedLines.add(String.format(INSTRUMENTATION_STATUS_FORMAT, currentKey,
+                    currentValue));
+        }
+        preProcessedLines.add(String.format(INSTRUMENTATION_STATUS_CODE_FORMAT,
+                testStatus.getResultCode()));
+        parser.processNewLines(preProcessedLines.toArray(new String[preProcessedLines.size()]));
+    }
+
+    /**
+     * Preprocess the instrumentation session status which includes the instrumentation test
+     * results and the session status code to shell output format for further processing by
+     * InstrumentationResultParser.
+     *
+     * @param SessionStatus The {@link SessionStatus} holding the current instrumentation session
+     *            info collected during the test run.
+     */
+    public void processSessionStatus(SessionStatus sessionStatus) {
+
+        List<String> preProcessedLines = new LinkedList<>();
+        ResultsBundle results = sessionStatus.getResults();
+        for (ResultsBundleEntry entry : results.getEntriesList()) {
+            String currentKey = entry.getKey();
+            String currentValue = "";
+            if (entry.hasValueString()) {
+                currentValue = entry.getValueString();
+                String lines[] = currentValue.split("\\r?\\n");
+                int lineCount = 1;
+                for (String line : lines) {
+                    if (lineCount == 1) {
+                        // Only first line should have the Result code prefix.
+                        preProcessedLines.add(String.format(INSTRUMENTATION_RESULT_FORMAT,
+                                currentKey,
+                                line));
+                        lineCount++;
+                        continue;
+                    }
+                    preProcessedLines.add(line);
+                }
+            } else if (entry.hasValueInt()) {
+                currentValue = String.valueOf(entry.getValueInt());
+                preProcessedLines.add(String.format(INSTRUMENTATION_RESULT_FORMAT, currentKey,
+                        currentValue));
+            }
+        }
+        if (results.isInitialized()) {
+            preProcessedLines.add(String.format(INSTRUMENTATION_CODE_FORMAT,
+                    sessionStatus.getResultCode()));
+        }
+
+        parser.processNewLines(preProcessedLines.toArray(new String[preProcessedLines.size()]));
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.IShellOutputReceiver#addOutput(byte[], int, int)
+     */
+    @Override
+    public void addOutput(byte[] protoData, int bytes, int length) {
+        // TODO : Process the streaming proto instrumentation results.
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.IShellOutputReceiver#flush()
+     */
+    @Override
+    public void flush() {
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.ddmlib.IShellOutputReceiver#isCancelled()
+     */
+    @Override
+    public boolean isCancelled() {
+        return false;
+    }
+
+    /** Set to True to enforce searching for a final time stamp, and fail the run if missing. */
+    public void setEnforceTimeStamp(boolean isEnforced) {
+        parser.setEnforceTimeStamp(isEnforced);
+    }
+}
diff --git a/src/com/android/tradefed/result/proto/FileProtoResultReporter.java b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
index 3bb8f03..0d2fe41 100644
--- a/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
@@ -29,10 +29,9 @@
 
     @Option(
         name = "proto-output-file",
-        description = "File where the proto output will be saved",
-        mandatory = true
+        description = "File where the proto output will be saved. If unset, reporter will be inop."
     )
-    private File mOutputFile;
+    private File mOutputFile = null;
 
     @Option(
         name = "periodic-proto-writing",
@@ -44,7 +43,6 @@
 
     // Current index of the sequence of proto output
     private int mIndex = 0;
-    private File mCurrentOutputFile;
 
     @Override
     public void processStartInvocation(
@@ -74,9 +72,6 @@
     /** Sets the file where to output the result. */
     public void setFileOutput(File output) {
         mOutputFile = output;
-        if (mPeriodicWriting) {
-            mCurrentOutputFile = new File(mOutputFile.getAbsolutePath() + mIndex);
-        }
     }
 
     /** Enable writing each module individualy to a file. */
@@ -85,12 +80,15 @@
     }
 
     private void writeProto(TestRecord record) {
+        if (mOutputFile == null) {
+            return;
+        }
         File outputFile = mOutputFile;
         if (mPeriodicWriting) {
-            outputFile = mCurrentOutputFile;
+            outputFile = new File(mOutputFile.getAbsolutePath() + mIndex);
         }
-        try {
-            record.writeDelimitedTo(new FileOutputStream(outputFile));
+        try (FileOutputStream output = new FileOutputStream(outputFile)) {
+            record.writeDelimitedTo(output);
             if (mPeriodicWriting) {
                 nextOutputFile();
             }
@@ -102,6 +100,5 @@
 
     private void nextOutputFile() {
         mIndex++;
-        mCurrentOutputFile = new File(mOutputFile.getAbsolutePath() + mIndex);
     }
 }
diff --git a/src/com/android/tradefed/result/proto/ProtoResultParser.java b/src/com/android/tradefed/result/proto/ProtoResultParser.java
index 50e4340..c715a99 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultParser.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultParser.java
@@ -236,7 +236,9 @@
         // Get final context in case it changed.
         Any anyDescription = endInvocationProto.getDescription();
         if (!anyDescription.is(Context.class)) {
-            throw new RuntimeException("Expected Any description of type Context");
+            throw new RuntimeException(
+                    String.format(
+                            "Expected Any description of type Context, was %s", anyDescription));
         }
         try {
             IInvocationContext context =
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
index 227f518..f415b4c 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -170,7 +170,9 @@
 
         if (mInvocationFailure != null) {
             DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
-            debugBuilder.setErrorMessage(mInvocationFailure.getMessage());
+            if (mInvocationFailure.getMessage() != null) {
+                debugBuilder.setErrorMessage(mInvocationFailure.getMessage());
+            }
             debugBuilder.setTrace(StreamUtil.getStackTrace(mInvocationFailure));
             mInvocationRecordBuilder.setDebugInfo(debugBuilder);
         }
diff --git a/src/com/android/tradefed/result/retry/ISupportGranularResults.java b/src/com/android/tradefed/result/retry/ISupportGranularResults.java
new file mode 100644
index 0000000..7afe5eb
--- /dev/null
+++ b/src/com/android/tradefed/result/retry/ISupportGranularResults.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result.retry;
+
+import com.android.tradefed.result.ITestInvocationListener;
+
+/**
+ * Interface specifying whether a {@link ITestInvocationListener} supports receiving the granular
+ * results or not.
+ */
+public interface ISupportGranularResults {
+
+    /** Returns True if the reporter support granular results, false otherwise. */
+    public boolean supportGranularResults();
+}
diff --git a/src/com/android/tradefed/sandbox/SandboxConfigDump.java b/src/com/android/tradefed/sandbox/SandboxConfigDump.java
index a0d526c..0cff43f 100644
--- a/src/com/android/tradefed/sandbox/SandboxConfigDump.java
+++ b/src/com/android/tradefed/sandbox/SandboxConfigDump.java
@@ -121,7 +121,11 @@
             pw = new PrintWriter(resFile);
             if (DumpCmd.NON_VERSIONED_CONFIG.equals(cmd)) {
                 // Remove elements that are versioned.
-                config.dumpXml(pw, new ArrayList<>(VERSIONED_ELEMENTS));
+                config.dumpXml(
+                        pw,
+                        new ArrayList<>(VERSIONED_ELEMENTS),
+                        true, /* Don't print unchanged options */
+                        false);
             } else {
                 // FULL_XML in that case.
                 config.dumpXml(pw);
diff --git a/src/com/android/tradefed/sandbox/SandboxConfigUtil.java b/src/com/android/tradefed/sandbox/SandboxConfigUtil.java
index 34d3e30..2f67e82 100644
--- a/src/com/android/tradefed/sandbox/SandboxConfigUtil.java
+++ b/src/com/android/tradefed/sandbox/SandboxConfigUtil.java
@@ -86,13 +86,14 @@
             mCmdArgs.add(arg);
         }
         CommandResult result = runUtil.runTimedCmd(DUMP_TIMEOUT, mCmdArgs.toArray(new String[0]));
-        CLog.d("stdout: %s", result.getStdout());
-        if (result.getStderr() != null && !result.getStderr().isEmpty()) {
-            CLog.d("stderr: %s", result.getStderr());
-        }
         if (CommandStatus.SUCCESS.equals(result.getStatus())) {
             return destination;
         }
+
+        if (result.getStderr() != null && !result.getStderr().isEmpty()) {
+            CLog.d("stderr: %s", result.getStderr());
+        }
+
         FileUtil.deleteFile(destination);
         // Do not delete the global configuration file here in this case, it might still be used.
         String errorMessage = "Error when dumping the config.";
diff --git a/src/com/android/tradefed/sandbox/TradefedSandbox.java b/src/com/android/tradefed/sandbox/TradefedSandbox.java
index e1f6724..eafa82c 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandbox.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandbox.java
@@ -211,6 +211,7 @@
             return e;
         }
 
+        PrettyPrintDelimiter.printStageDelimiter("Sandbox Configuration Preparation");
         // Prepare the configuration
         Exception res = prepareConfiguration(context, config, listener);
         if (res != null) {
@@ -327,8 +328,14 @@
                                 .contains(
                                         String.format(
                                                 "Could not find configuration '%s'", args[0]))) {
+                    CLog.w(
+                            "Child version doesn't contains '%s'. Attempting to backfill missing parent configuration.",
+                            args[0]);
                     File parentConfig = handleChildMissingConfig(args);
                     if (parentConfig != null) {
+                        try (InputStreamSource source = new FileInputStreamSource(parentConfig)) {
+                            listener.testLog("sandbox-parent-config", LogDataType.XML, source);
+                        }
                         try {
                             mSerializedConfiguration =
                                     SandboxConfigUtil.dumpConfigForVersion(
@@ -429,8 +436,9 @@
             File tmpParentConfig =
                     FileUtil.createTempFile("parent-config", ".xml", mSandboxTmpFolder);
             PrintWriter pw = new PrintWriter(tmpParentConfig);
-            // Do not print deprecated options to avoid compatibility issues
-            parentConfig.dumpXml(pw, new ArrayList<>(), false);
+            // Do not print deprecated options to avoid compatibility issues, and do not print
+            // unchanged options.
+            parentConfig.dumpXml(pw, new ArrayList<>(), false, false);
             return tmpParentConfig;
         } catch (ConfigurationException | IOException e) {
             CLog.e("Parent doesn't understand the command either:");
diff --git a/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java b/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java
index 92950ee..87366e9 100644
--- a/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java
+++ b/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java
@@ -20,6 +20,9 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
+import com.android.tradefed.util.ProcessInfo;
+
+import java.util.Map;
 
 /**
  * Check if the pid of system_server has changed from before and after a module run. A new pid would
@@ -27,18 +30,17 @@
  */
 public class SystemServerStatusChecker implements ISystemStatusChecker {
 
-    private String mSystemServerPid = null;
+    private ProcessInfo mSystemServerProcess;
     private Long mModuleStartTime = null;
 
     /** {@inheritDoc} */
     @Override
     public StatusCheckerResult preExecutionCheck(ITestDevice device)
             throws DeviceNotAvailableException {
-        mSystemServerPid = null;
-        mSystemServerPid = device.executeShellCommand("pidof system_server");
+        mSystemServerProcess = device.getProcessByName("system_server");
         StatusCheckerResult result = new StatusCheckerResult(CheckStatus.SUCCESS);
-        if (mSystemServerPid == null) {
-            String message = "Failed to get system_server pid.";
+        if (mSystemServerProcess == null) {
+            String message = "No valid system_server process is found.";
             CLog.w(message);
             result.setStatus(CheckStatus.FAILED);
             result.setBugreportNeeded(true);
@@ -46,13 +48,6 @@
             mModuleStartTime = null;
             return result;
         }
-        mSystemServerPid = mSystemServerPid.trim();
-        if (!checkValidPid(mSystemServerPid)) {
-            CLog.w(
-                    "Invalid pid response found: '%s'. Skipping the system checker.",
-                    mSystemServerPid);
-            mSystemServerPid = null;
-        }
         mModuleStartTime = getCurrentTime();
         return result;
     }
@@ -61,34 +56,53 @@
     @Override
     public StatusCheckerResult postExecutionCheck(ITestDevice device)
             throws DeviceNotAvailableException {
-        if (mSystemServerPid == null) {
-            CLog.d("No valid known value of system_server pid, skipping system checker.");
+        if (mSystemServerProcess == null) {
+            CLog.d(
+                    "No valid system_server process was found in preExecutionCheck, "
+                            + "skipping system_server postExecutionCheck.");
             return new StatusCheckerResult(CheckStatus.SUCCESS);
         }
-        String tmpSystemServerPid = device.executeShellCommand("pidof system_server");
-        if (tmpSystemServerPid != null) {
-            tmpSystemServerPid = tmpSystemServerPid.trim();
+        String message = null;
+        ProcessInfo currSystemServerProcess = device.getProcessByName("system_server");
+        if (currSystemServerProcess == null) {
+            message = "system_server is down";
+            CLog.w(message);
+            StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
+            result.setBugreportNeeded(true);
+            result.setErrorMessage(message);
+            return result;
         }
-        if (mSystemServerPid.equals(tmpSystemServerPid)) {
+
+        if (currSystemServerProcess.getPid() == mSystemServerProcess.getPid()
+                && currSystemServerProcess.getStartTime() == mSystemServerProcess.getStartTime()) {
             return new StatusCheckerResult(CheckStatus.SUCCESS);
         }
-        String message =
-                String.format(
-                        "system_server has a different pid after the module run. from %s to %s",
-                        mSystemServerPid, tmpSystemServerPid);
+        //system_server restarted
+        Map<Long, String> bootHistory =
+                device.getBootHistorySince(mSystemServerProcess.getStartTime());
+        CLog.i("The device reboot with boot history: %s", bootHistory);
+        if (bootHistory.isEmpty()) {
+            message = "system_server restarted without device reboot";
+        } else {
+            message = "system_server restarted with device boot history: " + bootHistory.toString();
+            // Check if there is a TF triggered reboot with device.doReboot
+            long lastExpectedReboot = device.getLastExpectedRebootTimeMillis();
+            if (mModuleStartTime != null && lastExpectedReboot < mModuleStartTime) {
+                // The reboot is not triggered by Tradefed host.
+                CLog.w(
+                        "System_server restarted and Tradefed didn't trigger a reboot: "
+                                + "last expected reboot: %s, module start time: %s, "
+                                + "something went wrong.",
+                        lastExpectedReboot, mModuleStartTime);
+            } else {
+                // The reboot is triggered by Tradefed host
+                CLog.i("Tradefed triggered reboot detected");
+                return new StatusCheckerResult(CheckStatus.SUCCESS);
+            }
+        }
         CLog.w(message);
         StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
-        // TODO: evaluate if we still need to fail the checker if it was a TF reboot
-        long lastExpectedReboot = device.getLastExpectedRebootTimeMillis();
-        if (mModuleStartTime != null && lastExpectedReboot < mModuleStartTime) {
-            // In case no Tradefed reboot was triggered, we capture a bugreport to figure this out.
-            CLog.w(
-                    "System_server pid changed and Tradefed didn't trigger a reboot: "
-                            + "last expected reboot: %s, module start time: %s, "
-                            + "something went wrong.",
-                    lastExpectedReboot, mModuleStartTime);
-            result.setBugreportNeeded(true);
-        }
+        result.setBugreportNeeded(true);
         result.setErrorMessage(message);
         return result;
     }
@@ -99,17 +113,4 @@
         return System.currentTimeMillis();
     }
 
-    /** Validate that pid is an integer and not empty. */
-    private boolean checkValidPid(String output) {
-        if (output.isEmpty()) {
-            return false;
-        }
-        try {
-            Integer.parseInt(output);
-        } catch (NumberFormatException e) {
-            return false;
-        }
-
-        return true;
-    }
 }
diff --git a/src/com/android/tradefed/suite/checker/UserChecker.java b/src/com/android/tradefed/suite/checker/UserChecker.java
index aa84de1..e900191 100644
--- a/src/com/android/tradefed/suite/checker/UserChecker.java
+++ b/src/com/android/tradefed/suite/checker/UserChecker.java
@@ -15,18 +15,17 @@
  */
 package com.android.tradefed.suite.checker;
 
-import java.util.List;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collection;
+import java.util.Map;
 
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
-import com.android.tradefed.util.UserUtil;
-import com.android.tradefed.util.UserUtil.UserSwitchFailedException;
 
 /**
  * Checks if users have changed during the test.
@@ -40,33 +39,58 @@
         name = "user-type",
         description = "The type of user to switch to before each module run."
     )
-    private UserUtil.UserType mUserToSwitchTo = UserUtil.UserType.CURRENT;
+    private UserInfo.UserType mUserToSwitchTo = UserInfo.UserType.CURRENT;
 
-    public static final String DEFAULT_NAME = "TFauto";
+    @Option(
+        name = "user-cleanup",
+        description =
+                "If true, attempt to cleanup any changes made to users:"
+                        + "\n - switch to previous current-user"
+                        + "\n - remove any created users"
+                        + "\n\nThis does NOT:"
+                        + "\n - attempt to re-create a user that was deleted"
+                        + "\n - start/stop existing users if their running status changed"
+    )
+    private boolean mCleanup = false;
 
-    private DeviceUserState mPreExecutionUserState;
+    private UserInfo mPreCurrentUserInfo = null;
+    private Map<Integer, UserInfo> mPreUsersInfo = null;
+    private int mSwitchedToUserId = -1;
 
     /** {@inheritDoc} */
     @Override
     public StatusCheckerResult preExecutionCheck(ITestDevice device)
             throws DeviceNotAvailableException {
 
-        String userSwitchErrorMsg = null;
-        try {
-            switchToExistingOrCreateUserType(device);
-        } catch (UserSwitchFailedException err) {
-            userSwitchErrorMsg = err.toString();
+        mPreUsersInfo = device.getUserInfos();
+        mPreCurrentUserInfo = mPreUsersInfo.get(device.getCurrentUser());
+
+        if (mPreCurrentUserInfo.isUserType(mUserToSwitchTo, mPreCurrentUserInfo.userId())) {
+            CLog.i(
+                    "Current user %d is already user type %s, no action.",
+                    mPreCurrentUserInfo.userId(), mUserToSwitchTo.toString());
+            return new StatusCheckerResult(CheckStatus.SUCCESS);
         }
 
-        mPreExecutionUserState = new DeviceUserState(device);
-        CLog.d("preExecutionUsers=" + mPreExecutionUserState);
+        mSwitchedToUserId = findMatchingUser(mPreUsersInfo.values());
+        if (mSwitchedToUserId < 0) {
+            mSwitchedToUserId =
+                    device.createUser(
+                            /* name= */ "Tf" + mUserToSwitchTo.toString().toLowerCase(),
+                            /* guest= */ mUserToSwitchTo.isGuest(),
+                            /* ephemeral= */ false);
+            CLog.i(
+                    "No user of type %s found, created user %d",
+                    mUserToSwitchTo.toString(), mSwitchedToUserId);
+        }
 
-        if (userSwitchErrorMsg == null) {
-            return new StatusCheckerResult(CheckStatus.SUCCESS);
+        CLog.i(
+                "Current user is %d, switching to user %s of type %s",
+                mPreCurrentUserInfo.userId(), mSwitchedToUserId, mUserToSwitchTo);
+        if (!device.switchUser(mSwitchedToUserId)) {
+            return statusFail(String.format("Failed to switch to user %d", mSwitchedToUserId));
         } else {
-            StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
-            result.setErrorMessage(userSwitchErrorMsg);
-            return result;
+            return new StatusCheckerResult(CheckStatus.SUCCESS);
         }
     }
 
@@ -74,145 +98,79 @@
     @Override
     public StatusCheckerResult postExecutionCheck(ITestDevice device)
             throws DeviceNotAvailableException {
-        DeviceUserState postDeviceUserState = new DeviceUserState(device);
-        CLog.d("postExecutionUsers=" + postDeviceUserState);
+        Map<Integer, UserInfo> postUsersInfo = device.getUserInfos();
+        UserInfo postCurrentUserInfo = postUsersInfo.get(device.getCurrentUser());
 
         ArrayList<String> errors = new ArrayList<>();
 
-        for (Integer removedUser : mPreExecutionUserState.findRemovedUsers(postDeviceUserState)) {
-            errors.add(String.format("User %d no longer exists after test", removedUser));
+        if (mPreCurrentUserInfo.userId() != postCurrentUserInfo.userId()) {
+            if (postCurrentUserInfo.userId() != mSwitchedToUserId) {
+                errors.add(
+                        String.format(
+                                "User %d was the currentUser before, has changed to %d",
+                                mPreCurrentUserInfo.userId(), postCurrentUserInfo.userId()));
+            }
+            if (mCleanup) {
+                if (!device.switchUser(mPreCurrentUserInfo.userId())) {
+                    errors.add(
+                            String.format(
+                                    "Failed to switch back to previous current user %d."
+                                            + " Check if it was removed.",
+                                    mPreCurrentUserInfo.userId()));
+                }
+            }
         }
 
-        for (Integer addedUser : mPreExecutionUserState.findAddedUsers(postDeviceUserState)) {
-            errors.add(
-                    String.format(
-                            "User %d was created during the test and not deleted", addedUser));
+        for (UserInfo preUserInfo : mPreUsersInfo.values()) {
+            int preUserId = preUserInfo.userId();
+            if (!postUsersInfo.containsKey(preUserId)) {
+                errors.add(String.format("User %d no longer exists after test", preUserId));
+                continue;
+            }
+
+            UserInfo postUserInfo = postUsersInfo.get(preUserId);
+            if (preUserInfo.isRunning() != postUserInfo.isRunning()) {
+                CLog.w(
+                        "User %d running status changed from %b -> %b",
+                        preUserId, preUserInfo.isRunning(), postUserInfo.isRunning());
+            }
         }
 
-        if (mPreExecutionUserState.currentUserChanged(postDeviceUserState)) {
-            errors.add(
-                    String.format(
-                            "User %d was the currentUser before, has changed to %d",
-                            mPreExecutionUserState.getCurrentUser(),
-                            postDeviceUserState.getCurrentUser()));
-        }
-
-        for (int userId : mPreExecutionUserState.findStoppedUsers(postDeviceUserState)) {
-            CLog.w("User %d was running but is now stopped.", userId);
-        }
-
-        for (int userId : mPreExecutionUserState.findStartedUsers(postDeviceUserState)) {
-            CLog.w("User %d was stopped but is now running.", userId);
+        for (int postUserId : postUsersInfo.keySet()) {
+            if (!mPreUsersInfo.containsKey(postUserId)) {
+                if (mSwitchedToUserId != postUserId) {
+                    errors.add(
+                            String.format(
+                                    "User %d was created during test and not deleted", postUserId));
+                }
+                if (mCleanup) {
+                    if (!device.removeUser(postUserId)) {
+                        errors.add(String.format("Failed to remove new user %d", postUserId));
+                    }
+                }
+            }
         }
 
         if (errors.size() > 0) {
-            StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
-            result.setErrorMessage(String.join("\n", errors));
-            return result;
+            return statusFail(String.join("\n", errors));
         } else {
             return new StatusCheckerResult(CheckStatus.SUCCESS);
         }
     }
 
-    /**
-     * Switches to the mUserType, creating if necessary.
-     *
-     * <p>Returns null if success, the error string if there is an error.
-     */
-    private void switchToExistingOrCreateUserType(ITestDevice device)
-            throws DeviceNotAvailableException, UserSwitchFailedException {
-        try {
-            UserUtil.switchToUserType(device, mUserToSwitchTo);
-        } catch (UserUtil.SecondaryUserNotFoundException attemptCreate) {
-            CLog.d("No secondary users exist, creating one.");
-            int secondary = device.createUserNoThrow(DEFAULT_NAME);
-            if (secondary <= 0) {
-                throw new UserSwitchFailedException("Failed to create secondary user");
+    /** Return the userId of a matching user, or -1 if none match. */
+    private int findMatchingUser(Collection<UserInfo> usersInfo) {
+        for (UserInfo userInfo : mPreUsersInfo.values()) {
+            if (userInfo.isUserType(mUserToSwitchTo, mPreCurrentUserInfo.userId())) {
+                return userInfo.userId();
             }
-            UserUtil.switchToUserType(device, mUserToSwitchTo);
         }
+        return -1;
     }
 
-    /** Class for monitoring changes to the user state between pre/post check. */
-    static class DeviceUserState {
-        private final int mCurrentUser;
-        private final ArrayList<Integer> mUsers;
-        private final HashMap<Integer, Boolean> mUserRunningStates;
-
-        DeviceUserState(ITestDevice device) throws DeviceNotAvailableException {
-            mCurrentUser = device.getCurrentUser();
-            mUsers = device.listUsers();
-            mUserRunningStates = new HashMap<>(mUsers.size());
-            for (Integer userId : mUsers) {
-                mUserRunningStates.put(userId, device.isUserRunning(userId));
-            }
-        }
-
-        public int getCurrentUser() {
-            return mCurrentUser;
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder builder = new StringBuilder();
-            builder.append(String.format("currentUser=%d;", getCurrentUser()));
-            for (Integer userId : mUsers) {
-                String running = mUserRunningStates.get(userId) ? "running" : "stopped";
-                builder.append(String.format(" %d:%s", userId, running));
-            }
-            return builder.toString();
-        }
-
-        List<Integer> findRemovedUsers(DeviceUserState otherState) {
-            ArrayList<Integer> removedUsers = new ArrayList<>();
-            for (Integer userId : mUsers) {
-                if (!otherState.containsUser(userId)) {
-                    removedUsers.add(userId);
-                }
-            }
-            return removedUsers;
-        }
-
-        List<Integer> findAddedUsers(DeviceUserState otherState) {
-            ArrayList<Integer> addedUsers = new ArrayList<>();
-            for (Integer userId : otherState.mUsers) {
-                if (!this.containsUser(userId)) {
-                    addedUsers.add(userId);
-                }
-            }
-            return addedUsers;
-        }
-
-        boolean currentUserChanged(DeviceUserState otherState) {
-            return this.getCurrentUser() != otherState.getCurrentUser();
-        }
-
-        List<Integer> findStartedUsers(DeviceUserState otherState) {
-            ArrayList<Integer> startedUsers = new ArrayList<>();
-            for (Integer userId : mUsers) {
-                if (!this.isUserRunning(userId) && otherState.isUserRunning(userId)) {
-                    startedUsers.add(userId);
-                }
-            }
-            return startedUsers;
-        }
-
-        List<Integer> findStoppedUsers(DeviceUserState otherState) {
-            ArrayList<Integer> stoppedUsers = new ArrayList<>();
-            for (Integer userId : mUsers) {
-                if (this.isUserRunning(userId) && !otherState.isUserRunning(userId)) {
-                    stoppedUsers.add(userId);
-                }
-            }
-            return stoppedUsers;
-        }
-
-        private boolean containsUser(int userId) {
-            return mUserRunningStates.containsKey(userId);
-        }
-
-        private boolean isUserRunning(int userId) {
-            return mUserRunningStates.getOrDefault(userId, /* default= */ false);
-        }
+    private static StatusCheckerResult statusFail(String msg) {
+        StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
+        result.setErrorMessage(msg);
+        return result;
     }
 }
diff --git a/src/com/android/tradefed/targetprep/AppSetup.java b/src/com/android/tradefed/targetprep/AppSetup.java
index c1268a0..0bc25da 100644
--- a/src/com/android/tradefed/targetprep/AppSetup.java
+++ b/src/com/android/tradefed/targetprep/AppSetup.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.targetprep;
 
-import com.android.tradefed.build.IAppBuildInfo;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.VersionedFile;
 import com.android.tradefed.config.Option;
@@ -86,10 +85,10 @@
     @Override
     public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
             DeviceNotAvailableException, BuildError {
-        if (!(buildInfo instanceof IAppBuildInfo)) {
-            throw new IllegalArgumentException("Provided buildInfo is not a AppBuildInfo");
+        List<VersionedFile> apps = buildInfo.getAppPackageFiles();
+        if (apps.isEmpty()) {
+            return;
         }
-        IAppBuildInfo appBuild = (IAppBuildInfo)buildInfo;
         CLog.i("Performing setup on %s", device.getSerialNumber());
 
         // double check that device is clean, in case it has unexpected cruft on it
@@ -103,7 +102,7 @@
         }
 
         if (mInstall) {
-            for (VersionedFile apkFile : appBuild.getAppPackageFiles()) {
+            for (VersionedFile apkFile : apps) {
                 if (mCheckMinSdk) {
                     AaptParser aaptParser = doAaptParse(apkFile.getFile());
                     if (aaptParser == null) {
diff --git a/src/com/android/tradefed/targetprep/CreateUserPreparer.java b/src/com/android/tradefed/targetprep/CreateUserPreparer.java
index c379be4..aeb835d 100644
--- a/src/com/android/tradefed/targetprep/CreateUserPreparer.java
+++ b/src/com/android/tradefed/targetprep/CreateUserPreparer.java
@@ -43,11 +43,18 @@
         } catch (IllegalStateException e) {
             throw new TargetSetupError("Failed to create user.", e, device.getDeviceDescriptor());
         }
+        if (!device.startUser(mCreatedUserId, true)) {
+            throw new TargetSetupError(
+                    String.format("Failed to start to user '%s'", mCreatedUserId),
+                    device.getDeviceDescriptor());
+        }
         if (!device.switchUser(mCreatedUserId)) {
             throw new TargetSetupError(
                     String.format("Failed to switch to user '%s'", mCreatedUserId),
                     device.getDeviceDescriptor());
         }
+        device.waitForDeviceAvailable();
+        device.postBootSetup();
     }
 
     @Override
diff --git a/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java b/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java
new file mode 100644
index 0000000..e10ee76
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.BuildInfoUtil;
+
+/**
+ * A {@link ITargetPreparer} that replaces build info fields with attributes read from device
+ *
+ * <p>This is useful for testing devices with builds generated from an external source (e.g.
+ * external partner devices)
+ *
+ * @see {@link DeviceBuildInfoInjector}, {@link BootstrapBuildProvider}
+ */
+public class DeviceBuildInfoBootStrapper extends BaseTargetPreparer {
+
+    @Option(name = "override-device-build-id", description = "the device buid id to inject.")
+    private String mOverrideDeviceBuildId = null;
+
+    @Option(name = "override-device-build-alias", description = "the device buid alias to inject.")
+    private String mOverrideDeviceBuildAlias = null;
+
+    @Option(
+        name = "override-device-build-flavor",
+        description = "the device build flavor to inject."
+    )
+    private String mOverrideDeviceBuildFlavor = null;
+
+    @Option(
+        name = "override-device-build-branch",
+        description = "the device build branch to inject."
+    )
+    private String mOverrideDeviceBuildBranch = null;
+
+    @Override
+    public void setUp(ITestDevice device, IBuildInfo buildInfo)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        BuildInfoUtil.bootstrapDeviceBuildAttributes(
+                buildInfo,
+                device,
+                mOverrideDeviceBuildId,
+                mOverrideDeviceBuildFlavor,
+                mOverrideDeviceBuildBranch,
+                mOverrideDeviceBuildAlias);
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
index 41d41e3..24d72d1 100644
--- a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
@@ -32,6 +32,8 @@
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.concurrent.TimeUnit;
@@ -106,6 +108,12 @@
     )
     private Collection<String> mFastbootFlashOptions = new ArrayList<>();
 
+    @Option(
+            name = "flash-ramdisk",
+            description =
+                    "flashes ramdisk (boot partition) in addition " + "to regular system image")
+    private boolean mShouldFlashRamdisk = false;
+
     /**
      * Sets the device boot time
      * <p/>
@@ -176,6 +184,11 @@
         if (!(buildInfo instanceof IDeviceBuildInfo)) {
             throw new IllegalArgumentException("Provided buildInfo is not a IDeviceBuildInfo");
         }
+        IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) buildInfo;
+        if (mShouldFlashRamdisk && deviceBuild.getRamdiskFile() == null) {
+            throw new IllegalArgumentException(
+                    "ramdisk flashing enabled but no ramdisk file was found in build info");
+        }
         // don't allow interruptions during flashing operations.
         getRunUtil().allowInterrupt(false);
         IDeviceManager deviceManager = getDeviceManager();
@@ -183,7 +196,6 @@
         long flashingTime = -1;
         long start = -1;
         try {
-            IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo)buildInfo;
             checkDeviceProductType(device, deviceBuild);
             device.setRecoveryMode(RecoveryMode.ONLINE);
             IDeviceFlasher flasher = createFlasher(device);
@@ -200,6 +212,7 @@
                 flasher.setUserDataFlashOption(mUserDataFlashOption);
                 flasher.setForceSystemFlash(mForceSystemFlash);
                 flasher.setDataWipeSkipList(mDataWipeSkipList);
+                flasher.setShouldFlashRamdisk(mShouldFlashRamdisk);
                 if (flasher instanceof FastbootDeviceFlasher) {
                     ((FastbootDeviceFlasher) flasher).setFlashOptions(mFastbootFlashOptions);
                 }
@@ -447,4 +460,14 @@
             String serial, long queueTime, long flashingTime, CommandStatus flashingStatus) {
         // no-op as default implementation
     }
+
+    /**
+     * Sets the option for whether ramdisk should be flashed
+     *
+     * @param shouldFlashRamdisk
+     */
+    @VisibleForTesting
+    void setShouldFlashRamdisk(boolean shouldFlashRamdisk) {
+        mShouldFlashRamdisk = shouldFlashRamdisk;
+    }
 }
diff --git a/src/com/android/tradefed/targetprep/DeviceImageZipFlashingTargetPreparer.java b/src/com/android/tradefed/targetprep/DeviceImageZipFlashingTargetPreparer.java
new file mode 100644
index 0000000..d75558d
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/DeviceImageZipFlashingTargetPreparer.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.ZipUtil2;
+
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * A target preparer that flashes the device with device images provided via a specific format.
+ *
+ * <p>High level requirements for the device image format:
+ *
+ * <ul>
+ *   <li>Device image file must be a zip file
+ *   <li>The zip file must include a flash-all.sh script at the root
+ *   <li>The script must assume that the device is in userspace visible to <code>adb devices</code>
+ *   <li>The rest of the zip file will be extracted into the same location as script with the same
+ *       directory layout, and the script may make reference to any files packaged in the zip via
+ *       relative path
+ *   <li>After flashing, the script must return the device to the same state
+ *   <li>An environment variable <code>ANDROID_SERIAL</code> will be set to device serial number as
+ *       part of the execution environment
+ *   <li>The script may assume that it has <code>adb</code> and <code>fastboot</code> on PATH
+ * </ul>
+ *
+ * This target preparer will unpack the device image zip file and execute the enclosed <code>flash-
+ * all.sh</code> under the assumptions outline in requirements above.
+ */
+public class DeviceImageZipFlashingTargetPreparer extends DeviceUpdateTargetPreparer {
+
+    private static final String ANDROID_SERIAL_ENV = "ANDROID_SERIAL";
+
+    @Option(name = "device-image-zip", description = "the device image zip file to be flashed")
+    private File mDeviceImageZip = null;
+
+    @Option(
+        name = "flashing-timeout",
+        description = "timeout for flashing the device images",
+        isTimeVal = true
+    )
+    // defaults to 10m: assuming USB 2.0 transfer speed, concurrency and some buffer
+    private long mFlashingTimeout = 10 * 60 * 1000;
+
+    @Option(
+        name = "flashing-script",
+        description =
+                "the name of the flashing script bundled within " + "the device image zip file"
+    )
+    private String mFlashingScript = "flash-all.sh";
+
+    /** {@inheritDoc} */
+    @Override
+    protected File getDeviceUpdateImage() {
+        return mDeviceImageZip;
+    }
+
+    /** No-op */
+    @Override
+    protected void preUpdateActions(File deviceUpdateImage, ITestDevice device)
+            throws DeviceNotAvailableException, TargetSetupError {}
+
+    /** No-op */
+    @Override
+    protected void postUpdateActions(File deviceUpdateImage, ITestDevice device)
+            throws DeviceNotAvailableException, TargetSetupError {}
+
+    /** Expands the device image update zip and calls the enclosed flashing script */
+    @Override
+    protected void performDeviceUpdate(File deviceUpdateImage, ITestDevice device)
+            throws DeviceNotAvailableException, TargetSetupError {
+        // first unzip the package
+        File extractedImage = null;
+        try {
+            extractedImage = extractZip(device, getDeviceUpdateImage());
+            File flashingScript = new File(extractedImage, mFlashingScript);
+            if (!flashingScript.exists()) {
+                throw new TargetSetupError(
+                        String.format(
+                                "Flashing script \"%s\" not found inside " + "the device image zip",
+                                mFlashingScript),
+                        device.getDeviceDescriptor());
+            }
+            IRunUtil runUtil = new RunUtil();
+            runUtil.setEnvVariable(ANDROID_SERIAL_ENV, device.getSerialNumber());
+            runUtil.setWorkingDir(extractedImage);
+            CLog.i("Starting flashing on %s", device.getSerialNumber());
+            CommandResult result =
+                    runUtil.runTimedCmd(
+                            mFlashingTimeout, "bash", "-x", flashingScript.getAbsolutePath());
+            CommandStatus status = result.getStatus();
+            StringBuilder sb = new StringBuilder();
+            sb.append(
+                    String.format(
+                            "Flashing command finished with status: %s\n", status.toString()));
+            sb.append(String.format("Flashing command stdout:\n%s\n", result.getStdout()));
+            sb.append(String.format("Flashing command stderr:\n%s\n", result.getStderr()));
+            if (!CommandStatus.SUCCESS.equals(status)) {
+                CLog.w(sb.toString());
+            } else {
+                CLog.v(sb.toString());
+            }
+            String message =
+                    String.format(
+                            "Flashing script failed (status: %s), "
+                                    + "check host logs above for details",
+                            status.toString());
+            switch (status) {
+                case SUCCESS:
+                    break;
+                case FAILED:
+                    throw new TargetSetupError(message, device.getDeviceDescriptor());
+                case EXCEPTION:
+                    throw new TargetSetupError(message, device.getDeviceDescriptor());
+                case TIMED_OUT:
+                    throw new TargetSetupError(message, device.getDeviceDescriptor());
+                default:
+                    throw new IllegalStateException("Failsafe: not expected");
+            }
+        } finally {
+            FileUtil.recursiveDelete(extractedImage);
+        }
+    }
+
+    /**
+     * Extract a zip file and return temporary directory with contents.
+     *
+     * @param device the {@link ITestDevice}
+     * @param zip {@link File} to unzip
+     * @throws TargetSetupError if any operation fails
+     */
+    private static File extractZip(ITestDevice device, File zip) throws TargetSetupError {
+        ZipFile zFile = null;
+        File outputDir;
+        try {
+            zFile = new ZipFile(zip);
+            File fastbootTmpDir =
+                    GlobalConfiguration.getInstance().getHostOptions().getFastbootTmpDir();
+            outputDir =
+                    FileUtil.createTempDir(
+                            DeviceImageZipFlashingTargetPreparer.class.getSimpleName()
+                                    + "-tmp-files",
+                            fastbootTmpDir);
+            ZipUtil2.extractZip(zFile, outputDir);
+        } catch (IOException | IllegalStateException exception) {
+            throw new TargetSetupError(
+                    exception.getMessage(), exception, device.getDeviceDescriptor());
+        } finally {
+            ZipUtil2.closeZip(zFile);
+        }
+        return outputDir;
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java b/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java
new file mode 100644
index 0000000..9c81fb1
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceUnresponsiveException;
+import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.ITestDevice.RecoveryMode;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An abstract {@link ITargetPreparer} that takes care of common steps around updating devices with
+ * a device image file from an external source (as opposed to a build service). The actual update
+ * mechanism is delegated to implementor of subclasses.
+ */
+public abstract class DeviceUpdateTargetPreparer extends DeviceBuildInfoBootStrapper {
+
+    @Option(
+        name = "device-boot-time",
+        description = "max time to wait for device to boot.",
+        isTimeVal = true
+    )
+    private long mDeviceBootTime = 5 * 60 * 1000;
+
+    @Option(
+        name = "bootstrap-build-info",
+        description =
+                "whether build info should be"
+                        + "bootstrapped based on device attributes after flashing"
+    )
+    private boolean mBootStrapBuildInfo = true;
+
+    /** {@inheritDoc} */
+    @Override
+    public void setUp(ITestDevice device, IBuildInfo buildInfo)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        File deviceUpdateImage = getDeviceUpdateImage();
+        if (deviceUpdateImage == null) {
+            CLog.i("No device image zip file provided, assuming no-op; skipping ...");
+            return;
+        }
+        if (!deviceUpdateImage.exists()) {
+            throw new TargetSetupError(
+                    "Device image file not found: " + deviceUpdateImage.getAbsolutePath(),
+                    device.getDeviceDescriptor());
+        }
+        preUpdateActions(deviceUpdateImage, device);
+        // flashing concurrency control
+        long start = System.currentTimeMillis();
+        IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance();
+        deviceManager.takeFlashingPermit();
+        CLog.v(
+                "Flashing permit obtained after %ds",
+                TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
+        try {
+            performDeviceUpdate(deviceUpdateImage, device);
+        } finally {
+            CLog.v(
+                    "Flashing finished after %ds",
+                    TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
+            deviceManager.returnFlashingPermit();
+        }
+        postUpdateActions(deviceUpdateImage, device);
+        CLog.i(
+                "Flashing completed successfully on %s, waiting for device to boot up.",
+                device.getSerialNumber());
+        device.waitForDeviceOnline();
+        // device may lose date setting if wiped, update with host side date in case anything on
+        // device side malfunction with an invalid date
+        if (device.getOptions().isEnableAdbRoot()) {
+            boolean rootEnabled = device.enableAdbRoot();
+            if (rootEnabled) {
+                device.setDate(null);
+            }
+        }
+        try {
+            device.setRecoveryMode(RecoveryMode.AVAILABLE);
+            device.waitForDeviceAvailable(mDeviceBootTime);
+        } catch (DeviceUnresponsiveException e) {
+            // assume this is a build problem
+            throw new DeviceFailedToBootError(
+                    String.format(
+                            "Device %s did not become available after flashing %s",
+                            device.getSerialNumber(), deviceUpdateImage.getAbsolutePath()),
+                    device.getDeviceDescriptor());
+        }
+        CLog.i("Device update completed on %s", device.getDeviceDescriptor());
+        // calling this last because we want to inject device side build info after device boots up
+        if (mBootStrapBuildInfo) {
+            super.setUp(device, buildInfo);
+        }
+    }
+
+    /**
+     * Provides a {@link File} instance representing the device image file to be used for updating
+     */
+    protected abstract File getDeviceUpdateImage();
+
+    /**
+     * Actions to be performed before the device is updated. This method will be called outside of
+     * flashing concurrency control.
+     *
+     * @param deviceUpdateImage
+     * @param device
+     * @throws TargetSetupError
+     */
+    protected abstract void preUpdateActions(File deviceUpdateImage, ITestDevice device)
+            throws DeviceNotAvailableException, TargetSetupError;
+
+    /**
+     * Performs the device image update on device
+     *
+     * @param deviceUpdateImage
+     * @param device
+     * @throws TargetSetupError
+     */
+    protected abstract void performDeviceUpdate(File deviceUpdateImage, ITestDevice device)
+            throws DeviceNotAvailableException, TargetSetupError;
+
+    /**
+     * Actions to be performed after the device is updated but before post update setup steps are
+     * performed. This method will be called outside of flashing concurrency control.
+     *
+     * @param deviceUpdateImage
+     * @param device
+     * @throws TargetSetupError
+     */
+    protected abstract void postUpdateActions(File deviceUpdateImage, ITestDevice device)
+            throws DeviceNotAvailableException, TargetSetupError;
+}
diff --git a/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java b/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java
index 6d73d68..c1a6f8f 100644
--- a/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java
+++ b/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java
@@ -43,10 +43,8 @@
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
-/**
- * A class that relies on fastboot to flash an image on physical Android hardware.
- */
-public class FastbootDeviceFlasher implements IDeviceFlasher  {
+/** A class that relies on fastboot to flash an image on physical Android hardware. */
+public class FastbootDeviceFlasher implements IDeviceFlasher {
     public static final String BASEBAND_IMAGE_NAME = "radio";
 
     private static final String FASTBOOT_VERSION = "fastboot_version";
@@ -55,6 +53,7 @@
 
     private static final String SLOT_PROP = "ro.boot.slot_suffix";
     private static final String SLOT_VAR = "current-slot";
+    private static final String SKIP_REBOOT_PARAM = "--skip-reboot";
 
     private long mWipeTimeout = 4 * 60 * 1000;
 
@@ -74,6 +73,8 @@
 
     private CommandStatus mSystemFlashStatus;
 
+    private boolean mShouldFlashRamdisk = false;
+
     /**
      * {@inheritDoc}
      */
@@ -110,7 +111,7 @@
         // Lazily initialize the TestZipInstaller.
         if (mTestsZipInstaller == null) {
             if (mDataWipeSkipList == null) {
-                mDataWipeSkipList = new ArrayList<String> ();
+                mDataWipeSkipList = new ArrayList<String>();
             }
             if (mDataWipeSkipList.isEmpty()) {
                 // To maintain backwards compatibility. Keep media by default.
@@ -164,9 +165,14 @@
         checkAndFlashSystem(device, systemBuildId, systemBuildFlavor, deviceBuild);
     }
 
-    private String[] buildFastbootCommand(String action, String... args) {
+    private String[] buildFastbootCommand(String action, boolean skipReboot, String... args) {
         List<String> cmdArgs = new ArrayList<>();
         if ("flash".equals(action) || "update".equals(action)) {
+            if (skipReboot) {
+                // need to skip reboot if flashing root ramdisk, because this will be typically
+                // used together with flashing of user build, and
+                cmdArgs.add(SKIP_REBOOT_PARAM);
+            }
             cmdArgs.addAll(mFlashOptions);
         }
         cmdArgs.add(action);
@@ -216,7 +222,9 @@
             throws DeviceNotAvailableException, TargetSetupError {
         CLog.d("fastboot flash %s %s", partition, imgFile.getAbsolutePath());
         executeLongFastbootCmd(
-                device, buildFastbootCommand("flash", partition, imgFile.getAbsolutePath()));
+                device,
+                buildFastbootCommand(
+                        "flash", mShouldFlashRamdisk, partition, imgFile.getAbsolutePath()));
     }
 
     /**
@@ -257,8 +265,7 @@
      *
      * @param device the {@link ITestDevice} to download resources for
      * @param localBuild the {@link IDeviceBuildInfo} to populate. Assumes device image file is
-     * already set
-     *
+     *     already set
      * @throws DeviceNotAvailableException if device is not available
      * @throws TargetSetupError if failed to retrieve resources
      */
@@ -284,8 +291,10 @@
         // only set bootloader image if this build doesn't have one already
         // TODO: move this logic to the BuildProvider step
         if (bootloaderVersion != null && localBuild.getBootloaderImageFile() == null) {
-           localBuild.setBootloaderImageFile(getFlashingResourcesRetriever().retrieveFile(
-                   getBootloaderFilePrefix(device), bootloaderVersion), bootloaderVersion);
+            localBuild.setBootloaderImageFile(
+                    getFlashingResourcesRetriever()
+                            .retrieveFile(getBootloaderFilePrefix(device), bootloaderVersion),
+                    bootloaderVersion);
         }
         String basebandVersion = resourceParser.getRequiredBasebandVersion();
         // only set baseband image if this build doesn't have one already
@@ -298,18 +307,18 @@
 
     /**
      * Verify that the device's product type supports the build-to-be-flashed.
-     * <p/>
-     * The base implementation will verify that the deviceProductType is included in the
-     * {@link IFlashingResourcesParser#getRequiredBoards()} collection. Subclasses may override
-     * as desired.
+     *
+     * <p>The base implementation will verify that the deviceProductType is included in the {@link
+     * IFlashingResourcesParser#getRequiredBoards()} collection. Subclasses may override as desired.
      *
      * @param device the {@link ITestDevice} to be flashed
      * @param resourceParser the {@link IFlashingResourcesParser}
      * @param deviceProductType the <var>device</var>'s product type
      * @throws TargetSetupError if the build's required board info did not match the device
      */
-    protected void verifyRequiredBoards(ITestDevice device, IFlashingResourcesParser resourceParser,
-            String deviceProductType) throws TargetSetupError {
+    protected void verifyRequiredBoards(
+            ITestDevice device, IFlashingResourcesParser resourceParser, String deviceProductType)
+            throws TargetSetupError {
         if (!containsIgnoreCase(resourceParser.getRequiredBoards(), deviceProductType)) {
             throw new TargetSetupError(String.format("Device %s is %s. Expected %s",
                     device.getSerialNumber(), deviceProductType,
@@ -398,7 +407,10 @@
         executeFastbootCmd(
                 device,
                 buildFastbootCommand(
-                        "flash", getBootPartitionName(), bootloaderImageFile.getAbsolutePath()));
+                        "flash",
+                        mShouldFlashRamdisk,
+                        getBootPartitionName(),
+                        bootloaderImageFile.getAbsolutePath()));
         device.rebootIntoBootloader();
     }
 
@@ -426,8 +438,8 @@
     }
 
     /**
-     * If needed, flash the baseband image on device. Will only flash baseband if current version
-     * on device != required version
+     * If needed, flash the baseband image on device. Will only flash baseband if current version on
+     * device != required version
      *
      * @param device the {@link ITestDevice} to flash
      * @param deviceBuild the {@link IDeviceBuildInfo} that contains the baseband image to flash
@@ -514,7 +526,7 @@
             case FLASH_IMG_ZIP:
                 flashUserDataFromDeviceImageFile(device, deviceBuild);
                 break;
-            case FORCE_WIPE:  // intentional fallthrough
+            case FORCE_WIPE: // intentional fallthrough
             case WIPE:
                 CLog.i("Wiping userdata %s", device.getSerialNumber());
                 wipePartition(device, "userdata");
@@ -544,13 +556,15 @@
 
     /**
      * Extracts the userdata.img from device image file and flashes it onto device
+     *
      * @param device the {@link ITestDevice} to flash
      * @param deviceBuild the {@link IDeviceBuildInfo} that contains the files to flash
      * @throws DeviceNotAvailableException if device is not available
      * @throws TargetSetupError if failed to extract or flash user data
      */
-    protected void flashUserDataFromDeviceImageFile(ITestDevice device,
-            IDeviceBuildInfo deviceBuild) throws DeviceNotAvailableException, TargetSetupError {
+    protected void flashUserDataFromDeviceImageFile(
+            ITestDevice device, IDeviceBuildInfo deviceBuild)
+            throws DeviceNotAvailableException, TargetSetupError {
         File userdataImg = null;
         try {
             try (ZipFile zip = new ZipFile(deviceBuild.getDeviceImageFile())) {
@@ -599,16 +613,23 @@
             String systemBuildFlavor,
             IDeviceBuildInfo deviceBuild)
             throws DeviceNotAvailableException, TargetSetupError {
-       if (shouldFlashSystem(systemBuildId, systemBuildFlavor, deviceBuild)) {
+        if (shouldFlashSystem(systemBuildId, systemBuildFlavor, deviceBuild)) {
             CLog.i("Flashing system %s", deviceBuild.getDeviceBuildId());
             flashSystem(device, deviceBuild);
             return true;
-       }
-       CLog.i("System is already version %s and build flavor %s, skipping flashing",
-               systemBuildId, systemBuildFlavor);
-       // reboot
-       device.rebootUntilOnline();
-       return false;
+        }
+        CLog.i(
+                "System is already version %s and build flavor %s, skipping flashing",
+                systemBuildId, systemBuildFlavor);
+        if (mShouldFlashRamdisk) {
+            // even if we don't flash system, still flash ramdisk just in case: because the fact
+            // that the system had a different ramdisk won't be captured by a simple build check
+            flashRamdiskIfNeeded(device, deviceBuild);
+            CLog.i("Flashed ramdisk anyways per flasher settings.");
+        }
+        // reboot
+        device.rebootUntilOnline();
+        return false;
     }
 
     /**
@@ -655,7 +676,10 @@
             executeLongFastbootCmd(
                     device,
                     buildFastbootCommand(
-                            "update", deviceBuild.getDeviceImageFile().getAbsolutePath()));
+                            "update",
+                            mShouldFlashRamdisk,
+                            deviceBuild.getDeviceImageFile().getAbsolutePath()));
+            flashRamdiskIfNeeded(device, deviceBuild);
             // only transfer last fastboot command status over to system flash status after having
             // flashing the system partitions
             mSystemFlashStatus = mFbCmdStatus;
@@ -690,8 +714,9 @@
                 return matcher.group(1);
             } else {
                 attempts++;
-                CLog.w("Could not find version for '%s'. Output '%s', retrying.",
-                            imageName, queryOutput);
+                CLog.w(
+                        "Could not find version for '%s'. Output '%s', retrying.",
+                        imageName, queryOutput);
                 getRunUtil().sleep(RETRY_SLEEP * (attempts - 1)
                         + new Random(System.currentTimeMillis()).nextInt(RETRY_SLEEP));
                 continue;
@@ -740,9 +765,8 @@
      *
      * @param device the {@link ITestDevice} to execute command on
      * @param cmdArgs the arguments to provide to fastboot
-     * @return String the stderr output from command if non-empty. Otherwise returns the stdout
-     * Some fastboot commands are weird in that they dump output to stderr on success case
-     *
+     * @return String the stderr output from command if non-empty. Otherwise returns the stdout Some
+     *     fastboot commands are weird in that they dump output to stderr on success case
      * @throws DeviceNotAvailableException if device is not available
      * @throws TargetSetupError if fastboot command fails
      */
@@ -755,16 +779,15 @@
 
     /**
      * Helper method to execute a long-running fastboot command.
-     * <p/>
-     * Note: Most fastboot commands normally execute within the timeout allowed by
-     * {@link ITestDevice#executeFastbootCommand(String...)}. However, when multiple devices are
-     * flashing devices at once, fastboot commands can take much longer than normal.
+     *
+     * <p>Note: Most fastboot commands normally execute within the timeout allowed by {@link
+     * ITestDevice#executeFastbootCommand(String...)}. However, when multiple devices are flashing
+     * devices at once, fastboot commands can take much longer than normal.
      *
      * @param device the {@link ITestDevice} to execute command on
      * @param cmdArgs the arguments to provide to fastboot
-     * @return String the stderr output from command if non-empty. Otherwise returns the stdout
-     * Some fastboot commands are weird in that they dump output to stderr on success case
-     *
+     * @return String the stderr output from command if non-empty. Otherwise returns the stdout Some
+     *     fastboot commands are weird in that they dump output to stderr on success case
      * @throws DeviceNotAvailableException if device is not available
      * @throws TargetSetupError if fastboot command fails
      */
@@ -827,9 +850,9 @@
     @Override
     public void setDataWipeSkipList(Collection<String> dataWipeSkipList) {
         if (dataWipeSkipList == null) {
-            dataWipeSkipList = new ArrayList<String> ();
+            dataWipeSkipList = new ArrayList<String>();
         }
-        if(dataWipeSkipList.isEmpty()) {
+        if (dataWipeSkipList.isEmpty()) {
             // To maintain backwards compatibility.
             // TODO: deprecate and remove.
             dataWipeSkipList.add("media");
@@ -852,4 +875,25 @@
     public CommandStatus getSystemFlashingStatus() {
         return mSystemFlashStatus;
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setShouldFlashRamdisk(boolean shouldFlashRamdisk) {
+        mShouldFlashRamdisk = shouldFlashRamdisk;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean shouldFlashRamdisk() {
+        return mShouldFlashRamdisk;
+    }
+
+    protected void flashRamdiskIfNeeded(ITestDevice device, IDeviceBuildInfo deviceBuild)
+            throws TargetSetupError, DeviceNotAvailableException {
+        if (mShouldFlashRamdisk) {
+            executeLongFastbootCmd(
+                    device, "flash", "boot", deviceBuild.getRamdiskFile().getAbsolutePath());
+            device.reboot();
+        }
+    }
 }
diff --git a/src/com/android/tradefed/targetprep/FlashingResourcesParser.java b/src/com/android/tradefed/targetprep/FlashingResourcesParser.java
index 318cf06..b206bc0 100644
--- a/src/com/android/tradefed/targetprep/FlashingResourcesParser.java
+++ b/src/com/android/tradefed/targetprep/FlashingResourcesParser.java
@@ -18,6 +18,8 @@
 
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.util.MultiMap;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -29,9 +31,7 @@
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
-import java.util.zip.ZipFile;
 
 /**
  * A class that parses out required versions of auxiliary image files needed to flash a device.
@@ -256,11 +256,19 @@
      */
     static AndroidInfo getBuildRequirements(File deviceImgZipFile,
             Map<String, Constraint> constraints) throws TargetSetupError {
+        if (!deviceImgZipFile.exists()) {
+            throw new TargetSetupError(
+                    String.format(
+                            "Device image zip %s doesn't not exist", deviceImgZipFile.getName()),
+                    null,
+                    null);
+        }
+
         ZipFile deviceZip = null;
         BufferedReader infoReader = null;
         try {
             deviceZip = new ZipFile(deviceImgZipFile);
-            ZipEntry androidInfoEntry = deviceZip.getEntry(ANDROID_INFO_FILE_NAME);
+            ZipArchiveEntry androidInfoEntry = deviceZip.getEntry(ANDROID_INFO_FILE_NAME);
             if (androidInfoEntry == null) {
                 DeviceDescriptor nullDescriptor = null;
                 throw new TargetSetupError(String.format("Could not find %s in device image zip %s",
diff --git a/src/com/android/tradefed/targetprep/IDeviceFlasher.java b/src/com/android/tradefed/targetprep/IDeviceFlasher.java
index c79a09b..5db33d9 100644
--- a/src/com/android/tradefed/targetprep/IDeviceFlasher.java
+++ b/src/com/android/tradefed/targetprep/IDeviceFlasher.java
@@ -119,4 +119,20 @@
      */
     public CommandStatus getSystemFlashingStatus();
 
+    /**
+     * Sets if an additional ramdisk should be flashed after updating device via image zip
+     *
+     * @param shouldFlashRamdisk
+     */
+    public default void setShouldFlashRamdisk(boolean shouldFlashRamdisk) {
+        // Ignore
+    }
+
+    /**
+     * Checks if the flasher is set to have an additional ramdisk should be flashed after updating
+     * device via image zip
+     */
+    public default boolean shouldFlashRamdisk() {
+        return false;
+    }
 }
diff --git a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
index e65f9bd..9be70dd 100644
--- a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
@@ -23,10 +23,12 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.ITestDevice.ApexInfo;
+import com.android.tradefed.device.PackageInfo;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
 import com.android.tradefed.util.AaptParser;
 import com.android.tradefed.util.BundletoolUtil;
+import com.android.tradefed.util.RunUtil;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -54,24 +56,26 @@
     private static final String SPLIT_APKS_SUFFIX = ".apks";
     private static final String TRAIN_WITH_APEX_INSTALL_OPTION = "install-multi-package";
 
-    private List<ApexInfo> mTestApexInfoList;
-    private Set<String> mApkToInstall;
-    private List<String> mApkInstalled;
-    private List<String> mSplitsInstallArgs;
+    private List<ApexInfo> mTestApexInfoList = new ArrayList<>();
+    private Set<String> mApkToInstall = new LinkedHashSet<>();
+    private List<String> mApkInstalled = new ArrayList<>();
+    private List<String> mSplitsInstallArgs = new ArrayList<>();
     private BundletoolUtil mBundletoolUtil;
 
     @Option(name = "bundletool-file-name", description = "The file name of the bundletool jar.")
     private String mBundletoolFilename;
 
+    @Option(
+        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;
+
     @Override
     public void setUp(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, DeviceNotAvailableException {
 
-        mApkInstalled = new ArrayList<>();
-        mApkToInstall = new LinkedHashSet<>();
-        mTestApexInfoList = new ArrayList<>();
-        mSplitsInstallArgs = new ArrayList<>();
-
         if (getTestsFileName().isEmpty()) {
             CLog.i("No apk/apex module file to install. Skipping.");
             return;
@@ -81,31 +85,28 @@
 
         List<String> testAppFileNames = getTestsFileName();
         if (containsApks(testAppFileNames)) {
-            try {
-                installUsingBundleTool(buildInfo, device);
-                if (mTestApexInfoList.isEmpty()) {
-                    CLog.i("No Apex module in the train. Skipping reboot.");
-                    return;
-                } else {
-                    device.reboot();
-                }
-            } catch (IOException e) {
-                throw new TargetSetupError(
-                        "Failed to create tmp spec file for device.", device.getDeviceDescriptor());
-            }
-        } else {
-            // Only contain .apk module.
-            if (!containsApex(testAppFileNames)) {
-                super.installer(device, buildInfo, testAppFileNames);
+            installUsingBundleTool(buildInfo, device);
+            if (mTestApexInfoList.isEmpty()) {
+                CLog.i("No Apex module in the train. Skipping reboot.");
                 return;
             } else {
-                // Any kind of combination of apex/apk.
-                installer(device, buildInfo, testAppFileNames);
+                RunUtil.getDefault().sleep(mApexStagingWaitTime);
                 device.reboot();
             }
+        } else {
+            installer(device, buildInfo, testAppFileNames);
+            if (containsApex(testAppFileNames)
+                    || containsPersistentApk(testAppFileNames, device, buildInfo)) {
+                device.reboot();
+            }
+            if (mTestApexInfoList.isEmpty()) {
+                CLog.i("Train activation succeed.");
+                return;
+            }
         }
 
         Set<ApexInfo> activatedApexes = device.getActiveApexes();
+
         if (activatedApexes.isEmpty()) {
             throw new TargetSetupError(
                     String.format(
@@ -133,7 +134,7 @@
                             listApexInfo(failToActivateApex).toString(), device.getSerialNumber()),
                     device.getDeviceDescriptor());
         }
-        CLog.i("Installation succeed.");
+        CLog.i("Train activation succeed.");
     }
 
     @Override
@@ -158,21 +159,19 @@
 
     // TODO(b/124461631): Remove after ddmlib supports install-multi-package.
     @Override
-    protected void installer(ITestDevice device, IBuildInfo buildInfo, List<String> appNames)
+    protected void installer(
+            ITestDevice device, IBuildInfo buildInfo, List<String> testAppFileNames)
             throws TargetSetupError, DeviceNotAvailableException {
-        for (String appFilename : getTestsFileName()) {
-            File appFile = getLocalPathForFilename(buildInfo, appFilename, device);
-            if (isApex(appFile)) {
-                ApexInfo apexInfo = retrieveApexInfo(appFile, device.getDeviceDescriptor());
-                mTestApexInfoList.add(apexInfo);
-            }
+        if (containsApex(testAppFileNames)) {
+            mTestApexInfoList = collectApexInfoFromApexModules(testAppFileNames, device, buildInfo);
         }
-        if (appNames.size() > 1) {
-            installMultiPackageContainingApex(device, buildInfo, appNames);
-        } else {
-            // Single apex file install.
-            super.installer(device, buildInfo, appNames);
+        if (containsPersistentApk(testAppFileNames, device, buildInfo)) {
+            // When there is a persistent apk in the train, use '--staged' to install full train
+            // Otherwise, do normal install without '--staged'
+            installTrain(device, buildInfo, testAppFileNames, new String[] {"--staged"});
+            return;
         }
+        installTrain(device, buildInfo, testAppFileNames, null);
     }
 
     /**
@@ -183,14 +182,22 @@
      * @param moduleFilenames List of String. The list of filenames of the mainline modules to be
      *     installed.
      */
-    protected void installMultiPackageContainingApex(
-            ITestDevice device, IBuildInfo buildInfo, Collection<String> moduleFilenames)
+    protected void installTrain(
+            ITestDevice device,
+            IBuildInfo buildInfo,
+            Collection<String> moduleFilenames,
+            final String[] extraArgs)
             throws TargetSetupError, DeviceNotAvailableException {
 
         List<String> apkPackageNames = new ArrayList<>();
         List<String> trainInstallCmd = new ArrayList<>();
 
         trainInstallCmd.add(TRAIN_WITH_APEX_INSTALL_OPTION);
+        if (extraArgs != null) {
+            for (String arg : extraArgs) {
+                trainInstallCmd.add(arg);
+            }
+        }
 
         for (String fileName : moduleFilenames) {
             File moduleFile = getLocalPathForFilename(buildInfo, fileName, device);
@@ -206,6 +213,11 @@
             }
         }
         String log = device.executeAdbCommand(trainInstallCmd.toArray(new String[0]));
+
+        // Wait until all apexes are fully staged and ready.
+        // TODO: should have adb level solution b/130039562
+        RunUtil.getDefault().sleep(mApexStagingWaitTime);
+
         if (log.contains("Success")) {
             CLog.d(
                     "Train is staged successfully. Cmd: %s, Output: %s.",
@@ -227,7 +239,7 @@
      * @param buildInfo build artifact information
      */
     protected void installUsingBundleTool(IBuildInfo buildInfo, ITestDevice device)
-            throws TargetSetupError, DeviceNotAvailableException, IOException {
+            throws TargetSetupError, DeviceNotAvailableException {
         File bundletoolJar = getLocalPathForFilename(buildInfo, getBundletoolFileName(), device);
         if (bundletoolJar == null) {
             throw new TargetSetupError(
@@ -236,7 +248,17 @@
                     device.getDeviceDescriptor());
         }
         mBundletoolUtil = new BundletoolUtil(bundletoolJar);
-        String deviceSpecFilePath = getBundletoolUtil().generateDeviceSpecFile(device);
+        String deviceSpecFilePath = "";
+        try {
+            deviceSpecFilePath = getBundletoolUtil().generateDeviceSpecFile(device);
+        } catch (IOException e) {
+            throw new TargetSetupError(
+                    String.format(
+                            " Failed to generate device spec file on %s.",
+                            device.getSerialNumber()),
+                    e,
+                    device.getDeviceDescriptor());
+        }
 
         if (getTestsFileName().size() == 1) {
             // Installs single .apks module.
@@ -464,6 +486,64 @@
         return splitsArgs;
     }
 
+    /**
+     * Checks if the input files contain any persistent apk.
+     *
+     * @param testAppFileNames The list of the file names of the modules to install
+     * @param device The test device
+     * @param buildInfo build artifact information
+     * @return <code>true</code> if the input files contains a persistent apk module.
+     */
+    protected boolean containsPersistentApk(
+            List<String> testAppFileNames, ITestDevice device, IBuildInfo buildInfo)
+            throws TargetSetupError, DeviceNotAvailableException {
+        for (String moduleFileName : testAppFileNames) {
+            if (moduleFileName.endsWith(APK_SUFFIX) &&
+                isPersistentApk(moduleFileName, device, buildInfo)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks if an apk is a persistent apk.
+     *
+     * @param filename The apk module file to check
+     * @param device The test device
+     * @param buildInfo build artifact information
+     * @return <code>true</code> if this is a persistent apk module.
+     */
+    protected boolean isPersistentApk(String filename, ITestDevice device, IBuildInfo buildInfo)
+            throws TargetSetupError, DeviceNotAvailableException {
+        File moduleFile = getLocalPathForFilename(buildInfo, filename, device);
+        PackageInfo pkgInfo =
+            device.getAppPackageInfo(parsePackageName(moduleFile, device.getDeviceDescriptor()));
+        return pkgInfo.isPersistentApp();
+    }
+
+    /**
+     * Collects apex info from the apex modules for activation check.
+     *
+     * @param testAppFileNames The list of the file names of the modules to install
+     * @param device The test device
+     * @param buildInfo build artifact information
+     * @return a list containing the apexinfo of the apex modules in the input file lists
+     */
+    protected List<ApexInfo> collectApexInfoFromApexModules(
+            List<String> testAppFileNames, ITestDevice device, IBuildInfo buildInfo)
+            throws TargetSetupError {
+        List<ApexInfo> apexInfoList = new ArrayList<>();
+        for (String appFilename : getTestsFileName()) {
+            File appFile = getLocalPathForFilename(buildInfo, appFilename, device);
+            if (isApex(appFile)) {
+                ApexInfo apexInfo = retrieveApexInfo(appFile, device.getDeviceDescriptor());
+                apexInfoList.add(apexInfo);
+            }
+        }
+        return apexInfoList;
+    }
+
     @VisibleForTesting
     protected String getBundletoolFileName() {
         return mBundletoolFilename;
diff --git a/src/com/android/tradefed/targetprep/KeyValueConfigPreparer.java b/src/com/android/tradefed/targetprep/KeyValueConfigPreparer.java
deleted file mode 100644
index 115b257..0000000
--- a/src/com/android/tradefed/targetprep/KeyValueConfigPreparer.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.targetprep;
-
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.ddmlib.IDevice;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-
-/**
- * A {@link ITargetPreparer} which creates and pushes a simple key/value config file to the device.
- */
-@OptionClass(alias = "key-value-config")
-public class KeyValueConfigPreparer extends BaseTargetPreparer {
-
-    @Option(name = "path", description = "The path of the config file on the device",
-            mandatory = true)
-    private String mPath = null;
-
-    @Option(name = "config", description = "The key/value pairs of the config")
-    private Map<String, String> mKeys = new HashMap<String, String>();
-
-    @Option(name = "separator", description = "The separator used between key and value")
-    private String mSep = "=";
-
-    @Option(name = "interpolate", description = "Interpolate path variable")
-    private boolean mInterpolate = false;
-
-    /**
-     * {@inheritDoc}
-     * @throws TargetSetupError
-     */
-    @Override
-    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws DeviceNotAvailableException,
-            TargetSetupError {
-        if (mPath == null) {
-            throw new TargetSetupError("Option path must be specified",
-                    device.getDeviceDescriptor());
-        }
-
-        StringBuilder config = new StringBuilder();
-
-        for (Entry<String, String> entry : mKeys.entrySet()) {
-            config.append(String.format("%s%s%s\n", entry.getKey(), mSep, entry.getValue()));
-        }
-
-        String content = config.toString();
-
-        if (mInterpolate) {
-            final String externalStorageString = "${EXTERNAL_STORAGE}";
-            if (content.contains(externalStorageString)) {
-                final String externalStoragePath =
-                        device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
-                content = content.replace(externalStorageString, externalStoragePath);
-            }
-        }
-
-        device.pushString(content, mPath);
-    }
-}
diff --git a/src/com/android/tradefed/targetprep/PushFilePreparer.java b/src/com/android/tradefed/targetprep/PushFilePreparer.java
index dd76cc4..e7ed4da 100644
--- a/src/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/src/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -41,6 +41,7 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -62,10 +63,11 @@
 
     private IAbi mAbi;
 
+    @Deprecated
     @Option(
         name = "push",
         description =
-                "A push-spec, formatted as "
+                "Deprecated. Please use push-file instead. A push-spec, formatted as "
                         + "'/localpath/to/srcfile.txt->/devicepath/to/destfile.txt' "
                         + "or '/localpath/to/srcfile.txt->/devicepath/to/destdir/'. "
                         + "May be repeated. The local path may be relative to the test cases "
@@ -78,9 +80,10 @@
         name = "push-file",
         description =
                 "A push-spec, specifying the local file to the path where it should be pushed on "
-                        + "device. May be repeated."
+                        + "device. May be repeated. If multiple files are configured to be pushed "
+                        + "to the same remote path, the latest one will be pushed."
     )
-    private Map<File, String> mPushFileSpecs = new HashMap<>();
+    private Map<File, String> mPushFileSpecs = new LinkedHashMap<>();
 
     @Option(name="post-push", description=
             "A command to run on the device (with `adb shell (yourcommand)`) after all pushes " +
@@ -202,7 +205,28 @@
                     }
                 }
             }
-
+            // Search top-level matches
+            for (File searchDir : scanDirs) {
+                try {
+                    Set<File> allMatch = FileUtil.findFilesObject(searchDir, fileName);
+                    if (allMatch.size() > 1) {
+                        CLog.d(
+                                "Several match for filename '%s', searching for top-level match.",
+                                fileName);
+                        for (File f : allMatch) {
+                            // Bias toward direct child / top level nodes
+                            if (f.getParent().equals(searchDir.getAbsolutePath())) {
+                                return f;
+                            }
+                        }
+                    } else if (allMatch.size() == 1) {
+                        return allMatch.iterator().next();
+                    }
+                } catch (IOException e) {
+                    CLog.w("Failed to find test files from directory.");
+                }
+            }
+            // Fall-back to searching everything
             try {
                 // Search the full tests dir if no target dir is available.
                 src = FileUtil.findFile(fileName, null, scanDirs.toArray(new File[] {}));
@@ -224,26 +248,29 @@
         if (mRemount) {
             device.remountSystemWritable();
         }
+
+        Map<String, File> remoteToLocalMapping = new HashMap<>();
         for (String pushspec : mPushSpecs) {
             String[] pair = pushspec.split("->");
             if (pair.length != 2) {
                 fail(String.format("Invalid pushspec: '%s'", Arrays.asList(pair)), device);
                 continue;
             }
-            Log.d(LOG_TAG, String.format("Trying to push local '%s' to remote '%s'", pair[0],
-                    pair[1]));
-            File src = new File(pair[0]);
-            String remotePath = pair[1];
-            evaluatePushingPair(device, buildInfo, src, remotePath);
+            remoteToLocalMapping.put(pair[1], new File(pair[0]));
         }
         // Push the file structure
-        for (File src : mPushFileSpecs.keySet()) {
-            String remotePath = mPushFileSpecs.get(src);
+        for (File local : mPushFileSpecs.keySet()) {
+            remoteToLocalMapping.put(mPushFileSpecs.get(local), local);
+        }
+
+        for (String remotePath : remoteToLocalMapping.keySet()) {
+            File local = remoteToLocalMapping.get(remotePath);
             Log.d(
                     LOG_TAG,
                     String.format(
-                            "Trying to push local '%s' to remote '%s'", src.getPath(), remotePath));
-            evaluatePushingPair(device, buildInfo, src, remotePath);
+                            "Trying to push local '%s' to remote '%s'",
+                            local.getPath(), remotePath));
+            evaluatePushingPair(device, buildInfo, local, remotePath);
         }
 
         for (String command : mPostPushCommands) {
@@ -272,18 +299,6 @@
         }
     }
 
-    private void addPushedFile(ITestDevice device, String remotePath) throws TargetSetupError {
-        if (mFilesPushed.contains(remotePath)) {
-            throw new TargetSetupError(
-                    String.format(
-                            "We pushed two files to the %s location. Check "
-                                    + "your configuration of this target_preparer",
-                            remotePath),
-                    device.getDeviceDescriptor());
-        }
-        mFilesPushed.add(remotePath);
-    }
-
     private void evaluatePushingPair(
             ITestDevice device, IBuildInfo buildInfo, File src, String remotePath)
             throws TargetSetupError, DeviceNotAvailableException {
@@ -325,7 +340,7 @@
                 if (deleteContentOnly) {
                     remotePath += "/*";
                 }
-                addPushedFile(device, remotePath);
+                mFilesPushed.add(remotePath);
             }
         } else {
             if (!device.pushFile(src, remotePath)) {
@@ -335,7 +350,7 @@
                         device);
                 return;
             } else {
-                addPushedFile(device, remotePath);
+                mFilesPushed.add(remotePath);
             }
         }
     }
diff --git a/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java b/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java
index 4b50c3d..9f05a65 100644
--- a/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java
@@ -141,5 +141,10 @@
             }
         }
     }
+
+    /** Add a command that will be run by the preparer. */
+    public final void addRunCommand(String cmd) {
+        mCommands.add(cmd);
+    }
 }
 
diff --git a/src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java b/src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java
index 836c4a8..54ce9ac 100644
--- a/src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.targetprep;
 
-import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -34,7 +33,7 @@
  * TargetSetupError}, and same applies to any OTA sideload error detected.
  */
 @OptionClass(alias = "sideload-ota")
-public class SideloadOtaTargetPreparer extends DeviceBuildInfoInjector {
+public class SideloadOtaTargetPreparer extends DeviceUpdateTargetPreparer {
 
     private static final String SIDELOAD_CMD = "sideload";
     // the timeout for state transition from sideload finishes to recovery mode, not making this
@@ -52,32 +51,38 @@
     // defaults to 10m: assuming USB 2.0 transfer speed, concurrency and some buffer
     private long mSideloadTimeout = 10 * 60 * 1000;
 
-    @Option(
-        name = "inject-build-info",
-        description =
-                "whether build info should be injected "
-                        + "based on device attributes after sideloading"
-    )
-    private boolean mInjectBuildInfo = true;
-
+    /** {@inheritDoc} */
     @Override
-    public void setUp(ITestDevice device, IBuildInfo buildInfo)
-            throws TargetSetupError, BuildError, DeviceNotAvailableException {
-        if (mSideloadOtaPackage == null) {
-            CLog.i("No sideload file provided, assuming no-op; skipping ...");
-            return;
-        }
+    protected File getDeviceUpdateImage() {
+        return mSideloadOtaPackage;
+    }
+
+    /** Reboots the device into sideload mode in preparation */
+    @Override
+    protected void preUpdateActions(File deviceUpdateImage, ITestDevice device)
+            throws DeviceNotAvailableException, TargetSetupError {
         device.rebootIntoSideload();
-        String filePath = mSideloadOtaPackage.getAbsolutePath();
-        CLog.d("Sideloading package from %s ...", filePath);
-        device.executeAdbCommand(mSideloadTimeout, SIDELOAD_CMD, filePath);
+    }
+
+    /** Waits for device to transition from sideload to recovery, then reboot to userspace */
+    @Override
+    protected void postUpdateActions(File deviceUpdateImage, ITestDevice device)
+            throws DeviceNotAvailableException, TargetSetupError {
         // after applying sideload, device should transition to recovery mode
         device.waitForDeviceInRecovery(POST_SIDELOAD_TRANSITION_TIMEOUT);
-        // now reboot and wait for the device to become available
+        CLog.i(
+                "Sideloading completed on %s, rebooting and waiting for boot complete.",
+                device.getDeviceDescriptor());
+        // now reboot to userspace
         device.reboot();
-        // calling this last because we want to inject device side build info after device boots up
-        if (mInjectBuildInfo) {
-            super.setUp(device, buildInfo);
-        }
+    }
+
+    /** Performs the sideload of OTA package */
+    @Override
+    protected void performDeviceUpdate(File deviceUpdateImage, ITestDevice device)
+            throws DeviceNotAvailableException, TargetSetupError {
+        String filePath = getDeviceUpdateImage().getAbsolutePath();
+        CLog.i("Sideloading package from %s onto %s", filePath, device.getSerialNumber());
+        device.executeAdbCommand(mSideloadTimeout, SIDELOAD_CMD, filePath);
     }
 }
diff --git a/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java b/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
index 891e651..57ba004 100644
--- a/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
@@ -21,8 +21,10 @@
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.UserUtil;
+
+import java.util.Map;
 
 /**
  * A {@link ITargetPreparer} that switches to the specified user kind in setUp. By default it
@@ -36,7 +38,7 @@
         name = "user-type",
         description = "The type of user to switch to before the module run."
     )
-    private UserUtil.UserType mUserToSwitchTo = UserUtil.UserType.CURRENT;
+    private UserInfo.UserType mUserToSwitchTo = UserInfo.UserType.CURRENT;
 
     private int mPreExecutionCurrentUser;
 
@@ -45,17 +47,36 @@
             throws TargetSetupError, DeviceNotAvailableException {
 
         mPreExecutionCurrentUser = device.getCurrentUser();
+        Map<Integer, UserInfo> userInfos = device.getUserInfos();
 
-        try {
-            UserUtil.switchToUserType(device, mUserToSwitchTo);
-        } catch (UserUtil.UserSwitchFailedException err) {
-            throw new TargetSetupError(
-                    String.format("Failed switch to user type %s", mUserToSwitchTo),
-                    err,
-                    device.getDeviceDescriptor());
+        if (userInfos
+                .get(mPreExecutionCurrentUser)
+                .isUserType(mUserToSwitchTo, mPreExecutionCurrentUser)) {
+            CLog.i(
+                    "User %d is already user type %s, no action.",
+                    mPreExecutionCurrentUser, mUserToSwitchTo.toString());
+            return;
         }
 
-        CLog.d("Successfully switched to user type %s", mUserToSwitchTo);
+        for (UserInfo userInfo : userInfos.values()) {
+            if (userInfo.isUserType(mUserToSwitchTo, mPreExecutionCurrentUser)) {
+                CLog.i(
+                        "User %d is user type %s, switching from %d",
+                        userInfo.userId(), mUserToSwitchTo.toString(), mPreExecutionCurrentUser);
+                if (!device.switchUser(userInfo.userId())) {
+                    throw new TargetSetupError(
+                            String.format("Device failed to switch to user %d", userInfo.userId()),
+                            device.getDeviceDescriptor());
+                }
+                return;
+            }
+        }
+
+        throw new TargetSetupError(
+                String.format(
+                        "Failed switch to user type %s, no user of that type exists",
+                        mUserToSwitchTo),
+                device.getDeviceDescriptor());
     }
 
     @Override
diff --git a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
index f1b46f7..f1ad19a 100644
--- a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
@@ -381,7 +381,7 @@
      * Attempt to install a package or split package on the device.
      *
      * @param device the {@link ITestDevice} to install package
-     * @param apkFiles List of Files. If apkFiles contains only one apk file, the app will be
+     * @param appFiles List of Files. If apkFiles contains only one apk file, the app will be
      *     installed as a whole package with single file. If apkFiles contains more than one name,
      *     the app will be installed as split apk with multiple files.
      */
diff --git a/src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java b/src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java
index 9867c74..769065a 100644
--- a/src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java
+++ b/src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java
@@ -15,7 +15,6 @@
  */
 package com.android.tradefed.targetprep.app;
 
-import com.android.tradefed.build.AppBuildInfo;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationReceiver;
@@ -43,12 +42,7 @@
     @Override
     public void setUp(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
-        if (!(buildInfo instanceof AppBuildInfo)) {
-            CLog.d("Build info is not a AppBuildInfo skipping.");
-            return;
-        }
-        AppBuildInfo appBuild = (AppBuildInfo) buildInfo;
-        if (appBuild.getAppPackageFiles().isEmpty()) {
+        if (buildInfo.getAppPackageFiles().isEmpty()) {
             CLog.d("No app to install, skipping the tests");
 
             for (IDeviceConfiguration deviceConfig : mConfiguration.getDeviceConfig()) {
diff --git a/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java b/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java
new file mode 100644
index 0000000..174bb9c
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep.multi;
+
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.CollectingOutputReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil;
+import com.android.tradefed.util.ZipUtil2;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
+/**
+ * An {@link com.android.tradefed.targetprep.multi.IMultiTargetPreparer} that set up a system
+ * build's images on top of a device build with the Dynamic System Update
+ */
+@OptionClass(alias = "dynamic-system-update")
+public class DynamicSystemPreparer extends BaseMultiTargetPreparer {
+    static final int DSU_MAX_WAIT_SEC = 10 * 60;
+
+    private static final String DEST_PATH = "/sdcard";
+
+    @Option(name = "device-label", description = "the label for the device.")
+    private String mDeviceLabel = "device";
+
+    @Option(
+        name = "system-label",
+        description = "the label for the null-device used to store the system image information."
+    )
+    private String mSystemLabel = "system";
+
+    private boolean isDSURunning(ITestDevice device) throws DeviceNotAvailableException {
+        CollectingOutputReceiver receiver = new CollectingOutputReceiver();
+        device.executeShellCommand("gsi_tool status", receiver);
+        return receiver.getOutput().contains("running");
+    }
+
+    @Override
+    public void setUp(IInvocationContext context)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+
+        ITestDevice device = context.getDevice(mDeviceLabel);
+
+        ITestDevice systemNullDevice = context.getDevice(mSystemLabel);
+        IDeviceBuildInfo systemBuildInfo =
+                (IDeviceBuildInfo) context.getBuildInfo(systemNullDevice);
+
+        File systemImage = null;
+        File systemImageGZ = null;
+
+        ZipFile zipFile = null;
+        try {
+            zipFile = new ZipFile(systemBuildInfo.getDeviceImageFile());
+            systemImage = ZipUtil2.extractFileFromZip(zipFile, "system.img");
+            //     The prequest here is the system.img must be an unsparsed image.
+            //     Is there any way to detect the actual format and convert it accordingly.
+            systemImageGZ = new File("system.raw.gz");
+            long rawSize = systemImage.length();
+            ZipUtil.gzipFile(systemImage, systemImageGZ);
+            String remotePath = String.format("%s/%s", DEST_PATH, systemImageGZ.getName());
+            CLog.i("Pushing %s to %s", systemImageGZ.getAbsolutePath(), remotePath);
+            if (!device.pushFile(systemImageGZ, remotePath)) {
+                throw new TargetSetupError(
+                        String.format(
+                                "Failed to push %s to %s", systemImageGZ.getName(), remotePath),
+                        device.getDeviceDescriptor());
+            }
+            device.setProperty("persist.sys.fflag.override.settings_dynamic_system", "true");
+
+            String command =
+                    "am start-activity "
+                            + "-n com.android.dynsystem/com.android.dynsystem.VerificationActivity "
+                            + "-a android.os.image.action.START_INSTALL "
+                            + "-d file://"
+                            + remotePath
+                            + " "
+                            + "--el KEY_SYSTEM_SIZE "
+                            + rawSize
+                            + " "
+                            + "--el KEY_USERDATA_SIZE 8589934592 "
+                            + "--ez KEY_ENABLE_WHEN_COMPLETED true";
+            device.executeShellCommand(command);
+            // Check if device shows as unavailable (as expected after the activity finished).
+            device.waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000);
+            device.waitForDeviceOnline();
+            // the waitForDeviceOnline may block and we need to correct the 'i'
+            // which is used to measure timeout accordingly
+            if (!isDSURunning(device)) {
+                throw new TargetSetupError("Timeout to boot into DSU");
+            }
+            CommandResult result = device.executeShellV2Command("gsi_tool enable");
+            if (CommandStatus.SUCCESS.equals(result.getStatus())) {
+                // success
+                return;
+            } else {
+                throw new TargetSetupError("fail on gsi_tool enable");
+            }
+        } catch (IOException e) {
+            CLog.e(e);
+            throw new TargetSetupError(
+                    "fail to install the DynamicSystemUpdate", e, device.getDeviceDescriptor());
+        } finally {
+            FileUtil.deleteFile(systemImage);
+            FileUtil.deleteFile(systemImageGZ);
+            ZipUtil2.closeZip(zipFile);
+        }
+    }
+
+    @Override
+    public void tearDown(IInvocationContext context, Throwable e)
+            throws DeviceNotAvailableException {
+        if (e instanceof DeviceNotAvailableException) {
+            CLog.e("skip tearDown on DeviceNotAvailableException");
+            return;
+        }
+        ITestDevice device = context.getDevice(mDeviceLabel);
+        // Disable the DynamicSystemUpdate installation
+        device.executeShellCommand("gsi_tool disable");
+        // Enable the one-shot mode when DynamicSystemUpdate is disabled
+        device.executeShellCommand("gsi_tool enable -s");
+        // Disable the DynamicSystemUpdate installation
+        device.executeShellCommand("gsi_tool disable");
+        // Reboot into the original system image
+        device.reboot();
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java b/src/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
new file mode 100644
index 0000000..8e70114
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep.multi;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IBuildInfo.BuildInfoProperties;
+import com.android.tradefed.build.IDeviceBuildInfo;
+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.IInvocationContext;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.ZipUtil;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/** An {@link IMultiTargetPreparer} that mixes a system build's images in a device build. */
+@OptionClass(alias = "mix-image-zip")
+public class MixImageZipPreparer extends BaseMultiTargetPreparer {
+
+    @Option(name = "device-label", description = "the label for the device.")
+    private String mDeviceLabel = "device";
+
+    @Option(
+        name = "system-label",
+        description = "the label for the null-device used to store the system image information."
+    )
+    private String mSystemLabel = "system";
+
+    @Option(
+        name = "resource-label",
+        description = "the label for the null-device used to store the extra build information."
+    )
+    private String mResourceLabel = "resource";
+
+    @Option(
+        name = "extra-build-test-resource-name",
+        description =
+                "the name of the extra build file copied to device build. " + "Can be repeated."
+    )
+    private Set<String> mExtraBuildResourceFiles = new TreeSet<>();
+
+    @Option(
+        name = "system-build-file-name",
+        description =
+                "the name of the image file copied from system build to device build. "
+                        + "Can be repeated.",
+        mandatory = true
+    )
+    private Set<String> mSystemFileNames = new TreeSet<>();
+
+    @Option(
+        name = "compression-level",
+        description =
+                "the compression level of the mixed image zip. It is an integer between 0 "
+                        + "and 9. Larger value indicates longer time and smaller output."
+    )
+    private int mCompressionLevel = Deflater.DEFAULT_COMPRESSION;
+
+    /** The interface that creates {@link InputStream} from a file or a compressed file. */
+    @VisibleForTesting
+    static interface InputStreamFactory {
+        /** Create a new stream. The caller should close it. */
+        InputStream createInputStream() throws IOException;
+
+        /** Return the uncompressed size of the data. */
+        long getSize();
+
+        /** Return the CRC32 of the data. */
+        long getCrc32() throws IOException;
+    }
+
+    @Override
+    public void setUp(IInvocationContext context)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+
+        ITestDevice device = context.getDevice(mDeviceLabel);
+        IDeviceBuildInfo deviceBuildInfo = (IDeviceBuildInfo) context.getBuildInfo(device);
+
+        ITestDevice systemNullDevice = context.getDevice(mSystemLabel);
+        IDeviceBuildInfo systemBuildInfo =
+                (IDeviceBuildInfo) context.getBuildInfo(systemNullDevice);
+
+        IBuildInfo resourceBuildInfo = null;
+        if (!mExtraBuildResourceFiles.isEmpty()) {
+            ITestDevice resourceNullDevice = context.getDevice(mResourceLabel);
+            resourceBuildInfo = context.getBuildInfo(resourceNullDevice);
+        }
+
+        ZipFile deviceImageZip = null;
+        ZipFile systemImageZip = null;
+        File mixedImageZip = null;
+        try {
+            deviceImageZip = new ZipFile(deviceBuildInfo.getDeviceImageFile());
+
+            // Get all files from device build.
+            Map<String, InputStreamFactory> files =
+                    getInputStreamFactoriesFromImageZip(deviceImageZip, file -> true);
+            Map<String, InputStreamFactory> filesNotInDeviceBuild =
+                    new HashMap<String, InputStreamFactory>();
+
+            // Get specified files from system build and replace those in device build.
+            systemImageZip = new ZipFile(systemBuildInfo.getDeviceImageFile());
+            Map<String, InputStreamFactory> systemFiles =
+                    getInputStreamFactoriesFromImageZip(
+                            systemImageZip, file -> mSystemFileNames.contains(file));
+            systemFiles = replaceExistingEntries(systemFiles, files);
+            filesNotInDeviceBuild.putAll(systemFiles);
+
+            if (resourceBuildInfo != null) {
+                // Get specified files from resource build and replace those in device build.
+                Map<String, InputStreamFactory> resourceFiles =
+                        getBuildFiles(resourceBuildInfo, mExtraBuildResourceFiles);
+                resourceFiles = replaceExistingEntries(resourceFiles, files);
+                filesNotInDeviceBuild.putAll(resourceFiles);
+            }
+
+            if (!filesNotInDeviceBuild.isEmpty()) {
+                throw new TargetSetupError(
+                        String.join(",", filesNotInDeviceBuild.keySet()) + " not in device build.",
+                        device.getDeviceDescriptor());
+            }
+
+            CLog.d("Create mixed image zip.");
+            mixedImageZip = createZip(files, mCompressionLevel);
+        } catch (IOException e) {
+            throw new TargetSetupError(
+                    "Could not create mixed image zip", e, device.getDeviceDescriptor());
+        } finally {
+            ZipUtil.closeZip(deviceImageZip);
+            ZipUtil.closeZip(systemImageZip);
+        }
+
+        IBuildInfo mixedBuildInfo =
+                createBuildCopy(
+                        deviceBuildInfo,
+                        systemBuildInfo.getBuildFlavor(),
+                        systemBuildInfo.getBuildId(),
+                        mixedImageZip);
+        // Replace the build
+        context.addDeviceBuildInfo(mDeviceLabel, mixedBuildInfo);
+        // Clean up the original build
+        deviceBuildInfo.cleanUp();
+    }
+
+    /**
+     * Get {@link InputStreamFactory} from entries in an image zip. The zip must not be closed when
+     * the returned {@link InputStreamFactory} are in use.
+     *
+     * @param zipFile image zip.
+     * @param predicate function that takes a file name as the argument and determines whether the
+     *     file name and the content should be added to the output map.
+     * @return map from file name to {@link InputStreamFactory}.
+     * @throws IOException if fails to create the temporary directory.
+     */
+    private static Map<String, InputStreamFactory> getInputStreamFactoriesFromImageZip(
+            final ZipFile zipFile, Predicate<String> predicate) throws IOException {
+        Map<String, InputStreamFactory> factories = new HashMap<String, InputStreamFactory>();
+        Enumeration<? extends ZipEntry> entries = zipFile.entries();
+        while (entries.hasMoreElements()) {
+            final ZipEntry entry = entries.nextElement();
+            if (entry.isDirectory()) {
+                CLog.w("Image zip contains subdirectory %s.", entry.getName());
+                continue;
+            }
+
+            String name = new File(entry.getName()).getName();
+            if (!predicate.test(name)) {
+                continue;
+            }
+
+            if (entry.getSize() < 0) {
+                throw new IllegalArgumentException("Invalid size.");
+            }
+            if (entry.getCrc() < 0) {
+                throw new IllegalArgumentException("Invalid CRC value.");
+            }
+
+            factories.put(
+                    name,
+                    new InputStreamFactory() {
+                        @Override
+                        public InputStream createInputStream() throws IOException {
+                            return zipFile.getInputStream(entry);
+                        }
+
+                        @Override
+                        public long getSize() {
+                            return entry.getSize();
+                        }
+
+                        @Override
+                        public long getCrc32() {
+                            return entry.getCrc();
+                        }
+                    });
+        }
+        return factories;
+    }
+
+    /**
+     * Get {@link InputStreamFactory} from {@link IBuildInfo} by name.
+     *
+     * @param buildInfo {@link IBuildInfo} that contains files.
+     * @param buildFileNames collection of file names.
+     * @return map from file name to {@link InputStreamFactory}.
+     * @throws IOException if fails to get files from the build info.
+     */
+    private static Map<String, InputStreamFactory> getBuildFiles(
+            IBuildInfo buildInfo, Collection<String> buildFileNames) throws IOException {
+        Map<String, InputStreamFactory> factories = new HashMap<String, InputStreamFactory>();
+        for (String fileName : buildFileNames) {
+            final File file = buildInfo.getFile(fileName);
+            if (file == null) {
+                throw new IOException(String.format("Could not get file with name: %s", fileName));
+            }
+            factories.put(
+                    fileName,
+                    new InputStreamFactory() {
+                        @Override
+                        public InputStream createInputStream() throws IOException {
+                            return new FileInputStream(file);
+                        }
+
+                        @Override
+                        public long getSize() {
+                            return file.length();
+                        }
+
+                        @Override
+                        public long getCrc32() throws IOException {
+                            return FileUtil.calculateCrc32(file);
+                        }
+                    });
+        }
+        return factories;
+    }
+
+    private static void initStoredZipEntry(ZipEntry entry, InputStreamFactory factory)
+            throws IOException {
+        entry.setMethod(ZipOutputStream.STORED);
+        entry.setCompressedSize(factory.getSize());
+        entry.setSize(factory.getSize());
+        entry.setCrc(factory.getCrc32());
+    }
+
+    /**
+     * Create a zip file from {@link InputStreamFactory} instances.
+     *
+     * @param factories the map where the keys are the entry names and the values provide the data
+     *     to be compressed.
+     * @param compressionLevel an integer between 0 and 9. If the value is 0, this method creates
+     *     {@link ZipOutputStream#STORED} entries instead of default ones.
+     * @return the created zip file in temporary directory.
+     * @throws IOException if any file operation fails.
+     */
+    @VisibleForTesting
+    static File createZip(Map<String, ? extends InputStreamFactory> factories, int compressionLevel)
+            throws IOException {
+        File zipFile = null;
+        OutputStream out = null;
+        try {
+            zipFile = FileUtil.createTempFile("MixedImg", ".zip");
+            out = new FileOutputStream(zipFile);
+            out = new BufferedOutputStream(out);
+            out = new ZipOutputStream(out);
+            ZipOutputStream zipOut = (ZipOutputStream) out;
+            zipOut.setLevel(compressionLevel);
+
+            for (Map.Entry<String, ? extends InputStreamFactory> factory : factories.entrySet()) {
+                ZipEntry entry = new ZipEntry(factory.getKey());
+                // STORED is faster than the default DEFLATED in no compression mode.
+                if (compressionLevel == Deflater.NO_COMPRESSION) {
+                    initStoredZipEntry(entry, factory.getValue());
+                }
+                zipOut.putNextEntry(entry);
+                try (InputStream in =
+                        new BufferedInputStream(factory.getValue().createInputStream())) {
+                    StreamUtil.copyStreams(in, zipOut);
+                }
+                zipOut.closeEntry();
+            }
+
+            File returnValue = zipFile;
+            zipFile = null;
+            return returnValue;
+        } finally {
+            StreamUtil.close(out);
+            FileUtil.deleteFile(zipFile);
+        }
+    }
+
+    private static IBuildInfo createBuildCopy(
+            IDeviceBuildInfo deviceBuildInfo, String buildFlavor, String buildId, File imageZip) {
+        deviceBuildInfo.setProperties(BuildInfoProperties.DO_NOT_COPY_IMAGE_FILE);
+        IDeviceBuildInfo newBuildInfo = (IDeviceBuildInfo) deviceBuildInfo.clone();
+        newBuildInfo.setBuildFlavor(buildFlavor);
+        newBuildInfo.setDeviceImageFile(imageZip, buildId);
+        return newBuildInfo;
+    }
+
+    /**
+     * Replace the values if the keys exists in the map.
+     *
+     * @param replacement the map containing the entries to be added to the original map.
+     * @param original the map whose entries are replaced.
+     * @return the entries which are in the replacement map but not added to the original map.
+     */
+    private static <T> Map<String, T> replaceExistingEntries(
+            Map<String, T> replacement, Map<String, T> original) {
+        Map<String, T> remaining = new HashMap<String, T>();
+        for (Map.Entry<String, T> entry : replacement.entrySet()) {
+            String key = entry.getKey();
+            if (original.containsKey(key)) {
+                original.put(key, entry.getValue());
+            } else {
+                remaining.put(key, entry.getValue());
+            }
+        }
+        return remaining;
+    }
+
+    @VisibleForTesting
+    void addSystemFileName(String fileName) {
+        mSystemFileNames.add(fileName);
+    }
+
+    @VisibleForTesting
+    void addResourceFileName(String fileName) {
+        mExtraBuildResourceFiles.add(fileName);
+    }
+
+    @VisibleForTesting
+    void setCompressionLevel(int compressionLevel) {
+        mCompressionLevel = compressionLevel;
+    }
+}
diff --git a/src/com/android/tradefed/testtype/GTest.java b/src/com/android/tradefed/testtype/GTest.java
index a3a4907..2898afa 100644
--- a/src/com/android/tradefed/testtype/GTest.java
+++ b/src/com/android/tradefed/testtype/GTest.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.NativeCodeCoverageFlusher;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -34,6 +35,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -58,6 +60,24 @@
             description = "Stops the Java application runtime before test execution.")
     private boolean mStopRuntime = false;
 
+    @Option(
+        name = "coverage-flush",
+        description = "Forces coverage data to be flushed at the end of the test."
+    )
+    private boolean mCoverageFlush = false;
+
+    @Option(
+        name = "coverage-processes",
+        description = "Name of processes to collect coverage data from."
+    )
+    private List<String> mCoverageProcesses = new ArrayList<>();
+
+    @Option(
+        name = "coverage-clear-before-test",
+        description = "Clears all coverage counters before test execution."
+    )
+    private boolean mCoverageClearBeforeTest = true;
+
     // Max characters allowed for executing GTest via command line
     private static final int GTEST_CMD_CHAR_LIMIT = 1000;
     /**
@@ -68,6 +88,11 @@
         mDevice = device;
     }
 
+    @VisibleForTesting
+    void setCoverageProcesses(List<String> coverageProcesses) {
+        mCoverageProcesses = new ArrayList<>(coverageProcesses);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -365,14 +390,26 @@
         }
         // Insert the coverage listener if code coverage collection is enabled.
         listener = addNativeCoverageListenerIfEnabled(mDevice, listener);
+        NativeCodeCoverageFlusher flusher = new NativeCodeCoverageFlusher(mDevice);
+
         Throwable throwable = null;
         try {
+            if (isCoverageEnabled() && mCoverageClearBeforeTest) {
+                if (mCoverageFlush) {
+                    flusher.forceCoverageFlush(mCoverageProcesses);
+                }
+                flusher.clearCoverageMeasurements();
+            }
             doRunAllTestsInSubdirectory(testPath, mDevice, listener);
         } catch (Throwable t) {
             throwable = t;
             throw t;
         } finally {
             if (!(throwable instanceof DeviceNotAvailableException)) {
+                if (isCoverageEnabled() && mCoverageFlush) {
+                    flusher.forceCoverageFlush(mCoverageProcesses);
+                }
+
                 if (mStopRuntime) {
                     mDevice.executeShellCommand("start");
                     mDevice.waitForDeviceAvailable();
diff --git a/src/com/android/tradefed/testtype/GTestBase.java b/src/com/android/tradefed/testtype/GTestBase.java
index 7766621..eee17c5 100644
--- a/src/com/android/tradefed/testtype/GTestBase.java
+++ b/src/com/android/tradefed/testtype/GTestBase.java
@@ -93,7 +93,7 @@
     private long mMaxTestTimeMs = 1 * 60 * 1000L;
 
     @Option(
-        name = "native-coverage",
+        name = "coverage",
         description =
                 "Collect code coverage for this test run. Note that the build under test must be a "
                         + "coverage build or else this will fail."
@@ -352,6 +352,11 @@
         return mIsSharded;
     }
 
+    /** Gets coverage flag. */
+    public boolean isCoverageEnabled() {
+        return mCoverage;
+    }
+
     /**
      * Define get filter method.
      *
diff --git a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
index 412770d..622742e 100644
--- a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
+++ b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
@@ -27,11 +27,8 @@
 import com.android.tradefed.device.metric.IMetricCollectorReceiver;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.BugreportCollector;
 import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.testdefs.XmlDefsTest;
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.ListInstrumentationParser;
 import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget;
@@ -52,9 +49,6 @@
                 IMetricCollectorReceiver,
                 IInvocationContextReceiver {
 
-    /** the metric key name for the test coverage target value */
-    // TODO: move this to a more generic location
-    public static final String COVERAGE_TARGET_KEY = XmlDefsTest.COVERAGE_TARGET_KEY;
     private static final String PM_LIST_CMD = "pm list instrumentation";
     private static final String LINE_SEPARATOR = "\\r?\\n";
 
@@ -94,8 +88,9 @@
             "if first device becomes unavailable.")
     private boolean mIsResumeMode = false;
 
-    @Option(name = "send-coverage",
-            description = "Send coverage target info to test listeners.")
+    /** @deprecated delete when we are sure it's not used anywhere. */
+    @Deprecated
+    @Option(name = "send-coverage", description = "Send coverage target info to test listeners.")
     private boolean mSendCoverage = false;
 
     @Option(name = "bugreport-on-failure", description = "Sets which failed testcase events " +
@@ -221,15 +216,6 @@
     }
 
     /**
-     * Set the send coverage flag.
-     * <p/>
-     * Exposed for unit testing.
-     */
-    void setSendCoverage(boolean sendCoverage) {
-        mSendCoverage = sendCoverage;
-    }
-
-    /**
      * Gets the list of {@link InstrumentationTest}s contained within.
      * <p/>
      * Exposed for unit testing.
@@ -342,9 +328,6 @@
 
             CLog.d("Running test %s on %s", test.getPackageName(), getDevice().getSerialNumber());
 
-            if (mSendCoverage && test.getCoverageTarget() != null) {
-                sendCoverage(test.getPackageName(), test.getCoverageTarget(), listener);
-            }
             test.setDevice(getDevice());
             if (mTestClass != null) {
                 test.setClassName(mTestClass);
@@ -363,26 +346,6 @@
         }
     }
 
-    /**
-     * Forwards the tests coverage target info as a test metric.
-     *
-     * @param packageName
-     * @param coverageTarget
-     * @param listener
-     */
-    private void sendCoverage(String packageName, String coverageTarget,
-            ITestInvocationListener listener) {
-        HashMap<String, Metric> coverageMetric = new HashMap<String, Metric>();
-        Metric metric =
-                Metric.newBuilder()
-                        .setMeasurements(
-                                Measurements.newBuilder().setSingleString(coverageTarget).build())
-                        .build();
-        coverageMetric.put(COVERAGE_TARGET_KEY, metric);
-        listener.testRunStarted(packageName, 0);
-        listener.testRunEnded(0, coverageMetric);
-    }
-
     long getShellTimeout() {
         return mShellTimeout;
     }
diff --git a/src/com/android/tradefed/testtype/InstrumentationTest.java b/src/com/android/tradefed/testtype/InstrumentationTest.java
index f468fdc..aaab43c 100644
--- a/src/com/android/tradefed/testtype/InstrumentationTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationTest.java
@@ -861,6 +861,7 @@
         if (!mIsRerun) {
             listener = addBugreportListenerIfEnabled(listener);
             listener = addJavaCoverageListenerIfEnabled(listener);
+            listener = addNativeCoverageListenerIfEnabled(listener);
 
             // TODO: Convert to device-side collectors when possible.
             for (IMetricCollector collector : mCollectors) {
@@ -927,6 +928,17 @@
     }
 
     /**
+     * Returns a listener that will collect native coverage measurements, or the original {@code
+     * listener} if this feature is disabled.
+     */
+    ITestInvocationListener addNativeCoverageListenerIfEnabled(ITestInvocationListener listener) {
+        if (mCoverage) {
+            return new NativeCodeCoverageListener(getDevice(), listener);
+        }
+        return listener;
+    }
+
+    /**
      * Execute the test run, but re-run incomplete tests individually if run fails to complete.
      *
      * @param listener the {@link ITestInvocationListener}
@@ -990,6 +1002,9 @@
         }
         if (mRebootBeforeReRun) {
             mDevice.reboot();
+        } else {
+            // Ensure device is online and responsive before retrying.
+            mDevice.waitForDeviceAvailable();
         }
 
         IRemoteTest testReRunner = null;
diff --git a/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java b/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
index b1b6c8d..dca83db 100644
--- a/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
+++ b/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
@@ -28,6 +28,8 @@
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.ZipUtil;
 
+import com.google.common.base.Splitter;
+
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -42,7 +44,7 @@
  */
 public final class NativeCodeCoverageListener extends ResultForwarder {
 
-    private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace";
+    private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace/proc/self/cwd/out";
     private static final String COVERAGE_FILE_LIST_COMMAND =
             String.format("find %s -name '*.gcda'", NATIVE_COVERAGE_DEVICE_PATH);
 
@@ -70,10 +72,12 @@
         try {
             localDir = FileUtil.createTempDir("native_coverage");
 
+            // Enable abd root on the device, otherwise the list command will fail.
+            verify(mDevice.enableAdbRoot(), "Failed to enable adb root.");
             String findResult = mDevice.executeShellCommand(COVERAGE_FILE_LIST_COMMAND);
 
             Path devicePathRoot = Paths.get(NATIVE_COVERAGE_DEVICE_PATH);
-            for (String deviceFile : findResult.split("\n")) {
+            for (String deviceFile : Splitter.on("\n").omitEmptyStrings().split(findResult)) {
                 // Compute the relative path for the device file.
                 Path relativePath = devicePathRoot.relativize(Paths.get(deviceFile));
                 Path localFullPath = localDir.toPath().resolve(relativePath);
diff --git a/src/com/android/tradefed/testtype/NoisyDryRunTest.java b/src/com/android/tradefed/testtype/NoisyDryRunTest.java
index f2ce53c..9d034c7 100644
--- a/src/com/android/tradefed/testtype/NoisyDryRunTest.java
+++ b/src/com/android/tradefed/testtype/NoisyDryRunTest.java
@@ -156,7 +156,7 @@
                             ConfigurationFactory.getInstance()
                                     .createConfigurationFromArgs(args, null, new DryRunKeyStore());
                     // Do not resolve dynamic files
-                    config.validateOptions(false);
+                    config.validateOptions();
                 }
             } catch (ConfigurationException e) {
                 String errorMessage = String.format("Failed to parse command line: %s.", cmdLine);
@@ -182,7 +182,7 @@
                         .createConfigurationFromArgs(
                                 args, new DryRunKeyStore(), createSandbox(), createRunUtil());
         // Do not resolve dynamic files
-        config.validateOptions(false);
+        config.validateOptions();
     }
 
     /** Returns a {@link IRunUtil} implementation. */
diff --git a/src/com/android/tradefed/testtype/VersionedTfLauncher.java b/src/com/android/tradefed/testtype/VersionedTfLauncher.java
deleted file mode 100644
index 798934a..0000000
--- a/src/com/android/tradefed/testtype/VersionedTfLauncher.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype;
-
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.OptionCopier;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.device.NullDevice;
-import com.android.tradefed.device.StubDevice;
-import com.android.tradefed.util.StringEscapeUtils;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-/**
- * A {@link IRemoteTest} for running tests against a separate TF installation.
- *
- * <p>Launches an external java process to run the tests. Used for running the TF unit or functional
- * tests continuously.
- */
-public class VersionedTfLauncher extends SubprocessTfLauncher
-        implements IMultiDeviceTest, IShardableTest {
-
-    @Option(name = "tf-command-line", description = "The list string of original command line "
-            + "arguments.")
-    private List<String> mTfCommandline = new ArrayList<>();
-
-    private Map<ITestDevice, IBuildInfo> mDeviceInfos = null;
-
-    private int mShardCount = -1;
-
-    private int mShardIndex = -1;
-
-    public VersionedTfLauncher() {
-        super();
-    }
-
-    private VersionedTfLauncher(int shardCount, int shardIndex) {
-        this();
-        mShardCount = shardCount;
-        mShardIndex = shardIndex;
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p/>
-     * The method tokenizes the command line arguments specified by --tf-command-line, and
-     * appends the arguments to the subprocess of TF run. It also passes in the serial of the test
-     * device through --serial option, to force the subprocess to use the device selected by the
-     * parent TF process.
-     */
-    @Override
-    protected void preRun() {
-        super.preRun();
-
-        if (!mTfCommandline.isEmpty()) {
-            mCmdArgs.addAll(StringEscapeUtils.paramsToArgs(mTfCommandline));
-        }
-
-        // TODO: support multiple device test.
-        if (mDeviceInfos == null || mDeviceInfos.size() == 0) {
-            throw new RuntimeException("Device is not allocated for the test.");
-        } else if (mDeviceInfos.size() > 1) {
-            throw new RuntimeException("More than one devices are allocated for the test.");
-        } else {
-            ITestDevice device = mDeviceInfos.entrySet().iterator().next().getKey();
-            if (device.getIDevice() instanceof NullDevice) {
-                mCmdArgs.add("--null-device");
-            } else if (!(device.getIDevice() instanceof StubDevice)) {
-                String serial = device.getSerialNumber();
-                mCmdArgs.add("--serial");
-                mCmdArgs.add(serial);
-            }
-        }
-
-        if (0 <= mShardCount && 0 <= mShardIndex) {
-            mCmdArgs.add("--shard-count");
-            mCmdArgs.add(Integer.toString(mShardCount));
-            mCmdArgs.add("--shard-index");
-            mCmdArgs.add(Integer.toString(mShardIndex));
-        }
-
-        // Add option for the path to general-tests.zip
-        File generalTests = mBuildInfo.getFile("general-tests.zip");
-        if (generalTests != null) {
-            mCmdArgs.add("--additional-tests-zip");
-            mCmdArgs.add(generalTests.getAbsolutePath());
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setDeviceInfos(Map<ITestDevice, IBuildInfo> deviceInfos) {
-        mDeviceInfos = deviceInfos;
-    }
-
-    @Override
-    public Collection<IRemoteTest> split(int shardCountHint) {
-        if (shardCountHint <= 1) {
-            return null;
-        }
-        Collection<IRemoteTest> tests = new ArrayList<>();
-        for (int i = 0; i < shardCountHint; i++) {
-            tests.add(getTestShard(shardCountHint, i));
-        }
-        return tests;
-    }
-
-    private IRemoteTest getTestShard(int shardCount, int shardIndex) {
-        IRemoteTest shard = new VersionedTfLauncher(shardCount, shardIndex);
-        try {
-            OptionCopier.copyOptions(this, shard);
-        } catch (ConfigurationException e) {
-            // Bail out rather than run tests with unexpected options
-            throw new RuntimeException("failed to copy options", e);
-        }
-        return shard;
-    }
-
-}
diff --git a/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java b/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java
index e77ae62..63649fb 100644
--- a/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java
+++ b/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java
@@ -43,6 +43,8 @@
 import com.android.tradefed.util.ListInstrumentationParser;
 import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget;
 
+import com.google.common.base.Joiner;
+
 import org.junit.After;
 import org.junit.Assume;
 
@@ -52,6 +54,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * Base test class for running host JUnit4 style tests. This class provides support to install, run
@@ -601,7 +604,13 @@
                 throw new AssertionError(errorBuilder.toString());
             }
             // Assume not all tests have skipped (and rethrow AssumptionViolatedException if so)
+            List<TestResult> assumpFail =
+                    runResult.getTestsResultsInState(TestStatus.ASSUMPTION_FAILURE);
+            List<String> messages =
+                    assumpFail.stream().map(r -> r.getStackTrace()).collect(Collectors.toList());
+            String errors = Joiner.on("\n\n").join(messages);
             Assume.assumeTrue(
+                    errors,
                     runResult.getNumTests()
                             != runResult.getNumTestsInState(TestStatus.ASSUMPTION_FAILURE));
         }
diff --git a/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java b/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java
new file mode 100644
index 0000000..862dd36
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.ITestFilterReceiver;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Base implementation of {@link IRetryDecision}. Base implementation only take local signals into
+ * account.
+ */
+public class BaseRetryDecision implements IRetryDecision {
+
+    private RetryStrategy mRetryStrategy;
+    private IRemoteTest mCurrentlyConsideredTest;
+    private RetryStatsHelper mStatistics;
+
+    /** Constructor for the retry decision, always based on the {@link RetryStrategy}. */
+    public BaseRetryDecision(RetryStrategy strategy) {
+        mRetryStrategy = strategy;
+    }
+
+    @Override
+    public boolean shouldRetry(IRemoteTest test, List<TestRunResult> previousResults) {
+        // Keep track of some results for the test in progress for statistics purpose.
+        if (test != mCurrentlyConsideredTest) {
+            mCurrentlyConsideredTest = test;
+            mStatistics = new RetryStatsHelper();
+        }
+
+        switch (mRetryStrategy) {
+            case NO_RETRY:
+                // Return directly if we are not considering retry at all.
+                return false;
+            case ITERATIONS:
+                // For iterations, retry directly, we have nothing to setup
+                return true;
+            case RERUN_UNTIL_FAILURE:
+                // For retrying until failure, if any failures occurred, skip retry.
+                return !hasAnyFailures(previousResults);
+            default:
+                // Continue the logic for retry the failures.
+                break;
+        }
+
+        mStatistics.addResultsFromRun(previousResults);
+        if (!(test instanceof ITestFilterReceiver)) {
+            CLog.d(
+                    "%s does not implement ITestFilterReceiver, thus cannot work with auto-retry.",
+                    test);
+            return false;
+        }
+
+        // TODO(b/77548917): Right now we only support ITestFilterReceiver. We should expect to
+        // support ITestFile*Filter*Receiver in the future.
+        ITestFilterReceiver filterableTest = (ITestFilterReceiver) test;
+        return handleRetryFailures(filterableTest, previousResults);
+    }
+
+    @Override
+    public void addLastAttempt(List<TestRunResult> lastResults) {
+        mStatistics.addResultsFromRun(lastResults);
+    }
+
+    @Override
+    public RetryStatistics getRetryStats() {
+        return mStatistics.calculateStatistics();
+    }
+
+    /** Returns the set of failed test cases that should be retried. */
+    public static Set<TestDescription> getFailedTestCases(List<TestRunResult> previousResults) {
+        Set<TestDescription> failedTestCases = new HashSet<TestDescription>();
+        for (TestRunResult run : previousResults) {
+            if (run != null) {
+                failedTestCases.addAll(run.getFailedTests());
+            }
+        }
+        return failedTestCases;
+    }
+
+    private boolean handleRetryFailures(
+            ITestFilterReceiver test, List<TestRunResult> previousResults) {
+        if (hasRunFailures(previousResults)) {
+            return true;
+        }
+
+        // In case of test case failure, we retry with filters.
+        Set<TestDescription> previousFailedTests = getFailedTestCases(previousResults);
+        if (!previousFailedTests.isEmpty()) {
+            CLog.d("Retrying the test case failure.");
+            addRetriedTestsToIncludeFilters(test, previousFailedTests);
+            return true;
+        }
+
+        CLog.d("No test run or test case failures. No need to retry.");
+        return false;
+    }
+
+    /** Returns true if there are any failures in the previous results. */
+    private boolean hasAnyFailures(List<TestRunResult> previousResults) {
+        for (TestRunResult run : previousResults) {
+            if (run != null && (run.isRunFailure() || run.hasFailedTests())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Returns true if there are any run failures in the previous results. */
+    private boolean hasRunFailures(List<TestRunResult> previousResults) {
+        for (TestRunResult run : previousResults) {
+            if (run != null && run.isRunFailure()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Set the filters on the test runner for the retry. */
+    private void addRetriedTestsToIncludeFilters(
+            ITestFilterReceiver test, Set<TestDescription> testDescriptions) {
+        // Limit the re-run to the failure we include, so clear filters then put our failures
+        test.clearIncludeFilters();
+        for (TestDescription testCase : testDescriptions) {
+            String filter = testCase.toString();
+            test.addIncludeFilter(filter);
+        }
+    }
+}
diff --git a/src/com/android/tradefed/testtype/retry/IRetryDecision.java b/src/com/android/tradefed/testtype/retry/IRetryDecision.java
new file mode 100644
index 0000000..808520f
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/IRetryDecision.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.testtype.IRemoteTest;
+
+import java.util.List;
+
+/**
+ * Interface driving the retry decision and applying the filter on the class for more targeted
+ * retry.
+ */
+public interface IRetryDecision {
+
+    /**
+     * Decide whether or not retry should be attempted. Also make any necessary changes to the
+     * {@link IRemoteTest} to be retried (Applying filters, etc.).
+     *
+     * @param test The {@link IRemoteTest} that just ran.
+     * @param previousResults The list of {@link TestRunResult} of the test that just ran.
+     * @return True if we should retry, False otherwise.
+     */
+    public boolean shouldRetry(IRemoteTest test, List<TestRunResult> previousResults);
+
+    /**
+     * {@link #shouldRetry(IRemoteTest, List)} will most likely be called before the last retry
+     * attempt, so we might be missing the very last attempt results for statistics purpose. This
+     * method allows those results to be provided for proper statistics calculations.
+     *
+     * @param lastResults
+     */
+    public void addLastAttempt(List<TestRunResult> lastResults);
+
+    /** Returns the {@link RetryStatistics} representing the retry. */
+    public RetryStatistics getRetryStats();
+}
diff --git a/src/com/android/tradefed/testtype/retry/MergeStrategy.java b/src/com/android/tradefed/testtype/retry/MergeStrategy.java
new file mode 100644
index 0000000..d8c6869
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/MergeStrategy.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+/** Describes how the results should be aggregated when multiple attempts are present. */
+public enum MergeStrategy {
+    /** Merging should not be applied and will throw an exception. */
+    NO_MERGE,
+    /** If a single test case pass then we will consider the merged result passed. */
+    ONE_TESTCASE_PASS_IS_PASS,
+    /** If a single test run pass then we will consider the merged run result passed. */
+    ONE_TESTRUN_PASS_IS_PASS,
+    /** If a single run or test cases is a pass we will consider the merged results passed. */
+    ANY_PASS_IS_PASS,
+    /** If a single run or test cases is failed, status will be failed no matter what. */
+    ANY_FAIL_IS_FAIL;
+
+    /** Create a merge strategy based on the retry strategy. */
+    public static MergeStrategy getMergeStrategy(RetryStrategy retryStrategy) {
+        // TODO: Expand to take into account more context: postsubmit vs. presubmit
+        MergeStrategy strategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS;
+        switch (retryStrategy) {
+            case ITERATIONS:
+                strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
+                break;
+            case RERUN_UNTIL_FAILURE:
+                strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
+                break;
+            case RETRY_ANY_FAILURE:
+                strategy = MergeStrategy.ANY_PASS_IS_PASS;
+                break;
+            case NO_RETRY:
+                strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
+                break;
+        }
+        return strategy;
+    }
+}
diff --git a/src/com/android/tradefed/testtype/retry/ResultAggregator.java b/src/com/android/tradefed/testtype/retry/ResultAggregator.java
new file mode 100644
index 0000000..b8ed0bc
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/ResultAggregator.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.CollectingTestListener;
+import com.android.tradefed.result.ILogSaver;
+import com.android.tradefed.result.ILogSaverListener;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.LogFile;
+import com.android.tradefed.result.ResultAndLogForwarder;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestResult;
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.result.retry.ISupportGranularResults;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Special forwarder that aggregates the results when needed, based on the retry strategy that was
+ * taken.
+ */
+public class ResultAggregator extends CollectingTestListener {
+
+    /* Forwarder to ALL result reporters */
+    private ResultAndLogForwarder mAllForwarder;
+    /* Forwarder to result reporters that only support aggregated results */
+    private ResultAndLogForwarder mAggregatedForwarder;
+    /* Forwarder to result reporters that support the attempt reporting */
+    private ResultAndLogForwarder mDetailedForwarder;
+    private RetryStrategy mRetryStrategy;
+    // Track whether or not a module was started.
+    private boolean mModuleInProgress = false;
+    // Stores the results from non-module test runs until they are ready to be replayed.
+    private List<TestRunResult> mPureRunResults = new ArrayList<>();
+
+    public ResultAggregator(List<ITestInvocationListener> listeners, RetryStrategy strategy) {
+        mAllForwarder = new ResultAndLogForwarder(listeners);
+
+        List<ITestInvocationListener> supportDetails =
+                listeners
+                        .stream()
+                        .filter(
+                                i ->
+                                        ((i instanceof ISupportGranularResults)
+                                                && ((ISupportGranularResults) i)
+                                                        .supportGranularResults()))
+                        .collect(Collectors.toList());
+        List<ITestInvocationListener> noSupportDetails =
+                listeners
+                        .stream()
+                        .filter(
+                                i ->
+                                        !(i instanceof ISupportGranularResults)
+                                                || !((ISupportGranularResults) i)
+                                                        .supportGranularResults())
+                        .collect(Collectors.toList());
+
+        mAggregatedForwarder = new ResultAndLogForwarder(noSupportDetails);
+        mDetailedForwarder = new ResultAndLogForwarder(supportDetails);
+
+        mRetryStrategy = strategy;
+        MergeStrategy mergeStrategy = MergeStrategy.getMergeStrategy(mRetryStrategy);
+        setMergeStrategy(mergeStrategy);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void invocationStarted(IInvocationContext context) {
+        super.invocationStarted(context);
+        mAllForwarder.invocationStarted(context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void invocationFailed(Throwable cause) {
+        super.invocationFailed(cause);
+        mAllForwarder.invocationFailed(cause);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void invocationEnded(long elapsedTime) {
+        if (!mPureRunResults.isEmpty()) {
+            forwardTestRunResults(mPureRunResults, mAggregatedForwarder);
+            mPureRunResults.clear();
+        }
+        super.invocationEnded(elapsedTime);
+        mAllForwarder.invocationEnded(elapsedTime);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void testModuleStarted(IInvocationContext moduleContext) {
+        if (!mPureRunResults.isEmpty()) {
+            forwardTestRunResults(mPureRunResults, mAggregatedForwarder);
+            mPureRunResults.clear();
+        }
+
+        mModuleInProgress = true;
+        super.testModuleStarted(moduleContext);
+        mAllForwarder.testModuleStarted(moduleContext);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setLogSaver(ILogSaver logSaver) {
+        super.setLogSaver(logSaver);
+        mAllForwarder.setLogSaver(logSaver);
+    }
+
+    // ====== Forwarders to the detailed result reporters
+
+    @Override
+    public void testRunStarted(String name, int testCount, int attemptNumber, long startTime) {
+        if (!mPureRunResults.isEmpty() && !mPureRunResults.get(0).getName().equals(name)) {
+            forwardTestRunResults(mPureRunResults, mAggregatedForwarder);
+            mPureRunResults.clear();
+        }
+        super.testRunStarted(name, testCount, attemptNumber, startTime);
+        mDetailedForwarder.testRunStarted(name, testCount, attemptNumber, startTime);
+    }
+
+    @Override
+    public void testRunFailed(String errorMessage) {
+        super.testRunFailed(errorMessage);
+        mDetailedForwarder.testRunFailed(errorMessage);
+    }
+
+    @Override
+    public void testStarted(TestDescription test, long startTime) {
+        super.testStarted(test, startTime);
+        mDetailedForwarder.testStarted(test, startTime);
+    }
+
+    @Override
+    public void testIgnored(TestDescription test) {
+        super.testIgnored(test);
+        mDetailedForwarder.testIgnored(test);
+    }
+
+    @Override
+    public void testAssumptionFailure(TestDescription test, String trace) {
+        super.testAssumptionFailure(test, trace);
+        mDetailedForwarder.testAssumptionFailure(test, trace);
+    }
+
+    @Override
+    public void testFailed(TestDescription test, String trace) {
+        super.testFailed(test, trace);
+        mDetailedForwarder.testFailed(test, trace);
+    }
+
+    @Override
+    public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
+        super.testEnded(test, endTime, testMetrics);
+        mDetailedForwarder.testEnded(test, endTime, testMetrics);
+    }
+
+    @Override
+    public void logAssociation(String dataName, LogFile logFile) {
+        super.logAssociation(dataName, logFile);
+        mDetailedForwarder.logAssociation(dataName, logFile);
+    }
+
+    @Override
+    public void testLogSaved(
+            String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile) {
+        super.testLogSaved(dataName, dataType, dataStream, logFile);
+        mDetailedForwarder.testLogSaved(dataName, dataType, dataStream, logFile);
+    }
+
+    // ===== Forwarders to the aggregated reporters.
+
+    @Override
+    public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
+        super.testRunEnded(elapsedTime, runMetrics);
+        mDetailedForwarder.testRunEnded(elapsedTime, runMetrics);
+
+        // If we are not a module and we reach here. This allows to support non-suite scenarios
+        if (!mModuleInProgress) {
+            // We can't forward yet otherwise we might not have aggregated simple runs.
+            mPureRunResults.add(getCurrentRunResults());
+        }
+    }
+
+    @Override
+    public void testModuleEnded() {
+        mModuleInProgress = false;
+        super.testModuleEnded();
+        // We still forward the testModuleEnd to the detailed reporters
+        mDetailedForwarder.testModuleEnded();
+
+        List<TestRunResult> mergedResults = getMergedTestRunResults();
+        Set<String> resultNames = new HashSet<>();
+        int expectedTestCount = 0;
+        for (TestRunResult result : mergedResults) {
+            expectedTestCount += result.getExpectedTestCount();
+            resultNames.add(result.getName());
+        }
+
+        // Forward all the results aggregated
+        mAggregatedForwarder.testRunStarted(
+                getCurrentRunResults().getName(),
+                expectedTestCount,
+                /* Attempt*/ 0,
+                /* Start Time */ getCurrentRunResults().getStartTime());
+        for (TestRunResult runResult : mergedResults) {
+            forwardTestResults(runResult.getTestResults(), mAggregatedForwarder);
+            if (runResult.isRunFailure()) {
+                mAggregatedForwarder.testRunFailed(runResult.getRunFailureMessage());
+            }
+        }
+        // Provide a strong association of the run to its logs.
+        for (Entry<String, LogFile> logFile :
+                getCurrentRunResults().getRunLoggedFiles().entrySet()) {
+            mAggregatedForwarder.logAssociation(logFile.getKey(), logFile.getValue());
+        }
+        mAggregatedForwarder.testRunEnded(
+                getCurrentRunResults().getElapsedTime(),
+                getCurrentRunResults().getRunProtoMetrics());
+        mAggregatedForwarder.testModuleEnded();
+        // Ensure we don't carry results from one module to another.
+        for (String name : resultNames) {
+            clearResultsForName(name);
+        }
+    }
+
+    private void forwardTestResults(
+            Map<TestDescription, TestResult> testResults, ITestInvocationListener listener) {
+        for (Map.Entry<TestDescription, TestResult> testEntry : testResults.entrySet()) {
+            listener.testStarted(testEntry.getKey(), testEntry.getValue().getStartTime());
+            switch (testEntry.getValue().getStatus()) {
+                case FAILURE:
+                    listener.testFailed(testEntry.getKey(), testEntry.getValue().getStackTrace());
+                    break;
+                case ASSUMPTION_FAILURE:
+                    listener.testAssumptionFailure(
+                            testEntry.getKey(), testEntry.getValue().getStackTrace());
+                    break;
+                case IGNORED:
+                    listener.testIgnored(testEntry.getKey());
+                    break;
+                case INCOMPLETE:
+                    listener.testFailed(
+                            testEntry.getKey(), "Test did not complete due to exception.");
+                    break;
+                default:
+                    break;
+            }
+            // Provide a strong association of the test to its logs.
+            for (Entry<String, LogFile> logFile :
+                    testEntry.getValue().getLoggedFiles().entrySet()) {
+                if (listener instanceof ILogSaverListener) {
+                    ((ILogSaverListener) listener)
+                            .logAssociation(logFile.getKey(), logFile.getValue());
+                }
+            }
+            listener.testEnded(
+                    testEntry.getKey(),
+                    testEntry.getValue().getEndTime(),
+                    testEntry.getValue().getProtoMetrics());
+        }
+    }
+
+    /**
+     * Helper method to forward the results from multiple attempts of the same Test Run (same name).
+     */
+    private void forwardTestRunResults(List<TestRunResult> results, ILogSaverListener listener) {
+        TestRunResult result =
+                TestRunResult.merge(results, MergeStrategy.getMergeStrategy(mRetryStrategy));
+
+        listener.testRunStarted(
+                result.getName(), result.getExpectedTestCount(), 0, result.getStartTime());
+        forwardTestResults(result.getTestResults(), listener);
+        if (result.isRunFailure()) {
+            listener.testRunFailed(result.getRunFailureMessage());
+        }
+        // Provide a strong association of the run to its logs.
+        for (Entry<String, LogFile> logFile : result.getRunLoggedFiles().entrySet()) {
+            listener.logAssociation(logFile.getKey(), logFile.getValue());
+        }
+        listener.testRunEnded(result.getElapsedTime(), result.getRunProtoMetrics());
+        // Ensure we don't keep track of the results we just forwarded
+        clearResultsForName(result.getName());
+    }
+}
diff --git a/src/com/android/tradefed/testtype/retry/RetryStatistics.java b/src/com/android/tradefed/testtype/retry/RetryStatistics.java
new file mode 100644
index 0000000..4417928
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/RetryStatistics.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.testtype.IRemoteTest;
+
+import java.util.List;
+
+/**
+ * Structure holding the statistics for a retry session of one {@link IRemoteTest}. Not all fields
+ * might be populated depending of the {@link RetryStrategy}.
+ */
+public class RetryStatistics {
+    // The time spent in retry. Always populated if retries or iterations occurred
+    public long mRetryTime = 0L;
+
+    // Success and failure counts. Populated for RETRY_ANY_FAILURE.
+    public long mRetrySuccess = 0L;
+    public long mRetryFailure = 0L;
+
+    /** Helper method to aggregate the statistics of several retries. */
+    public static final RetryStatistics aggregateStatistics(List<RetryStatistics> stats) {
+        RetryStatistics aggregatedStats = new RetryStatistics();
+        for (RetryStatistics s : stats) {
+            aggregatedStats.mRetryTime += s.mRetryTime;
+            aggregatedStats.mRetrySuccess += s.mRetrySuccess;
+            aggregatedStats.mRetryFailure += s.mRetryFailure;
+        }
+        return aggregatedStats;
+    }
+}
diff --git a/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java b/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java
new file mode 100644
index 0000000..c9748ce
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestRunResult;
+
+import com.google.common.collect.Sets;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/** Calculate the retry statistics and metrics based on attempts comparison. */
+public class RetryStatsHelper {
+
+    private List<List<TestRunResult>> mResults = new ArrayList<>();
+    private RetryStatistics mStats = new RetryStatistics();
+
+    /** Add the results from the latest run to be tracked for statistics purpose. */
+    public void addResultsFromRun(List<TestRunResult> mLatestResults) {
+        if (!mResults.isEmpty()) {
+            updateSuccess(mResults.get(mResults.size() - 1), mLatestResults);
+        }
+        mResults.add(mLatestResults);
+    }
+
+    /**
+     * Calculate the retry statistics based on currently known results and return the associated
+     * {@link RetryStatistics} to represent the results.
+     */
+    public RetryStatistics calculateStatistics() {
+        if (!mResults.isEmpty()) {
+            List<TestRunResult> attemptResults = mResults.get(mResults.size() - 1);
+            Set<TestDescription> attemptFailures =
+                    BaseRetryDecision.getFailedTestCases(attemptResults);
+            mStats.mRetryFailure = attemptFailures.size();
+        }
+        return mStats;
+    }
+
+    private void updateSuccess(
+            List<TestRunResult> previousResults, List<TestRunResult> latestResults) {
+        Set<TestDescription> diff =
+                Sets.difference(
+                        BaseRetryDecision.getFailedTestCases(previousResults),
+                        BaseRetryDecision.getFailedTestCases(latestResults));
+        mStats.mRetrySuccess += diff.size();
+    }
+}
diff --git a/src/com/android/tradefed/testtype/retry/RetryStrategy.java b/src/com/android/tradefed/testtype/retry/RetryStrategy.java
new file mode 100644
index 0000000..dcce258
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/RetryStrategy.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+/** The Retry Strategy to be used when re-running some tests. */
+public enum RetryStrategy {
+    /** Do not attempt any retry */
+    NO_RETRY,
+    /** Rerun all the tests for the number of attempts specified. */
+    ITERATIONS,
+    /**
+     * Rerun all the tests until the max count is reached or a failure occurs whichever come first.
+     */
+    RERUN_UNTIL_FAILURE,
+    /**
+     * Rerun all the test run and test cases failures until passed or the max number of attempts
+     * specified. Test run failures are rerun in priority (a.k.a. if a run failure and a test case
+     * failure occur, the run failure is rerun).
+     */
+    RETRY_ANY_FAILURE,
+}
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index e66b850..9c01b43 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -17,22 +17,25 @@
 
 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
 import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.suite.params.ModuleParameters;
 import com.android.tradefed.util.ArrayUtil;
+import com.android.tradefed.util.FileUtil;
 
 import com.google.common.annotations.VisibleForTesting;
 
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -53,6 +56,7 @@
     public static final char TEST_OPTION_SHORT_NAME = 't';
     public static final String CONFIG_PATTERNS_OPTION = "config-patterns";
     private static final String MODULE_ARG_OPTION = "module-arg";
+    private static final int MAX_FILTER_DISPLAY = 20;
 
     @Option(
         name = INCLUDE_FILTER_OPTION,
@@ -148,6 +152,14 @@
     private boolean mEnableParameter = false;
 
     @Option(
+        name = "enable-optional-parameterization",
+        description =
+                "Whether or not to enable optional parameters. Optional parameters are "
+                        + "parameters not usually used by default."
+    )
+    private boolean mEnableOptionalParameter = false;
+
+    @Option(
         name = "module-parameter",
         description =
                 "Allows to run only one module parameter type instead of all the combinations. "
@@ -181,14 +193,62 @@
             SuiteModuleLoader.addFilters(mIncludeFilters, mIncludeFiltersParsed, abis);
             SuiteModuleLoader.addFilters(mExcludeFilters, mExcludeFiltersParsed, abis);
 
+            String includeFilter = mIncludeFiltersParsed.toString();
+            if (mIncludeFiltersParsed.size() > MAX_FILTER_DISPLAY) {
+                if (isSplitting()) {
+                    includeFilter = includeFilter.substring(0, 100) + "...";
+                } else {
+                    File suiteIncludeFilters = null;
+                    try {
+                        suiteIncludeFilters =
+                                FileUtil.createTempFile("suite-include-filters", ".txt");
+                        FileUtil.writeToFile(mIncludeFiltersParsed.toString(), suiteIncludeFilters);
+                        logFilterFile(
+                                suiteIncludeFilters,
+                                suiteIncludeFilters.getName(),
+                                LogDataType.TEXT);
+                        includeFilter = String.format("See %s", suiteIncludeFilters.getName());
+                    } catch (IOException e) {
+                        CLog.e(e);
+                    } finally {
+                        FileUtil.deleteFile(suiteIncludeFilters);
+                    }
+                }
+            }
+
+            String excludeFilter = mExcludeFiltersParsed.toString();
+            if (mExcludeFiltersParsed.size() > MAX_FILTER_DISPLAY) {
+                if (isSplitting()) {
+                    excludeFilter = excludeFilter.substring(0, 100) + "...";
+                } else {
+                    File suiteExcludeFilters = null;
+                    try {
+                        suiteExcludeFilters =
+                                FileUtil.createTempFile("suite-exclude-filters", ".txt");
+                        FileUtil.writeToFile(mExcludeFiltersParsed.toString(), suiteExcludeFilters);
+                        logFilterFile(
+                                suiteExcludeFilters,
+                                suiteExcludeFilters.getName(),
+                                LogDataType.TEXT);
+                        excludeFilter = String.format("See %s", suiteExcludeFilters.getName());
+                    } catch (IOException e) {
+                        CLog.e(e);
+                    } finally {
+                        FileUtil.deleteFile(suiteExcludeFilters);
+                    }
+                }
+            }
+
             CLog.d(
                     "Initializing ModuleRepo\nABIs:%s\n"
                             + "Test Args:%s\nModule Args:%s\nIncludes:%s\nExcludes:%s",
-                    abis, mTestArgs, mModuleArgs, mIncludeFiltersParsed, mExcludeFiltersParsed);
+                    abis, mTestArgs, mModuleArgs, includeFilter, excludeFilter);
+
             mModuleRepo =
                     createModuleLoader(
                             mIncludeFiltersParsed, mExcludeFiltersParsed, mTestArgs, mModuleArgs);
             mModuleRepo.setParameterizedModules(mEnableParameter);
+            mModuleRepo.setOptionalParameterizedModules(mEnableOptionalParameter);
             mModuleRepo.setModuleParameter(mForceParameter);
             mModuleRepo.setExcludedModuleParameters(mExcludedModuleParameters);
 
@@ -250,15 +310,6 @@
         return loadedConfigs;
     }
 
-    public File getTestsDir() throws FileNotFoundException {
-        IBuildInfo build = getBuildInfo();
-        if (build instanceof IDeviceBuildInfo) {
-            return ((IDeviceBuildInfo) build).getTestsDir();
-        }
-        // TODO: handle multi build?
-        throw new FileNotFoundException("Could not found a tests dir folder.");
-    }
-
     /** {@inheritDoc} */
     @Override
     public void setBuild(IBuildInfo buildInfo) {
@@ -405,4 +456,14 @@
     protected void setPrioritizeHostConfig(boolean prioritizeHostConfig) {
         mPrioritizeHostConfig = prioritizeHostConfig;
     }
+
+    /** Log a file directly to the result reporter. */
+    private void logFilterFile(File filterFile, String dataName, LogDataType type) {
+        if (getCurrentTestLogger() == null) {
+            return;
+        }
+        try (FileInputStreamSource source = new FileInputStreamSource(filterFile)) {
+            getCurrentTestLogger().testLog(dataName, type, source);
+        }
+    }
 }
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index 4b243ed..172ac97 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -29,25 +29,23 @@
 import com.android.tradefed.result.ILogSaver;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogSaverResultForwarder;
-import com.android.tradefed.result.MergeStrategy;
-import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestCollector;
 import com.android.tradefed.testtype.ITestFilterReceiver;
-import com.android.tradefed.testtype.suite.ITestSuite.RetryStrategy;
+import com.android.tradefed.testtype.retry.BaseRetryDecision;
+import com.android.tradefed.testtype.retry.IRetryDecision;
+import com.android.tradefed.testtype.retry.MergeStrategy;
+import com.android.tradefed.testtype.retry.RetryStatistics;
+import com.android.tradefed.testtype.retry.RetryStrategy;
+import com.android.tradefed.util.StreamUtil;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Sets;
 
 import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 /**
  * A wrapper class works on the {@link IRemoteTest} to granulate the IRemoteTest in testcase level.
@@ -87,16 +85,9 @@
     private boolean mCollectTestsOnly = false;
 
     // Tracking of the metrics
-    /** How much time are we spending doing the retry attempts */
-    private long mRetryTime = 0L;
-    /** The number of test cases that passed after a failed attempt */
-    private long mSuccessRetried = 0L;
-    /** The number of test cases that remained failed after all retry attempts */
-    private long mFailedRetried = 0L;
-    /** Store the test that successfully re-run and at which attempt they passed */
-    private Map<String, Integer> mAttemptSuccess = new HashMap<>();
+    private RetryStatistics mRetryStats = null;
 
-    private RetryStrategy mRetryStrategy = RetryStrategy.RETRY_TEST_CASE_FAILURE;
+    private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
     private boolean mRebootAtLastRetry = false;
 
     public GranularRetriableTestWrapper(
@@ -204,7 +195,9 @@
         }
 
         // The module collectors itself are added: this list will be very limited.
-        for (IMetricCollector collector : mModuleConfiguration.getMetricCollectors()) {
+        // We clone them since the configuration object is shared across shards.
+        for (IMetricCollector collector :
+                CollectorHelper.cloneCollectors(mModuleConfiguration.getMetricCollectors())) {
             if (collector.isDisabled()) {
                 CLog.d("%s has been disabled. Skipping.", collector);
             } else {
@@ -232,74 +225,23 @@
             return;
         }
 
-        // If the very first attempt failed, then don't proceed.
-        if (RetryStrategy.RERUN_UNTIL_FAILURE.equals(mRetryStrategy)) {
-            Set<TestDescription> lastRun = getFailedTestCases(0);
-            // If we encountered a failure
-            if (!lastRun.isEmpty() || mMainGranularRunListener.hasRunCrashedAtAttempt(0)) {
-                CLog.w("%s failed after the first run. Stopping.", lastRun);
-                return;
-            }
+        // Bail out early if there is no need to retry at all.
+        IRetryDecision retryDecision = new BaseRetryDecision(mRetryStrategy);
+        if (!retryDecision.shouldRetry(mTest, mMainGranularRunListener.getTestRunForAttempts(0))) {
+            return;
         }
 
         // Deal with retried attempted
         long startTime = System.currentTimeMillis();
-        Set<TestDescription> previousFailedTests = null;
-        Set<String> originalFilters = new HashSet<>();
-
-        // TODO(b/77548917): Right now we only support ITestFilterReceiver. We should expect to
-        // support ITestFile*Filter*Receiver in the future.
-        if (mTest instanceof ITestFilterReceiver) {
-            ITestFilterReceiver test = (ITestFilterReceiver) mTest;
-            originalFilters = new LinkedHashSet<>(test.getIncludeFilters());
-        } else if (!shouldHandleFailure(mRetryStrategy)) {
-            // TODO: improve this for test run failures, since they rerun the full run we should
-            // be able to rerun even non-ITestFilterReceiver
-            CLog.d("RetryStrategy does not involved moving filters proceeding with retry.");
-        } else {
-            CLog.d(
-                    "%s does not implement ITestFilterReceiver, thus cannot work with "
-                            + "intra-module retry.",
-                    mTest);
-            return;
-        }
-
         try {
             CLog.d("Starting intra-module retry.");
             for (int attemptNumber = 1; attemptNumber < mMaxRunLimit; attemptNumber++) {
-                CLog.d("Retry attempt number %s", attemptNumber);
-                // Reset the filters to original.
-                if (mTest instanceof ITestFilterReceiver) {
-                    ((ITestFilterReceiver) mTest).clearIncludeFilters();
-                    ((ITestFilterReceiver) mTest).addAllIncludeFilters(originalFilters);
-                }
-                // TODO: sort out the collection of metrics for each strategy
-                if (shouldHandleFailure(mRetryStrategy)) {
-                    boolean shouldContinue = false;
-                    // In case of test run failure and we should retry test runs
-                    if (RetryStrategy.RETRY_TEST_RUN_FAILURE.equals(mRetryStrategy)
-                            || RetryStrategy.RETRY_ANY_FAILURE.equals(mRetryStrategy)) {
-                        if (mMainGranularRunListener.hasRunCrashedAtAttempt(attemptNumber - 1)) {
-                            CLog.d("Retrying the run failure.");
-                            shouldContinue = true;
-                        }
-                    }
-
-                    if (RetryStrategy.RETRY_TEST_CASE_FAILURE.equals(mRetryStrategy)
-                            || RetryStrategy.RETRY_ANY_FAILURE.equals(mRetryStrategy)) {
-                        // In case of test case failure, we retry with filters.
-                        previousFailedTests = getFailedTestCases(attemptNumber - 1);
-                        if (previousFailedTests.size() > 0 && !shouldContinue) {
-                            CLog.d("Retrying the test case failure.");
-                            shouldContinue = true;
-                            addRetriedTestsToIncludeFilters(mTest, previousFailedTests);
-                        }
-                    }
-
-                    if (!shouldContinue) {
-                        CLog.d("No test run or test case failures. No need to retry.");
-                        break;
-                    }
+                boolean retry =
+                        retryDecision.shouldRetry(
+                                mTest,
+                                mMainGranularRunListener.getTestRunForAttempts(attemptNumber - 1));
+                if (!retry) {
+                    return;
                 }
                 // Reboot device at the last intra-module retry if reboot-at-last-retry is set.
                 if (mRebootAtLastRetry && (attemptNumber == (mMaxRunLimit-1))) {
@@ -313,80 +255,14 @@
                 }
                 // Run the tests again
                 intraModuleRun(allListeners);
-
-                Set<TestDescription> lastRun = getFailedTestCases(attemptNumber);
-                if (shouldHandleFailure(mRetryStrategy)) {
-                    // Evaluate success from what we just ran
-                    if (previousFailedTests != null) {
-                        Set<TestDescription> diff = Sets.difference(previousFailedTests, lastRun);
-                        mSuccessRetried += diff.size();
-                        final int currentAttempt = attemptNumber;
-                        diff.forEach(
-                                (desc) -> mAttemptSuccess.put(desc.toString(), currentAttempt));
-                        previousFailedTests = lastRun;
-                    }
-                }
-
-                if (RetryStrategy.RERUN_UNTIL_FAILURE.equals(mRetryStrategy)) {
-                    // If we encountered a failure do not proceed
-                    if (!lastRun.isEmpty()
-                            || mMainGranularRunListener.hasRunCrashedAtAttempt(attemptNumber)) {
-                        CLog.w("%s failed at iteration %s. Stopping.", lastRun, attemptNumber);
-                        break;
-                    }
-                }
             }
+            // Feed the last attempt if we reached here.
+            retryDecision.addLastAttempt(
+                    mMainGranularRunListener.getTestRunForAttempts(mMaxRunLimit - 1));
         } finally {
-            if (previousFailedTests != null) {
-                mFailedRetried += previousFailedTests.size();
-            }
+            mRetryStats = retryDecision.getRetryStats();
             // Track how long we spend in retry
-            mRetryTime = System.currentTimeMillis() - startTime;
-        }
-    }
-
-    /**
-     * If the strategy needs to handle some failures return True. If it needs to retry no matter
-     * what like {@link RetryStrategy#ITERATIONS} returns False.
-     */
-    private boolean shouldHandleFailure(RetryStrategy retryStrategy) {
-        return RetryStrategy.RETRY_ANY_FAILURE.equals(retryStrategy)
-                || RetryStrategy.RETRY_TEST_RUN_FAILURE.equals(retryStrategy)
-                || RetryStrategy.RETRY_TEST_CASE_FAILURE.equals(retryStrategy);
-    }
-
-    /**
-     * Collect failed test cases from listener.
-     *
-     * @param attemptNumber the 0-indexed integer indicating which attempt to gather failed cases.
-     */
-    private Set<TestDescription> getFailedTestCases(int attemptNumber) {
-        Set<TestDescription> failedTestCases = new HashSet<TestDescription>();
-        for (String runName : mMainGranularRunListener.getTestRunNames()) {
-            TestRunResult run =
-                    mMainGranularRunListener.getTestRunAtAttempt(runName, attemptNumber);
-            if (run != null) {
-                failedTestCases.addAll(run.getFailedTests());
-            }
-        }
-        return failedTestCases;
-    }
-
-    /**
-     * Update the arguments of {@link IRemoteTest} to only run failed tests. This arguments/logic is
-     * implemented differently for each IRemoteTest testtype in the overridden
-     * ITestFilterReceiver.addIncludeFilter method.
-     *
-     * @param test The {@link IRemoteTest} to evaluate as ITestFilterReceiver.
-     * @param testDescriptions The set of failed testDescriptions to retry.
-     */
-    private void addRetriedTestsToIncludeFilters(
-            IRemoteTest test, Set<TestDescription> testDescriptions) {
-        if (test instanceof ITestFilterReceiver) {
-            for (TestDescription testCase : testDescriptions) {
-                String filter = testCase.toString();
-                ((ITestFilterReceiver) test).addIncludeFilter(filter);
-            }
+            mRetryStats.mRetryTime = System.currentTimeMillis() - startTime;
         }
     }
 
@@ -416,7 +292,7 @@
             CLog.e("Module '%s' - test '%s' threw exception:", mModuleId, mTest.getClass());
             CLog.e(re);
             CLog.e("Proceeding to the next test.");
-            runListener.testRunFailed(re.getMessage());
+            runListener.testRunFailed(StreamUtil.getStackTrace(re));
         } catch (DeviceUnresponsiveException due) {
             // being able to catch a DeviceUnresponsiveException here implies that recovery was
             // successful, and test execution should proceed to next module.
@@ -439,26 +315,7 @@
 
     /** Get the merged TestRunResults from each {@link IRemoteTest} run. */
     public final List<TestRunResult> getFinalTestRunResults() {
-        // TODO: Once we are ready to report break-down of results and option will override this.
-        MergeStrategy strategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS;
-        switch (mRetryStrategy) {
-            case ITERATIONS:
-                strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
-                break;
-            case RERUN_UNTIL_FAILURE:
-                strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
-                break;
-            case RETRY_ANY_FAILURE:
-                strategy = MergeStrategy.ANY_PASS_IS_PASS;
-                break;
-            case RETRY_TEST_CASE_FAILURE:
-                strategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS;
-                break;
-            case RETRY_TEST_RUN_FAILURE:
-                strategy = MergeStrategy.ONE_TESTRUN_PASS_IS_PASS;
-                break;
-        }
-
+        MergeStrategy strategy = MergeStrategy.getMergeStrategy(mRetryStrategy);
         mMainGranularRunListener.setMergeStrategy(strategy);
         return mMainGranularRunListener.getMergedTestRunResults();
     }
@@ -477,11 +334,6 @@
         return CollectorHelper.cloneCollectors(originalCollectors);
     }
 
-    /** Check if any testRunResult has ever failed. This check is used for bug report only. */
-    public boolean hasFailed() {
-        return mMainGranularRunListener.hasFailed();
-    }
-
     /**
      * Calculate the number of testcases in the {@link IRemoteTest}. This value distincts the same
      * testcases that are rescheduled multiple times.
@@ -490,19 +342,12 @@
         return mMainGranularRunListener.getExpectedTests();
     }
 
-    /** Returns the elapsed time in retry attempts. */
-    public final long getRetryTime() {
-        return mRetryTime;
-    }
-
-    /** Returns the number of tests we managed to change status from failed to pass. */
-    public final long getRetrySuccess() {
-        return mSuccessRetried;
-    }
-
-    /** Returns the number of tests we couldn't change status from failed to pass. */
-    public final long getRetryFailed() {
-        return mFailedRetried;
+    /**
+     * Returns the {@link RetryStatistics} representating the retry information. Null if no retry
+     * occurred.
+     */
+    public final RetryStatistics getRetryStatistics() {
+        return mRetryStats;
     }
 
     /** Returns the listener containing all the results. */
@@ -510,11 +355,6 @@
         return mMainGranularRunListener;
     }
 
-    /** Returns the attempts that turned into success. */
-    public Map<String, Integer> getAttemptSuccessStats() {
-        return mAttemptSuccess;
-    }
-
     /** Forwarder that also handles passing the current attempt we are at. */
     private class RetryLogSaverResultForwarder extends LogSaverResultForwarder {
 
diff --git a/src/com/android/tradefed/testtype/suite/ITestSuite.java b/src/com/android/tradefed/testtype/suite/ITestSuite.java
index faa79be..370d6e5 100644
--- a/src/com/android/tradefed/testtype/suite/ITestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/ITestSuite.java
@@ -18,7 +18,9 @@
 import com.android.annotations.VisibleForTesting;
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.IDeviceConfiguration;
@@ -58,6 +60,7 @@
 import com.android.tradefed.testtype.IRuntimeHintProvider;
 import com.android.tradefed.testtype.IShardableTest;
 import com.android.tradefed.testtype.ITestCollector;
+import com.android.tradefed.testtype.retry.RetryStrategy;
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.MultiMap;
@@ -66,6 +69,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 
+import java.io.File;
+import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -80,6 +85,7 @@
 import java.util.Map.Entry;
 import java.util.Random;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Abstract class used to run Test Suite. This class provide the base of how the Suite will be run.
@@ -98,30 +104,8 @@
                 IMetricCollectorReceiver,
                 IConfigurationReceiver,
                 IReportNotExecuted,
-                ITokenRequest {
-
-    /** The Retry Strategy to be used when re-running some tests. */
-    public enum RetryStrategy {
-        /** Rerun all the tests for the number of attempts specified. */
-        ITERATIONS,
-        /**
-         * Rerun all the tests until the max count is reached or a failure occurs whichever come
-         * first.
-         */
-        RERUN_UNTIL_FAILURE,
-        /**
-         * Rerun all the test case failures until passed or the max number of attempts specified.
-         */
-        RETRY_TEST_CASE_FAILURE,
-        /** Rerun all the test run failures until passed or the max number of attempts specified. */
-        RETRY_TEST_RUN_FAILURE,
-        /**
-         * Rerun all the test run and test cases failures until passed or the max number of attempts
-         * specified. Test run failures are rerun in priority (a.k.a. if a run failure and a test
-         * case failure occur, the run failure is rerun).
-         */
-        RETRY_ANY_FAILURE,
-    }
+                ITokenRequest,
+                ITestLoggerReceiver {
 
     public static final String SKIP_SYSTEM_STATUS_CHECKER = "skip-system-status-check";
     public static final String RUNNER_WHITELIST = "runner-whitelist";
@@ -135,6 +119,8 @@
     public static final String TOKEN_KEY = "token";
     public static final String MODULE_METADATA_INCLUDE_FILTER = "module-metadata-include-filter";
     public static final String MODULE_METADATA_EXCLUDE_FILTER = "module-metadata-exclude-filter";
+    public static final String RANDOM_SEED = "random-seed";
+    public static final String REBOOT_BEFORE_TEST = "reboot-before-test";
 
     private static final String PRODUCT_CPU_ABI_KEY = "ro.product.cpu.abi";
 
@@ -183,6 +169,12 @@
         description = "Reboot the device at the last intra-module retry")
     private boolean mRebootAtLastRetry = false;
 
+    @Option(
+        name = REBOOT_BEFORE_TEST,
+        description = "Reboot the device before the test suite starts."
+    )
+    private boolean mRebootBeforeTest = false;
+
     @Option(name = "skip-all-system-status-check",
             description = "Whether all system status check between modules should be skipped")
     private boolean mSkipAllSystemStatusCheck = false;
@@ -211,7 +203,7 @@
     private boolean mRandomOrder = false;
 
     @Option(
-        name = "random-seed",
+        name = RANDOM_SEED,
         description = "Seed to randomize the order of the modules."
     )
     private long mRandomSeed = -1;
@@ -293,13 +285,8 @@
     )
     private boolean mIsolatedModule = false;
 
-    @Option(
-        name = "reboot-before-test",
-        description = "Reboot the device before the test suite starts."
-    )
-    private boolean mRebootBeforeTest = false;
-
-    // [Options relate to module retry and intra-module retry][
+    /** @deprecated to be deleted when next version is deployed */
+    @Deprecated
     @Option(
         name = "max-testcase-run-count",
         description =
@@ -308,14 +295,17 @@
     )
     private int mMaxRunLimit = 1;
 
+    /** @deprecated to be deleted when next version is deployed */
+    @Deprecated
     @Option(
         name = "retry-strategy",
         description =
                 "The retry strategy to be used when re-running some tests with "
                         + "--max-testcase-run-count"
     )
-    private RetryStrategy mRetryStrategy = RetryStrategy.RETRY_TEST_CASE_FAILURE;
+    private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
 
+    // [Options relate to module retry and intra-module retry][
     @Option(
         name = "merge-attempts",
         description = "Whether or not to use the merge the results of the different attempts."
@@ -341,6 +331,17 @@
 
     // Current modules to run, null if not started to run yet.
     private List<ModuleDefinition> mRunModules = null;
+    // Logger to be used to files.
+    private ITestLogger mCurrentLogger = null;
+    // Whether or not we are currently in split
+    private boolean mIsSplitting = false;
+
+    private DynamicRemoteFileResolver mDynamicResolver = new DynamicRemoteFileResolver();
+
+    @VisibleForTesting
+    void setDynamicResolver(DynamicRemoteFileResolver resolver) {
+        mDynamicResolver = resolver;
+    }
 
     /**
      * Get the current Guice {@link Injector} from the invocation. It should allow us to continue
@@ -382,6 +383,15 @@
         }
     }
 
+    public File getTestsDir() throws FileNotFoundException {
+        IBuildInfo build = getBuildInfo();
+        if (build instanceof IDeviceBuildInfo) {
+            return ((IDeviceBuildInfo) build).getTestsDir();
+        }
+        // TODO: handle multi build?
+        throw new FileNotFoundException("Could not found a tests dir folder.");
+    }
+
     private LinkedHashMap<String, IConfiguration> loadAndFilter() {
         LinkedHashMap<String, IConfiguration> runConfig = loadTests();
         if (runConfig.isEmpty()) {
@@ -391,6 +401,7 @@
         // Apply our guice scope to all modules objects
         applyGuiceInjection(runConfig);
 
+        Set<String> moduleNames = new HashSet<>();
         LinkedHashMap<String, IConfiguration> filteredConfig = new LinkedHashMap<>();
         for (Entry<String, IConfiguration> config : runConfig.entrySet()) {
             if (!mModuleMetadataIncludeFilter.isEmpty()
@@ -411,11 +422,51 @@
             }
             filterPreparers(config.getValue(), mAllowedPreparers);
             filteredConfig.put(config.getKey(), config.getValue());
+            moduleNames.add(config.getValue().getConfigurationDescription().getModuleName());
         }
+
+        if (mBuildInfo != null
+                && mBuildInfo.getRemoteFiles() != null
+                && mBuildInfo.getRemoteFiles().size() > 0) {
+            stageTestArtifacts(moduleNames);
+        }
+
         runConfig.clear();
         return filteredConfig;
     }
 
+    /** Helper to download all artifacts for the given modules. */
+    private void stageTestArtifacts(Set<String> modules) {
+        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$");
+        for (File remoteFile : mBuildInfo.getRemoteFiles()) {
+            try {
+                mDynamicResolver.resolvePartialDownloadZip(
+                        getTestsDir(), remoteFile.toString(), includeFilters, excludeFilters);
+            } catch (ConfigurationException | FileNotFoundException e) {
+                CLog.e(
+                        String.format(
+                                "Failed to download partial zip from %s for modules: %s",
+                                remoteFile, String.join(", ", modules)));
+                CLog.e(e);
+                throw new RuntimeException(e);
+            }
+        }
+        long elapsedTime = System.currentTimeMillis() - startTime;
+        CLog.i(
+                String.format(
+                        "Staging test artifacts for %d modules finished in %s.",
+                        modules.size(), TimeUtil.formatElapsedTime(elapsedTime)));
+    }
+
     /** Helper that creates and returns the list of {@link ModuleDefinition} to be executed. */
     private List<ModuleDefinition> createExecutionList() {
         List<ModuleDefinition> runModules = new ArrayList<>();
@@ -478,6 +529,7 @@
         }
         CLog.i("Randomizing all the modules with seed: %s", randomSeed);
         Collections.shuffle(runModules, new Random(randomSeed));
+        mBuildInfo.addBuildAttribute(RANDOM_SEED, String.valueOf(randomSeed));
     }
 
     private void checkClassLoad(Set<String> classes, String type) {
@@ -518,6 +570,7 @@
     /** Generic run method for all test loaded from {@link #loadTests()}. */
     @Override
     public final void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+        mCurrentLogger = listener;
         // Load and check the module checkers, runners and preparers in black and whitelist
         checkClassLoad(mSystemStatusCheckBlacklist, SKIP_SYSTEM_STATUS_CHECKER);
         checkClassLoad(mAllowedRunners, RUNNER_WHITELIST);
@@ -682,12 +735,17 @@
         // Pass the main invocation logSaver
         module.setLogSaver(mMainConfiguration.getLogSaver());
         // Pass the retry strategy to the module
-        module.setRetryStrategy(mRetryStrategy, mMergeAttempts);
+        module.setRetryStrategy(
+                getConfiguration().getCommandOptions().getRetryStrategy(), mMergeAttempts);
         // Pass the reboot strategy at the last intra-module retry to the module
         module.setRebootAtLastRetry(mRebootAtLastRetry);
 
         // Actually run the module
-        module.run(listener, moduleListeners, failureListener, mMaxRunLimit);
+        module.run(
+                listener,
+                moduleListeners,
+                failureListener,
+                getConfiguration().getCommandOptions().getMaxRetryCount());
 
         if (!mSkipAllSystemStatusCheck) {
             runPostModuleCheck(module.getId(), mSystemStatusCheckers, mDevice, listener);
@@ -813,6 +871,11 @@
                 System.currentTimeMillis() - startTime, new HashMap<String, Metric>());
     }
 
+    /** Returns true if we are currently in {@link #split(int)}. */
+    public boolean isSplitting() {
+        return mIsSplitting;
+    }
+
     /** {@inheritDoc} */
     @Override
     public Collection<IRemoteTest> split(int shardCountHint) {
@@ -820,39 +883,47 @@
             // cannot shard or already sharded
             return null;
         }
+        mIsSplitting = true;
+        try {
+            LinkedHashMap<String, IConfiguration> runConfig = loadAndFilter();
+            if (runConfig.isEmpty()) {
+                CLog.i("No config were loaded. Nothing to run.");
+                return null;
+            }
+            injectInfo(runConfig);
 
-        LinkedHashMap<String, IConfiguration> runConfig = loadAndFilter();
-        if (runConfig.isEmpty()) {
-            CLog.i("No config were loaded. Nothing to run.");
-            return null;
+            // We split individual tests on double the shardCountHint to provide better average.
+            // The test pool mechanism prevent this from creating too much overhead.
+            List<ModuleDefinition> splitModules =
+                    ModuleSplitter.splitConfiguration(
+                            runConfig,
+                            shardCountHint,
+                            mShouldMakeDynamicModule,
+                            mIntraModuleSharding);
+            runConfig.clear();
+            runConfig = null;
+
+            // Clean up the parent that will get sharded: It is fine to clean up before copying the
+            // options, because the sharded module is already created/populated so there is no need
+            // to carry these extra data.
+            cleanUpSuiteSetup();
+
+            // create an association of one ITestSuite <=> one ModuleDefinition as the smallest
+            // execution unit supported.
+            List<IRemoteTest> splitTests = new ArrayList<>();
+            for (ModuleDefinition m : splitModules) {
+                ITestSuite suite = createInstance();
+                OptionCopier.copyOptionsNoThrow(this, suite);
+                suite.mIsSharded = true;
+                suite.mDirectModule = m;
+                splitTests.add(suite);
+            }
+            // return the list of ITestSuite with their ModuleDefinition assigned
+            return splitTests;
+        } finally {
+            // Done splitting at that point
+            mIsSplitting = false;
         }
-        injectInfo(runConfig);
-
-        // We split individual tests on double the shardCountHint to provide better average.
-        // The test pool mechanism prevent this from creating too much overhead.
-        List<ModuleDefinition> splitModules =
-                ModuleSplitter.splitConfiguration(
-                        runConfig, shardCountHint, mShouldMakeDynamicModule, mIntraModuleSharding);
-        runConfig.clear();
-        runConfig = null;
-
-        // Clean up the parent that will get sharded: It is fine to clean up before copying the
-        // options, because the sharded module is already created/populated so there is no need
-        // to carry these extra data.
-        cleanUpSuiteSetup();
-
-        // create an association of one ITestSuite <=> one ModuleDefinition as the smallest
-        // execution unit supported.
-        List<IRemoteTest> splitTests = new ArrayList<>();
-        for (ModuleDefinition m : splitModules) {
-            ITestSuite suite = createInstance();
-            OptionCopier.copyOptionsNoThrow(this, suite);
-            suite.mIsSharded = true;
-            suite.mDirectModule = m;
-            splitTests.add(suite);
-        }
-        // return the list of ITestSuite with their ModuleDefinition assigned
-        return splitTests;
     }
 
     /**
@@ -963,9 +1034,14 @@
         mContext = invocationContext;
     }
 
-    /** Set the max number of run attempt for each module. */
-    public final void setMaxRunLimit(int maxRunLimit) {
-        mMaxRunLimit = maxRunLimit;
+    /** {@inheritDoc} */
+    @Override
+    public void setTestLogger(ITestLogger testLogger) {
+        mCurrentLogger = testLogger;
+    }
+
+    public ITestLogger getCurrentTestLogger() {
+        return mCurrentLogger;
     }
 
     /** {@inheritDoc} */
@@ -987,6 +1063,11 @@
         mMainConfiguration = configuration;
     }
 
+    /** Returns the invocation {@link IConfiguration}. */
+    public final IConfiguration getConfiguration() {
+        return mMainConfiguration;
+    }
+
     /** {@inheritDoc} */
     @Override
     public void reportNotExecuted(ITestInvocationListener listener) {
@@ -1154,6 +1235,11 @@
         return mInjector;
     }
 
+    /** Sets reboot-before-test to true. */
+    public final void enableRebootBeforeTest() {
+        mRebootBeforeTest = true;
+    }
+
     /**
      * Apply the metadata filter to the config and see if the config should run.
      *
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index 45b78b0..665ec6f 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -56,7 +56,8 @@
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.IRuntimeHintProvider;
 import com.android.tradefed.testtype.ITestCollector;
-import com.android.tradefed.testtype.suite.ITestSuite.RetryStrategy;
+import com.android.tradefed.testtype.retry.RetryStatistics;
+import com.android.tradefed.testtype.retry.RetryStrategy;
 import com.android.tradefed.testtype.suite.module.BaseModuleController;
 import com.android.tradefed.testtype.suite.module.IModuleController.RunStrategy;
 import com.android.tradefed.util.StreamUtil;
@@ -106,8 +107,6 @@
     public static final String RETRY_SUCCESS_COUNT = "MODULE_RETRY_SUCCESS";
     public static final String RETRY_FAIL_COUNT = "MODULE_RETRY_FAILED";
 
-    private static final String FLAKE_DATE_PREFIX = "FLAKE_DATA:";
-
     private final IInvocationContext mModuleInvocationContext;
     private final IConfiguration mModuleConfiguration;
     private ILogSaver mLogSaver;
@@ -135,13 +134,9 @@
     private long mStartTestTime = 0l;
 
     // Tracking of retry performance
-    private long mRetryTime = 0L;
-    /** The number of test cases that passed after a failed attempt */
-    private long mSuccessRetried = 0L;
-    /** The number of test cases that remained failed after all retry attempts */
-    private long mFailedRetried = 0L;
+    private List<RetryStatistics> mRetryStats = new ArrayList<>();
 
-    private RetryStrategy mRetryStrategy = RetryStrategy.RETRY_TEST_CASE_FAILURE;
+    private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
     private boolean mMergeAttempts = true;
     private boolean mRebootAtLastRetry = false;
 
@@ -491,15 +486,14 @@
 
                     mExpectedTests += retriableTest.getExpectedTestsCount();
                     // Get information about retry
-                    mRetryTime += retriableTest.getRetryTime();
-                    mSuccessRetried += retriableTest.getRetrySuccess();
-                    mFailedRetried += retriableTest.getRetryFailed();
-
-                    addAttemptStatsToBuild(mBuild, retriableTest.getAttemptSuccessStats());
+                    RetryStatistics res = retriableTest.getRetryStatistics();
+                    if (res != null) {
+                        mRetryStats.add(res);
+                    }
                 }
                 // After the run, if the test failed (even after retry the final result passed) has
                 // failed, capture a bugreport.
-                if (retriableTest.hasFailed()) {
+                if (retriableTest.getResultListener().hasFailed()) {
                     captureBugreport(listener, getId());
                 }
             }
@@ -655,13 +649,16 @@
         metricsProto.put(
                 TEST_TIME, TfMetricProtoUtil.createSingleValue(elapsedTime, "milliseconds"));
         // Report all the retry informations
-        if (mRetryTime > 0L) {
+        if (!mRetryStats.isEmpty()) {
+            RetryStatistics agg = RetryStatistics.aggregateStatistics(mRetryStats);
             metricsProto.put(
-                    RETRY_TIME, TfMetricProtoUtil.createSingleValue(mRetryTime, "milliseconds"));
+                    RETRY_TIME,
+                    TfMetricProtoUtil.createSingleValue(agg.mRetryTime, "milliseconds"));
             metricsProto.put(
-                    RETRY_SUCCESS_COUNT, TfMetricProtoUtil.createSingleValue(mSuccessRetried, ""));
+                    RETRY_SUCCESS_COUNT,
+                    TfMetricProtoUtil.createSingleValue(agg.mRetrySuccess, ""));
             metricsProto.put(
-                    RETRY_FAIL_COUNT, TfMetricProtoUtil.createSingleValue(mFailedRetried, ""));
+                    RETRY_FAIL_COUNT, TfMetricProtoUtil.createSingleValue(agg.mRetryFailure, ""));
         }
 
         if (totalExpectedTests != numResults) {
@@ -968,11 +965,4 @@
         }
         return RunStrategy.RUN;
     }
-
-    private void addAttemptStatsToBuild(IBuildInfo build, Map<String, Integer> attemptStats) {
-        for (Entry<String, Integer> entry : attemptStats.entrySet()) {
-            String key = String.format("%s%s:%s", FLAKE_DATE_PREFIX, getId(), entry.getKey());
-            build.addBuildAttribute(key, Integer.toString(entry.getValue()));
-        }
-    }
 }
diff --git a/src/com/android/tradefed/testtype/suite/ModuleListener.java b/src/com/android/tradefed/testtype/suite/ModuleListener.java
index 5c78229..86057d2 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleListener.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleListener.java
@@ -26,7 +26,6 @@
 import com.android.tradefed.result.LogFile;
 import com.android.tradefed.result.LogSaverResultForwarder;
 import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.testtype.IRemoteTest;
 
 import java.util.HashMap;
@@ -61,6 +60,11 @@
 
     @Override
     public void testRunStarted(String name, int numTests, int attemptNumber) {
+        testRunStarted(name, numTests, attemptNumber, System.currentTimeMillis());
+    }
+
+    @Override
+    public void testRunStarted(String name, int numTests, int attemptNumber, long startTime) {
         mRunInProgress = true;
         // In case of retry of the same run, do not add the expected count again. This allows
         // situation where test runner has a built-in retry (like InstrumentationTest) and calls
@@ -68,10 +72,11 @@
         if (getTestRunAtAttempt(name, attemptNumber) != null) {
             numTests = 0;
         }
-        super.testRunStarted(name, numTests, attemptNumber);
+        super.testRunStarted(name, numTests, attemptNumber, startTime);
         if (attemptNumber != 0) {
             mTestsRan = 1;
         }
+        CLog.d("ModuleListener.testRunStarted(%s, %s, %s)", name, numTests, attemptNumber);
     }
 
     /** {@inheritDoc} */
@@ -111,7 +116,12 @@
     private void logTestPassed(String testName) {
         if (!mTestFailed && !mCollectTestsOnly) {
             CLog.logAndDisplay(
-                    LogLevel.INFO, "[%d/%d] %s pass", mTestsRan, getExpectedTests(), testName);
+                    LogLevel.INFO,
+                    "[%d/%d] %s %s pass",
+                    mTestsRan,
+                    getExpectedTests(),
+                    getCurrentRunResults().getName(),
+                    testName);
         }
         mTestsRan++;
     }
@@ -137,9 +147,10 @@
         }
         CLog.logAndDisplay(
                 LogLevel.INFO,
-                "[%d/%d] %s fail:\n%s",
+                "[%d/%d] %s %s fail:\n%s",
                 mTestsRan,
                 getExpectedTests(),
+                getCurrentRunResults().getName(),
                 test.toString(),
                 trace);
         mTestFailed = true;
@@ -188,20 +199,4 @@
             }
         }
     }
-
-    /**
-     * Check if any runs in the given attempt have incompleted (aka "run failure").
-     *
-     * @param attemptNumber indicates which attempt should the test runs come from.
-     * @return true if any of the runs in the given attempt has crashed.
-     */
-    public boolean hasRunCrashedAtAttempt(int attemptNumber) {
-        for (String runName : getTestRunNames()) {
-            TestRunResult run = getTestRunAtAttempt(runName, attemptNumber);
-            if (run != null && run.isRunFailure()) {
-                return true;
-            }
-        }
-        return false;
-    }
 }
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index 78d7b2f..08dd626 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -15,12 +15,13 @@
  */
 package com.android.tradefed.testtype.suite;
 
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
+import com.android.tradefed.config.ConfigurationDescriptor;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
 import com.android.tradefed.config.ConfigurationUtil;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationFactory;
+import com.android.tradefed.config.OptionDef;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.testtype.IAbi;
@@ -71,6 +72,7 @@
     private IConfigurationFactory mConfigFactory = ConfigurationFactory.getInstance();
 
     private boolean mAllowParameterizedModules = false;
+    private boolean mAllowOptionalParameterizedModules = false;
     private ModuleParameters mForcedModuleParameter = null;
     private Set<ModuleParameters> mExcludedModuleParameters = new HashSet<>();
 
@@ -100,6 +102,11 @@
         mAllowParameterizedModules = allowed;
     }
 
+    /** Sets whether or not to allow optional parameterized modules. */
+    public final void setOptionalParameterizedModules(boolean allowed) {
+        mAllowOptionalParameterizedModules = allowed;
+    }
+
     /** Sets the only {@link ModuleParameters} type that should be run. */
     public final void setModuleParameter(ModuleParameters param) {
         mForcedModuleParameter = param;
@@ -156,17 +163,16 @@
      *
      * @param test The {@link IRemoteTest} that is being considered.
      * @param abi The Abi we are currently working on.
-     * @param name The name of the module.
+     * @param moduleId The id of the module (usually abi + module name).
      * @param includeFilters The formatted and parsed include filters.
      * @param excludeFilters The formatted and parsed exclude filters.
      */
     public void addFiltersToTest(
             IRemoteTest test,
             IAbi abi,
-            String name,
+            String moduleId,
             Map<String, List<SuiteTestFilter>> includeFilters,
             Map<String, List<SuiteTestFilter>> excludeFilters) {
-        String moduleId = AbiUtils.createId(abi.getName(), name);
         if (!(test instanceof ITestFilterReceiver)) {
             CLog.e("Test in module %s does not implement ITestFilterReceiver.", moduleId);
             return;
@@ -174,10 +180,10 @@
         List<SuiteTestFilter> mdIncludes = getFilterList(includeFilters, moduleId);
         List<SuiteTestFilter> mdExcludes = getFilterList(excludeFilters, moduleId);
         if (!mdIncludes.isEmpty()) {
-            addTestIncludes((ITestFilterReceiver) test, mdIncludes, name);
+            addTestIncludes((ITestFilterReceiver) test, mdIncludes, moduleId);
         }
         if (!mdExcludes.isEmpty()) {
-            addTestExcludes((ITestFilterReceiver) test, mdExcludes, name);
+            addTestExcludes((ITestFilterReceiver) test, mdExcludes, moduleId);
         }
     }
 
@@ -204,7 +210,8 @@
             if (mForcedModuleParameter != null) {
                 mForcedParameter =
                         ModuleParametersHelper.getParameterHandler(
-                                mForcedModuleParameter, /* optionalParams */ false);
+                                mForcedModuleParameter, /* optionalParams */
+                                mAllowOptionalParameterizedModules);
             }
 
             // Invokes parser to process the test module config file
@@ -296,6 +303,12 @@
                         if (shouldRunParameterized(baseId, fullId)) {
                             IConfiguration paramConfig =
                                     mConfigFactory.createConfigurationFromArgs(pathArg);
+                            // Mark the parameter in the metadata
+                            paramConfig
+                                    .getConfigurationDescription()
+                                    .addMetadata(
+                                            ConfigurationDescriptor.PARAMETER_KEY,
+                                            param.getParameterIdentifier());
                             setUpConfig(name, baseId, fullId, paramConfig, abi);
                             param.applySetup(paramConfig);
                             toRun.put(fullId, paramConfig);
@@ -402,9 +415,11 @@
     }
 
     private void addTestIncludes(
-            ITestFilterReceiver test, List<SuiteTestFilter> includes, String name) {
+            ITestFilterReceiver test, List<SuiteTestFilter> includes, String moduleId) {
         if (test instanceof ITestFileFilterReceiver) {
-            File includeFile = createFilterFile(name, ".include", includes);
+            // module id can contain spaces, avoid them for file names.
+            String escapedFileName = moduleId.replaceAll(" ", "_");
+            File includeFile = createFilterFile(escapedFileName, ".include", includes);
             ((ITestFileFilterReceiver) test).setIncludeTestFile(includeFile);
         } else {
             // add test includes one at a time
@@ -507,7 +522,7 @@
             String optionValueString = remainder.substring(optionNameSep + 1);
             // TODO: See if QuotationTokenizer can be improved for multi-character delimiter.
             // or change the delimiter to a single char.
-            String[] tokens = optionValueString.split(":=");
+            String[] tokens = optionValueString.split(":=", 2);
             OptionDef option = null;
             if (tokens.length == 1) {
                 option = new OptionDef(optionName, tokens[0], moduleName);
@@ -545,13 +560,15 @@
             }
             // Do not consider the excluded parameterization dimension
             if (mExcludedModuleParameters.contains(suiteParam)) {
-                CLog.d("'%s' was excluded via exclude-module-parameters.");
+                CLog.d("'%s' was excluded via exclude-module-parameters.", moduleName);
                 continue;
             }
             IModuleParameter handler =
                     ModuleParametersHelper.getParameterHandler(
-                            suiteParam, /* optionalParams */ false);
-            params.add(handler);
+                            suiteParam, /* optionalParams */ mAllowOptionalParameterizedModules);
+            if (handler != null) {
+                params.add(handler);
+            }
         }
         return params;
     }
@@ -561,7 +578,7 @@
      *
      * @param name The base name of the module
      * @param id The base id name of the module.
-     * @param fullId The full id of the module.
+     * @param fullId The full id of the module (usually abi + module name + parameters)
      * @param config The module configuration.
      * @param abi The abi of the module.
      * @throws ConfigurationException
@@ -595,7 +612,7 @@
             if (mTestOptions.containsKey(className)) {
                 config.injectOptionValues(mTestOptions.get(className));
             }
-            addFiltersToTest(test, abi, name, mIncludeFilters, mExcludeFilters);
+            addFiltersToTest(test, abi, fullId, mIncludeFilters, mExcludeFilters);
             if (test instanceof IAbiReceiver) {
                 ((IAbiReceiver) test).setAbi(abi);
             }
@@ -605,7 +622,7 @@
         config.getConfigurationDescription().setAbi(abi);
         config.getConfigurationDescription().setModuleName(name);
 
-        config.validateOptions(false);
+        config.validateOptions();
     }
 
     /** Whether or not the base configuration should be created for all abis or not. */
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index c0dc16a..b13ffa7 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -18,6 +18,7 @@
 import com.android.tradefed.config.ConfigurationDescriptor;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.testmapping.TestInfo;
 import com.android.tradefed.util.testmapping.TestMapping;
 import com.android.tradefed.util.testmapping.TestOption;
@@ -29,6 +30,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
+
 /**
  * Implementation of {@link BaseTestSuite} to run tests specified by option include-filter, or
  * TEST_MAPPING files from build, as a suite.
@@ -56,6 +59,14 @@
     )
     private Set<String> mKeywords = new HashSet<>();
 
+    @Option(
+        name = "force-test-mapping-module",
+        description =
+                "Run the specified tests only. The tests loaded from all TEST_MAPPING files in "
+                        + "the source code will be filtered again to force run the specified tests."
+    )
+    private Set<String> mTestModulesForced = new HashSet<>();
+
     /** Special definition in the test mapping structure. */
     private static final String TEST_MAPPING_INCLUDE_FILTER = "include-filter";
 
@@ -91,6 +102,11 @@
             throw new RuntimeException(
                     "Must specify --test-mapping-test-group when applying --test-mapping-keyword.");
         }
+        if (mTestGroup == null && !mTestModulesForced.isEmpty()) {
+            throw new RuntimeException(
+                    "Must specify --test-mapping-test-group when applying "
+                            + "--force-test-mapping-module.");
+        }
         if (mTestGroup != null && !includeFilter.isEmpty()) {
             throw new RuntimeException(
                     "If options --test-mapping-test-group is set, option --include-filter should "
@@ -101,6 +117,14 @@
             Set<TestInfo> testsToRun =
                     TestMapping.getTests(
                             getBuildInfo(), mTestGroup, getPrioritizeHostConfig(), mKeywords);
+            if (!mTestModulesForced.isEmpty()) {
+                CLog.i("Filtering tests for the given names: %s", mTestModulesForced);
+                testsToRun =
+                        testsToRun
+                                .stream()
+                                .filter(testInfo -> mTestModulesForced.contains(testInfo.getName()))
+                                .collect(Collectors.toSet());
+            }
             if (testsToRun.isEmpty()) {
                 throw new RuntimeException(
                         String.format("No test found for the given group: %s.", mTestGroup));
diff --git a/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java b/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
index 8fca54d..ad77eee 100644
--- a/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
+++ b/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
@@ -24,7 +24,8 @@
     MULTI_ABI("multi_abi", "multi_abi_family"),
     NOT_MULTI_ABI("not_multi_abi", "multi_abi_family"),
 
-    SECONDARY_USER("secondary_user", "secondary_user_family");
+    SECONDARY_USER("secondary_user", "secondary_user_family"),
+    NOT_SECONDARY_USER("not_secondary_user", "secondary_user_family");
 
     public static final String INSTANT_APP_FAMILY = "instant_app_family";
     public static final String MULTI_ABI_FAMILY = "multi_abi_family";
diff --git a/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java b/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java
index e009976..9a378e4 100644
--- a/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java
+++ b/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java
@@ -41,6 +41,7 @@
 
     static {
         sOptionalHandlerMap.put(ModuleParameters.SECONDARY_USER, new SecondaryUserHandler());
+        sOptionalHandlerMap.put(ModuleParameters.NOT_SECONDARY_USER, new NegativeHandler());
     }
 
     /**
diff --git a/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java b/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
index f4a133d..cdaaca4 100644
--- a/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
+++ b/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
@@ -19,6 +19,7 @@
 import com.android.tradefed.config.IDeviceConfiguration;
 import com.android.tradefed.targetprep.CreateUserPreparer;
 import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.RunCommandTargetPreparer;
 
 import java.util.List;
 
@@ -34,7 +35,17 @@
     public void applySetup(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());
+            // Add a preparer to setup the location settings on the new user
+            preparers.add(1, createLocationPreparer());
         }
     }
+
+    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/retry/ResultsPlayer.java b/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java
index 3b7f914..96538ec 100644
--- a/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java
+++ b/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java
@@ -15,6 +15,9 @@
  */
 package com.android.tradefed.testtype.suite.retry;
 
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
@@ -38,7 +41,8 @@
 import java.util.Map.Entry;
 
 /** Special runner that replays the results given to it. */
-public final class ResultsPlayer implements IRemoteTest, IInvocationContextReceiver {
+public final class ResultsPlayer
+        implements IRemoteTest, IInvocationContextReceiver, IConfigurationReceiver {
 
     private class ReplayModuleHolder {
         public IInvocationContext mModuleContext;
@@ -47,6 +51,7 @@
 
     private IInvocationContext mContext;
     private Map<TestRunResult, ReplayModuleHolder> mModuleResult;
+    private IConfiguration mConfiguration;
 
     /** Ctor. */
     public ResultsPlayer() {
@@ -66,7 +71,13 @@
         }
 
         long startReplay = System.currentTimeMillis();
-        CLog.d("Start replaying the previous results.");
+        CLog.logAndDisplay(
+                LogLevel.DEBUG,
+                "Start replaying the previous results. Please wait this can take a few minutes.");
+        // Change the logging level to avoid too much logs from the replay.
+        LogLevel originalLevel = mConfiguration.getLogOutput().getLogLevel();
+        mConfiguration.getLogOutput().setLogLevel(LogLevel.WARN);
+
         for (TestRunResult module : mModuleResult.keySet()) {
             ReplayModuleHolder holder = mModuleResult.get(module);
 
@@ -96,7 +107,10 @@
             // memory early
             holder.mResults.clear();
         }
-        CLog.d(
+        // Restore the original log level to continue execution with the requested log level.
+        mConfiguration.getLogOutput().setLogLevel(originalLevel);
+        CLog.logAndDisplay(
+                LogLevel.DEBUG,
                 "Done replaying results in %s",
                 TimeUtil.formatElapsedTime(System.currentTimeMillis() - startReplay));
         mModuleResult.clear();
@@ -131,6 +145,12 @@
         mContext = invocationContext;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void setConfiguration(IConfiguration configuration) {
+        mConfiguration = configuration;
+    }
+
     private void forwardTestResults(
             TestRunResult module,
             Collection<Entry<TestDescription, TestResult>> testSet,
diff --git a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
index 5358794..666bd44 100644
--- a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
+++ b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
@@ -37,7 +37,9 @@
 import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.suite.BaseTestSuite;
+import com.android.tradefed.testtype.suite.ITestSuite;
 import com.android.tradefed.testtype.suite.SuiteTestFilter;
+import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.QuotationAwareTokenizer;
 import com.android.tradefed.util.TestRecordInterpreter;
 
@@ -85,6 +87,13 @@
     )
     private Set<String> mExcludeFilters = new HashSet<>();
 
+    // Carry some options from suites that are convenient and don't impact the tests selection.
+    @Option(
+        name = ITestSuite.REBOOT_BEFORE_TEST,
+        description = "Reboot the device before the test suite starts."
+    )
+    private boolean mRebootBeforeTest = false;
+
     public static final String PREVIOUS_LOADER_NAME = "previous_loader";
 
     private IConfiguration mConfiguration;
@@ -168,6 +177,10 @@
         // Do the customization of the configuration for specialized use cases.
         customizeConfig(previousLoader, originalConfig);
 
+        if (mRebootBeforeTest) {
+            suite.enableRebootBeforeTest();
+        }
+
         mRescheduledConfiguration = originalConfig;
 
         if (mRescheduler != null) {
@@ -222,10 +235,28 @@
         } else {
             types.add(mRetryType);
         }
+
+        // Expand the exclude-filter in case no abi is specified.
+        Set<String> extendedExcludeRetryFilters = new HashSet<>();
+        for (String excludeFilter : mExcludeFilters) {
+            SuiteTestFilter suiteFilter = SuiteTestFilter.createFrom(excludeFilter);
+            // Keep the current exclude-filter
+            extendedExcludeRetryFilters.add(excludeFilter);
+            if (suiteFilter.getAbi() == null) {
+                // If no abi is specified, exclude them all.
+                Set<String> abis = AbiUtils.getAbisSupportedByCompatibility();
+                for (String abi : abis) {
+                    SuiteTestFilter namingFilter =
+                            new SuiteTestFilter(abi, suiteFilter.getName(), suiteFilter.getTest());
+                    extendedExcludeRetryFilters.add(namingFilter.toString());
+                }
+            }
+        }
+
         // Prepare exclusion filters
         for (TestRunResult moduleResult : results.getMergedTestRunResults()) {
             // If the module is explicitly excluded from retries, preserve the original results.
-            if (!mExcludeFilters.contains(moduleResult.getName())
+            if (!extendedExcludeRetryFilters.contains(moduleResult.getName())
                     && RetryResultHelper.shouldRunModule(moduleResult, types)) {
                 if (types.contains(RetryType.NOT_EXECUTED)) {
                     // Clear the run failure since we are attempting to rerun all non-executed
diff --git a/src/com/android/tradefed/testtype/testdefs/InstrumentationTestDef.java b/src/com/android/tradefed/testtype/testdefs/InstrumentationTestDef.java
deleted file mode 100644
index 0a0b4b3..0000000
--- a/src/com/android/tradefed/testtype/testdefs/InstrumentationTestDef.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-/**
- * Container object for a single test def's data
- */
-class InstrumentationTestDef {
-    private final String mName;
-    private final String mPackage;
-    private String mRunner = null;
-    private String mClassName = null;
-    private boolean mIsContinuous = false;
-    private String mCoverageTarget = null;
-
-    /**
-     * Creates a {@link InstrumentationTestDef}.
-     *
-     * @param testName the unique test name
-     * @param packageName the Android manifest package name of the test.
-     */
-    public InstrumentationTestDef(String testName, String packageName) {
-        mName = testName;
-        mPackage = packageName;
-    }
-
-    void setRunner(String runnerName) {
-        mRunner = runnerName;
-    }
-
-    void setClassName(String className) {
-        mClassName = className;
-    }
-
-    void setContinuous(boolean isContinuous) {
-        mIsContinuous = isContinuous;
-    }
-
-    void setCoverageTarget(String coverageTarget) {
-        mCoverageTarget = coverageTarget;
-    }
-
-    /**
-     * Returns the unique name of the test definition.
-     */
-    String getName() {
-        return mName;
-    }
-
-    /**
-     * Returns the Android Manifest package name of the test application.
-     */
-    String getPackage() {
-        return mPackage;
-    }
-
-    /**
-     * Returns the fully specified  name of the instrumentation runner to use. <code>null</code>
-     * if not specified.
-     */
-    String getRunner() {
-        return mRunner;
-    }
-
-    /**
-     * Returns the fully specified  name of the test class to run. <code>null</code> if not
-     * specified.
-     */
-    String getClassName() {
-        return mClassName;
-    }
-
-    /**
-     * Returns <code>true</code> if test is part of the continuous test.
-     */
-    boolean isContinuous() {
-        return mIsContinuous;
-    }
-
-    /**
-     * Returns the coverage target for the test.
-     */
-    String getCoverageTarget() {
-        return mCoverageTarget;
-    }
-}
diff --git a/src/com/android/tradefed/testtype/testdefs/XmlDefsParser.java b/src/com/android/tradefed/testtype/testdefs/XmlDefsParser.java
deleted file mode 100644
index e489052..0000000
--- a/src/com/android/tradefed/testtype/testdefs/XmlDefsParser.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-import com.android.tradefed.util.xml.AbstractXmlParser;
-
-import org.xml.sax.Attributes;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
-
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Parses a test_defs.xml file.
- * <p/>
- * See development/testrunner/test_defs.xsd for expected format
- */
-class XmlDefsParser extends AbstractXmlParser {
-
-    private Map<String, InstrumentationTestDef> mTestDefsMap;
-
-    /**
-     * SAX callback object. Handles parsing data from the xml tags.
-     */
-    private class DefsHandler extends DefaultHandler {
-
-        private static final String TEST_TAG = "test";
-
-        @Override
-        public void startElement(String uri, String localName, String name, Attributes attributes)
-                throws SAXException {
-            if (TEST_TAG.equals(localName)) {
-                final String defName = attributes.getValue("name");
-                InstrumentationTestDef def = new InstrumentationTestDef(defName,
-                        attributes.getValue("package"));
-                def.setClassName(attributes.getValue("class"));
-                def.setRunner(attributes.getValue("runner"));
-                def.setContinuous("true".equals(attributes.getValue("continuous")));
-                def.setCoverageTarget(attributes.getValue("coverage_target"));
-                mTestDefsMap.put(defName, def);
-            }
-        }
-    }
-
-    XmlDefsParser() {
-        // Uses a LinkedHashmap to have predictable iteration order
-        mTestDefsMap = new LinkedHashMap<String, InstrumentationTestDef>();
-    }
-
-    /**
-     * Gets the list of parsed test definitions. The element order should be consistent with the
-     * order of elements in the parsed input.
-     */
-    public Collection<InstrumentationTestDef> getTestDefs() {
-        return mTestDefsMap.values();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    protected DefaultHandler createXmlHandler() {
-        return new DefsHandler();
-    }
-}
diff --git a/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java b/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
deleted file mode 100644
index 03e408d..0000000
--- a/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
+++ /dev/null
@@ -1,416 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.Option.Importance;
-import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.IDeviceTest;
-import com.android.tradefed.testtype.IRemoteTest;
-import com.android.tradefed.testtype.IResumableTest;
-import com.android.tradefed.testtype.IShardableTest;
-import com.android.tradefed.testtype.InstrumentationTest;
-import com.android.tradefed.util.FileUtil;
-import com.android.tradefed.util.proto.TfMetricProtoUtil;
-import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-
-/**
- * Runs a set of instrumentation test's defined in test_defs.xml files.
- * <p/>
- * The test definition files can either be one or more files on local file system, and/or one or
- * more files stored on the device under test.
- */
-@OptionClass(alias = "xml-defs")
-public class XmlDefsTest implements IDeviceTest, IResumableTest,
-        IShardableTest {
-
-    private static final String LOG_TAG = "XmlDefsTest";
-
-    /** the metric key name for the test coverage target value */
-    // TODO: move this to a more generic location
-    public static final String COVERAGE_TARGET_KEY = "coverage_target";
-
-    private ITestDevice mDevice;
-
-    /**
-     * @deprecated use shell-timeout or test-timeout instead.
-     */
-    @Deprecated
-    @Option(name = "timeout",
-            description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
-    private Integer mTimeout = null;
-
-    @Option(name = "shell-timeout",
-            description="The defined timeout (in milliseconds) is used as a maximum waiting time "
-                    + "when expecting the command output from the device. At any time, if the "
-                    + "shell command does not output anything for a period longer than defined "
-                    + "timeout the TF run terminates. For no timeout, set to 0.")
-    private long mShellTimeout = 10 * 60 * 1000;  // default to 10 minutes
-
-    @Option(name = "test-timeout",
-            description="Sets timeout (in milliseconds) that will be applied to each test. In the "
-                    + "event of a test timeout it will log the results and proceed with executing "
-                    + "the next test. For no timeout, set to 0.")
-    private int mTestTimeout = 10 * 60 * 1000;  // default to 10 minutes
-
-    @Option(name = "size",
-            description = "Restrict tests to a specific test size. " +
-            "One of 'small', 'medium', 'large'",
-            importance = Importance.IF_UNSET)
-    private String mTestSize = null;
-
-    @Option(name = "rerun",
-            description = "Rerun unexecuted tests individually on same device if test run " +
-            "fails to complete.")
-    private boolean mIsRerunMode = true;
-
-    @Option(name = "resume",
-            description = "Schedule unexecuted tests for resumption on another device " +
-            "if first device becomes unavailable.")
-    private boolean mIsResumeMode = false;
-
-    @Option(name = "local-file-path",
-            description = "local file path to test_defs.xml file to run.")
-    private Collection<File> mLocalFiles = new ArrayList<File>();
-
-    @Option(name = "device-file-path",
-            description = "file path on device to test_defs.xml file to run.",
-            importance = Importance.IF_UNSET)
-    private Collection<String> mRemotePaths = new ArrayList<String>();
-
-    @Option(name = "send-coverage",
-            description = "Send coverage target info to test listeners.")
-    private boolean mSendCoverage = true;
-
-    @Option(name = "num-shards",
-            description = "Shard this test into given number of separately runnable chunks.")
-    private int mNumShards = 0;
-
-    private List<InstrumentationTest> mTests = null;
-
-    public XmlDefsTest() {
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public ITestDevice getDevice() {
-        return mDevice;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setDevice(ITestDevice device) {
-        mDevice = device;
-    }
-
-    /**
-     * Adds a remote test def file path.
-     * <p/>
-     * Exposed for unit testing.
-     */
-    void addRemoteFilePath(String path) {
-        mRemotePaths.add(path);
-    }
-
-    /**
-     * Adds a local test def file path.
-     * <p/>
-     * Exposed for unit testing.
-     */
-    void addLocalFilePath(File file) {
-        mLocalFiles.add(file);
-    }
-
-    /**
-     * Set the send coverage flag.
-     * <p/>
-     * Exposed for unit testing.
-     */
-    void setSendCoverage(boolean sendCoverage) {
-        mSendCoverage = sendCoverage;
-    }
-
-    /**
-     * Sets the number of shards test should be split into
-     * <p/>
-     * Exposed for unit testing.
-     */
-    void setNumShards(int shards) {
-        mNumShards = shards;
-    }
-
-    /**
-     * Gets the list of parsed {@link InstrumentationTest}s contained within.
-     * <p/>
-     * Exposed for unit testing.
-     */
-    List<InstrumentationTest> getTests() {
-        return mTests;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
-        if (getDevice() == null) {
-            throw new IllegalArgumentException("Device has not been set");
-        }
-        buildTests();
-        doRun(listener);
-    }
-
-    /**
-     * Build the list of tests to run from the xml files, if not done already.
-     * @throws DeviceNotAvailableException
-     */
-    private void buildTests() throws DeviceNotAvailableException {
-        if (mTests == null) {
-            if (mLocalFiles.isEmpty() && mRemotePaths.isEmpty()) {
-                throw new IllegalArgumentException("No test definition files (local-file-path or " +
-                        "device-file-path) have been provided.");
-            }
-            XmlDefsParser parser = createParser();
-            for (File testDefFile : mLocalFiles) {
-                parseFile(parser, testDefFile);
-            }
-            for (File testDefFile : getRemoteFile(mRemotePaths)) {
-                try {
-                    parseFile(parser, testDefFile);
-                } finally {
-                    testDefFile.delete();
-                }
-            }
-
-            mTests = new LinkedList<InstrumentationTest>();
-            for (InstrumentationTestDef def : parser.getTestDefs()) {
-                // only run continuous for now. Consider making this configurable
-                if (def.isContinuous()) {
-                    InstrumentationTest test = createInstrumentationTest();
-
-                    test.setDevice(getDevice());
-                    test.setPackageName(def.getPackage());
-                    if (def.getRunner() != null) {
-                        test.setRunnerName(def.getRunner());
-                    }
-                    if (def.getClassName() != null) {
-                        test.setClassName(def.getClassName());
-                    }
-                    test.setRerunMode(mIsRerunMode);
-                    test.setResumeMode(mIsResumeMode);
-                    test.setTestSize(getTestSize());
-                    if (mTimeout != null) {
-                        LogUtil.CLog
-                                .w("\"timeout\" argument is deprecated and should not be used! \"shell-timeout\""
-                                        + " argument value is overwritten with %d ms", mTimeout);
-                        setShellTimeout(mTimeout);
-                    }
-                    test.setShellTimeout(getShellTimeout());
-                    test.setTestTimeout(getTestTimeout());
-                    test.setCoverageTarget(def.getCoverageTarget());
-                    mTests.add(test);
-                }
-            }
-        }
-    }
-
-    /**
-     * Parse the given xml def file
-     *
-     * @param parser
-     * @param testDefFile
-     */
-    private void parseFile(XmlDefsParser parser, File testDefFile) {
-        try {
-            Log.i(LOG_TAG, String.format("Parsing test def file %s",
-                    testDefFile.getAbsolutePath()));
-            parser.parse(new FileInputStream(testDefFile));
-        } catch (FileNotFoundException e) {
-            Log.e(LOG_TAG, String.format("Could not find test def file %s",
-                    testDefFile.getAbsolutePath()));
-        } catch (ParseException e) {
-            Log.e(LOG_TAG, String.format("Could not parse test def file %s: %s",
-                    testDefFile.getAbsolutePath(), e.getMessage()));
-        }
-    }
-
-    /**
-     * Run the previously built tests.
-     *
-     * @param listener the {@link ITestInvocationListener}
-     * @throws DeviceNotAvailableException
-     */
-    private void doRun(ITestInvocationListener listener) throws DeviceNotAvailableException {
-        while (!mTests.isEmpty()) {
-            InstrumentationTest test = mTests.get(0);
-
-            Log.d(LOG_TAG, String.format("Running test %s on %s", test.getPackageName(),
-                        getDevice().getSerialNumber()));
-
-            if (mSendCoverage && test.getCoverageTarget() != null) {
-                sendCoverage(test.getPackageName(), test.getCoverageTarget(), listener);
-            }
-            test.setDevice(getDevice());
-            test.run(listener);
-            // test completed, remove from list
-            mTests.remove(0);
-        }
-    }
-
-    /**
-     * Forwards the tests coverage target info as a test metric.
-     *
-     * @param packageName
-     * @param coverageTarget
-     * @param listener
-     */
-    private void sendCoverage(String packageName, String coverageTarget,
-            ITestInvocationListener listener) {
-        HashMap<String, Metric> coverageMetric = new HashMap<>(1);
-        coverageMetric.put(COVERAGE_TARGET_KEY, TfMetricProtoUtil.stringToMetric(coverageTarget));
-        listener.testRunStarted(packageName, 0);
-        listener.testRunEnded(0, coverageMetric);
-    }
-
-    /**
-     * Retrieves a set of files from device into temporary files on local filesystem.
-     *
-     * @param remoteFilePaths
-     */
-    private Collection<File> getRemoteFile(Collection<String> remoteFilePaths)
-            throws DeviceNotAvailableException {
-        Collection<File> files = new ArrayList<File>();
-        if (getDevice() == null) {
-            Log.d(LOG_TAG, "Device not set, skipping collection of remote file");
-            return files;
-        }
-        for (String remoteFilePath : remoteFilePaths) {
-            try {
-                File tmpFile = FileUtil.createTempFile("test_defs_", ".xml");
-                getDevice().pullFile(remoteFilePath, tmpFile);
-                files.add(tmpFile);
-            } catch (IOException e) {
-                Log.e(LOG_TAG, "Failed to create temp file");
-                Log.e(LOG_TAG, e);
-            }
-        }
-        return files;
-    }
-
-    void setShellTimeout(long timeout) {
-        mShellTimeout = timeout;
-    }
-
-    long getShellTimeout() {
-        return mShellTimeout;
-    }
-
-    int getTestTimeout() {
-        return mTestTimeout;
-    }
-
-    String getTestSize() {
-        return mTestSize;
-    }
-
-    /**
-     * Creates the {@link XmlDefsParser} to use. Exposed for unit testing.
-     */
-    XmlDefsParser createParser() {
-        return new XmlDefsParser();
-    }
-
-    /**
-     * Creates the {@link InstrumentationTest} to use. Exposed for unit testing.
-     */
-    InstrumentationTest createInstrumentationTest() {
-        return new InstrumentationTest();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean isResumable() {
-        // hack to not resume if tests were never run
-        // TODO: fix this properly in TestInvocation
-        if (mTests == null) {
-            return false;
-        }
-        return mIsResumeMode;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Collection<IRemoteTest> split() {
-        if (mLocalFiles.isEmpty()) {
-            Log.w(LOG_TAG, "sharding is only supported if local xml files have been specified");
-            return null;
-        }
-        if (mNumShards <= 1) {
-            return null;
-        }
-
-        try {
-            buildTests();
-        } catch (DeviceNotAvailableException e) {
-            // should never happen
-        }
-        if (mTests.size() <= 1) {
-            Log.w(LOG_TAG, "no tests to shard!");
-            return null;
-        }
-
-        // treat shardQueue as a circular queue, to sequentially distribute tests among shards
-        Queue<IRemoteTest> shardQueue = new LinkedList<IRemoteTest>();
-        // don't create more shards than the number of tests we have!
-        for (int i = 0; i < mNumShards && i < mTests.size(); i++) {
-            XmlDefsTest shard = new XmlDefsTest();
-            shard.mTests = new LinkedList<InstrumentationTest>();
-            shardQueue.add(shard);
-        }
-        while (!mTests.isEmpty()) {
-            InstrumentationTest test = mTests.remove(0);
-            XmlDefsTest shard = (XmlDefsTest)shardQueue.poll();
-            shard.mTests.add(test);
-            shardQueue.add(shard);
-        }
-        return shardQueue;
-    }
-}
diff --git a/src/com/android/tradefed/util/BuildInfoUtil.java b/src/com/android/tradefed/util/BuildInfoUtil.java
new file mode 100644
index 0000000..345fae5
--- /dev/null
+++ b/src/com/android/tradefed/util/BuildInfoUtil.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceProperties;
+import com.android.tradefed.device.ITestDevice;
+
+/** A util class to help manipulate {@link IBuildInfo} */
+public class BuildInfoUtil {
+
+    /**
+     * Reads build attributes from device and use them to override the relevant build info fields
+     *
+     * <p>Note: because branch information is not stored on device as build attributes, the injected
+     * branch info will be the following fields concatenated via dashes:
+     *
+     * <ul>
+     *   <li><code>ro.product.brand</code>
+     *   <li><code>ro.product.name</code>
+     *   <li><code>ro.product.vendor.device</code> (maybe different on older API levels)
+     *   <li><code>ro.build.version.release</code>
+     * </ul>
+     *
+     * @param buildInfo the build info where device build attributes will be injected
+     * @param device the device to read build attributes from
+     * @param overrideBuildId instead of reading from device, override build id to this value;
+     *     <code>null</code> for no override
+     * @param overrideBuildFlavor instead of reading from device, override build flavor to this
+     *     value; <code>null</code> for no override
+     * @param overrideBuildBranch instead of concatenating device attributes as substitute for
+     *     branch, override it to this value; <code>null</code> for no override
+     * @param overrideBuildAlias instead of reading from device, override build alias to this value;
+     *     <code>null</code> for no override
+     * @throws DeviceNotAvailableException
+     */
+    public static void bootstrapDeviceBuildAttributes(
+            IBuildInfo buildInfo,
+            ITestDevice device,
+            String overrideBuildId,
+            String overrideBuildFlavor,
+            String overrideBuildBranch,
+            String overrideBuildAlias)
+            throws DeviceNotAvailableException {
+        String buildId, buildAlias, buildFlavor, branch;
+        // inject build id
+        if (overrideBuildId != null) {
+            buildId = overrideBuildId;
+        } else {
+            buildId = device.getBuildId();
+        }
+        buildInfo.setBuildId(buildId);
+
+        // inject build alias
+        if (overrideBuildAlias != null) {
+            buildAlias = overrideBuildAlias;
+        } else {
+            buildAlias = device.getBuildAlias();
+        }
+        buildInfo.addBuildAttribute("build_alias", buildAlias);
+
+        // inject build flavor
+        if (overrideBuildFlavor != null) {
+            buildFlavor = overrideBuildFlavor;
+        } else {
+            buildFlavor = device.getBuildFlavor();
+        }
+        buildInfo.setBuildFlavor(buildFlavor);
+
+        // generate branch information, either via parameter override, or via concatenating fields
+        if (overrideBuildBranch != null) {
+            branch = overrideBuildBranch;
+        } else {
+            branch =
+                    String.format(
+                            "%s-%s-%s-%s",
+                            device.getProperty(DeviceProperties.BRAND),
+                            device.getProperty(DeviceProperties.PRODUCT),
+                            device.getProductVariant(),
+                            device.getProperty(DeviceProperties.RELEASE_VERSION));
+        }
+        buildInfo.setBuildBranch(branch);
+    }
+}
diff --git a/src/com/android/tradefed/util/BundletoolUtil.java b/src/com/android/tradefed/util/BundletoolUtil.java
index 3e29b90..4bd117c 100644
--- a/src/com/android/tradefed/util/BundletoolUtil.java
+++ b/src/com/android/tradefed/util/BundletoolUtil.java
@@ -25,10 +25,13 @@
 import com.google.common.annotations.VisibleForTesting;
 
 import java.io.File;
+import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Utility class that uses bundletool command line to install the .apks on deivce. Bundletool doc
@@ -42,6 +45,7 @@
     private static final String DEVICE_SPEC_OUTPUT_FLAG = "--output=";
     private static final String APKS_TO_EXTRACT_FLAG = "--apks=";
     private static final String DEVICE_SPEC_FLAG = "--device-spec=";
+    private static final String DEVICE_ID_FLAG = "--device-id=";
     private static final String EXTRACT_APKS_OPTION = "extract-apks";
     private static final String INSTALL_APKS_OPTION = "install-apks";
     private static final String DEVICE_SPEC_FILE_EXTENSION = ".json";
@@ -55,7 +59,7 @@
         mRunUtil = new RunUtil();
     }
 
-    public File getBundletoolFile() {
+    protected File getBundletoolFile() {
         return mBundleToolFile;
     }
 
@@ -65,33 +69,42 @@
      * @param device the connected device
      * @return a {@link String} representing the path of the device specification file.
      */
-    public String generateDeviceSpecFile(ITestDevice device) {
+    public String generateDeviceSpecFile(ITestDevice device) throws IOException {
         Path specFilePath =
                 Paths.get(
                         getBundletoolFile().getParentFile().getAbsolutePath(),
                         device.getSerialNumber() + DEVICE_SPEC_FILE_EXTENSION);
-        if (Files.exists(specFilePath)) {
-            return specFilePath.toString();
-        }
+
+        Files.deleteIfExists(specFilePath);
 
         String outputDirArg = DEVICE_SPEC_OUTPUT_FLAG + specFilePath.toString();
 
-        String adbArg = "--adb=" + getAdbPath();
+        String deviceIdArg = DEVICE_ID_FLAG + device.getSerialNumber();
 
-        String[] cmd =
-                new String[] {
-                    "java",
-                    "-jar",
-                    getBundletoolFile().getAbsolutePath(),
-                    GET_DEVICE_SPEC_OPTION,
-                    outputDirArg,
-                    adbArg
-                };
-        CommandResult res = getRunUtil().runTimedCmd(CMD_TIME_OUT, cmd);
+        List<String> generateDeviceSpecCmd =
+                new ArrayList<String>(
+                        Arrays.asList(
+                                "java",
+                                "-jar",
+                                getBundletoolFile().getAbsolutePath(),
+                                GET_DEVICE_SPEC_OPTION,
+                                outputDirArg,
+                                deviceIdArg));
+
+        if (getAdbPath() != null) {
+            generateDeviceSpecCmd.add("--adb=" + getAdbPath());
+        }
+
+        CommandResult res =
+                getRunUtil()
+                        .runTimedCmd(
+                                CMD_TIME_OUT,
+                                generateDeviceSpecCmd.toArray(
+                                        new String[generateDeviceSpecCmd.size()]));
         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
             CLog.e(
                     "Failed to generated device spec file. Cmd is %s. Error: %s.",
-                    Arrays.toString(cmd), res.getStderr());
+                    generateDeviceSpecCmd.toString(), res.getStderr());
             return null;
         }
         return specFilePath.toString();
@@ -151,22 +164,33 @@
      */
     public void installApks(File apks, ITestDevice device) throws TargetSetupError {
         String inputPathArg = "--apks=" + apks.getAbsolutePath();
-        String adbArg = "--adb=" + getAdbPath();
-        String[] installApksCmd =
-                new String[] {
-                    "java",
-                    "-jar",
-                    getBundletoolFile().getAbsolutePath(),
-                    INSTALL_APKS_OPTION,
-                    inputPathArg,
-                    adbArg
-                };
-        CommandResult res = getRunUtil().runTimedCmd(CMD_TIME_OUT, installApksCmd);
+
+        String deviceIdArg = DEVICE_ID_FLAG + device.getSerialNumber();
+
+        List<String> installApksCmd =
+                new ArrayList<String>(
+                        Arrays.asList(
+                                "java",
+                                "-jar",
+                                getBundletoolFile().getAbsolutePath(),
+                                INSTALL_APKS_OPTION,
+                                inputPathArg,
+                                deviceIdArg));
+
+        if (getAdbPath() != null) {
+            installApksCmd.add("--adb=" + getAdbPath());
+        }
+
+        CommandResult res =
+                getRunUtil()
+                        .runTimedCmd(
+                                CMD_TIME_OUT,
+                                installApksCmd.toArray(new String[installApksCmd.size()]));
         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
             throw new TargetSetupError(
                     String.format(
                             "Failed to install split apk. Cmd: %s. Error: %s.",
-                            Arrays.toString(installApksCmd), res.getStderr()),
+                            installApksCmd.toString(), res.getStderr()),
                     device.getDeviceDescriptor());
         }
         CLog.i("%s is installed successfully", apks.getName());
@@ -180,6 +204,11 @@
 
     @VisibleForTesting
     protected String getAdbPath() {
-        return GlobalConfiguration.getDeviceManagerInstance().getAdbPath();
+        String adbPath = GlobalConfiguration.getDeviceManagerInstance().getAdbPath();
+        // No explicit adb path passed from device manager.
+        if (!new File(adbPath).exists()) {
+            return null;
+        }
+        return adbPath;
     }
 }
diff --git a/src/com/android/tradefed/util/GoogleApiClientUtil.java b/src/com/android/tradefed/util/GoogleApiClientUtil.java
index e8a720c..187968a 100644
--- a/src/com/android/tradefed/util/GoogleApiClientUtil.java
+++ b/src/com/android/tradefed/util/GoogleApiClientUtil.java
@@ -224,6 +224,7 @@
     private static class RetryResponseHandler implements HttpUnsuccessfulResponseHandler {
         // Initial interval to wait before retrying if a request fails.
         private static final int INITIAL_RETRY_INTERVAL = 1000;
+        private static final int MAX_RETRY_INTERVAL = 3 * 60000; // Set max interval to 3 minutes.
 
         private final HttpUnsuccessfulResponseHandler backOffHandler;
 
@@ -232,6 +233,7 @@
                     new HttpBackOffUnsuccessfulResponseHandler(
                             new ExponentialBackOff.Builder()
                                     .setInitialIntervalMillis(INITIAL_RETRY_INTERVAL)
+                                    .setMaxIntervalMillis(MAX_RETRY_INTERVAL)
                                     .build());
         }
 
diff --git a/src/com/android/tradefed/util/LocalRunInstructionBuilder.java b/src/com/android/tradefed/util/LocalRunInstructionBuilder.java
index 8dec663..7117264 100644
--- a/src/com/android/tradefed/util/LocalRunInstructionBuilder.java
+++ b/src/com/android/tradefed/util/LocalRunInstructionBuilder.java
@@ -16,12 +16,14 @@
 
 package com.android.tradefed.util;
 
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
 import com.android.tradefed.config.ConfigurationDescriptor;
 import com.android.tradefed.config.ConfigurationDescriptor.LocalTestRunner;
+import com.android.tradefed.config.OptionDef;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.TestDescription;
 
+import java.util.List;
+
 /** Utility to compile the instruction to run test locally. */
 public class LocalRunInstructionBuilder {
 
@@ -106,6 +108,14 @@
                 instruction.append(" " + option);
             }
         }
+        // Ensure repro is aligned with parameterized modules.
+        List<String> paramMetadata =
+                configDescriptor.getMetaData(ConfigurationDescriptor.PARAMETER_KEY);
+        if (paramMetadata != null
+                && paramMetadata.size() > 0
+                && "instant".equals(paramMetadata.get(0))) {
+            instruction.append(" --instant");
+        }
         return instruction.toString();
     }
 }
diff --git a/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java b/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java
new file mode 100644
index 0000000..b53542a
--- /dev/null
+++ b/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * A utility class that clears native coverage measurements and forces a flush of native coverage
+ * data from processes on the device.
+ */
+public final class NativeCodeCoverageFlusher {
+
+    private static final String COVERAGE_FLUSH_COMMAND_FORMAT = "kill -37 %s";
+    private static final String CLEAR_NATIVE_COVERAGE_FILES = "rm -rf /data/misc/trace/*";
+
+    private final ITestDevice mDevice;
+
+    public NativeCodeCoverageFlusher(ITestDevice device) {
+        mDevice = device;
+    }
+
+    /**
+     * Clears coverage measurements from disk on the device. Device must be in adb root.
+     *
+     * @throws DeviceNotAvailableException
+     */
+    public void clearCoverageMeasurements() throws DeviceNotAvailableException {
+        checkState(mDevice.isAdbRoot(), "adb root is required to clear coverage files.");
+        mDevice.executeShellCommand(CLEAR_NATIVE_COVERAGE_FILES);
+    }
+
+    /**
+     * Forces a flush of native coverage data from processes running on the device. Device must be
+     * in adb root.
+     *
+     * @param processNames the name of processes to target for flushing; if empty, flushes from all
+     *     running native processes on the device.
+     * @throws DeviceNotAvailableException
+     */
+    public void forceCoverageFlush(List<String> processNames) throws DeviceNotAvailableException {
+        checkState(mDevice.isAdbRoot(), "adb root is required to flush native coverage data.");
+
+        if ((processNames == null) || processNames.isEmpty()) {
+            // Use the special pid -1 to trigger a coverage flush of all running processes.
+            mDevice.executeShellCommand(String.format(COVERAGE_FLUSH_COMMAND_FORMAT, "-1"));
+        } else {
+            // Look up the pid of the processes to send them the coverage flush signal.
+            StringJoiner pidString = new StringJoiner(" ");
+            for (String processName : processNames) {
+                String pid = mDevice.getProcessPid(processName);
+                if (pid == null) {
+                    CLog.w("Did not find pid for process \"%s\".", processName);
+                } else {
+                    pidString.add(pid);
+                }
+            }
+
+            if (pidString.length() > 0) {
+                mDevice.executeShellCommand(
+                        String.format(COVERAGE_FLUSH_COMMAND_FORMAT, pidString.toString()));
+            }
+        }
+    }
+}
diff --git a/src/com/android/tradefed/util/ProcessInfo.java b/src/com/android/tradefed/util/ProcessInfo.java
deleted file mode 100644
index 2effb49..0000000
--- a/src/com/android/tradefed/util/ProcessInfo.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.util;
-
-/**
- * Used to store process related(USER, PID and NAME) information.
- */
-public class ProcessInfo {
-
-    private String mUser;
-    private int mPid;
-    private String mName;
-
-    /**
-     * Constructs the process info object based on the user, process id and
-     * name of the process.
-     *
-     * @param user username of process owner
-     * @param pid process id number
-     * @param name process name
-     */
-    ProcessInfo(String user, int pid, String name) {
-        mUser = user;
-        mPid = pid;
-        mName = name;
-    }
-
-    /** Returns the username of the process's owner. */
-    public String getUser() {
-        return mUser;
-    }
-
-    /** Returns the process ID number. */
-    public int getPid() {
-        return mPid;
-    }
-
-    /** Returns the process name. */
-    public String getName() {
-        return mName;
-    }
-
-}
-
diff --git a/src/com/android/tradefed/util/SubprocessEventHelper.java b/src/com/android/tradefed/util/SubprocessEventHelper.java
index f97defb..50028d9 100644
--- a/src/com/android/tradefed/util/SubprocessEventHelper.java
+++ b/src/com/android/tradefed/util/SubprocessEventHelper.java
@@ -65,24 +65,28 @@
         public String mRunName = null;
         public Integer mTestCount = null;
         public Integer mAttempt = null;
+        public Long mStartTime = null;
 
         /** Keep this constructor for legacy compatibility. */
         public TestRunStartedEventInfo(String runName, int testCount) {
             mRunName = runName;
             mTestCount = testCount;
             mAttempt = 0;
+            mStartTime = System.currentTimeMillis();
         }
 
-        public TestRunStartedEventInfo(String runName, int testCount, int attempt) {
+        public TestRunStartedEventInfo(String runName, int testCount, int attempt, long startTime) {
             mRunName = runName;
             mTestCount = testCount;
             mAttempt = attempt;
+            mStartTime = startTime;
         }
 
         public TestRunStartedEventInfo(JSONObject jsonObject) throws JSONException {
             mRunName = jsonObject.getString(RUNNAME_KEY);
             mTestCount = jsonObject.getInt(TESTCOUNT_KEY);
             mAttempt = jsonObject.optInt(ATTEMPT_KEY, 0);
+            mStartTime = jsonObject.optLong(START_TIME, System.currentTimeMillis());
         }
 
         @Override
@@ -98,6 +102,9 @@
                 if (mAttempt != null) {
                     tags.put(ATTEMPT_KEY, mAttempt.intValue());
                 }
+                if (mStartTime != null) {
+                    tags.put(START_TIME, mStartTime.longValue());
+                }
             } catch (JSONException e) {
                 CLog.e(e);
             }
diff --git a/src/com/android/tradefed/util/SubprocessTestResultsParser.java b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
index b72690c..9bd7cbc 100644
--- a/src/com/android/tradefed/util/SubprocessTestResultsParser.java
+++ b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
@@ -338,8 +338,9 @@
         @Override
         public void handleEvent(String eventJson) throws JSONException {
             TestRunStartedEventInfo rsi = new TestRunStartedEventInfo(new JSONObject(eventJson));
-            if (rsi.mAttempt != null && rsi.mAttempt != 0) {
-                mListener.testRunStarted(rsi.mRunName, rsi.mTestCount, rsi.mAttempt);
+            if (rsi.mAttempt != null) {
+                mListener.testRunStarted(
+                        rsi.mRunName, rsi.mTestCount, rsi.mAttempt, rsi.mStartTime);
             } else {
                 mListener.testRunStarted(rsi.mRunName, rsi.mTestCount);
             }
@@ -477,8 +478,8 @@
             LogAssociationEventInfo assosInfo =
                     new LogAssociationEventInfo(new JSONObject(eventJson));
             if (mListener instanceof ILogSaverListener) {
-                ((ILogSaverListener) mListener)
-                        .logAssociation(assosInfo.mDataName, assosInfo.mLoggedFile);
+                String name = String.format("subprocess-%s", assosInfo.mDataName);
+                ((ILogSaverListener) mListener).logAssociation(name, assosInfo.mLoggedFile);
             }
         }
     }
diff --git a/src/com/android/tradefed/util/TarUtil.java b/src/com/android/tradefed/util/TarUtil.java
index d9dfb2a..672696f 100644
--- a/src/com/android/tradefed/util/TarUtil.java
+++ b/src/com/android/tradefed/util/TarUtil.java
@@ -81,6 +81,15 @@
                     }
                 } else {
                     CLog.i(String.format("Creating output file %s.", outputFile.getAbsolutePath()));
+                    final File parent = outputFile.getParentFile();
+                    if (parent != null && !parent.exists()) {
+                        if (!parent.mkdirs()) {
+                            throw new IOException(
+                                    String.format(
+                                            "Couldn't create directory %s.",
+                                            parent.getAbsolutePath()));
+                        }
+                    }
                     final OutputStream outputFileStream = new FileOutputStream(outputFile);
                     IOUtils.copy(debInputStream, outputFileStream);
                     StreamUtil.close(outputFileStream);
@@ -153,6 +162,33 @@
     }
 
     /**
+     * Untar and ungzip a tar.gz file to a temp directory.
+     *
+     * @param targzFile the tar.gz file to extract.
+     * @param nameHint the prefix for the temp directory.
+     * @return the temp directory.
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    public static File extractTarGzipToTemp(File targzFile, String nameHint)
+            throws FileNotFoundException, IOException {
+        File unGzipDir = null;
+        File unTarDir = null;
+        try {
+            unGzipDir = FileUtil.createTempDir("extractTarGzip");
+            File tarFile = TarUtil.unGzip(targzFile, unGzipDir);
+            unTarDir = FileUtil.createTempDir(nameHint);
+            TarUtil.unTar(tarFile, unTarDir);
+            return unTarDir;
+        } catch (IOException e) {
+            FileUtil.recursiveDelete(unTarDir);
+            throw e;
+        } finally {
+            FileUtil.recursiveDelete(unGzipDir);
+        }
+    }
+
+    /**
      * Helper to extract and log to the reporters a tar gz file and its content
      *
      * @param listener the {@link ITestLogger} where to log the files.
diff --git a/src/com/android/tradefed/util/UserUtil.java b/src/com/android/tradefed/util/UserUtil.java
deleted file mode 100644
index cdab730..0000000
--- a/src/com/android/tradefed/util/UserUtil.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.util;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-
-public class UserUtil {
-    // From the UserInfo class.
-    public static final int FLAG_PRIMARY = 0x00000001;
-    public static final int FLAG_GUEST = 0x00000004;
-    public static final int FLAG_RESTRICTED = 0x00000008;
-    public static final int FLAG_MANAGED_PROFILE = 0x00000020;
-    public static final int USER_SYSTEM = 0;
-
-    public static final int FLAGS_NOT_SECONDARY =
-            FLAG_PRIMARY | FLAG_MANAGED_PROFILE | FLAG_GUEST | FLAG_RESTRICTED;
-
-    /** Thrown if a user switch could not happen. */
-    public static class UserSwitchFailedException extends Exception {
-        public UserSwitchFailedException(String message) {
-            super(message);
-        }
-    }
-
-    /** Thrown if a user switch could not happen because the secondary user could not be found. */
-    public static class SecondaryUserNotFoundException extends UserSwitchFailedException {
-        public SecondaryUserNotFoundException() {
-            super("Secondary User Not Found");
-        }
-    }
-
-    /** Parameters that specify which user to run the test module as. */
-    public enum UserType {
-        // TODO:(b/123077733) Add support for guest
-
-        /** current foreground user of the device */
-        CURRENT,
-        /** user flagged as primary on the device; most often primary = system user = user 0 */
-        PRIMARY,
-        /** system user = user 0 */
-        SYSTEM,
-        /** secondary user, i.e. non-primary and non-system. */
-        SECONDARY,
-    }
-
-    /**
-     * Attempt to switch to a user type.
-     *
-     * @returns true if successful, false if not.
-     */
-    public static void switchToUserType(ITestDevice device, UserType userType)
-            throws DeviceNotAvailableException, UserSwitchFailedException {
-        switch (userType) {
-            case CURRENT:
-                return; // do nothing
-            case SYSTEM:
-                switchUser(device, USER_SYSTEM);
-                return;
-            case PRIMARY:
-                switchUser(device, device.getPrimaryUserId());
-                return;
-            case SECONDARY:
-                switchToSecondaryUser(device);
-                return;
-            default:
-                throw new RuntimeException("userType case not covered: " + userType);
-        }
-    }
-
-    /**
-     * Attempt to switch to a secondary user, creating one if necessary.
-     *
-     * @returns true if successful, false if not.
-     */
-    private static void switchToSecondaryUser(ITestDevice device)
-            throws DeviceNotAvailableException, UserSwitchFailedException {
-        int currentUser = device.getCurrentUser();
-        if (device.isUserSecondary(currentUser)) {
-            CLog.d("currentUser is already secondary, no action.");
-            return;
-        }
-
-        int secondary = findExistingSecondary(device);
-        if (secondary <= 0) {
-            throw new SecondaryUserNotFoundException();
-        }
-
-        switchUser(device, secondary);
-    }
-
-    private static void switchUser(ITestDevice device, int userId)
-            throws DeviceNotAvailableException, UserSwitchFailedException {
-        if (!device.switchUser(userId)) {
-            throw new UserSwitchFailedException("Failed to switch to user " + userId);
-        }
-    }
-
-    /**
-     * Finds an arbitrary secondary user and returns the userId.
-     *
-     * <p>TODO: evaluate if a more comprehensive API is needed for this or not.
-     *
-     * @return id of the secondary user or -1 if one could not be found.
-     * @throws DeviceNotAvailableException
-     */
-    private static int findExistingSecondary(ITestDevice device)
-            throws DeviceNotAvailableException {
-        for (int userId : device.listUsers()) {
-            if (device.isUserSecondary(userId)) {
-                return userId;
-            }
-        }
-        // Returns a negative id if we couldn't find a proper existing secondary user.
-        return -1;
-    }
-}
diff --git a/src/com/android/tradefed/util/ZipUtil.java b/src/com/android/tradefed/util/ZipUtil.java
deleted file mode 100644
index 61f46a2..0000000
--- a/src/com/android/tradefed/util/ZipUtil.java
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.util;
-
-import com.android.tradefed.log.LogUtil.CLog;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.Enumeration;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.zip.GZIPOutputStream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipException;
-import java.util.zip.ZipFile;
-import java.util.zip.ZipOutputStream;
-
-/**
- * A helper class for compression-related operations
- */
-public class ZipUtil {
-
-    private static final String DEFAULT_DIRNAME = "dir";
-    private static final String DEFAULT_FILENAME = "files";
-    private static final String ZIP_EXTENSION = ".zip";
-
-    /**
-     * Utility method to verify that a zip file is not corrupt.
-     *
-     * @param zipFile the {@link File} to check
-     * @param thorough Whether to attempt to fully extract the archive.  If {@code false}, this
-     *        method will fail to detect CRC errors in a well-formed archive.
-     * @throws IOException if the file could not be opened or read
-     * @return {@code false} if the file appears to be corrupt; {@code true} otherwise
-     */
-    public static boolean isZipFileValid(File zipFile, boolean thorough) throws IOException {
-        if (zipFile != null && !zipFile.exists()) {
-            CLog.d("Zip file does not exist: %s", zipFile.getAbsolutePath());
-            return false;
-        }
-
-        try (ZipFile z = new ZipFile(zipFile)) {
-            if (thorough) {
-                // Reading the entire file is the only way to detect CRC errors within the archive
-                final File extractDir = FileUtil.createTempDir("extract-" + zipFile.getName());
-                try {
-                    extractZip(z, extractDir);
-                } finally {
-                    FileUtil.recursiveDelete(extractDir);
-                }
-            }
-        } catch (ZipException e) {
-            // File is likely corrupted
-            CLog.d("Detected corrupt zip file %s:", zipFile.getCanonicalPath());
-            CLog.e(e);
-            return false;
-        }
-
-        return true;
-    }
-
-    /**
-     * Utility method to extract entire contents of zip file into given directory
-     *
-     * @param zipFile the {@link ZipFile} to extract
-     * @param destDir the local dir to extract file to
-     * @throws IOException if failed to extract file
-     */
-    public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
-        Enumeration<? extends ZipEntry> entries = zipFile.entries();
-        while (entries.hasMoreElements()) {
-
-            ZipEntry entry = entries.nextElement();
-            File childFile = new File(destDir, entry.getName());
-            childFile.getParentFile().mkdirs();
-            if (entry.isDirectory()) {
-                continue;
-            } else {
-                FileUtil.writeToFile(zipFile.getInputStream(entry), childFile);
-            }
-        }
-    }
-
-    /**
-     * Utility method to extract one specific file from zip file into a tmp file
-     *
-     * @param zipFile the {@link ZipFile} to extract
-     * @param filePath the filePath of to extract
-     * @throws IOException if failed to extract file
-     * @return the {@link File} or null if not found
-     */
-    public static File extractFileFromZip(ZipFile zipFile, String filePath) throws IOException {
-        ZipEntry entry = zipFile.getEntry(filePath);
-        if (entry == null) {
-            return null;
-        }
-        File createdFile = FileUtil.createTempFile("extracted",
-                FileUtil.getExtension(filePath));
-        FileUtil.writeToFile(zipFile.getInputStream(entry), createdFile);
-        return createdFile;
-    }
-
-    /**
-     * Utility method to create a temporary zip file containing the given directory and
-     * all its contents.
-     *
-     * @param dir the directory to zip
-     * @return a temporary zip {@link File} containing directory contents
-     * @throws IOException if failed to create zip file
-     */
-    public static File createZip(File dir) throws IOException {
-        return createZip(dir, DEFAULT_DIRNAME);
-    }
-
-    /**
-     * Utility method to create a temporary zip file containing the given directory and
-     * all its contents.
-     *
-     * @param dir the directory to zip
-     * @param name the base name of the zip file created without the extension.
-     * @return a temporary zip {@link File} containing directory contents
-     * @throws IOException if failed to create zip file
-     */
-    public static File createZip(File dir, String name) throws IOException {
-        File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
-        createZip(dir, zipFile);
-        return zipFile;
-    }
-
-    /**
-     * Utility method to create a zip file containing the given directory and
-     * all its contents.
-     *
-     * @param dir the directory to zip
-     * @param zipFile the zip file to create - it should not already exist
-     * @throws IOException if failed to create zip file
-     */
-    public static void createZip(File dir, File zipFile) throws IOException {
-        ZipOutputStream out = null;
-        try {
-            FileOutputStream fileStream = new FileOutputStream(zipFile);
-            out = new ZipOutputStream(new BufferedOutputStream(fileStream));
-            addToZip(out, dir, new LinkedList<String>());
-        } catch (IOException e) {
-            zipFile.delete();
-            throw e;
-        } catch (RuntimeException e) {
-            zipFile.delete();
-            throw e;
-        } finally {
-            StreamUtil.close(out);
-        }
-    }
-
-    /**
-     * Utility method to create a temporary zip file containing the given files
-     *
-     * @param files list of files to zip
-     * @return a temporary zip {@link File} containing directory contents
-     * @throws IOException if failed to create zip file
-     */
-    public static File createZip(List<File> files) throws IOException {
-        return createZip(files, DEFAULT_FILENAME);
-    }
-
-    /**
-     * Utility method to create a temporary zip file containing the given files.
-     *
-     * @param files list of files to zip
-     * @param name the base name of the zip file created without the extension.
-     * @return a temporary zip {@link File} containing directory contents
-     * @throws IOException if failed to create zip file
-     */
-    public static File createZip(List<File> files, String name) throws IOException {
-        File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
-        createZip(files, zipFile);
-        return zipFile;
-    }
-
-    /**
-     * Utility method to create a zip file containing the given files
-     *
-     * @param files list of files to zip
-     * @param zipFile the zip file to create - it should not already exist
-     * @throws IOException if failed to create zip file
-     */
-    public static void createZip(List<File> files, File zipFile) throws IOException {
-        ZipOutputStream out = null;
-        try {
-            FileOutputStream fileStream = new FileOutputStream(zipFile);
-            out = new ZipOutputStream(new BufferedOutputStream(fileStream));
-            for (File file : files) {
-                addToZip(out, file, new LinkedList<String>());
-            }
-        } catch (IOException|RuntimeException e) {
-            zipFile.delete();
-            throw e;
-        } finally {
-            StreamUtil.close(out);
-        }
-    }
-
-    /**
-     * Recursively adds given file and its contents to ZipOutputStream
-     *
-     * @param out the {@link ZipOutputStream}
-     * @param file the {@link File} to add to the stream
-     * @param relativePathSegs the relative path of file, including separators
-     * @throws IOException if failed to add file to zip
-     */
-    public static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
-            throws IOException {
-        relativePathSegs.add(file.getName());
-        if (file.isDirectory()) {
-            // note: it appears even on windows, ZipEntry expects '/' as a path separator
-            relativePathSegs.add("/");
-        }
-        ZipEntry zipEntry = new ZipEntry(buildPath(relativePathSegs));
-        out.putNextEntry(zipEntry);
-        if (file.isFile()) {
-            writeToStream(file, out);
-        }
-        out.closeEntry();
-        if (file.isDirectory()) {
-            // recursively add contents
-            File[] subFiles = file.listFiles();
-            if (subFiles == null) {
-                throw new IOException(String.format("Could not read directory %s",
-                        file.getAbsolutePath()));
-            }
-            for (File subFile : subFiles) {
-                addToZip(out, subFile, relativePathSegs);
-            }
-            // remove the path separator
-            relativePathSegs.remove(relativePathSegs.size()-1);
-        }
-        // remove the last segment, added at beginning of method
-        relativePathSegs.remove(relativePathSegs.size()-1);
-    }
-
-    /**
-     * Close an open {@link ZipFile}, ignoring any exceptions.
-     *
-     * @param zipFile the file to close
-     */
-    public static void closeZip(ZipFile zipFile) {
-        if (zipFile != null) {
-            try {
-                zipFile.close();
-            } catch (IOException e) {
-                // ignore
-            }
-        }
-    }
-
-    /**
-     * Helper method to create a gzipped version of a single file.
-     *
-     * @param file the original file
-     * @param gzipFile the file to place compressed contents in
-     * @throws IOException
-     */
-    public static void gzipFile(File file, File gzipFile) throws IOException {
-        GZIPOutputStream out = null;
-        try {
-            FileOutputStream fileStream = new FileOutputStream(gzipFile);
-            out = new GZIPOutputStream(new BufferedOutputStream(fileStream, 64 * 1024));
-            writeToStream(file, out);
-        } catch (IOException e) {
-            gzipFile.delete();
-            throw e;
-        } catch (RuntimeException e) {
-            gzipFile.delete();
-            throw e;
-        } finally {
-            StreamUtil.close(out);
-        }
-    }
-
-    /**
-     * Helper method to write input file contents to output stream.
-     *
-     * @param file the input {@link File}
-     * @param out the {@link OutputStream}
-     *
-     * @throws IOException
-     */
-    private static void writeToStream(File file, OutputStream out) throws IOException {
-        InputStream inputStream = null;
-        try {
-            inputStream = new BufferedInputStream(new FileInputStream(file));
-            StreamUtil.copyStreams(inputStream, out);
-        } finally {
-            StreamUtil.close(inputStream);
-        }
-    }
-
-    /**
-     * Builds a file system path from a stack of relative path segments
-     *
-     * @param relativePathSegs the list of relative paths
-     * @return a {@link String} containing all relativePathSegs
-     */
-    private static String buildPath(List<String> relativePathSegs) {
-        StringBuilder pathBuilder = new StringBuilder();
-        for (String segment : relativePathSegs) {
-            pathBuilder.append(segment);
-        }
-        return pathBuilder.toString();
-    }
-
-    /**
-     * Extract a zip file to a temp directory prepended with a string
-     *
-     * @param zipFile the zip file to extract
-     * @param nameHint a prefix for the temp directory
-     * @return a {@link File} pointing to the temp directory
-     */
-    public static File extractZipToTemp(File zipFile, String nameHint)
-            throws IOException, ZipException {
-        File localRootDir = FileUtil.createTempDir(nameHint);
-        try (ZipFile zip = new ZipFile(zipFile)) {
-            extractZip(zip, localRootDir);
-            return localRootDir;
-        } catch (IOException e) {
-            // clean tmp file since we couldn't extract.
-            FileUtil.recursiveDelete(localRootDir);
-            throw e;
-        }
-    }
-}
diff --git a/src/com/android/tradefed/util/sl4a/Sl4aClient.java b/src/com/android/tradefed/util/sl4a/Sl4aClient.java
index a9954bb..b066c91 100644
--- a/src/com/android/tradefed/util/sl4a/Sl4aClient.java
+++ b/src/com/android/tradefed/util/sl4a/Sl4aClient.java
@@ -191,7 +191,7 @@
             mSocket = new Socket("localhost", mHostPort);
             CLog.i("is sl4a socket connected: %s", mSocket.isConnected());
             String rep = sendCommand(Sl4aClient.INIT);
-            CLog.i("response sl4a INIT: %s", rep);
+            CLog.i("response sl4a INIT: '%s', from device %s", rep, mDevice.getSerialNumber());
             JSONObject init = new JSONObject(rep);
             mUid = init.getInt("uid");
             startEventDispatcher();
@@ -223,11 +223,11 @@
         PrintWriter out = new PrintWriter(mSocket.getOutputStream(), true);
         out.print(message.toString());
         out.print('\n');
-        CLog.i("flushing");
+        CLog.d("flushing");
         out.flush();
-        CLog.i("sent");
+        CLog.d("sent");
         BufferedReader in = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
-        CLog.i("reading");
+        CLog.d("reading");
         String response = in.readLine();
         return response;
     }
@@ -240,14 +240,14 @@
      * @throws IOException
      */
     private synchronized Object sendThroughSocket(String message) throws IOException {
-        CLog.d("preparing sending: '%s'", message.toString());
+        CLog.d("preparing sending: '%s' to device %s", message, mDevice.getSerialNumber());
         PrintWriter out = new PrintWriter(mSocket.getOutputStream(), false);
-        out.print(message.toString());
+        out.print(message);
         out.print('\n');
         out.flush();
         BufferedReader in = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
         String response = in.readLine();
-        CLog.d("response: '%s'", response);
+        CLog.d("response: '%s' from device %s", response, mDevice.getSerialNumber());
         try {
             JSONObject resp = new JSONObject(response);
             if (!resp.isNull("error")) {
diff --git a/src/com/android/tradefed/util/testmapping/TestMapping.java b/src/com/android/tradefed/util/testmapping/TestMapping.java
index 79da7c2..fdc019d 100644
--- a/src/com/android/tradefed/util/testmapping/TestMapping.java
+++ b/src/com/android/tradefed/util/testmapping/TestMapping.java
@@ -42,6 +42,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -63,6 +65,10 @@
     private static final String DISABLED_PRESUBMIT_TESTS_FILE = "disabled-presubmit-tests";
 
     private Map<String, Set<TestInfo>> mTestCollection = null;
+    // Pattern used to identify comments start with "//" or "#" in TEST_MAPPING.
+    private static final Pattern COMMENTS_REGEX = Pattern.compile(
+            "(?m)[\\s\\t]*(//|#).*|(\".*?\")");
+    private static final Set<String> COMMENTS = new HashSet<>(Arrays.asList("#", "//"));
 
     /**
      * Constructor to create a {@link TestMapping} object from a path to TEST_MAPPING file.
@@ -75,7 +81,8 @@
         String relativePath = testMappingsDir.relativize(path.getParent()).toString();
         String errorMessage = null;
         try {
-            String content = String.join("", Files.readAllLines(path, StandardCharsets.UTF_8));
+            String content = removeComments(
+                    String.join("\n", Files.readAllLines(path, StandardCharsets.UTF_8)));
             if (content != null) {
                 JSONTokener tokener = new JSONTokener(content);
                 JSONObject root = new JSONObject(tokener);
@@ -139,6 +146,26 @@
     }
 
     /**
+     * Helper to remove comments in a TEST_MAPPING file to valid format. Only "//" and "#" are
+     * regarded as comments.
+     *
+     * @param jsonContent A {@link String} of json which content is from a TEST_MAPPING file.
+     * @return A {@link String} of valid json without comments.
+     */
+    @VisibleForTesting
+    static String removeComments(String jsonContent) {
+        StringBuffer out = new StringBuffer();
+        Matcher matcher = COMMENTS_REGEX.matcher(jsonContent);
+        while (matcher.find()) {
+            if (COMMENTS.contains(matcher.group(1))) {
+                matcher.appendReplacement(out, "");
+            }
+        }
+        matcher.appendTail(out);
+        return out.toString();
+    }
+
+    /**
      * Helper to get all tests set in a TEST_MAPPING file for a given group.
      *
      * @param testGroup A {@link String} of the test group.
diff --git a/src/com/android/tradefed/util/xml/AndroidManifestWriter.java b/src/com/android/tradefed/util/xml/AndroidManifestWriter.java
deleted file mode 100644
index 4f72f14..0000000
--- a/src/com/android/tradefed/util/xml/AndroidManifestWriter.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
- *
- * 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.xml;
-
-import com.android.tradefed.log.LogUtil.CLog;
-
-import org.w3c.dom.Attr;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
-
-import java.io.File;
-import java.io.IOException;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.Result;
-import javax.xml.transform.Source;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerConfigurationException;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
-
-/**
- * Helper class for modifying an AndroidManifest.
- * <p/>
- * copied from
- * <android source>/platform/sdk/.../adt-tests/com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper.
- * TODO: Find a way of sharing this code with adt-tests
- */
-public class AndroidManifestWriter {
-
-    private final Document mDoc;
-    private final String mOsManifestFilePath;
-
-    private static final String NODE_USES_SDK = "uses-sdk";
-    private static final String ATTRIBUTE_MIN_SDK_VERSION = "minSdkVersion";
-    /** Namespace for the resource XML, i.e. "http://schemas.android.com/apk/res/android" */
-    private final static String NS_RESOURCES = "http://schemas.android.com/apk/res/android";
-
-    private AndroidManifestWriter(Document doc, String osManifestFilePath) {
-        mDoc = doc;
-        mOsManifestFilePath = osManifestFilePath;
-    }
-
-    /**
-     * Sets the minimum SDK version for this manifest.
-     *
-     * @param minSdkVersion - the minimim sdk version to use
-     * @return <code>true</code> on success, false otherwise
-     */
-    public boolean setMinSdkVersion(String minSdkVersion) {
-        Element usesSdkElement = null;
-        NodeList nodeList = mDoc.getElementsByTagName(NODE_USES_SDK);
-        if (nodeList.getLength() > 0) {
-            usesSdkElement = (Element) nodeList.item(0);
-        } else {
-            usesSdkElement = mDoc.createElement(NODE_USES_SDK);
-            mDoc.getDocumentElement().appendChild(usesSdkElement);
-        }
-        Attr minSdkAttr = mDoc.createAttributeNS(NS_RESOURCES, ATTRIBUTE_MIN_SDK_VERSION);
-        String prefix = mDoc.lookupPrefix(NS_RESOURCES);
-        minSdkAttr.setPrefix(prefix);
-        minSdkAttr.setValue(minSdkVersion);
-        usesSdkElement.setAttributeNodeNS(minSdkAttr);
-        return saveXmlToFile();
-    }
-
-    private boolean saveXmlToFile() {
-        try {
-            // Prepare the DOM document for writing
-            Source source = new DOMSource(mDoc);
-
-            // Prepare the output file
-            File file = new File(mOsManifestFilePath);
-            Result result = new StreamResult(file);
-
-            // Write the DOM document to the file
-            Transformer xformer = TransformerFactory.newInstance().newTransformer();
-            xformer.transform(source, result);
-        } catch (TransformerConfigurationException e) {
-            CLog.e("Failed to write xml file %s", mOsManifestFilePath);
-            CLog.e(e);
-            return false;
-        } catch (TransformerException e) {
-            CLog.e("Failed to write xml file %s", mOsManifestFilePath);
-            CLog.e(e);
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Parses the manifest file, and collects data.
-     *
-     * @param osManifestFilePath The OS path of the manifest file to parse.
-     * @return an {@link AndroidManifestWriter} or null if parsing failed
-     */
-    public static AndroidManifestWriter parse(String osManifestFilePath) {
-        try {
-            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
-            docFactory.setNamespaceAware(true);
-            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
-            docBuilder.setErrorHandler(new DefaultHandler());
-            Document doc = docBuilder.parse(osManifestFilePath);
-            return new AndroidManifestWriter(doc, osManifestFilePath);
-        } catch (ParserConfigurationException e) {
-            CLog.e("Error parsing file %s", osManifestFilePath);
-            CLog.e(e);
-            return null;
-        } catch (SAXException e) {
-            CLog.e("Error parsing file %s", osManifestFilePath);
-            CLog.e(e);
-            return null;
-        } catch (IOException e) {
-            CLog.e("Error parsing file %s", osManifestFilePath);
-            CLog.e(e);
-            return null;
-        }
-    }
-}
diff --git a/test_framework/Android.bp b/test_framework/Android.bp
new file mode 100644
index 0000000..561d9a8
--- /dev/null
+++ b/test_framework/Android.bp
@@ -0,0 +1,25 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+    name: "tradefed-test-framework",
+    defaults: ["tradefed_defaults"],
+    srcs: [
+        "com/**/*.java",
+    ],
+    libs: [
+        "tradefed-lib-core",
+    ],
+}
+
diff --git a/src/com/android/tradefed/testtype/PythonUnitTestResultParser.java b/test_framework/com/android/tradefed/testtype/PythonUnitTestResultParser.java
similarity index 100%
rename from src/com/android/tradefed/testtype/PythonUnitTestResultParser.java
rename to test_framework/com/android/tradefed/testtype/PythonUnitTestResultParser.java
diff --git a/src/com/android/tradefed/testtype/PythonUnitTestRunner.java b/test_framework/com/android/tradefed/testtype/PythonUnitTestRunner.java
similarity index 100%
rename from src/com/android/tradefed/testtype/PythonUnitTestRunner.java
rename to test_framework/com/android/tradefed/testtype/PythonUnitTestRunner.java
diff --git a/src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
similarity index 95%
rename from src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
rename to test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
index 0185d23..302fca2 100644
--- a/src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
@@ -27,8 +27,10 @@
 import com.android.tradefed.testtype.IRuntimeHintProvider;
 import com.android.tradefed.testtype.IShardableTest;
 import com.android.tradefed.testtype.ITestCollector;
+import com.android.tradefed.util.StreamUtil;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -77,6 +79,8 @@
                         // Do not actually run the test if we are dry running it.
                         runBinary(path, listener, description);
                     }
+                } catch (IOException e) {
+                    listener.testFailed(description, StreamUtil.getStackTrace(e));
                 } finally {
                     listener.testEnded(description, new HashMap<String, Metric>());
                     listener.testRunEnded(
@@ -104,7 +108,7 @@
      */
     public abstract void runBinary(
             String binaryPath, ITestInvocationListener listener, TestDescription description)
-            throws DeviceNotAvailableException;
+            throws DeviceNotAvailableException, IOException;
 
     /** {@inheritDoc} */
     @Override
diff --git a/src/com/android/tradefed/testtype/binary/ExecutableHostTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
similarity index 67%
rename from src/com/android/tradefed/testtype/binary/ExecutableHostTest.java
rename to test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
index b22cb7f..949344a 100644
--- a/src/com/android/tradefed/testtype/binary/ExecutableHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
@@ -24,8 +24,11 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
+import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.testtype.IBuildReceiver;
 import com.android.tradefed.testtype.IDeviceTest;
@@ -36,6 +39,7 @@
 import com.android.tradefed.util.RunUtil;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -49,6 +53,8 @@
 public class ExecutableHostTest extends ExecutableBaseTest implements IDeviceTest, IBuildReceiver {
 
     private static final String ANDROID_SERIAL = "ANDROID_SERIAL";
+    private static final String LOG_STDOUT_TAG = "-binary-stdout-";
+    private static final String LOG_STDERR_TAG = "-binary-stderr-";
 
     @Option(
         name = "per-binary-timeout",
@@ -57,6 +63,14 @@
     )
     private long mTimeoutPerBinaryMs = 5 * 60 * 1000L;
 
+    @Option(
+        name = "relative-path-execution",
+        description =
+                "Some scripts assume a relative location to their tests file, this allows to"
+                        + " execute with that relative location."
+    )
+    private boolean mExecuteRelativeToScript = false;
+
     private ITestDevice mDevice;
     private IBuildInfo mBuild;
 
@@ -97,7 +111,7 @@
     @Override
     public void runBinary(
             String binaryPath, ITestInvocationListener listener, TestDescription description)
-            throws DeviceNotAvailableException {
+            throws DeviceNotAvailableException, IOException {
         IRunUtil runUtil = createRunUtil();
         // Output everything in stdout
         runUtil.setRedirectStderrToStdout(true);
@@ -109,20 +123,42 @@
         FileUtil.chmodRWXRecursively(new File(binaryPath));
 
         List<String> command = new ArrayList<>();
-        command.add(binaryPath);
-        CommandResult res =
-                runUtil.runTimedCmd(mTimeoutPerBinaryMs, command.toArray(new String[0]));
-        if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
-            // Everything should be outputted in stdout with our redirect above.
-            String errorMessage = res.getStdout();
-            if (CommandStatus.TIMED_OUT.equals(res.getStatus())) {
-                errorMessage += "\nTimeout.";
-            }
-            if (res.getExitCode() != null) {
-                errorMessage += String.format("\nExit Code: %s", res.getExitCode());
-            }
-            listener.testFailed(description, errorMessage);
+        String scriptName = new File(binaryPath).getName();
+        if (mExecuteRelativeToScript) {
+            String parentDir = new File(binaryPath).getParent();
+            command.add("bash");
+            command.add("-c");
+            command.add(String.format("pushd %s; ./%s;", parentDir, scriptName));
+        } else {
+            command.add(binaryPath);
         }
+        File stdout = FileUtil.createTempFile(scriptName + LOG_STDOUT_TAG, ".txt");
+        File stderr = FileUtil.createTempFile(scriptName + LOG_STDERR_TAG, ".txt");
+
+        try (FileOutputStream stdoutStream = new FileOutputStream(stdout);
+                FileOutputStream stderrStream = new FileOutputStream(stderr); ) {
+            CommandResult res =
+                    runUtil.runTimedCmd(
+                            mTimeoutPerBinaryMs,
+                            stdoutStream,
+                            stderrStream,
+                            command.toArray(new String[0]));
+            if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
+                // Everything should be outputted in stdout with our redirect above.
+                String errorMessage = FileUtil.readStringFromFile(stdout);
+                if (CommandStatus.TIMED_OUT.equals(res.getStatus())) {
+                    errorMessage += "\nTimeout.";
+                }
+                if (res.getExitCode() != null) {
+                    errorMessage += String.format("\nExit Code: %s", res.getExitCode());
+                }
+                listener.testFailed(description, errorMessage);
+            }
+        } finally {
+            logFile(stdout, listener);
+            logFile(stderr, listener);
+        }
+
         if (!(mDevice.getIDevice() instanceof StubDevice)) {
             // Ensure that the binary did not leave the device offline.
             CLog.d("Checking whether device is still online after %s", binaryPath);
@@ -155,4 +191,10 @@
     IRunUtil createRunUtil() {
         return new RunUtil();
     }
+
+    private void logFile(File logFile, ITestLogger logger) {
+        try (FileInputStreamSource source = new FileInputStreamSource(logFile, true)) {
+            logger.testLog(logFile.getName(), LogDataType.TEXT, source);
+        }
+    }
 }
diff --git a/src/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java b/test_framework/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java
similarity index 100%
rename from src/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java
rename to test_framework/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java
diff --git a/src/com/android/tradefed/testtype/python/PythonBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
similarity index 92%
rename from src/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
rename to test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
index 3a9ab1a..0286ea5 100644
--- a/src/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
@@ -177,12 +177,16 @@
         File updatedAdb = mBuildInfo.getFile(AdbStopServerPreparer.ADB_BINARY_KEY);
         if (updatedAdb == null) {
             String adbPath = getAdbPath();
-            updatedAdb = new File(adbPath);
-            if (!updatedAdb.exists()) {
-                updatedAdb = null;
+            // Don't check if it's the adb on the $PATH
+            if (!adbPath.equals("adb")) {
+                updatedAdb = new File(adbPath);
+                if (!updatedAdb.exists()) {
+                    updatedAdb = null;
+                }
             }
         }
         if (updatedAdb != null) {
+            CLog.d("Testing with adb binary at: %s", updatedAdb);
             // If a special adb version is used, pass it to the PATH
             CommandResult pathResult =
                     getRunUtil()
@@ -289,7 +293,19 @@
         @Override
         public void testRunStarted(String runName, int testCount) {
             // Replace run name
-            super.testRunStarted(mRunName, testCount);
+            testRunStarted(runName, testCount, 0);
+        }
+
+        @Override
+        public void testRunStarted(String runName, int testCount, int attempt) {
+            // Replace run name
+            testRunStarted(runName, testCount, attempt, System.currentTimeMillis());
+        }
+
+        @Override
+        public void testRunStarted(String runName, int testCount, int attempt, long startTime) {
+            // Replace run name
+            super.testRunStarted(mRunName, testCount, attempt, startTime);
         }
     }
 }
diff --git a/src/com/android/tradefed/testtype/suite/module/CarModuleController.java b/test_framework/com/android/tradefed/testtype/suite/module/CarModuleController.java
similarity index 100%
rename from src/com/android/tradefed/testtype/suite/module/CarModuleController.java
rename to test_framework/com/android/tradefed/testtype/suite/module/CarModuleController.java
diff --git a/src/com/android/tradefed/testtype/suite/module/NativeBridgeModuleController.java b/test_framework/com/android/tradefed/testtype/suite/module/NativeBridgeModuleController.java
similarity index 100%
rename from src/com/android/tradefed/testtype/suite/module/NativeBridgeModuleController.java
rename to test_framework/com/android/tradefed/testtype/suite/module/NativeBridgeModuleController.java
diff --git a/src/com/android/tradefed/testtype/suite/module/TestFailureModuleController.java b/test_framework/com/android/tradefed/testtype/suite/module/TestFailureModuleController.java
similarity index 100%
rename from src/com/android/tradefed/testtype/suite/module/TestFailureModuleController.java
rename to test_framework/com/android/tradefed/testtype/suite/module/TestFailureModuleController.java
diff --git a/test_result_interfaces/Android.bp b/test_result_interfaces/Android.bp
new file mode 100644
index 0000000..9a8cc0b
--- /dev/null
+++ b/test_result_interfaces/Android.bp
@@ -0,0 +1,27 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+    name: "tradefed-result-interfaces",
+    defaults: ["tradefed_defaults"],
+    srcs: [
+        "com/**/*.java",
+    ],
+    libs: [
+        "ddmlib-prebuilt",
+        "tradefed-common-util",
+        "tradefed-protos",
+    ],
+}
+
diff --git a/src/com/android/tradefed/log/ITestLogger.java b/test_result_interfaces/com/android/tradefed/log/ITestLogger.java
similarity index 96%
rename from src/com/android/tradefed/log/ITestLogger.java
rename to test_result_interfaces/com/android/tradefed/log/ITestLogger.java
index fbd29ef..d2d902d 100644
--- a/src/com/android/tradefed/log/ITestLogger.java
+++ b/test_result_interfaces/com/android/tradefed/log/ITestLogger.java
@@ -16,7 +16,6 @@
 
 package com.android.tradefed.log;
 
-import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 
diff --git a/src/com/android/tradefed/result/ITestLifeCycleReceiver.java b/test_result_interfaces/com/android/tradefed/result/ITestLifeCycleReceiver.java
similarity index 100%
rename from src/com/android/tradefed/result/ITestLifeCycleReceiver.java
rename to test_result_interfaces/com/android/tradefed/result/ITestLifeCycleReceiver.java
diff --git a/src/com/android/tradefed/result/ITestLoggerReceiver.java b/test_result_interfaces/com/android/tradefed/result/ITestLoggerReceiver.java
similarity index 100%
rename from src/com/android/tradefed/result/ITestLoggerReceiver.java
rename to test_result_interfaces/com/android/tradefed/result/ITestLoggerReceiver.java
diff --git a/src/com/android/tradefed/result/TestDescription.java b/test_result_interfaces/com/android/tradefed/result/TestDescription.java
similarity index 100%
rename from src/com/android/tradefed/result/TestDescription.java
rename to test_result_interfaces/com/android/tradefed/result/TestDescription.java
diff --git a/src/com/android/tradefed/util/proto/TestRecordProtoUtil.java b/test_result_interfaces/com/android/tradefed/util/proto/TestRecordProtoUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/proto/TestRecordProtoUtil.java
rename to test_result_interfaces/com/android/tradefed/util/proto/TestRecordProtoUtil.java
diff --git a/src/com/android/tradefed/util/proto/TfMetricProtoUtil.java b/test_result_interfaces/com/android/tradefed/util/proto/TfMetricProtoUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/proto/TfMetricProtoUtil.java
rename to test_result_interfaces/com/android/tradefed/util/proto/TfMetricProtoUtil.java
diff --git a/tests/OWNERS b/tests/OWNERS
index f53b0d8..d24ee00 100644
--- a/tests/OWNERS
+++ b/tests/OWNERS
@@ -1,4 +1,3 @@
-# tests ownership is wider: base OWNERS + folks familiar with testing
-allenhair@google.com
-gelanchezhian@google.com
-mrosenfeld@google.com
+# tests ownership is wider for Tradefed Unit tests
+
+per-file *.java, *.xml = *
diff --git a/tests/res/config/suite/stub-parameterized.xml b/tests/res/config/suite/stub-parameterized.xml
index 78fab09..5925e14 100644
--- a/tests/res/config/suite/stub-parameterized.xml
+++ b/tests/res/config/suite/stub-parameterized.xml
@@ -15,6 +15,7 @@
 -->
 <configuration description="A stub module with a parameterized metadata">
     <option name="metadata" key="parameter" value="instant_app" />
+    <option name="metadata" key="parameter" value="secondary_user" />
 
     <option name="test-suite-tag" value="example-suite-parameters" />
     <test class="com.android.tradefed.testtype.suite.TestSuiteStub" />
diff --git a/tests/res/testdata/test_mapping_golden1 b/tests/res/testdata/test_mapping_golden1
new file mode 100644
index 0000000..db3998d
--- /dev/null
+++ b/tests/res/testdata/test_mapping_golden1
@@ -0,0 +1,14 @@
+{
+  "presubmit": [
+    {
+      "name": "test1",
+      "host": true,
+      "include-filter": "testClass#testMethod"
+    }
+  ],
+  "imports": [
+    {
+      "path": "path1//path2//path3"
+    }
+  ]
+}
diff --git a/tests/res/testdata/test_mapping_golden2 b/tests/res/testdata/test_mapping_golden2
new file mode 100644
index 0000000..07486c0
--- /dev/null
+++ b/tests/res/testdata/test_mapping_golden2
@@ -0,0 +1,48 @@
+{
+  "presubmit": [
+    {
+      "name": "test1",
+      "host": true
+    },
+    {
+      "name": "suite/stub1"
+    },
+    {
+      "name": "suite/stub2",
+      "keywords": ["key_1"]
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "test2",
+      "options": [
+        {
+          "instrumentation-arg": "annotation=android.platform.test.annotations.Presubmit"
+        }
+      ]
+    },
+    {
+      "name": "instrument",
+      "options": [
+        {
+          "run-name": "some-name"
+        }
+      ]
+    }
+  ],
+  "othertype": [
+    {
+      "name": "test3",
+      "options": [
+        {
+          "just-an-option": ""
+        }
+      ]
+    }
+  ],
+  "imports": [
+    {
+      "path": "folder1//folder2//folder3"
+    }
+  ]
+}
diff --git a/tests/res/testdata/test_mapping_with_comments1 b/tests/res/testdata/test_mapping_with_comments1
new file mode 100644
index 0000000..3f4083f
--- /dev/null
+++ b/tests/res/testdata/test_mapping_with_comments1
@@ -0,0 +1,16 @@
+{#comments1
+  "presubmit": [//comments2 // comments3 # comment4
+  #comments3
+    { #comments4
+      "name": "test1",#comments5
+//comments6
+      "host": true,//comments7
+      "include-filter": "testClass#testMethod" #comment11 // another comments
+    }#comments8
+  ],#comments9 // another comments
+  "imports": [
+    {
+      "path": "path1//path2//path3"#comment12
+    }
+  ]
+}#comments10
diff --git a/tests/res/testdata/test_mapping_with_comments2 b/tests/res/testdata/test_mapping_with_comments2
new file mode 100644
index 0000000..da5a503
--- /dev/null
+++ b/tests/res/testdata/test_mapping_with_comments2
@@ -0,0 +1,49 @@
+{//comment..// another comment # another comment
+  "presubmit": [ #comment //another comment #// another comment
+    { # comment
+      "name": "test1",//comment
+      "host": true#comment
+    },#comment
+    {
+      "name": "suite/stub1" // comment # comment
+    },
+    {
+      "name": "suite/stub2",
+      "keywords": ["key_1"] # comment
+    } # comment
+  ], // comment
+  "postsubmit": [
+    {
+      "name": "test2",
+      "options": [
+        {
+          "instrumentation-arg": "annotation=android.platform.test.annotations.Presubmit" //comment
+        }
+      ]
+    },
+    {
+      "name": "instrument",
+      "options": [
+        {
+          "run-name": "some-name"
+        }
+      ]
+    }
+  ],
+  "othertype": [ // another comment
+    {
+      "name": "test3",
+      "options": [
+        {
+          "just-an-option": ""##comment
+        }////another comment
+      //// another comment...
+      ]
+    }
+  ],
+  "imports": [
+    {
+      "path": "folder1//folder2//folder3"//comment... # another comment // another comment
+    }
+  ]
+}
diff --git a/tests/res/util/partial_zip.zip b/tests/res/util/partial_zip.zip
new file mode 100644
index 0000000..1f50fef
--- /dev/null
+++ b/tests/res/util/partial_zip.zip
Binary files differ
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index c84ef69..1f5647d 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -26,6 +26,7 @@
 import com.android.tradefed.build.GCSTestResourceProviderTest;
 import com.android.tradefed.build.LocalDeviceBuildProviderTest;
 import com.android.tradefed.build.OtaZipfileBuildProviderTest;
+import com.android.tradefed.clearcut.ClearcutClientTest;
 import com.android.tradefed.command.CommandFileParserTest;
 import com.android.tradefed.command.CommandFileWatcherTest;
 import com.android.tradefed.command.CommandInterrupterTest;
@@ -33,7 +34,6 @@
 import com.android.tradefed.command.CommandRunnerTest;
 import com.android.tradefed.command.CommandSchedulerTest;
 import com.android.tradefed.command.ConsoleTest;
-import com.android.tradefed.command.VerifyTest;
 import com.android.tradefed.command.remote.RemoteManagerTest;
 import com.android.tradefed.command.remote.RemoteOperationTest;
 import com.android.tradefed.config.ArgsOptionParserTest;
@@ -52,22 +52,20 @@
 import com.android.tradefed.config.gcs.GCSConfigurationFactoryTest;
 import com.android.tradefed.config.gcs.GCSConfigurationServerTest;
 import com.android.tradefed.config.remote.GcsRemoteFileResolverTest;
+import com.android.tradefed.config.remote.HttpRemoteFileResolverTest;
+import com.android.tradefed.config.remote.LocalFileResolverTest;
 import com.android.tradefed.device.AndroidDebugBridgeWrapperTest;
 import com.android.tradefed.device.BackgroundDeviceActionTest;
-import com.android.tradefed.device.CpuStatsCollectorTest;
 import com.android.tradefed.device.DeviceManagerTest;
 import com.android.tradefed.device.DeviceSelectionOptionsTest;
 import com.android.tradefed.device.DeviceStateMonitorTest;
-import com.android.tradefed.device.DeviceUtilStatsMonitorTest;
 import com.android.tradefed.device.DumpsysPackageReceiverTest;
 import com.android.tradefed.device.FastbootHelperTest;
 import com.android.tradefed.device.ManagedDeviceListTest;
 import com.android.tradefed.device.ManagedTestDeviceFactoryTest;
 import com.android.tradefed.device.NativeDeviceTest;
-import com.android.tradefed.device.ReconnectingRecoveryTest;
 import com.android.tradefed.device.RemoteAndroidDeviceTest;
 import com.android.tradefed.device.TestDeviceTest;
-import com.android.tradefed.device.TopHelperTest;
 import com.android.tradefed.device.WaitDeviceRecoveryTest;
 import com.android.tradefed.device.WifiHelperTest;
 import com.android.tradefed.device.cloud.AcloudConfigParserTest;
@@ -75,6 +73,7 @@
 import com.android.tradefed.device.cloud.GceManagerTest;
 import com.android.tradefed.device.cloud.GceRemoteCmdFormatterTest;
 import com.android.tradefed.device.cloud.GceSshTunnelMonitorTest;
+import com.android.tradefed.device.cloud.ManagedRemoteDeviceTest;
 import com.android.tradefed.device.cloud.NestedRemoteDeviceTest;
 import com.android.tradefed.device.cloud.RemoteFileUtilTest;
 import com.android.tradefed.device.contentprovider.ContentProviderHandlerTest;
@@ -121,6 +120,7 @@
 import com.android.tradefed.invoker.TestInvocationMultiTest;
 import com.android.tradefed.invoker.TestInvocationTest;
 import com.android.tradefed.invoker.UnexecutedTestReporterThreadTest;
+import com.android.tradefed.invoker.logger.InvocationMetricLoggerTest;
 import com.android.tradefed.invoker.monitor.InvocationsMonitorTest;
 import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecutionTest;
 import com.android.tradefed.invoker.shard.ShardHelperTest;
@@ -130,6 +130,7 @@
 import com.android.tradefed.log.FileLoggerTest;
 import com.android.tradefed.log.HistoryLoggerTest;
 import com.android.tradefed.log.LogRegistryTest;
+import com.android.tradefed.log.SimpleFileLoggerTest;
 import com.android.tradefed.log.TerribleFailureEmailHandlerTest;
 import com.android.tradefed.postprocessor.AggregatePostProcessorTest;
 import com.android.tradefed.postprocessor.AveragePostProcessorTest;
@@ -158,6 +159,7 @@
 import com.android.tradefed.result.TestRunResultTest;
 import com.android.tradefed.result.TestSummaryTest;
 import com.android.tradefed.result.XmlResultReporterTest;
+import com.android.tradefed.result.ddmlib.InstrumentationResultProtoParserTest;
 import com.android.tradefed.result.ddmlib.TestRunToTestInvocationForwarderTest;
 import com.android.tradefed.result.proto.FileProtoResultReporterTest;
 import com.android.tradefed.result.proto.ProtoResultParserTest;
@@ -213,7 +215,9 @@
 import com.android.tradefed.targetprep.UserCleanerTest;
 import com.android.tradefed.targetprep.adb.AdbStopServerPreparerTest;
 import com.android.tradefed.targetprep.app.NoApkTestSkipperTest;
+import com.android.tradefed.targetprep.multi.DynamicSystemPreparerTest;
 import com.android.tradefed.targetprep.multi.MergeMultiBuildTargetPreparerTest;
+import com.android.tradefed.targetprep.multi.MixImageZipPreparerTest;
 import com.android.tradefed.targetprep.suite.SuiteApkInstallerTest;
 import com.android.tradefed.testtype.AndroidJUnitTestTest;
 import com.android.tradefed.testtype.DeviceBatteryLevelCheckerTest;
@@ -245,13 +249,13 @@
 import com.android.tradefed.testtype.PythonUnitTestResultParserTest;
 import com.android.tradefed.testtype.PythonUnitTestRunnerTest;
 import com.android.tradefed.testtype.TfTestLauncherTest;
-import com.android.tradefed.testtype.VersionedTfLauncherTest;
 import com.android.tradefed.testtype.binary.ExecutableHostTestTest;
 import com.android.tradefed.testtype.host.CoverageMeasurementForwarderTest;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4TestTest;
 import com.android.tradefed.testtype.junit4.DeviceParameterizedRunnerTest;
 import com.android.tradefed.testtype.junit4.LongevityHostRunnerTest;
 import com.android.tradefed.testtype.python.PythonBinaryHostTestTest;
+import com.android.tradefed.testtype.retry.ResultAggregatorTest;
 import com.android.tradefed.testtype.suite.AtestRunnerTest;
 import com.android.tradefed.testtype.suite.BaseTestSuiteTest;
 import com.android.tradefed.testtype.suite.GranularRetriableTestWrapperTest;
@@ -276,8 +280,6 @@
 import com.android.tradefed.testtype.suite.params.ModuleParametersHelperTest;
 import com.android.tradefed.testtype.suite.retry.ResultsPlayerTest;
 import com.android.tradefed.testtype.suite.retry.RetryReschedulerTest;
-import com.android.tradefed.testtype.testdefs.XmlDefsParserTest;
-import com.android.tradefed.testtype.testdefs.XmlDefsTestTest;
 import com.android.tradefed.util.AaptParserTest;
 import com.android.tradefed.util.AbiFormatterTest;
 import com.android.tradefed.util.AbiUtilsTest;
@@ -306,6 +308,7 @@
 import com.android.tradefed.util.LocalRunInstructionBuilderTest;
 import com.android.tradefed.util.LogcatEventParserTest;
 import com.android.tradefed.util.MultiMapTest;
+import com.android.tradefed.util.NativeCodeCoverageFlusherTest;
 import com.android.tradefed.util.NullUtilTest;
 import com.android.tradefed.util.PairTest;
 import com.android.tradefed.util.PropertyChangerTest;
@@ -329,7 +332,6 @@
 import com.android.tradefed.util.TestLoaderTest;
 import com.android.tradefed.util.TimeUtilTest;
 import com.android.tradefed.util.TimeValTest;
-import com.android.tradefed.util.UserUtilTest;
 import com.android.tradefed.util.VersionParserTest;
 import com.android.tradefed.util.ZipUtil2Test;
 import com.android.tradefed.util.ZipUtilTest;
@@ -348,7 +350,7 @@
 import com.android.tradefed.util.statsd.MetricUtilTest;
 import com.android.tradefed.util.testmapping.TestInfoTest;
 import com.android.tradefed.util.testmapping.TestMappingTest;
-import com.android.tradefed.util.xml.AndroidManifestWriterTest;
+import com.android.tradefed.util.zip.MergedZipEntryCollectionTest;
 
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
@@ -375,6 +377,9 @@
     LocalDeviceBuildProviderTest.class,
     OtaZipfileBuildProviderTest.class,
 
+    // clearcut
+    ClearcutClientTest.class,
+
     // command
     CommandFileParserTest.class,
     CommandFileWatcherTest.class,
@@ -383,7 +388,6 @@
     CommandRunnerTest.class,
     CommandSchedulerTest.class,
     ConsoleTest.class,
-    VerifyTest.class,
 
     // command.remote
     RemoteManagerTest.class,
@@ -410,25 +414,23 @@
 
     // config.remote
     GcsRemoteFileResolverTest.class,
+    HttpRemoteFileResolverTest.class,
+    LocalFileResolverTest.class,
 
     // device
     AndroidDebugBridgeWrapperTest.class,
     BackgroundDeviceActionTest.class,
-    CpuStatsCollectorTest.class,
     DeviceManagerTest.class,
     DeviceSelectionOptionsTest.class,
     DeviceStateMonitorTest.class,
-    DeviceUtilStatsMonitorTest.class,
     DumpsysPackageReceiverTest.class,
     FastbootHelperTest.class,
     ManagedDeviceListTest.class,
     ManagedTestDeviceFactoryTest.class,
     NativeDeviceTest.class,
-    ReconnectingRecoveryTest.class,
     RemoteAndroidDeviceTest.class,
     PropertyChangerTest.class,
     TestDeviceTest.class,
-    TopHelperTest.class,
     WaitDeviceRecoveryTest.class,
     WifiHelperTest.class,
 
@@ -438,6 +440,7 @@
     GceManagerTest.class,
     GceRemoteCmdFormatterTest.class,
     GceSshTunnelMonitorTest.class,
+    ManagedRemoteDeviceTest.class,
     NestedRemoteDeviceTest.class,
     RemoteAndroidDeviceTest.class,
     RemoteFileUtilTest.class,
@@ -502,6 +505,9 @@
     TestInvocationTest.class,
     UnexecutedTestReporterThreadTest.class,
 
+    // invoker.logger
+    InvocationMetricLoggerTest.class,
+
     // invoker.monitor
     InvocationsMonitorTest.class,
 
@@ -520,6 +526,7 @@
     FileLoggerTest.class,
     HistoryLoggerTest.class,
     LogRegistryTest.class,
+    SimpleFileLoggerTest.class,
     TerribleFailureEmailHandlerTest.class,
 
     // postprocessor
@@ -554,6 +561,7 @@
     XmlResultReporterTest.class,
 
     // result.ddmlib
+    InstrumentationResultProtoParserTest.class,
     TestRunToTestInvocationForwarderTest.class,
 
     // result.proto
@@ -578,6 +586,7 @@
     DeviceStorageFillerTest.class,
     DeviceStringPusherTest.class,
     DisableSELinuxTargetPreparerTest.class,
+    DynamicSystemPreparerTest.class,
     FastbootDeviceFlasherTest.class,
     FlashingResourcesParserTest.class,
     InstallAllTestZipAppsSetupTest.class,
@@ -608,6 +617,7 @@
 
     // targetprep.multi
     MergeMultiBuildTargetPreparerTest.class,
+    MixImageZipPreparerTest.class,
 
     // targetprep.suite
     SuiteApkInstallerTest.class,
@@ -663,7 +673,6 @@
     PythonUnitTestResultParserTest.class,
     PythonUnitTestRunnerTest.class,
     TfTestLauncherTest.class,
-    VersionedTfLauncherTest.class,
 
     // testtype/binary
     ExecutableHostTestTest.class,
@@ -676,6 +685,9 @@
     // testtype/python
     PythonBinaryHostTestTest.class,
 
+    // testtype/retry
+    ResultAggregatorTest.class,
+
     // testtype/suite
     AtestRunnerTest.class,
     BaseTestSuiteTest.class,
@@ -708,10 +720,6 @@
     ResultsPlayerTest.class,
     RetryReschedulerTest.class,
 
-    // testtype/testdefs
-    XmlDefsParserTest.class,
-    XmlDefsTestTest.class,
-
     // util
     AaptParserTest.class,
     AbiFormatterTest.class,
@@ -741,6 +749,8 @@
     ListInstrumentationParserTest.class,
     LogcatEventParserTest.class,
     MultiMapTest.class,
+    MergedZipEntryCollectionTest.class,
+    NativeCodeCoverageFlusherTest.class,
     NullUtilTest.class,
     PairTest.class,
     PsParserTest.class,
@@ -763,7 +773,6 @@
     TestLoaderTest.class,
     TimeUtilTest.class,
     TimeValTest.class,
-    UserUtilTest.class,
     VersionParserTest.class,
     ZipUtilTest.class,
     ZipUtil2Test.class,
@@ -796,9 +805,6 @@
     // util/testmapping
     TestInfoTest.class,
     TestMappingTest.class,
-
-    // util/xml
-    AndroidManifestWriterTest.class,
 })
 public class UnitTests {
     // empty of purpose
diff --git a/tests/src/com/android/tradefed/build/BuildInfoTest.java b/tests/src/com/android/tradefed/build/BuildInfoTest.java
index c54291e..46d4bd0 100644
--- a/tests/src/com/android/tradefed/build/BuildInfoTest.java
+++ b/tests/src/com/android/tradefed/build/BuildInfoTest.java
@@ -184,11 +184,20 @@
     /** Test that the build info can be described in its proto format. */
     @Test
     public void testProtoSerialization() throws Exception {
+        List<String> remoteFiles = Arrays.asList("remote/file1", "remote/file2");
+        for (String file : remoteFiles) {
+            mBuildInfo.setFile(
+                    IBuildInfo.REMOTE_FILE_PREFIX + file,
+                    new File(file),
+                    IBuildInfo.REMOTE_FILE_VERSION);
+        }
+
         BuildInformation.BuildInfo proto = mBuildInfo.toProto();
+
         assertEquals("1", proto.getBuildId());
         assertEquals(BuildInfo.class.getCanonicalName(), proto.getBuildInfoClass());
         assertEquals("value", proto.getAttributes().get("attribute"));
-        assertEquals(1, proto.getVersionedFileList().size());
+        assertEquals(3, proto.getVersionedFileList().size());
         assertNotNull(proto.getVersionedFileList().get(0));
 
         IBuildInfo deserialized = BuildInfo.fromProto(proto);
@@ -196,6 +205,11 @@
         // Build flavor was not set, so it's null
         assertNull(deserialized.getBuildFlavor());
         assertNotNull(deserialized.getVersionedFile(FILE_KEY));
+
+        // Check the remote files are restored.
+        for (String file : remoteFiles) {
+            assertTrue(deserialized.getRemoteFiles().contains(new File(file)));
+        }
     }
 
     /** Test {@link BuildInfo#getTestResource(List, String)} */
diff --git a/tests/src/com/android/tradefed/clearcut/ClearcutClientTest.java b/tests/src/com/android/tradefed/clearcut/ClearcutClientTest.java
new file mode 100644
index 0000000..fcab272
--- /dev/null
+++ b/tests/src/com/android/tradefed/clearcut/ClearcutClientTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+/** Unit tests for {@link ClearcutClient}. */
+@RunWith(JUnit4.class)
+public class ClearcutClientTest {
+
+    private ClearcutClient mClient;
+
+    @Before
+    public void setUp() {
+        mClient =
+                new ClearcutClient("url") {
+                    @Override
+                    boolean isGoogleUser() {
+                        return false;
+                    }
+                };
+    }
+
+    @After
+    public void tearDown() {
+        mClient.stop();
+    }
+
+    @Test
+    public void testGetGroupingKey() throws Exception {
+        File testFile = FileUtil.createTempFile("uuid-test", "");
+        try {
+            mClient.setCachedUuidFile(testFile);
+            String grouping = mClient.getGroupingKey();
+            // Key was created and written to cached file.
+            assertEquals(grouping, FileUtil.readStringFromFile(testFile));
+        } finally {
+            FileUtil.deleteFile(testFile);
+        }
+    }
+
+    @Test
+    public void testGetGroupingKey_exists() throws Exception {
+        File testFile = FileUtil.createTempFile("uuid-test", "");
+        try {
+            FileUtil.writeToFile("test", testFile);
+            mClient.setCachedUuidFile(testFile);
+            String grouping = mClient.getGroupingKey();
+            assertEquals("test", grouping);
+        } finally {
+            FileUtil.deleteFile(testFile);
+        }
+    }
+
+    @Test
+    public void testDisableClient() {
+        ClearcutClient c =
+                new ClearcutClient("url") {
+                    @Override
+                    boolean isClearcutDisabled() {
+                        return true;
+                    }
+
+                    @Override
+                    boolean isGoogleUser() {
+                        throw new RuntimeException("Should not be called if disabled");
+                    }
+                };
+        try {
+            c.notifyTradefedStartEvent();
+            c.notifyTradefedStartEvent();
+            c.notifyTradefedStartEvent();
+            assertEquals(0, c.getQueueSize());
+        } finally {
+            c.stop();
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
index 8e51cb0..8c9e9f4 100644
--- a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
@@ -34,7 +34,6 @@
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationFactory;
 import com.android.tradefed.config.IDeviceConfiguration;
-import com.android.tradefed.config.IGlobalConfiguration;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceSelectionOptions;
@@ -52,8 +51,6 @@
 import com.android.tradefed.invoker.ITestInvocation;
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.log.ILogRegistry.EventType;
-import com.android.tradefed.log.ITerribleFailureHandler;
-import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.RunUtil;
@@ -301,8 +298,8 @@
         String[] args2 = new String[] {"test"};
         setCreateConfigExpectations(args2, 1);
         setExpectedInvokeCalls(1);
-        mMockConfiguration.validateOptions(false);
         mMockConfiguration.validateOptions();
+        EasyMock.expectLastCall().times(2);
 
         replayMocks();
         mScheduler.start();
@@ -465,40 +462,6 @@
         }
     }
 
-    /** Verify that scheduler goes into shutdown mode when a {@link FatalHostError} is thrown. */
-    @Test
-    public void testRun_fatalError() throws Throwable {
-        mMockInvocation.invoke((IInvocationContext)EasyMock.anyObject(),
-                (IConfiguration)EasyMock.anyObject(), (IRescheduler)EasyMock.anyObject(),
-                (ITestInvocationListener)EasyMock.anyObject());
-        EasyMock.expectLastCall().andThrow(new FatalHostError("error"));
-        // set up a mock global config and wtfhandler to handle CLog.wtf when FatalHostError occurs
-        IGlobalConfiguration mockGc = EasyMock.createMock(IGlobalConfiguration.class);
-        CLog.setGlobalConfigInstance(mockGc);
-        try {
-            ITerribleFailureHandler mockWtf = EasyMock.createMock(ITerribleFailureHandler.class);
-            EasyMock.expect(mockGc.getWtfHandler()).andReturn(mockWtf).anyTimes();
-            EasyMock.expect(mockWtf.onTerribleFailure((String)EasyMock.anyObject(),
-                    (Throwable)EasyMock.anyObject())).andReturn(Boolean.TRUE);
-            String[] args = new String[] {"test"};
-            mMockManager.setNumDevices(2);
-            setCreateConfigExpectations(args, 1);
-            mMockConfiguration.validateOptions();
-            replayMocks(mockGc, mockWtf);
-            mScheduler.start();
-            mScheduler.addCommand(args);
-            // no need to call shutdown explicitly - scheduler should shutdown by itself
-            mScheduler.join(2 * 1000);
-            // We don't verify the mockManager for this test since after failure, the device might
-            // not have time to go back to list before shutdown on scheduler.
-            EasyMock.verify(
-                    mMockConfigFactory, mMockConfiguration, mMockInvocation, mockGc, mockWtf);
-        } finally {
-            // reset global config to null, which means 'not overloaded/use default'
-            CLog.setGlobalConfigInstance(null);
-        }
-    }
-
     /**
      * Test{@link CommandScheduler#run()} when config is matched to a specific device serial number
      *
diff --git a/tests/src/com/android/tradefed/command/VerifyTest.java b/tests/src/com/android/tradefed/command/VerifyTest.java
deleted file mode 100644
index e92b5d4..0000000
--- a/tests/src/com/android/tradefed/command/VerifyTest.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.command;
-
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.util.FileUtil;
-
-import junit.framework.TestCase;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Unit tests for {@link Verify}
- */
-public class VerifyTest extends TestCase {
-    private static final String TEST_CMD_FILE_PATH = "/testCmdFiles";
-    private final Verify mVerify;
-
-    public VerifyTest() throws ConfigurationException {
-        mVerify = new Verify();
-
-        OptionSetter option = new OptionSetter(mVerify);
-        option.setOptionValue("quiet", "true");
-    }
-
-    /**
-     * Extract an embedded command file into a temporary file, which we can feed to the
-     * CommandFileParser
-     */
-    private File extractTestCmdFile(String name) throws IOException {
-        final InputStream cmdFileStream = getClass().getResourceAsStream(
-                String.format("%s/%s.txt", TEST_CMD_FILE_PATH, name));
-        final String tmpFileName = String.format("VerifyTest_%s_", name);
-        File tmpFile = FileUtil.createTempFile(tmpFileName, ".txt");
-        try {
-            FileUtil.writeToFile(cmdFileStream, tmpFile);
-        } catch (Throwable t) {
-            // Clean up tmpFile, if it was created.
-            FileUtil.deleteFile(tmpFile);
-            throw t;
-        }
-
-        return tmpFile;
-    }
-
-    /**
-     * Assert that the specified command file parses correctly, and clean up any temporary files
-     */
-    private void assertGoodCmdFile(String name) throws IOException {
-        File cmdFile = extractTestCmdFile(name);
-        try {
-            assertTrue(mVerify.runVerify(cmdFile));
-        } finally {
-            FileUtil.deleteFile(cmdFile);
-        }
-    }
-
-    /**
-     * Assert that the specified command file does not parse correctly, and clean up any temporary
-     * files
-     */
-    private void assertBadCmdFile(String name) throws IOException {
-        File cmdFile = extractTestCmdFile(name);
-        try {
-            assertFalse(mVerify.runVerify(cmdFile));
-        } finally {
-            FileUtil.deleteFile(cmdFile);
-        }
-    }
-
-    public void testBasic() throws IOException {
-        assertGoodCmdFile("basic");
-    }
-
-    public void testMissingMacroDef() throws IOException {
-        assertBadCmdFile("missing-macro-def");
-    }
-
-    public void testMissingBeginMacro() throws IOException {
-        assertBadCmdFile("missing-begin-macro");
-    }
-
-    public void testMissingEndMacro() throws IOException {
-        assertBadCmdFile("missing-end-macro");
-    }
-}
diff --git a/tests/src/com/android/tradefed/config/ConfigurationTest.java b/tests/src/com/android/tradefed/config/ConfigurationTest.java
index ed189a5..5649fb0 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationTest.java
@@ -20,7 +20,6 @@
 import com.android.tradefed.build.IBuildProvider;
 import com.android.tradefed.command.CommandOptions;
 import com.android.tradefed.command.ICommandOptions;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.IDeviceRecovery;
 import com.android.tradefed.device.IDeviceSelection;
@@ -32,6 +31,7 @@
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IDisableable;
 import com.android.tradefed.util.MultiMap;
 
 import junit.framework.TestCase;
@@ -71,7 +71,7 @@
         public boolean getBool();
     }
 
-    private static class TestConfigObject implements TestConfig {
+    private static class TestConfigObject implements TestConfig, IDisableable {
 
         @Option(name = OPTION_NAME, description = OPTION_DESCRIPTION, requiredForRerun = true)
         private boolean mBool;
@@ -79,6 +79,11 @@
         @Option(name = ALT_OPTION_NAME, description = OPTION_DESCRIPTION)
         private Map<String, Boolean> mBoolMap = new HashMap<String, Boolean>();
 
+        @Option(name = "mandatory-option", mandatory = true)
+        private String mMandatory = null;
+
+        private boolean mIsDisabled = false;
+
         @Override
         public boolean getBool() {
             return mBool;
@@ -87,6 +92,16 @@
         public Map<String, Boolean> getMap() {
             return mBoolMap;
         }
+
+        @Override
+        public void setDisable(boolean isDisabled) {
+            mIsDisabled = isDisabled;
+        }
+
+        @Override
+        public boolean isDisabled() {
+            return mIsDisabled;
+        }
     }
 
     private Configuration mConfig;
@@ -98,6 +113,11 @@
     protected void setUp() throws Exception {
         super.setUp();
         mConfig = new Configuration(CONFIG_NAME, CONFIG_DESCRIPTION);
+
+        try {
+            GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
+        } catch (IllegalStateException ignored) {
+        }
     }
 
     /**
@@ -570,6 +590,33 @@
     }
 
     /**
+     * Test that {@link Configuration#validateOptions()} throw when all mandatory fields are not set
+     * and object is not disabled.
+     */
+    public void testValidateOptions_nonDisabledObject() throws ConfigurationException {
+        TestConfigObject object = new TestConfigObject();
+        object.setDisable(false);
+        mConfig.setConfigurationObject("helper", object);
+        try {
+            mConfig.validateOptions();
+            fail("Should have thrown an exception.");
+        } catch (ConfigurationException expected) {
+            assertTrue(expected.getMessage().contains("Found missing mandatory options"));
+        }
+    }
+
+    /**
+     * Test that {@link Configuration#validateOptions()} doesn't throw when all mandatory fields are
+     * not set but the object is disabled.
+     */
+    public void testValidateOptions_disabledObject() throws ConfigurationException {
+        TestConfigObject object = new TestConfigObject();
+        object.setDisable(true);
+        mConfig.setConfigurationObject("helper", object);
+        mConfig.validateOptions();
+    }
+
+    /**
      * Test that {@link Configuration#validateOptions()} throws a config exception when shard
      * count is negative number.
      */
@@ -647,7 +694,8 @@
         mConfig.setDeviceOptions(deviceOptions);
 
         // No exception for download is thrown because no download occurred.
-        mConfig.validateOptions(true);
+        mConfig.validateOptions();
+        mConfig.resolveDynamicOptions();
         // Dynamic file is not resolved.
         assertEquals(fakeConfigFile, deviceOptions.getAvdConfigFile());
     }
diff --git a/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java b/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java
index 75535e4..bd4fb49 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java
@@ -47,8 +47,8 @@
     private static final String DEVICE_MANAGER_TYPE_NAME = "device_manager";
 
     /**
-     * Test {@link ConfigurationUtil#dumpClassToXml(KXmlSerializer, String, Object, List, boolean)}
-     * to create a dump of a configuration.
+     * Test {@link ConfigurationUtil#dumpClassToXml(KXmlSerializer, String, Object, List, boolean,
+     * boolean)} to create a dump of a configuration.
      */
     @Test
     public void testDumpClassToXml() throws Throwable {
@@ -67,6 +67,7 @@
                     DEVICE_MANAGER_TYPE_NAME,
                     deviceManager,
                     new ArrayList<String>(),
+                    true,
                     true);
 
             serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
@@ -87,8 +88,8 @@
     }
 
     /**
-     * Test {@link ConfigurationUtil#dumpClassToXml(KXmlSerializer, String, Object, List, boolean)}
-     * to create a dump of a configuration with filters
+     * Test {@link ConfigurationUtil#dumpClassToXml(KXmlSerializer, String, Object, List, boolean,
+     * boolean)} to create a dump of a configuration with filters
      */
     @Test
     public void testDumpClassToXml_filtered() throws Throwable {
@@ -107,12 +108,14 @@
                     GlobalConfiguration.DEVICE_MANAGER_TYPE_NAME,
                     deviceManager,
                     Arrays.asList("com.android.tradefed.device.DeviceManager"),
+                    true,
                     true);
             ConfigurationUtil.dumpClassToXml(
                     serializer,
                     GlobalConfiguration.SCHEDULER_TYPE_NAME,
                     new CommandScheduler(),
                     Arrays.asList("com.android.tradefed.device.DeviceManager"),
+                    true,
                     true);
 
             serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
@@ -284,7 +287,8 @@
                     Configuration.TARGET_PREPARER_TYPE_NAME,
                     preparer,
                     new ArrayList<String>(),
-                    false);
+                    false,
+                    true);
 
             serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
             serializer.endDocument();
@@ -304,4 +308,46 @@
             FileUtil.deleteFile(tmpXml);
         }
     }
+
+    /** Only print options that have been changed. */
+    @Test
+    public void testDumpClassToXml_filterNotChanged() throws Throwable {
+        File tmpXml = FileUtil.createTempFile("global_config", ".xml");
+        try {
+            PrintWriter output = new PrintWriter(tmpXml);
+            KXmlSerializer serializer = new KXmlSerializer();
+            serializer.setOutput(output);
+            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+            serializer.startDocument("UTF-8", null);
+            serializer.startTag(null, ConfigurationUtil.CONFIGURATION_NAME);
+
+            ITargetPreparer preparer = new TestTargetPreparer();
+            OptionSetter changeOneOption = new OptionSetter(preparer);
+            changeOneOption.setOptionValue("real-option", "true");
+            ConfigurationUtil.dumpClassToXml(
+                    serializer,
+                    Configuration.TARGET_PREPARER_TYPE_NAME,
+                    preparer,
+                    new ArrayList<String>(),
+                    true,
+                    false);
+
+            serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
+            serializer.endDocument();
+
+            // Read the dump XML file, make sure configurations can be loaded.
+            String content = FileUtil.readStringFromFile(tmpXml);
+            assertTrue(content.length() > 100);
+            assertTrue(content.contains("<configuration>"));
+            assertTrue(content.contains("<option name=\"real-option\" value=\"true\" />"));
+            // Does not contain any trace of the deprecated option
+            assertFalse(content.contains("deprecated-option"));
+            assertTrue(
+                    content.contains(
+                            "<target_preparer class=\"com.android.tradefed.config."
+                                    + "ConfigurationUtilTest$TestTargetPreparer\">"));
+        } finally {
+            FileUtil.deleteFile(tmpXml);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
index 9958fb1..fc1c130 100644
--- a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
+++ b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.config;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -32,9 +33,11 @@
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -44,6 +47,7 @@
 @RunWith(JUnit4.class)
 public class DynamicRemoteFileResolverTest {
 
+    @OptionClass(alias = "alias-remote-file")
     private static class RemoteFileOption {
         @Option(name = "remote-file")
         public File remoteFile = null;
@@ -74,7 +78,7 @@
                 new DynamicRemoteFileResolver() {
                     @Override
                     protected IRemoteFileResolver getResolver(String protocol) {
-                        if (protocol.equals(GcsRemoteFileResolver.PROTOCOL)) {
+                        if (GcsRemoteFileResolver.PROTOCOL.equals(protocol)) {
                             return mMockResolver;
                         }
                         return null;
@@ -106,7 +110,9 @@
 
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+                                EasyMock.eq(new File("gs:/fake/path")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
                 .andReturn(fake);
         EasyMock.replay(mMockResolver);
 
@@ -125,6 +131,82 @@
     }
 
     @Test
+    public void testResolveWithQuery() throws Exception {
+        RemoteFileOption object = new RemoteFileOption();
+        OptionSetter setter =
+                new OptionSetter(object) {
+                    @Override
+                    protected DynamicRemoteFileResolver createResolver() {
+                        return mResolver;
+                    }
+                };
+
+        File fake = FileUtil.createTempFile("gs-option-setter-test", "txt");
+
+        setter.setOptionValue("remote-file", "gs://fake/path?key=value");
+        assertEquals("gs:/fake/path?key=value", object.remoteFile.getPath());
+
+        Map<String, String> testMap = new HashMap<>();
+        testMap.put("key", "value");
+        EasyMock.expect(
+                        mMockResolver.resolveRemoteFiles(
+                                EasyMock.eq(new File("gs:/fake/path")),
+                                EasyMock.anyObject(),
+                                EasyMock.eq(testMap)))
+                .andReturn(fake);
+        EasyMock.replay(mMockResolver);
+
+        Set<File> downloadedFile = setter.validateRemoteFilePath();
+        try {
+            assertEquals(1, downloadedFile.size());
+            File downloaded = downloadedFile.iterator().next();
+            // The file has been replaced by the downloaded one.
+            assertEquals(downloaded.getAbsolutePath(), object.remoteFile.getAbsolutePath());
+        } finally {
+            for (File f : downloadedFile) {
+                FileUtil.recursiveDelete(f);
+            }
+        }
+        EasyMock.verify(mMockResolver);
+    }
+
+    /** Test to make sure that a dynamic download marked as "optional" does not throw */
+    @Test
+    public void testResolveOptional() throws Exception {
+        RemoteFileOption object = new RemoteFileOption();
+        OptionSetter setter =
+                new OptionSetter(object) {
+                    @Override
+                    protected DynamicRemoteFileResolver createResolver() {
+                        return mResolver;
+                    }
+                };
+
+        setter.setOptionValue("remote-file", "gs://fake/path?optional=true");
+        assertEquals("gs:/fake/path?optional=true", object.remoteFile.getPath());
+
+        Map<String, String> testMap = new HashMap<>();
+        testMap.put("optional", "true");
+        EasyMock.expect(
+                        mMockResolver.resolveRemoteFiles(
+                                EasyMock.eq(new File("gs:/fake/path")),
+                                EasyMock.anyObject(),
+                                EasyMock.eq(testMap)))
+                .andThrow(new ConfigurationException("Failed to download"));
+        EasyMock.replay(mMockResolver);
+
+        Set<File> downloadedFile = setter.validateRemoteFilePath();
+        try {
+            assertEquals(0, downloadedFile.size());
+        } finally {
+            for (File f : downloadedFile) {
+                FileUtil.recursiveDelete(f);
+            }
+        }
+        EasyMock.verify(mMockResolver);
+    }
+
+    @Test
     public void testResolve_remoteFileList() throws Exception {
         RemoteFileOption object = new RemoteFileOption();
         OptionSetter setter =
@@ -143,7 +225,9 @@
 
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+                                EasyMock.eq(new File("gs:/fake/path")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
                 .andReturn(fake);
         EasyMock.replay(mMockResolver);
 
@@ -186,16 +270,20 @@
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
                                 EasyMock.eq(new File("gs://success/fake/path")),
+                                EasyMock.anyObject(),
                                 EasyMock.anyObject()))
                 .andReturn(fake);
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
                                 EasyMock.eq(new File("gs://success/fake/path2")),
+                                EasyMock.anyObject(),
                                 EasyMock.anyObject()))
                 .andReturn(fake);
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs://failure/test")), EasyMock.anyObject()))
+                                EasyMock.eq(new File("gs://failure/test")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
                 .andThrow(new ConfigurationException("retrieval error"));
         EasyMock.replay(mMockResolver);
         try {
@@ -229,11 +317,15 @@
 
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+                                EasyMock.eq(new File("gs:/fake/path")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
                 .andReturn(fake);
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs:/fake/path2")), EasyMock.anyObject()))
+                                EasyMock.eq(new File("gs:/fake/path2")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
                 .andReturn(fake2);
         EasyMock.replay(mMockResolver);
 
@@ -276,15 +368,21 @@
 
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+                                EasyMock.eq(new File("gs:/fake/path")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
                 .andReturn(fake);
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs:/fake/path2")), EasyMock.anyObject()))
+                                EasyMock.eq(new File("gs:/fake/path2")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
                 .andReturn(fake2);
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs:/fake/path3")), EasyMock.anyObject()))
+                                EasyMock.eq(new File("gs:/fake/path3")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
                 .andReturn(fake3);
         EasyMock.replay(mMockResolver);
 
@@ -325,7 +423,9 @@
         // anymore
         EasyMock.expect(
                         mMockResolver.resolveRemoteFiles(
-                                EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+                                EasyMock.eq(new File("gs:/fake/path")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
                 .andReturn(fake);
         EasyMock.replay(mMockResolver);
 
@@ -421,4 +521,102 @@
         }
         EasyMock.verify(mMockResolver);
     }
+
+    @Test
+    public void testResolvePartialDownloadZip() throws Exception {
+        List<String> includeFilters = Arrays.asList("test1", "test2");
+        List<String> excludeFilters = Arrays.asList("[.]config");
+
+        Map<String, String> queryArgs = new HashMap<>();
+        queryArgs.put("partial_download_dir", "/tmp");
+        queryArgs.put("include_filters", "test1;test2");
+        queryArgs.put("exclude_filters", "[.]config");
+        EasyMock.expect(
+                        mMockResolver.resolveRemoteFiles(
+                                EasyMock.eq(new File("gs:/fake/path")),
+                                EasyMock.eq(null),
+                                EasyMock.eq(queryArgs)))
+                .andReturn(null);
+        EasyMock.replay(mMockResolver);
+
+        mResolver.resolvePartialDownloadZip(
+                new File("/tmp"), "gs:/fake/path", includeFilters, excludeFilters);
+        EasyMock.verify(mMockResolver);
+    }
+
+    /** Ignore any error if the download request is optional. */
+    @Test
+    public void testResolvePartialDownloadZip_optional() throws Exception {
+        List<String> includeFilters = Arrays.asList("test1", "test2");
+        List<String> excludeFilters = Arrays.asList("[.]config");
+
+        Map<String, String> queryArgs = new HashMap<>();
+        queryArgs.put("partial_download_dir", "/tmp");
+        queryArgs.put("include_filters", "test1;test2");
+        queryArgs.put("exclude_filters", "[.]config");
+        queryArgs.put("optional", "true");
+        EasyMock.expect(
+                        mMockResolver.resolveRemoteFiles(
+                                EasyMock.eq(new File("gs:/fake/path?optional=true")),
+                                EasyMock.eq(null),
+                                EasyMock.eq(queryArgs)))
+                .andThrow(new ConfigurationException("should not throw this exception."));
+        EasyMock.replay(mMockResolver);
+
+        mResolver.resolvePartialDownloadZip(
+                new File("/tmp"), "gs:/fake/path?optional=true", includeFilters, excludeFilters);
+        EasyMock.verify(mMockResolver);
+    }
+
+    /**
+     * Ensure that the same field on two different objects can be set with different remote values.
+     */
+    @Test
+    public void testResolveTwoObjects() throws Exception {
+        RemoteFileOption object1 = new RemoteFileOption();
+        RemoteFileOption object2 = new RemoteFileOption();
+        OptionSetter setter =
+                new OptionSetter(object1, object2) {
+                    @Override
+                    protected DynamicRemoteFileResolver createResolver() {
+                        return mResolver;
+                    }
+                };
+
+        File fake = FileUtil.createTempFile("gs-option-setter-test", "txt");
+        setter.setOptionValue("alias-remote-file:1:remote-file", "gs://fake/path");
+        assertEquals("gs:/fake/path", object1.remoteFile.getPath());
+
+        File fake2 = FileUtil.createTempFile("gs-option-setter-test", "txt");
+        setter.setOptionValue("alias-remote-file:2:remote-file", "gs://fake2/path2");
+        assertEquals("gs:/fake2/path2", object2.remoteFile.getPath());
+
+        EasyMock.expect(
+                        mMockResolver.resolveRemoteFiles(
+                                EasyMock.eq(new File("gs:/fake/path")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
+                .andReturn(fake);
+        EasyMock.expect(
+                        mMockResolver.resolveRemoteFiles(
+                                EasyMock.eq(new File("gs:/fake2/path2")),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
+                .andReturn(fake2);
+        EasyMock.replay(mMockResolver);
+
+        Set<File> downloadedFile = setter.validateRemoteFilePath();
+        try {
+            assertEquals(2, downloadedFile.size());
+            assertTrue(downloadedFile.contains(object1.remoteFile));
+            assertTrue(downloadedFile.contains(object2.remoteFile));
+
+            assertFalse(object1.remoteFile.equals(object2.remoteFile));
+        } finally {
+            for (File f : downloadedFile) {
+                FileUtil.recursiveDelete(f);
+            }
+        }
+        EasyMock.verify(mMockResolver);
+    }
 }
diff --git a/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java
index f73e4df..5d9e007 100644
--- a/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java
+++ b/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java
@@ -16,12 +16,17 @@
 package com.android.tradefed.config.remote;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.android.tradefed.build.BuildRetrievalError;
 import com.android.tradefed.build.gcs.GCSDownloaderHelper;
 import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -30,6 +35,8 @@
 import org.mockito.Mockito;
 
 import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
 
 /** Unit tests for {@link GcsRemoteFileResolver}. */
 @RunWith(JUnit4.class)
@@ -52,7 +59,8 @@
 
     @Test
     public void testResolve() throws Exception {
-        mResolver.resolveRemoteFiles(new File("gs:/fake/file"), Mockito.mock(Option.class));
+        mResolver.resolveRemoteFiles(
+                new File("gs:/fake/file"), Mockito.mock(Option.class), new HashMap<>());
 
         Mockito.verify(mMockHelper).fetchTestResource("gs:/fake/file");
     }
@@ -64,7 +72,8 @@
                 .fetchTestResource("gs:/fake/file");
 
         try {
-            mResolver.resolveRemoteFiles(new File("gs:/fake/file"), Mockito.mock(Option.class));
+            mResolver.resolveRemoteFiles(
+                    new File("gs:/fake/file"), Mockito.mock(Option.class), new HashMap<>());
             fail("Should have thrown an exception.");
         } catch (ConfigurationException expected) {
             assertEquals(
@@ -74,4 +83,50 @@
 
         Mockito.verify(mMockHelper).fetchTestResource("gs:/fake/file");
     }
+
+    /** Test that we can request a zip to be unzipped automatically. */
+    @Test
+    public void testResolve_unzip() throws Exception {
+        File testDir = FileUtil.createTempDir("test-resolve-dir");
+        File zipFile = ZipUtil.createZip(testDir);
+        File resolvedFile = null;
+        try {
+            Mockito.doReturn(zipFile).when(mMockHelper).fetchTestResource("gs:/fake/file");
+            Map<String, String> query = new HashMap<>();
+            query.put(DynamicRemoteFileResolver.UNZIP_KEY, /* Case doesn't matter */ "TrUe");
+            resolvedFile =
+                    mResolver.resolveRemoteFiles(
+                            new File("gs:/fake/file"), Mockito.mock(Option.class), query);
+            // File was unzipped
+            assertTrue(resolvedFile.isDirectory());
+            // Zip file was cleaned
+            assertFalse(zipFile.exists());
+
+            Mockito.verify(mMockHelper).fetchTestResource("gs:/fake/file");
+        } finally {
+            FileUtil.recursiveDelete(testDir);
+            FileUtil.deleteFile(zipFile);
+            FileUtil.recursiveDelete(resolvedFile);
+        }
+    }
+
+    /** Test that if we request to unzip a non-zip file, nothing is done. */
+    @Test
+    public void testResolve_notZip() throws Exception {
+        File testFile = FileUtil.createTempFile("test-resolve-file", ".txt");
+        try {
+            Mockito.doReturn(testFile).when(mMockHelper).fetchTestResource("gs:/fake/file");
+            Map<String, String> query = new HashMap<>();
+            query.put(DynamicRemoteFileResolver.UNZIP_KEY, /* Case doesn't matter */ "TrUe");
+            File resolvedFile =
+                    mResolver.resolveRemoteFiles(
+                            new File("gs:/fake/file"), Mockito.mock(Option.class), query);
+            // File was not unzipped since it's not one
+            assertEquals(testFile, resolvedFile);
+
+            Mockito.verify(mMockHelper).fetchTestResource("gs:/fake/file");
+        } finally {
+            FileUtil.deleteFile(testFile);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/config/remote/HttpRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/remote/HttpRemoteFileResolverTest.java
new file mode 100644
index 0000000..5419916
--- /dev/null
+++ b/tests/src/com/android/tradefed/config/remote/HttpRemoteFileResolverTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.net.IHttpHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+/** Unit tests for {@link HttpRemoteFileResolver}. */
+@RunWith(JUnit4.class)
+public class HttpRemoteFileResolverTest {
+    private HttpRemoteFileResolver mResolver;
+    private IHttpHelper mHttpDownloader;
+
+    @Before
+    public void setUp() {
+        mHttpDownloader = Mockito.mock(IHttpHelper.class);
+        mResolver =
+                new HttpRemoteFileResolver() {
+                    @Override
+                    protected IHttpHelper getDownloader() {
+                        return mHttpDownloader;
+                    }
+                };
+    }
+
+    @Test
+    public void testResolve() throws Exception {
+        File res =
+                mResolver.resolveRemoteFiles(
+                        new File("http:/fake/HttpRemoteFileResolverTest"),
+                        Mockito.mock(Option.class),
+                        new HashMap<>());
+        FileUtil.deleteFile(res);
+
+        Mockito.verify(mHttpDownloader)
+                .doGet(Mockito.eq("http://fake/HttpRemoteFileResolverTest"), Mockito.any());
+    }
+
+    @Test
+    public void testResolve_error() throws Exception {
+        Mockito.doThrow(new IOException("download failure"))
+                .when(mHttpDownloader)
+                .doGet(Mockito.eq("http://fake/HttpRemoteFileResolverTest"), Mockito.any());
+
+        try {
+            mResolver.resolveRemoteFiles(
+                    new File("http:/fake/HttpRemoteFileResolverTest"),
+                    Mockito.mock(Option.class),
+                    new HashMap<>());
+            fail("Should have thrown an exception.");
+        } catch (ConfigurationException expected) {
+            assertEquals(
+                    "Failed to download http://fake/HttpRemoteFileResolverTest due to: download failure",
+                    expected.getMessage());
+        }
+
+        Mockito.verify(mHttpDownloader)
+                .doGet(Mockito.eq("http://fake/HttpRemoteFileResolverTest"), Mockito.any());
+    }
+}
diff --git a/tests/src/com/android/tradefed/config/remote/LocalFileResolverTest.java b/tests/src/com/android/tradefed/config/remote/LocalFileResolverTest.java
new file mode 100644
index 0000000..86d35f6
--- /dev/null
+++ b/tests/src/com/android/tradefed/config/remote/LocalFileResolverTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.io.File;
+
+/** Unit tests for {@link LocalFileResolver}. */
+@RunWith(JUnit4.class)
+public class LocalFileResolverTest {
+
+    private LocalFileResolver mResolver;
+
+    @Before
+    public void setUp() {
+        mResolver = new LocalFileResolver();
+    }
+
+    @Test
+    public void testResolveLocalFile() throws Exception {
+        File testFile = FileUtil.createTempFile("test-local-file", ".txt");
+        try {
+            File markedFile = new File("file:" + testFile.getAbsolutePath());
+            File returned = mResolver.resolveRemoteFiles(markedFile, Mockito.mock(Option.class));
+            assertEquals(testFile, returned);
+        } finally {
+            FileUtil.deleteFile(testFile);
+        }
+    }
+
+    @Test
+    public void testResolveLocalFile_notFound() throws Exception {
+        File markedFile = new File("file:whateverpathsomewhere");
+        try {
+            mResolver.resolveRemoteFiles(markedFile, Mockito.mock(Option.class));
+            fail("Should have thrown an exception.");
+        } catch (ConfigurationException expected) {
+            // Expected
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/device/CpuStatsCollectorTest.java b/tests/src/com/android/tradefed/device/CpuStatsCollectorTest.java
deleted file mode 100644
index 9bf82bc..0000000
--- a/tests/src/com/android/tradefed/device/CpuStatsCollectorTest.java
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.device.CpuStatsCollector.CpuStats;
-import com.android.tradefed.device.CpuStatsCollector.TimeCategory;
-import com.android.tradefed.testtype.DeviceTestCase;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * Unit tests for {@link CpuStatsCollector}.
- */
-public class CpuStatsCollectorTest extends DeviceTestCase {
-    /**
-     * Single output for cpustats tool where frequencies are aggregated in the total.
-     */
-    private final static String[] SINGLE_OUTPUT = {
-        "Total,2,3,5,7,11,13,17,350000,19,700000,23,920000,29,1200000,31",
-        "cpu0,37,41,43,47,53,59,61,350000,67,700000,71,920000,73,1200000,79",
-        "cpu1,83,89,97,101,103,107,109,350000,113,700000,127,920000,131,1200000,137",
-        ""};
-
-    /**
-     * Single output for cpustats tool where frequencies are not aggregated in the total.
-     */
-    private final static String[] SINGLE_NON_AGGREGATE_OUTPUT = {
-        "Total,2,3,5,7,11,13,17",
-        "cpu0,37,41,43,47,53,59,61,350000,67,700000,71,920000,73,1200000,79",
-        "cpu1,83,89,97,101,103,107,109,350000,113,700000,127,920000,131,1200000,137",
-        ""};
-
-    /**
-     * Multiline output for cpustats tool.
-     */
-    private final static String[] MULTI_OUTPUT = {
-        "Total,246,0,69,283,0,0,0,350000,140,700000,22,920000,122,1200000,318",
-        "cpu0,68,0,10,221,0,0,0,350000,70,700000,11,920000,61,1200000,159",
-        "cpu1,177,0,60,63,0,0,0,350000,70,700000,11,920000,61,1200000,159",
-        "",
-        "Total,238,0,75,287,0,0,0,350000,124,700000,12,920000,68,1200000,396",
-        "cpu0,53,0,9,238,0,0,0,350000,62,700000,6,920000,34,1200000,198",
-        "cpu1,186,0,65,49,0,0,0,350000,62,700000,6,920000,34,1200000,198",
-        "",
-        "Total,230,0,71,299,0,0,0,350000,0,700000,0,920000,230,1200000,370",
-        "cpu0,2,0,3,295,0,0,0,350000,0,700000,0,920000,115,1200000,185",
-        "cpu1,228,0,69,3,0,0,0,350000,0,700000,0,920000,115,1200000,185",
-        "",
-        "Total,248,0,59,293,0,0,0,350000,50,700000,4,920000,330,1200000,216",
-        "cpu0,28,0,2,270,0,0,0,350000,25,700000,2,920000,165,1200000,108",
-        "cpu1,219,0,56,24,0,0,0,350000,25,700000,2,920000,165,1200000,108",
-        "",
-        "Total,250,0,63,288,0,0,0,350000,184,700000,22,920000,164,1200000,230",
-        "cpu0,79,0,21,201,0,0,0,350000,92,700000,11,920000,82,1200000,115",
-        "cpu1,171,0,43,86,0,0,0,350000,92,700000,11,920000,82,1200000,115",
-        "",
-        "Total,231,0,80,289,0,0,0,350000,96,700000,4,920000,176,1200000,322",
-        "cpu0,51,0,7,243,0,0,0,350000,48,700000,2,920000,88,1200000,161",
-        "cpu1,181,0,73,47,0,0,0,350000,48,700000,2,920000,88,1200000,161",
-        "",
-        "Total,248,0,69,283,0,0,0,350000,404,700000,22,920000,26,1200000,150",
-        "cpu0,161,0,13,125,0,0,0,350000,202,700000,11,920000,13,1200000,75",
-        "cpu1,86,0,55,158,0,0,0,350000,202,700000,11,920000,13,1200000,75",
-        "",
-        "Total,269,0,97,233,0,0,0,350000,214,700000,18,920000,18,1200000,350",
-        "cpu0,40,0,42,218,0,0,0,350000,107,700000,9,920000,9,1200000,175",
-        "cpu1,230,0,55,15,0,0,0,350000,107,700000,9,920000,9,1200000,175",
-        "",
-        "Total,260,0,100,235,0,0,0,350000,152,700000,10,920000,438,1200000,0",
-        "cpu0,207,0,72,20,0,0,0,350000,76,700000,5,920000,219,1200000,0",
-        "cpu1,53,0,29,214,0,0,0,350000,76,700000,5,920000,219,1200000,0",
-        "",
-        "Total,248,0,65,287,0,0,0,350000,50,700000,30,920000,336,1200000,184",
-        "cpu0,26,0,14,259,0,0,0,350000,25,700000,15,920000,168,1200000,92",
-        "cpu1,221,0,51,28,0,0,0,350000,25,700000,15,920000,168,1200000,92",
-        ""};
-
-    /**
-     * Multiline output for cpustats tool where frequencies are not aggregated in the total.
-     */
-    private final static String[] MULTI_NON_AGGREGATE_OUTPUT = {
-        "Total,246,0,69,283,0,0,0",
-        "cpu0,68,0,10,221,0,0,0,350000,70,700000,11,920000,61,1200000,159",
-        "cpu1,177,0,60,63,0,0,0,350000,70,700000,11,920000,61,1200000,159",
-        "",
-        "Total,238,0,75,287,0,0,0",
-        "cpu0,53,0,9,238,0,0,0,350000,62,700000,6,920000,34,1200000,198",
-        "cpu1,186,0,65,49,0,0,0,350000,62,700000,6,920000,34,1200000,198",
-        "",
-        "Total,230,0,71,299,0,0,0",
-        "cpu0,2,0,3,295,0,0,0,350000,0,700000,0,920000,115,1200000,185",
-        "cpu1,228,0,69,3,0,0,0,350000,0,700000,0,920000,115,1200000,185",
-        "",
-        "Total,248,0,59,293,0,0,0",
-        "cpu0,28,0,2,270,0,0,0,350000,25,700000,2,920000,165,1200000,108",
-        "cpu1,219,0,56,24,0,0,0,350000,25,700000,2,920000,165,1200000,108",
-        "",
-        "Total,250,0,63,288,0,0,0",
-        "cpu0,79,0,21,201,0,0,0,350000,92,700000,11,920000,82,1200000,115",
-        "cpu1,171,0,43,86,0,0,0,350000,92,700000,11,920000,82,1200000,115",
-        "",
-        "Total,231,0,80,289,0,0,0",
-        "cpu0,51,0,7,243,0,0,0,350000,48,700000,2,920000,88,1200000,161",
-        "cpu1,181,0,73,47,0,0,0,350000,48,700000,2,920000,88,1200000,161",
-        "",
-        "Total,248,0,69,283,0,0,0",
-        "cpu0,161,0,13,125,0,0,0,350000,202,700000,11,920000,13,1200000,75",
-        "cpu1,86,0,55,158,0,0,0,350000,202,700000,11,920000,13,1200000,75",
-        "",
-        "Total,269,0,97,233,0,0,0",
-        "cpu0,40,0,42,218,0,0,0,350000,107,700000,9,920000,9,1200000,175",
-        "cpu1,230,0,55,15,0,0,0,350000,107,700000,9,920000,9,1200000,175",
-        "",
-        "Total,260,0,100,235,0,0,0",
-        "cpu0,207,0,72,20,0,0,0,350000,76,700000,5,920000,219,1200000,0",
-        "cpu1,53,0,29,214,0,0,0,350000,76,700000,5,920000,219,1200000,0",
-        "",
-        "Total,248,0,65,287,0,0,0",
-        "cpu0,26,0,14,259,0,0,0,350000,25,700000,15,920000,168,1200000,92",
-        "cpu1,221,0,51,28,0,0,0,350000,25,700000,15,920000,168,1200000,92",
-        ""};
-
-    private CpuStatsCollector mCollector;
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mCollector = new CpuStatsCollector(null, 1);
-    }
-
-    /**
-     * Test that a single output from {@code cpustats} is parsed correctly and that {@link CpuStats}
-     * contains the correct data and calculates the correct data.
-     */
-    public void testCpuStatsParser_single() {
-        mCollector.getReceiver().processNewLines(SINGLE_OUTPUT);
-
-        Map<String, List<CpuStats>> cpuStats = mCollector.getCpuStats();
-        assertEquals(3, cpuStats.size());
-        assertTrue(cpuStats.containsKey("Total"));
-        assertTrue(cpuStats.containsKey("cpu0"));
-        assertTrue(cpuStats.containsKey("cpu1"));
-
-        assertEquals(1, cpuStats.get("Total").size());
-        CpuStats stats = cpuStats.get("Total").get(0);
-
-        // Time info
-        assertEquals(2, stats.mTimeStats.get(TimeCategory.USER).intValue());
-        assertEquals(3, stats.mTimeStats.get(TimeCategory.NICE).intValue());
-        assertEquals(5, stats.mTimeStats.get(TimeCategory.SYS).intValue());
-        assertEquals(7, stats.mTimeStats.get(TimeCategory.IDLE).intValue());
-        assertEquals(11, stats.mTimeStats.get(TimeCategory.IOW).intValue());
-        assertEquals(13, stats.mTimeStats.get(TimeCategory.IRQ).intValue());
-        assertEquals(17, stats.mTimeStats.get(TimeCategory.SIRQ).intValue());
-
-        // Percent info
-        assertEquals(100.0 * 2 / 58, stats.getPercentage(TimeCategory.USER), 0.01);
-        assertEquals(100.0 * 3 / 58, stats.getPercentage(TimeCategory.NICE), 0.01);
-        assertEquals(100.0 * 5 / 58, stats.getPercentage(TimeCategory.SYS), 0.01);
-        assertEquals(100.0 * 7 / 58, stats.getPercentage(TimeCategory.IDLE), 0.01);
-        assertEquals(100.0 * 11 / 58, stats.getPercentage(TimeCategory.IOW), 0.01);
-        assertEquals(100.0 * 13 / 58, stats.getPercentage(TimeCategory.IRQ), 0.01);
-        assertEquals(100.0 * 17 / 58, stats.getPercentage(TimeCategory.SIRQ), 0.01);
-
-        // Freq info
-        assertEquals(4, stats.mFreqStats.size());
-        assertEquals(19, stats.mFreqStats.get(350000).intValue());
-        assertEquals(23, stats.mFreqStats.get(700000).intValue());
-        assertEquals(29, stats.mFreqStats.get(920000).intValue());
-        assertEquals(31, stats.mFreqStats.get(1200000).intValue());
-        assertEquals((51 / 58.0) * (86630.0 / 102), stats.getEstimatedMhz(), 0.01);
-        assertEquals(100.0 * 86630.0 / (102 * 1200), stats.getUsedMhzPercentage(), 0.01);
-
-        // cpu0 raw stats
-        assertEquals(1, cpuStats.get("cpu0").size());
-        stats = cpuStats.get("cpu0").get(0);
-        assertEquals(37, stats.mTimeStats.get(TimeCategory.USER).intValue());
-        assertEquals(41, stats.mTimeStats.get(TimeCategory.NICE).intValue());
-        assertEquals(43, stats.mTimeStats.get(TimeCategory.SYS).intValue());
-        assertEquals(47, stats.mTimeStats.get(TimeCategory.IDLE).intValue());
-        assertEquals(53, stats.mTimeStats.get(TimeCategory.IOW).intValue());
-        assertEquals(59, stats.mTimeStats.get(TimeCategory.IRQ).intValue());
-        assertEquals(61, stats.mTimeStats.get(TimeCategory.SIRQ).intValue());
-        assertEquals(4, stats.mFreqStats.size());
-        assertEquals(67, stats.mFreqStats.get(350000).intValue());
-        assertEquals(71, stats.mFreqStats.get(700000).intValue());
-        assertEquals(73, stats.mFreqStats.get(920000).intValue());
-        assertEquals(79, stats.mFreqStats.get(1200000).intValue());
-
-        // cpu1 raw stats
-        assertEquals(1, cpuStats.get("cpu1").size());
-        stats = cpuStats.get("cpu1").get(0);
-        assertEquals(83, stats.mTimeStats.get(TimeCategory.USER).intValue());
-        assertEquals(89, stats.mTimeStats.get(TimeCategory.NICE).intValue());
-        assertEquals(97, stats.mTimeStats.get(TimeCategory.SYS).intValue());
-        assertEquals(101, stats.mTimeStats.get(TimeCategory.IDLE).intValue());
-        assertEquals(103, stats.mTimeStats.get(TimeCategory.IOW).intValue());
-        assertEquals(107, stats.mTimeStats.get(TimeCategory.IRQ).intValue());
-        assertEquals(109, stats.mTimeStats.get(TimeCategory.SIRQ).intValue());
-        assertEquals(4, stats.mFreqStats.size());
-        assertEquals(113, stats.mFreqStats.get(350000).intValue());
-        assertEquals(127, stats.mFreqStats.get(700000).intValue());
-        assertEquals(131, stats.mFreqStats.get(920000).intValue());
-        assertEquals(137, stats.mFreqStats.get(1200000).intValue());
-    }
-
-    /**
-     * Test that a single output from {@code cpustats} is parsed correctly when frequencies are not
-     * aggregated and that {@link CpuStats} contains the correct data and calculates the correct
-     * data.
-     */
-    public void testCpuStatsParser_single_non_aggregate() {
-        mCollector.getReceiver().processNewLines(SINGLE_NON_AGGREGATE_OUTPUT);
-
-        Map<String, List<CpuStats>> cpuStats = mCollector.getCpuStats();
-        assertEquals(3, cpuStats.size());
-        assertTrue(cpuStats.containsKey("Total"));
-        assertTrue(cpuStats.containsKey("cpu0"));
-        assertTrue(cpuStats.containsKey("cpu1"));
-
-        assertEquals(1, cpuStats.get("Total").size());
-        CpuStats stats = cpuStats.get("Total").get(0);
-
-        // Time info
-        assertEquals(2, stats.mTimeStats.get(TimeCategory.USER).intValue());
-        assertEquals(3, stats.mTimeStats.get(TimeCategory.NICE).intValue());
-        assertEquals(5, stats.mTimeStats.get(TimeCategory.SYS).intValue());
-        assertEquals(7, stats.mTimeStats.get(TimeCategory.IDLE).intValue());
-        assertEquals(11, stats.mTimeStats.get(TimeCategory.IOW).intValue());
-        assertEquals(13, stats.mTimeStats.get(TimeCategory.IRQ).intValue());
-        assertEquals(17, stats.mTimeStats.get(TimeCategory.SIRQ).intValue());
-
-        // Percent info
-        assertEquals(100.0 * 2 / 58, stats.getPercentage(TimeCategory.USER), 0.01);
-        assertEquals(100.0 * 3 / 58, stats.getPercentage(TimeCategory.NICE), 0.01);
-        assertEquals(100.0 * 5 / 58, stats.getPercentage(TimeCategory.SYS), 0.01);
-        assertEquals(100.0 * 7 / 58, stats.getPercentage(TimeCategory.IDLE), 0.01);
-        assertEquals(100.0 * 11 / 58, stats.getPercentage(TimeCategory.IOW), 0.01);
-        assertEquals(100.0 * 13 / 58, stats.getPercentage(TimeCategory.IRQ), 0.01);
-        assertEquals(100.0 * 17 / 58, stats.getPercentage(TimeCategory.SIRQ), 0.01);
-
-        // Freq info
-        assertEquals(0, stats.mFreqStats.size());
-        assertNull(stats.getEstimatedMhz());
-        assertNull(stats.getUsedMhzPercentage());
-    }
-
-    /**
-     * Tests that multiple lines of {@code cpustats} output are parsed correctly and that
-     * {@link CpuStatsCollector} calculates the correct means from the output.
-     */
-    public void testCpuStatsParser_multi() {
-        mCollector.getReceiver().processNewLines(MULTI_OUTPUT);
-
-        Map<String, List<CpuStats>> stats = mCollector.getCpuStats();
-        assertEquals(3, stats.size());
-        assertTrue(stats.containsKey("Total"));
-        assertTrue(stats.containsKey("cpu0"));
-        assertTrue(stats.containsKey("cpu1"));
-
-        assertEquals(10, stats.get("Total").size());
-        assertEquals(10, stats.get("cpu0").size());
-        assertEquals(10, stats.get("cpu1").size());
-
-        assertEquals(53.67, CpuStatsCollector.getTotalPercentageMean(stats.get("Total")), 0.01);
-        assertEquals(41.18, CpuStatsCollector.getUserPercentageMean(stats.get("Total")), 0.01);
-        assertEquals(12.49, CpuStatsCollector.getSystemPercentageMean(stats.get("Total")), 0.01);
-        assertEquals(0.0, CpuStatsCollector.getIowPercentageMean(stats.get("Total")), 0.01);
-        assertEquals(0.0, CpuStatsCollector.getIrqPercentageMean(stats.get("Total")), 0.01);
-        assertEquals(480.46, CpuStatsCollector.getEstimatedMhzMean(stats.get("Total")), 0.01);
-        assertEquals(74.91, CpuStatsCollector.getUsedMhzPercentageMean(stats.get("Total")), 0.01);
-
-        assertEquals(30.31, CpuStatsCollector.getTotalPercentageMean(stats.get("cpu0")), 0.01);
-        assertEquals(23.87, CpuStatsCollector.getUserPercentageMean(stats.get("cpu0")), 0.01);
-        assertEquals(6.44, CpuStatsCollector.getSystemPercentageMean(stats.get("cpu0")), 0.01);
-        assertEquals(0.0, CpuStatsCollector.getIowPercentageMean(stats.get("cpu0")), 0.01);
-        assertEquals(0.0, CpuStatsCollector.getIrqPercentageMean(stats.get("cpu0")), 0.01);
-        assertEquals(246.38, CpuStatsCollector.getEstimatedMhzMean(stats.get("cpu0")), 0.01);
-        assertEquals(74.91, CpuStatsCollector.getUsedMhzPercentageMean(stats.get("cpu0")), 0.01);
-
-        assertEquals(76.99, CpuStatsCollector.getTotalPercentageMean(stats.get("cpu1")), 0.01);
-        assertEquals(58.44, CpuStatsCollector.getUserPercentageMean(stats.get("cpu1")), 0.01);
-        assertEquals(18.55, CpuStatsCollector.getSystemPercentageMean(stats.get("cpu1")), 0.01);
-        assertEquals(0.0, CpuStatsCollector.getIowPercentageMean(stats.get("cpu1")), 0.01);
-        assertEquals(0.0, CpuStatsCollector.getIrqPercentageMean(stats.get("cpu1")), 0.01);
-        assertEquals(714.29, CpuStatsCollector.getEstimatedMhzMean(stats.get("cpu1")), 0.01);
-        assertEquals(74.91, CpuStatsCollector.getUsedMhzPercentageMean(stats.get("cpu1")), 0.01);
-    }
-
-    /**
-     * Tests that multiple lines of {@code cpustats} output are parsed correctly when frequencies
-     * are not aggregated and that {@link CpuStatsCollector} calculates the correct means from the
-     * output.
-     */
-    public void testCpuStatsParser_multi_non_aggregate() {
-        mCollector.getReceiver().processNewLines(MULTI_NON_AGGREGATE_OUTPUT);
-
-        Map<String, List<CpuStats>> stats = mCollector.getCpuStats();
-        assertEquals(3, stats.size());
-        assertTrue(stats.containsKey("Total"));
-        assertTrue(stats.containsKey("cpu0"));
-        assertTrue(stats.containsKey("cpu1"));
-
-        assertEquals(10, stats.get("Total").size());
-        assertEquals(10, stats.get("cpu0").size());
-        assertEquals(10, stats.get("cpu1").size());
-
-        assertEquals(53.67, CpuStatsCollector.getTotalPercentageMean(stats.get("Total")), 0.01);
-        assertEquals(41.18, CpuStatsCollector.getUserPercentageMean(stats.get("Total")), 0.01);
-        assertEquals(12.49, CpuStatsCollector.getSystemPercentageMean(stats.get("Total")), 0.01);
-        assertEquals(0.0, CpuStatsCollector.getIowPercentageMean(stats.get("Total")), 0.01);
-        assertEquals(0.0, CpuStatsCollector.getIrqPercentageMean(stats.get("Total")), 0.01);
-        assertNull(CpuStatsCollector.getEstimatedMhzMean(stats.get("Total")));
-        assertNull(CpuStatsCollector.getUsedMhzPercentageMean(stats.get("Total")));
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java b/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java
deleted file mode 100644
index 82edc22..0000000
--- a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.command.remote.DeviceDescriptor;
-import com.android.tradefed.device.IDeviceMonitor.DeviceLister;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Load test for {@link DeviceUtilStatsMonitor} Used to ensure memory used by monitor under heavy
- * load is reasonable
- */
-public class DeviceUtilStatsMonitorLoadTest extends TestCase {
-
-    private static final int NUM_DEVICES = 100;
-
-    private IDeviceManager mMockDeviceManager;
-    private DeviceUtilStatsMonitor mDeviceUtilMonitor;
-
-    @Override
-    public void setUp() {
-        mMockDeviceManager = EasyMock.createNiceMock(IDeviceManager.class);
-        mDeviceUtilMonitor = new DeviceUtilStatsMonitor() {
-            @Override
-            IDeviceManager getDeviceManager() {
-                return mMockDeviceManager;
-            }
-        };
-        mDeviceUtilMonitor.setDeviceLister(
-                new DeviceLister() {
-                    @Override
-                    public List<DeviceDescriptor> listDevices() {
-                        return mMockDeviceManager.listAllDevices();
-                    }
-
-                    @Override
-                    public DeviceDescriptor getDeviceDescriptor(String serial) {
-                        return mMockDeviceManager.getDeviceDescriptor(serial);
-                    }
-                });
-        mDeviceUtilMonitor.calculateMaxSamples();
-    }
-
-    /**
-     * Simulate a heavy load by generating constant allocation events length for
-     * all NUM_DEVICES devices.
-     * <p/>
-     * Intended to be run under a profiler.
-     * @throws InterruptedException
-     */
-    public void testManyRecords() throws InterruptedException {
-        List<DeviceDescriptor> deviceList = new ArrayList<>(NUM_DEVICES);
-        for (int i =0; i <NUM_DEVICES; i++) {
-            DeviceDescriptor device = createDeviceDesc("serial" + i, DeviceAllocationState.Allocated);
-            deviceList.add(device);
-        }
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andStubReturn(deviceList);
-        EasyMock.replay(mMockDeviceManager);
-
-        for (int i = 0; i < mDeviceUtilMonitor.getMaxSamples(); i++) {
-            mDeviceUtilMonitor.getSamplingTask().run();
-        }
-        // This takes ~ 1.9 MB in heap if DeviceUtilStatsMonitor uses a LinkedList<Byte> to
-        // store samples
-        // takes ~ 65K if CircularByteArray is used
-        Thread.sleep(5 * 60 * 1000);
-    }
-
-    /**
-     * Helper method to create a {@link DeviceDescriptor} using only serial and state.
-     */
-    private DeviceDescriptor createDeviceDesc(String serial, DeviceAllocationState state) {
-        return new DeviceDescriptor(serial, false, state, null, null, null, null, null);
-    }
-
-    public static void main(String[] args) {
-        //new DeviceUtilStatsMonitorLoadTest().testManyRecords();
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java b/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java
deleted file mode 100644
index 87bee7f..0000000
--- a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.tradefed.command.remote.DeviceDescriptor;
-import com.android.tradefed.device.DeviceUtilStatsMonitor.UtilizationDesc;
-import com.android.tradefed.device.IDeviceMonitor.DeviceLister;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Simple unit tests for {@link DeviceUtilStatsMonitor}
- */
-public class DeviceUtilStatsMonitorTest extends TestCase {
-
-    private IDeviceManager mMockDeviceManager;
-    private DeviceUtilStatsMonitor mDeviceUtilMonitor;
-
-    @Override
-    public void setUp() {
-        mMockDeviceManager = EasyMock.createNiceMock(IDeviceManager.class);
-        mDeviceUtilMonitor = new DeviceUtilStatsMonitor() {
-            @Override
-            IDeviceManager getDeviceManager() {
-                return mMockDeviceManager;
-            }
-        };
-        mDeviceUtilMonitor.setDeviceLister(
-                new DeviceLister() {
-                    @Override
-                    public List<DeviceDescriptor> listDevices() {
-                        return mMockDeviceManager.listAllDevices();
-                    }
-
-                    @Override
-                    public DeviceDescriptor getDeviceDescriptor(String serial) {
-                        return mMockDeviceManager.getDeviceDescriptor(serial);
-                    }
-                });
-        mDeviceUtilMonitor.calculateMaxSamples();
-    }
-
-    public void testEmpty() {
-        EasyMock.replay(mMockDeviceManager);
-        assertEquals(0, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-    }
-
-    /**
-     * Test case where device has been available but never allocated
-     */
-    public void testOnlyAvailable() {
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
-                DeviceAllocationState.Available));
-        EasyMock.replay(mMockDeviceManager);
-
-        mDeviceUtilMonitor.getSamplingTask().run();
-        UtilizationDesc desc = mDeviceUtilMonitor.getUtilizationStats();
-        assertEquals(0, desc.mTotalUtil);
-        assertEquals(1, desc.mDeviceUtil.size());
-        assertEquals(0L, (long)desc.mDeviceUtil.get("serial0"));
-    }
-
-    /**
-     * Test case where device has been allocated but never available
-     */
-    public void testOnlyAllocated() {
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
-                DeviceAllocationState.Allocated));
-        EasyMock.replay(mMockDeviceManager);
-
-        mDeviceUtilMonitor.getSamplingTask().run();
-        UtilizationDesc desc = mDeviceUtilMonitor.getUtilizationStats();
-        assertEquals(100L, desc.mTotalUtil);
-        assertEquals(1, desc.mDeviceUtil.size());
-        assertEquals(100L, (long)desc.mDeviceUtil.get("serial0"));
-    }
-
-    /**
-     * Test case where samples exceed max
-     */
-    public void testExceededSamples() {
-        mDeviceUtilMonitor.setMaxSamples(2);
-        // first return allocated, then return samples with device missing
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
-                DeviceAllocationState.Allocated));
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(DeviceAllocationState.Available));
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(DeviceAllocationState.Available));
-        EasyMock.replay(mMockDeviceManager);
-
-        mDeviceUtilMonitor.getSamplingTask().run();
-        // only 1 sample - allocated
-        assertEquals(100L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-        mDeviceUtilMonitor.getSamplingTask().run();
-        // 1 out of 2
-        assertEquals(50L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-        mDeviceUtilMonitor.getSamplingTask().run();
-        // 0 out of 2
-        assertEquals(0L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-    }
-
-    /**
-     * Test case where device disappears. Ensure util numbers are calculated until > max samples
-     * have been collected with it missing
-     */
-    public void testMissingDevice() {
-        mDeviceUtilMonitor.setMaxSamples(2);
-        // first return allocated, then return samples with device missing
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
-                DeviceAllocationState.Allocated));
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList());
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList());
-        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList());
-        EasyMock.replay(mMockDeviceManager);
-
-        // only 1 sample - allocated
-        mDeviceUtilMonitor.getSamplingTask().run();
-        assertEquals(100L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-
-        // 1 out of 2
-        mDeviceUtilMonitor.getSamplingTask().run();
-        assertEquals(50L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-
-        // 0 out of 2
-        mDeviceUtilMonitor.getSamplingTask().run();
-        assertEquals(0L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-
-        // now removed
-        mDeviceUtilMonitor.getSamplingTask().run();
-        assertEquals(0L, mDeviceUtilMonitor.getUtilizationStats().mDeviceUtil.size());
-
-    }
-
-    private List<DeviceDescriptor> buildDeviceList(DeviceAllocationState... states) {
-        List<DeviceDescriptor> deviceList = new ArrayList<>(states.length);
-        for (int i =0; i < states.length; i++) {
-            DeviceDescriptor device = createDeviceDesc("serial" + i, states[i]);
-            deviceList.add(device);
-        }
-        return deviceList;
-    }
-
-    /**
-     * Helper method to create a {@link DeviceDescriptor} using only serial and state.
-     */
-    private DeviceDescriptor createDeviceDesc(String serial, DeviceAllocationState state) {
-        return new DeviceDescriptor(serial, false, state, null, null, null, null, null);
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/NativeDeviceTest.java b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
index 091fc5a..9afd711 100644
--- a/tests/src/com/android/tradefed/device/NativeDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
@@ -70,6 +70,7 @@
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -2166,6 +2167,134 @@
         }
     }
 
+    /** Test get Process pid by process name */
+    @Test
+    public void testGetProcessPid() throws Exception {
+        final String fakePid = "914";
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn(fakePid).when(spy).executeShellCommand("pidof system_server");
+        EasyMock.replay(mMockIDevice);
+        assertEquals(fakePid, spy.getProcessPid("system_server"));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get Process pid by process name with adb shell return of extra new line */
+    @Test
+    public void testGetProcessPidWithNewLine() throws Exception {
+        final String fakePid = "914";
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn(fakePid + "\n").when(spy).executeShellCommand("pidof system_server");
+        EasyMock.replay(mMockIDevice);
+        assertEquals(fakePid, spy.getProcessPid("system_server"));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get Process pid return null with invalid shell command output */
+    @Test
+    public void testGetProcessPidInvalidOutput() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("invalid output").when(spy).executeShellCommand("pidof system_server");
+        EasyMock.replay(mMockIDevice);
+        assertNull(spy.getProcessPid("system_server"));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get Process pid return null with shell command empty output */
+    @Test
+    public void testGetProcessPidEmptyOutput() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("").when(spy).executeShellCommand("pidof system_server");
+        EasyMock.replay(mMockIDevice);
+        assertNull(spy.getProcessPid("system_server"));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get ProcessInfo by process name */
+    @Test
+    public void testGetProcessWithStartTimeByName() throws Exception {
+        final String fakePid = "914";
+        final String fakeCreationTime = "1559091922";
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn(fakePid).when(spy).executeShellCommand("pidof system_server");
+        doReturn(fakeCreationTime).when(spy).executeShellCommand("stat -c%Z /proc/" + fakePid);
+        doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/" + fakePid);
+        EasyMock.replay(mMockIDevice);
+        assertEquals(Integer.parseInt(fakePid), spy.getProcessByName("system_server").getPid());
+        assertEquals(
+                Long.parseLong(fakeCreationTime),
+                spy.getProcessByName("system_server").getStartTime());
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get ProcessInfo by process name return null for invalid process */
+    @Test
+    public void testGetProcessWithStartTimeByNameInvalidProcess() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("").when(spy).executeShellCommand("pidof system_server");
+        EasyMock.replay(mMockIDevice);
+        assertNull(spy.getProcessByName("system_server"));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get boot history */
+    @Test
+    public void testGetBootHistory() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn(
+                        "kernel_panic,1556587278\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+        Map<Long, String> history = new LinkedHashMap<Long, String>();
+        history.put(1556587278L, "kernel_panic");
+        history.put(1556238008L, "reboot");
+        history.put(1556237796L, "reboot");
+        history.put(1556237725L, "reboot");
+        EasyMock.replay(mMockIDevice);
+        assertEquals(history, spy.getBootHistory());
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get empty boot history */
+    @Test
+    public void testGetBootHistoryEmpty() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("").when(spy).getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        assertTrue(spy.getBootHistory().isEmpty());
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get invalid boot history */
+    @Test
+    public void testGetBootHistoryInvalid() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("invalid output").when(spy).getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        assertTrue(spy.getBootHistory().isEmpty());
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get boot history since */
+    @Test
+    public void testGetBootHistorySince() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn(
+                        "kernel_panic,1556587278\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+        Map<Long, String> history = new LinkedHashMap<Long, String>();
+        history.put(1556587278L, "kernel_panic");
+        EasyMock.replay(mMockIDevice);
+        assertEquals(history, spy.getBootHistorySince(1556238008L));
+        EasyMock.verify(mMockIDevice);
+    }
+
     /** Test validating valid MAC addresses */
     @Test
     public void testIsMacAddress() {
@@ -2630,4 +2759,5 @@
         assertEquals(2, result.size());
         EasyMock.verify(mMockIDevice);
     }
+
 }
diff --git a/tests/src/com/android/tradefed/device/ReconnectingRecoveryTest.java b/tests/src/com/android/tradefed/device/ReconnectingRecoveryTest.java
deleted file mode 100644
index f87e4e3..0000000
--- a/tests/src/com/android/tradefed/device/ReconnectingRecoveryTest.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.ddmlib.IDevice;
-import com.android.tradefed.util.IRunUtil;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-/**
- * Unit tests for {@link ReconnectingRecovery}.
- */
-public class ReconnectingRecoveryTest extends TestCase {
-
-    private static final String SERIAL = "serial";
-    private IDevice mMockDevice;
-    private IDeviceStateMonitor mMockMonitor;
-    private IRunUtil mMockRunUtil;
-    private ReconnectingRecovery mRecovery;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mMockRunUtil = EasyMock.createMock(IRunUtil.class);
-        mRecovery = new ReconnectingRecovery() {
-            @Override
-            protected IRunUtil getRunUtil() {
-                return mMockRunUtil;
-            }
-        };
-        mMockMonitor = EasyMock.createMock(IDeviceStateMonitor.class);
-        EasyMock.expect(mMockMonitor.getSerialNumber()).andStubReturn(SERIAL);
-        mMockDevice = EasyMock.createMock(IDevice.class);
-    }
-
-    /**
-     * Test {@link ReconnectingRecovery#recoverDevice(IDeviceStateMonitor, boolean)}
-     * when device is actually recoverable upon the first attempt.
-     */
-    public final void testRecoverDevice_successOnFirstTry() throws DeviceNotAvailableException {
-        expectInitialDisconnectConnectAttempt();
-        EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(mMockDevice);
-        EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
-        EasyMock.expect(mMockMonitor.waitForDeviceAvailable()).andReturn(mMockDevice);
-        replayMocks();
-        mRecovery.recoverDevice(mMockMonitor, false);
-        verifyMocks();
-    }
-
-    /**
-     * Test {@link ReconnectingRecovery#recoverDevice(IDeviceStateMonitor, boolean)}
-     * when device is actually recoverable, but not on the first attempt.
-     */
-    public final void testRecoverDevice_successRetrying() throws DeviceNotAvailableException {
-        expectInitialDisconnectConnectAttempt();
-        // fail 1st attempt
-        EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(null);
-        // then it should retry at least once
-        EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), connectCommand())).andReturn(
-                null);
-        EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(mMockDevice);
-        EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
-        EasyMock.expect(mMockMonitor.waitForDeviceAvailable()).andReturn(mMockDevice);
-        replayMocks();
-        mRecovery.recoverDevice(mMockMonitor, false);
-        verifyMocks();
-    }
-
-    /**
-     * Test {@link ReconnectingRecovery#recoverDevice(IDeviceStateMonitor, boolean)}
-     * when device is actually irrecoverable.
-     */
-    public final void testRecoverDevice_failure() throws DeviceNotAvailableException {
-        expectInitialDisconnectConnectAttempt();
-        EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andStubReturn(null);
-        EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), connectCommand()))
-                .andStubReturn(null);
-        EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
-        EasyMock.expect(mMockMonitor.waitForDeviceAvailable()).andReturn(null);
-        replayMocks();
-        try {
-            mRecovery.recoverDevice(mMockMonitor, false);
-            fail("DeviceUnresponsiveException not thrown");
-        } catch (DeviceUnresponsiveException e) {
-            assertTrue(true);
-        }
-        verifyMocks();
-    }
-
-    private void expectInitialDisconnectConnectAttempt() {
-        EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), disconnectCommand()))
-                .andStubReturn(null);
-        EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), connectCommand()))
-                .andStubReturn(null);
-    }
-
-    public final void testRecoverDeviceBootloader_notImplemented()
-            throws DeviceNotAvailableException {
-        replayMocks();
-        try {
-            mRecovery.recoverDeviceBootloader(mMockMonitor);
-            fail("should have thrown an UnsupportedOperationException");
-        } catch (java.lang.UnsupportedOperationException e) {
-            // expected
-        }
-        verifyMocks();
-    }
-
-    private String[] disconnectCommand() {
-        return new String[] { EasyMock.eq("adb"), EasyMock.eq("disconnect"), EasyMock.eq(SERIAL) };
-    }
-
-    private String[] connectCommand() {
-        return new String[] { EasyMock.eq("adb"), EasyMock.eq("connect"), EasyMock.eq(SERIAL) };
-    }
-
-    /**
-     * Verify all the mock objects
-     */
-    private void verifyMocks() {
-        EasyMock.verify(mMockRunUtil, mMockMonitor, mMockDevice);
-    }
-
-    /**
-     * Switch all the mock objects to replay mode
-     */
-    private void replayMocks() {
-        EasyMock.replay(mMockRunUtil, mMockMonitor, mMockDevice);
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
index 8a136fb..1f0de39 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
@@ -50,7 +50,7 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.nio.file.Files;
+import java.net.URLConnection;
 import java.util.Set;
 
 import javax.imageio.ImageIO;
@@ -836,8 +836,9 @@
         assertNotNull(screenshot);
         File testFile = FileUtil.createTempFile("test-screenshot", ".testpng");
         try {
-            FileUtil.writeToFile(screenshot.createInputStream(), testFile);
-            assertEquals("image/png", Files.probeContentType(testFile.toPath()));
+            assertEquals(
+                    "image/png",
+                    URLConnection.guessContentTypeFromStream(screenshot.createInputStream()));
         } finally {
             FileUtil.deleteFile(testFile);
             StreamUtil.close(screenshot);
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index d334d10..c3c1b00 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -48,7 +48,6 @@
 import com.android.tradefed.util.KeyguardControllerState;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
-import com.android.tradefed.util.UserUtil;
 import com.android.tradefed.util.ZipUtil2;
 
 import junit.framework.TestCase;
@@ -1186,7 +1185,7 @@
      */
     private void setEncryptedUnsupportedExpectations() throws Exception {
         setEnableAdbRootExpectations();
-        injectShellResponse("vdc cryptfs enablecrypto", "\r\n");
+        EasyMock.expect(mMockIDevice.getProperty("ro.crypto.state")).andReturn("unsupported");
     }
 
     /**
@@ -1194,9 +1193,7 @@
      */
     private void setEncryptedSupported() throws Exception {
         setEnableAdbRootExpectations();
-        injectShellResponse("vdc cryptfs enablecrypto",
-                "500 29805 Usage: cryptfs enablecrypto <wipe|inplace> "
-                + "default|password|pin|pattern [passwd] [noui]\r\n");
+        EasyMock.expect(mMockIDevice.getProperty("ro.crypto.state")).andReturn("encrypted");
     }
 
     /**
@@ -2488,6 +2485,46 @@
         assertEquals(3, actual.get(1).intValue());
     }
 
+    /** Test that a single user is handled by {@link TestDevice#listUsers()}. */
+    public void testListUsersInfo_oneUser() throws Exception {
+        final String listUsersCommand = "pm list users";
+        injectShellResponse(
+                listUsersCommand, ArrayUtil.join("\r\n", "Users:", "UserInfo{0:Foo:13} running"));
+        replayMocks();
+        Map<Integer, UserInfo> actual = mTestDevice.getUserInfos();
+        assertNotNull(actual);
+        assertEquals(1, actual.size());
+        UserInfo user0 = actual.get(0);
+        assertEquals(0, user0.userId());
+        assertEquals("Foo", user0.userName());
+        assertEquals(0x13, user0.flag());
+        assertEquals(true, user0.isRunning());
+    }
+
+    /** Test that multiple user is handled by {@link TestDevice#listUsers()}. */
+    public void testListUsersInfo_multiUsers() throws Exception {
+        final String listUsersCommand = "pm list users";
+        injectShellResponse(
+                listUsersCommand,
+                ArrayUtil.join(
+                        "\r\n", "Users:", "UserInfo{0:Foo:13} running", "UserInfo{10:FooBar:14}"));
+        replayMocks();
+        Map<Integer, UserInfo> actual = mTestDevice.getUserInfos();
+        assertNotNull(actual);
+        assertEquals(2, actual.size());
+        UserInfo user0 = actual.get(0);
+        assertEquals(0, user0.userId());
+        assertEquals("Foo", user0.userName());
+        assertEquals(0x13, user0.flag());
+        assertEquals(true, user0.isRunning());
+
+        UserInfo user10 = actual.get(10);
+        assertEquals(10, user10.userId());
+        assertEquals("FooBar", user10.userName());
+        assertEquals(0x14, user10.flag());
+        assertEquals(false, user10.isRunning());
+    }
+
     /**
      * Test that multi user output is handled by {@link TestDevice#getMaxNumberOfUsersSupported()}.
      */
@@ -2982,10 +3019,10 @@
                                         + "UserInfo{12:Secondary:0}\n\t"
                                         + "UserInfo{13:Managed:%x}\n\t"
                                         + "UserInfo{100:Restricted:%x}\n\t",
-                                UserUtil.FLAG_PRIMARY,
-                                UserUtil.FLAG_GUEST,
-                                UserUtil.FLAG_MANAGED_PROFILE,
-                                UserUtil.FLAG_RESTRICTED);
+                                UserInfo.FLAG_PRIMARY,
+                                UserInfo.FLAG_GUEST,
+                                UserInfo.FLAG_MANAGED_PROFILE,
+                                UserInfo.FLAG_RESTRICTED);
                     }
 
                     @Override
@@ -3151,8 +3188,12 @@
                     @Override
                     public String executeShellCommand(String command)
                             throws DeviceNotAvailableException {
-                        test.setName(getClass().getCanonicalName() + "#testSwitchUser_delay");
-                        test.start();
+                        if (!started) {
+                            started = true;
+                            test.setDaemon(true);
+                            test.setName(getClass().getCanonicalName() + "#testSwitchUser_delay");
+                            test.start();
+                        }
                         return "";
                     }
 
@@ -3176,6 +3217,7 @@
                         return 100;
                     }
 
+                    boolean started = false;
                     Thread test =
                             new Thread(
                                     new Runnable() {
@@ -4169,9 +4211,7 @@
                         return true;
                     }
                 };
-        injectShellResponse(
-                "vdc cryptfs enablecrypto",
-                "500 8674 Usage with ext4crypt: cryptfs enablecrypto inplace default noui\r\n");
+        EasyMock.expect(mMockIDevice.getProperty("ro.crypto.state")).andReturn("encrypted");
         EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
         assertTrue(mTestDevice.isEncryptionSupported());
         EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -4191,7 +4231,7 @@
                         return true;
                     }
                 };
-        injectShellResponse("vdc cryptfs enablecrypto", "500 8674 Command not recognized\r\n");
+        EasyMock.expect(mMockIDevice.getProperty("ro.crypto.state")).andReturn("unsupported");
         EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
         assertFalse(mTestDevice.isEncryptionSupported());
         EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -4310,7 +4350,7 @@
         mMockWifi.cleanUp();
         replayMocks();
         mTestDevice.getIpAddress();
-        mTestDevice.postInvocationTearDown();
+        mTestDevice.postInvocationTearDown(null);
         verifyMocks();
     }
 
@@ -4381,4 +4421,41 @@
         StreamUtil.close(source);
         verifyMocks();
     }
+
+    /** Test {@link TestDevice#doesFileExist(String)}. */
+    public void testDoesFileExists() throws Exception {
+        injectShellResponse("ls \"/data/local/tmp/file\"", "file");
+        EasyMock.replay(mMockIDevice);
+        assertTrue(mTestDevice.doesFileExist("/data/local/tmp/file"));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test {@link TestDevice#doesFileExist(String)} when the file does not exists. */
+    public void testDoesFileExists_notExists() throws Exception {
+        injectShellResponse(
+                "ls \"/data/local/tmp/file\"",
+                "ls: cannot access 'file': No such file or directory\n");
+        EasyMock.replay(mMockIDevice);
+        assertFalse(mTestDevice.doesFileExist("/data/local/tmp/file"));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /**
+     * Test {@link TestDevice#doesFileExist(String)} when the file exists on an sdcard from another
+     * user.
+     */
+    public void testDoesFileExists_sdcard() throws Exception {
+        mTestDevice =
+                new TestableTestDevice() {
+                    @Override
+                    public int getCurrentUser()
+                            throws DeviceNotAvailableException, DeviceRuntimeException {
+                        return 10;
+                    }
+                };
+        injectShellResponse("ls \"/storage/emulated/10/file\"", "file");
+        EasyMock.replay(mMockIDevice);
+        assertTrue(mTestDevice.doesFileExist("/sdcard/file"));
+        EasyMock.verify(mMockIDevice);
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/TopHelperTest.java b/tests/src/com/android/tradefed/device/TopHelperTest.java
deleted file mode 100644
index a216b7e..0000000
--- a/tests/src/com/android/tradefed/device/TopHelperTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.device.TopHelper.TopStats;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-import java.util.List;
-
-/**
- * Unit tests for {@link TopHelper}
- */
-public class TopHelperTest extends TestCase {
-    private ITestDevice mMockDevice;
-    private TopHelper mTop;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mMockDevice = EasyMock.createMock(ITestDevice.class);
-        mTop = new TopHelper(mMockDevice);
-    }
-
-    /**
-     * Test that all the output from top invoked once is supported and parsed (or not parsed).
-     */
-    public void testTopParser_parse() {
-        final String lines = ("User 7%, System 5%, IOW 3%, IRQ 2%\r\n" +
-                "User 3 + Nice 0 + Sys 6 + Idle 100 + IOW 0 + IRQ 0 + SIRQ 0 = 109\r\n" +
-                "\r\n" +
-                "  PID   TID PR CPU% S     VSS     RSS PCY UID      Thread          Proc\r\n" +
-                " 1388  1388  0  11% R   1160K    576K  fg shell    top             top\r\n" +
-                "    2     2  0   0% S      0K      0K  fg root     kthreadd\r\n" +
-                "\r\n" +
-                "\r\n" +
-                "\r\n");
-
-        mTop.getReceiver().processNewLines(lines.split("\r\n"));
-        List<TopStats> stats = mTop.getTopStats();
-
-        assertEquals(1, stats.size());
-
-        assertEquals(17.0, stats.get(0).mTotalPercent, 0.0001);
-        assertEquals(7.0, stats.get(0).mUserPercent, 0.0001);
-        assertEquals(5.0, stats.get(0).mSystemPercent, 0.0001);
-        assertEquals(3.0, stats.get(0).mIowPercent, 0.0001);
-        assertEquals(2.0, stats.get(0).mIrqPercent, 0.0001);
-    }
-
-    /**
-     * Test that the parser returns the correct averages for various ranges.
-     */
-    public void testTopParser_stats() {
-        final String lines = (
-                "User 15%, System 11%, IOW 7%, IRQ 3%\r\n" +
-                "User 16%, System 12%, IOW 8%, IRQ 4%\r\n" +
-                "User 17%, System 13%, IOW 9%, IRQ 5%\r\n" +
-                "User 18%, System 14%, IOW 10%, IRQ 6%\r\n" +
-                "User 19%, System 15%, IOW 11%, IRQ 7%\r\n" +
-                "User 20%, System 16%, IOW 12%, IRQ 8%\r\n" +
-                "User 21%, System 17%, IOW 13%, IRQ 9%\r\n");
-
-        List<TopStats> stats = mTop.getTopStats();
-
-        assertEquals(0, stats.size());
-
-        assertNull(TopHelper.getTotalAverage(stats));
-        assertNull(TopHelper.getUserAverage(stats));
-        assertNull(TopHelper.getSystemAverage(stats));
-        assertNull(TopHelper.getIowAverage(stats));
-        assertNull(TopHelper.getIrqAverage(stats));
-
-        mTop.getReceiver().processNewLines(lines.split("\r\n"));
-        stats = mTop.getTopStats();
-
-        assertEquals(7, mTop.getTopStats().size());
-
-        assertEquals(48.0, TopHelper.getTotalAverage(stats.subList(0, 7)), 0.001);
-        assertEquals(18.0, TopHelper.getUserAverage(stats.subList(0, 7)), 0.001);
-        assertEquals(14.0, TopHelper.getSystemAverage(stats.subList(0, 7)), 0.001);
-        assertEquals(10.0, TopHelper.getIowAverage(stats.subList(0, 7)), 0.001);
-        assertEquals(6.0, TopHelper.getIrqAverage(stats.subList(0, 7)), 0.001);
-
-        assertEquals(44.0, TopHelper.getTotalAverage(stats.subList(0, 7 - 2)), 0.001);
-        assertEquals(17.0, TopHelper.getUserAverage(stats.subList(0, 7 - 2)), 0.001);
-        assertEquals(13.0, TopHelper.getSystemAverage(stats.subList(0, 7 - 2)), 0.001);
-        assertEquals(9.0, TopHelper.getIowAverage(stats.subList(0, 7 - 2)), 0.001);
-        assertEquals(5.0, TopHelper.getIrqAverage(stats.subList(0, 7 - 2)), 0.001);
-
-        assertEquals(52.0, TopHelper.getTotalAverage(stats.subList(2, 7)), 0.001);
-        assertEquals(19.0, TopHelper.getUserAverage(stats.subList(2, 7)), 0.001);
-        assertEquals(15.0, TopHelper.getSystemAverage(stats.subList(2, 7)), 0.001);
-        assertEquals(11.0, TopHelper.getIowAverage(stats.subList(2, 7)), 0.001);
-        assertEquals(7.0, TopHelper.getIrqAverage(stats.subList(2, 7)), 0.001);
-
-        assertNull(TopHelper.getTotalAverage(stats.subList(3, 3)));
-        assertNull(TopHelper.getUserAverage(stats.subList(3, 3)));
-        assertNull(TopHelper.getSystemAverage(stats.subList(3, 3)));
-        assertNull(TopHelper.getIowAverage(stats.subList(3, 3)));
-        assertNull(TopHelper.getIrqAverage(stats.subList(3, 3)));
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java
new file mode 100644
index 0000000..7bf87e6
--- /dev/null
+++ b/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device.cloud;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.ddmlib.IDevice;
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.device.IDeviceMonitor;
+import com.android.tradefed.device.IDeviceStateMonitor;
+import com.android.tradefed.device.TestDeviceOptions;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link ManagedRemoteDevice}. */
+@RunWith(JUnit4.class)
+public class ManagedRemoteDeviceTest {
+    private ManagedRemoteDevice mDevice;
+    private IDevice mIDevice;
+    private IDeviceStateMonitor mStateMonitor;
+    private IDeviceMonitor mDeviceMonitor;
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        try {
+            GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
+        } catch (IllegalStateException e) {
+            // Ignore
+        }
+    }
+
+    @Before
+    public void setUp() {
+        mIDevice = Mockito.mock(IDevice.class);
+        mStateMonitor = Mockito.mock(IDeviceStateMonitor.class);
+        mDeviceMonitor = Mockito.mock(IDeviceMonitor.class);
+        mDevice = new ManagedRemoteDevice(mIDevice, mStateMonitor, mDeviceMonitor);
+    }
+
+    @Test
+    public void testGetOptions() {
+        TestDeviceOptions originalOptions = new TestDeviceOptions();
+        mDevice.setOptions(originalOptions);
+        TestDeviceOptions get = mDevice.getOptions();
+        assertFalse(get.equals(originalOptions));
+        TestDeviceOptions get2 = mDevice.getOptions();
+        assertTrue(get2.equals(get));
+    }
+}
diff --git a/tests/src/com/android/tradefed/device/cloud/NestedRemoteDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/NestedRemoteDeviceTest.java
index 4ac2b17..89616c5 100644
--- a/tests/src/com/android/tradefed/device/cloud/NestedRemoteDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/NestedRemoteDeviceTest.java
@@ -74,7 +74,7 @@
 
     @After
     public void tearDown() throws Exception {
-        mDevice.postInvocationTearDown();
+        mDevice.postInvocationTearDown(null);
     }
 
     /** Test that reset device returns true in case of success */
diff --git a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
index 3540bca..8a7eba7 100644
--- a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
@@ -49,6 +49,7 @@
 import com.google.common.net.HostAndPort;
 
 import org.easymock.EasyMock;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -56,6 +57,7 @@
 import org.mockito.Mockito;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -134,6 +136,11 @@
         mMockBuildInfo = new BuildInfo();
     }
 
+    @After
+    public void tearDown() {
+        FileUtil.deleteFile(mTestDevice.getExecuteShellCommandLog());
+    }
+
     /**
      * Test that an exception thrown in the parser should be propagated to the top level and should
      * not be caught.
@@ -241,7 +248,7 @@
     @Test
     public void testPreInvocationSetup() throws Exception {
         IBuildInfo mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
-        TestableRemoteAndroidVirtualDevice testDevice =
+        mTestDevice =
                 new TestableRemoteAndroidVirtualDevice() {
                     @Override
                     protected void launchGce(IBuildInfo buildInfo) throws TargetSetupError {
@@ -272,7 +279,7 @@
                 .andReturn(mMockIDevice);
         EasyMock.expect(mMockIDevice.getState()).andReturn(DeviceState.ONLINE);
         replayMocks(mMockBuildInfo);
-        testDevice.preInvocationSetup(mMockBuildInfo);
+        mTestDevice.preInvocationSetup(mMockBuildInfo);
         verifyMocks(mMockBuildInfo);
 
         Mockito.verify(mGceHandler).logStableHostImageInfos(mMockBuildInfo);
@@ -285,7 +292,7 @@
     @Test
     public void testPreInvocationSetup_fails() throws Exception {
         IBuildInfo mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
-        TestableRemoteAndroidVirtualDevice testDevice =
+        mTestDevice =
                 new TestableRemoteAndroidVirtualDevice() {
                     @Override
                     protected void launchGce(IBuildInfo buildInfo) throws TargetSetupError {
@@ -302,7 +309,7 @@
         EasyMock.expect(mMockIDevice.getState()).andReturn(DeviceState.OFFLINE).times(2);
         replayMocks(mMockBuildInfo);
         try {
-            testDevice.preInvocationSetup(mMockBuildInfo);
+            mTestDevice.preInvocationSetup(mMockBuildInfo);
             fail("Should have thrown an exception.");
         } catch (DeviceNotAvailableException expected) {
             // expected
@@ -310,7 +317,7 @@
         verifyMocks(mMockBuildInfo);
     }
 
-    /** Test {@link RemoteAndroidVirtualDevice#postInvocationTearDown()}. */
+    /** Test {@link RemoteAndroidVirtualDevice#postInvocationTearDown(Throwable)}. */
     @Test
     public void testPostInvocationTearDown() throws Exception {
         mTestDevice.setTestLogger(mTestLogger);
@@ -327,7 +334,7 @@
 
         // Initial serial is not set because we call postInvoc directly.
         replayMocks();
-        mTestDevice.postInvocationTearDown();
+        mTestDevice.postInvocationTearDown(null);
         verifyMocks();
         Mockito.verify(mGceSshMonitor).shutdown();
         Mockito.verify(mGceSshMonitor).joinMonitor();
@@ -418,7 +425,7 @@
         EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
         EasyMock.expect(mMockBuildInfo.getBuildFlavor()).andStubReturn("flavor");
         EasyMock.expect(mMockBuildInfo.getBuildId()).andStubReturn("id");
-        TestableRemoteAndroidVirtualDevice testDevice =
+        mTestDevice =
                 new TestableRemoteAndroidVirtualDevice() {
                     @Override
                     public IDevice getIDevice() {
@@ -450,10 +457,10 @@
                         return mockRunUtil;
                     }
                 };
-        testDevice.setTestLogger(mTestLogger);
+        mTestDevice.setTestLogger(mTestLogger);
         File tmpKeyFile = FileUtil.createTempFile("test-gce", "key");
         try {
-            OptionSetter setter = new OptionSetter(testDevice.getOptions());
+            OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
             setter.setOptionValue("gce-private-key-path", tmpKeyFile.getAbsolutePath());
             // We use a missing ssh to prevent the real tunnel from running.
             FileUtil.deleteFile(tmpKeyFile);
@@ -489,22 +496,22 @@
 
             replayMocks(mMockBuildInfo);
             // Run device a first time
-            testDevice.preInvocationSetup(mMockBuildInfo);
-            testDevice.getGceSshMonitor().joinMonitor();
+            mTestDevice.preInvocationSetup(mMockBuildInfo);
+            mTestDevice.getGceSshMonitor().joinMonitor();
             // We expect to find our Runtime exception for the ssh key
-            assertNotNull(testDevice.getGceSshMonitor().getLastException());
-            testDevice.postInvocationTearDown();
+            assertNotNull(mTestDevice.getGceSshMonitor().getLastException());
+            mTestDevice.postInvocationTearDown(null);
             // Bridge is set to null after tear down
-            assertNull(testDevice.getGceSshMonitor());
+            assertNull(mTestDevice.getGceSshMonitor());
 
             // run a second time on same device should yield exact same exception.
-            testDevice.preInvocationSetup(mMockBuildInfo);
-            testDevice.getGceSshMonitor().joinMonitor();
+            mTestDevice.preInvocationSetup(mMockBuildInfo);
+            mTestDevice.getGceSshMonitor().joinMonitor();
             // Should have the same result, the run time exception from ssh key
-            assertNotNull(testDevice.getGceSshMonitor().getLastException());
-            testDevice.postInvocationTearDown();
+            assertNotNull(mTestDevice.getGceSshMonitor().getLastException());
+            mTestDevice.postInvocationTearDown(null);
             // Bridge is set to null after tear down
-            assertNull(testDevice.getGceSshMonitor());
+            assertNull(mTestDevice.getGceSshMonitor());
 
             verifyMocks(mMockBuildInfo);
         } finally {
@@ -521,7 +528,7 @@
         EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
         EasyMock.expect(mMockBuildInfo.getBuildFlavor()).andStubReturn("flavor");
         EasyMock.expect(mMockBuildInfo.getBuildId()).andStubReturn("id");
-        TestableRemoteAndroidVirtualDevice testDevice =
+        mTestDevice =
                 new TestableRemoteAndroidVirtualDevice() {
                     @Override
                     public IDevice getIDevice() {
@@ -553,10 +560,10 @@
                         return mockRunUtil;
                     }
                 };
-        testDevice.setTestLogger(mTestLogger);
+        mTestDevice.setTestLogger(mTestLogger);
         File tmpKeyFile = FileUtil.createTempFile("test-gce", "key");
         try {
-            OptionSetter setter = new OptionSetter(testDevice.getOptions());
+            OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
             setter.setOptionValue("gce-private-key-path", tmpKeyFile.getAbsolutePath());
             // We use a missing ssh to prevent the real tunnel from running.
             FileUtil.deleteFile(tmpKeyFile);
@@ -586,11 +593,11 @@
 
             replayMocks(mMockBuildInfo);
             // Run device a first time
-            testDevice.preInvocationSetup(mMockBuildInfo);
-            testDevice.getGceSshMonitor().joinMonitor();
+            mTestDevice.preInvocationSetup(mMockBuildInfo);
+            mTestDevice.getGceSshMonitor().joinMonitor();
             // We expect to find our Runtime exception for the ssh key
-            assertNotNull(testDevice.getGceSshMonitor().getLastException());
-            testDevice.postInvocationTearDown();
+            assertNotNull(mTestDevice.getGceSshMonitor().getLastException());
+            mTestDevice.postInvocationTearDown(null);
             // shutdown was disabled, it should not have been called.
             verify(mGceHandler, never()).shutdownGce();
             verifyMocks(mMockBuildInfo);
@@ -608,7 +615,7 @@
         EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
         EasyMock.expect(mMockBuildInfo.getBuildFlavor()).andStubReturn("flavor");
         EasyMock.expect(mMockBuildInfo.getBuildId()).andStubReturn("id");
-        TestableRemoteAndroidVirtualDevice testDevice =
+        mTestDevice =
                 new TestableRemoteAndroidVirtualDevice() {
                     @Override
                     public IDevice getIDevice() {
@@ -640,10 +647,10 @@
                         return mockRunUtil;
                     }
                 };
-        testDevice.setTestLogger(mTestLogger);
+        mTestDevice.setTestLogger(mTestLogger);
         File tmpKeyFile = FileUtil.createTempFile("test-gce", "key");
         try {
-            OptionSetter setter = new OptionSetter(testDevice.getOptions());
+            OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
             setter.setOptionValue("gce-private-key-path", tmpKeyFile.getAbsolutePath());
             // We use a missing ssh to prevent the real tunnel from running.
             FileUtil.deleteFile(tmpKeyFile);
@@ -692,18 +699,48 @@
             replayMocks(mMockBuildInfo);
             // Run device a first time
             try {
-                testDevice.preInvocationSetup(mMockBuildInfo);
+                mTestDevice.preInvocationSetup(mMockBuildInfo);
                 fail("Should have thrown an exception.");
             } catch (DeviceNotAvailableException expected) {
                 assertEquals("AVD device booted but was in OFFLINE state", expected.getMessage());
             }
-            testDevice.getGceSshMonitor().joinMonitor();
+            mTestDevice.getGceSshMonitor().joinMonitor();
             // We expect to find our Runtime exception for the ssh key
-            assertNotNull(testDevice.getGceSshMonitor().getLastException());
-            testDevice.postInvocationTearDown();
+            assertNotNull(mTestDevice.getGceSshMonitor().getLastException());
+            mTestDevice.postInvocationTearDown(null);
             verifyMocks(mMockBuildInfo);
         } finally {
             FileUtil.deleteFile(tmpKeyFile);
         }
     }
+
+    @Test
+    public void testGetRemoteTombstone() throws Exception {
+        mTestDevice =
+                new TestableRemoteAndroidVirtualDevice() {
+                    @Override
+                    boolean fetchRemoteDir(File localDir, String remotePath) {
+                        try {
+                            FileUtil.createTempFile("tombstone_00", "", localDir);
+                            FileUtil.createTempFile("tombstone_01", "", localDir);
+                        } catch (IOException e) {
+                            throw new RuntimeException(e);
+                        }
+                        return true;
+                    }
+                };
+        OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
+        setter.setOptionValue(TestDeviceOptions.INSTANCE_TYPE_OPTION, "CUTTLEFISH");
+
+        replayMocks();
+        List<File> tombstones = mTestDevice.getTombstones();
+        try {
+            assertEquals(2, tombstones.size());
+        } finally {
+            for (File f : tombstones) {
+                FileUtil.deleteFile(f);
+            }
+        }
+        verifyMocks();
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java
index dd6bde5..0689106 100644
--- a/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java
@@ -17,6 +17,7 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.Mockito.times;
 
 import com.android.tradefed.config.OptionSetter;
@@ -104,6 +105,18 @@
         Assert.assertEquals(0, mBase.getBuildInfos().size());
     }
 
+    /** Test that multiple call to init are rejected. */
+    @Test
+    public void testMultiInit() {
+        mBase.init(mContext, mMockListener);
+        try {
+            mBase.init(mContext, mMockListener);
+            fail("Should have thrown an exception.");
+        } catch (IllegalStateException expected) {
+            // Expected
+        }
+    }
+
     /**
      * Test to ensure that the forwarding of events continues even if an exception occurs in the
      * collection.
@@ -424,7 +437,7 @@
         Mockito.verify(mMockListener, times(1))
                 .testEnded(Mockito.eq(test3), Mockito.anyLong(), mCapturedMetrics.capture());
         Mockito.verify(mMockListener, times(1))
-                .testRunEnded(Mockito.anyLong(), (HashMap<String, Metric>) Mockito.any());
+                .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
 
         List<HashMap<String, Metric>> allValues = mCapturedMetrics.getAllValues();
         // For test1
@@ -472,7 +485,7 @@
         Mockito.verify(mMockListener, times(1))
                 .testEnded(Mockito.eq(test3), Mockito.anyLong(), mCapturedMetrics.capture());
         Mockito.verify(mMockListener, times(1))
-                .testRunEnded(Mockito.anyLong(), (HashMap<String, Metric>) Mockito.any());
+                .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
 
         List<HashMap<String, Metric>> allValues = mCapturedMetrics.getAllValues();
         // For test1
@@ -485,4 +498,49 @@
         assertTrue(allValues.get(2).containsKey("onteststart"));
         assertTrue(allValues.get(2).containsKey("ontestend"));
     }
+
+    /**
+     * Test that onTestEnd with TestDescription formal supercedes the method signature without a
+     * TestDescription.
+     */
+    @Test
+    public void testOnTestEndWithTestDescription() throws Exception {
+        mBase =
+                new TwoMetricsBaseCollector() {
+                    @Override
+                    public void onTestEnd(
+                            DeviceMetricData testData,
+                            final Map<String, Metric> currentTestCaseMetrics,
+                            TestDescription test) {
+                        testData.addMetric(
+                                test.getTestName(),
+                                Metric.newBuilder()
+                                        .setMeasurements(
+                                                Measurements.newBuilder()
+                                                        .setSingleString("value1")));
+                    }
+                };
+        mBase.init(mContext, mMockListener);
+        mBase.invocationStarted(mContext);
+        mBase.testRunStarted("testRun", 1);
+        TestDescription test = new TestDescription("class", "method");
+        mBase.testStarted(test);
+        mBase.testEnded(test, new HashMap<String, Metric>());
+        mBase.testRunEnded(0L, new HashMap<String, Metric>());
+        mBase.invocationEnded(0L);
+
+        Mockito.verify(mMockListener, times(1)).invocationStarted(Mockito.any());
+        Mockito.verify(mMockListener, times(1)).testRunStarted("testRun", 1);
+        Mockito.verify(mMockListener, times(1)).testStarted(Mockito.eq(test), Mockito.anyLong());
+        Mockito.verify(mMockListener, times(1))
+                .testEnded(Mockito.eq(test), Mockito.anyLong(), mCapturedMetrics.capture());
+
+        Mockito.verify(mMockListener, times(1))
+                .testRunEnded(Mockito.anyLong(), (HashMap<String, Metric>) Mockito.any());
+
+        List<HashMap<String, Metric>> allValues = mCapturedMetrics.getAllValues();
+        assertTrue(allValues.get(0).containsKey("onteststart"));
+        assertTrue(allValues.get(0).containsKey("method"));
+        assertTrue(!allValues.get(0).containsKey("ontestend"));
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
index fa40866..d213104 100644
--- a/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
@@ -18,9 +18,11 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.ddmlib.IDevice;
 import com.android.tradefed.config.ConfigurationDef;
 import com.android.tradefed.device.ILogcatReceiver;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.NullDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
@@ -44,6 +46,7 @@
     private TestableLogcatOnFailureCollector mCollector;
     private ITestInvocationListener mMockListener;
     private ITestDevice mMockDevice;
+    private ITestDevice mNullMockDevice;
 
     private ITestInvocationListener mTestListener;
     private IInvocationContext mContext;
@@ -81,14 +84,19 @@
     @Before
     public void setUp() {
         mMockDevice = EasyMock.createMock(ITestDevice.class);
+        mNullMockDevice = EasyMock.createMock(ITestDevice.class);
         mMockListener = EasyMock.createMock(ITestInvocationListener.class);
         mMockReceiver = EasyMock.createMock(ILogcatReceiver.class);
         mMockRunUtil = EasyMock.createMock(IRunUtil.class);
         mCollector = new TestableLogcatOnFailureCollector();
         mContext = new InvocationContext();
         mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
+        mContext.addAllocatedDevice("second_null_device", mNullMockDevice);
 
         EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn("serial");
+        EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(EasyMock.createMock(IDevice.class));
+
+        EasyMock.expect(mNullMockDevice.getIDevice()).andStubReturn(new NullDevice("null-dev"));
     }
 
     @Test
@@ -116,14 +124,14 @@
                 EasyMock.eq(LogDataType.LOGCAT),
                 EasyMock.anyObject());
 
-        EasyMock.replay(mMockListener, mMockDevice, mMockReceiver);
+        EasyMock.replay(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
         mTestListener = mCollector.init(mContext, mMockListener);
         mTestListener.testRunStarted("runName", 1);
         mTestListener.testStarted(test);
         mTestListener.testFailed(test, "I failed");
         mTestListener.testEnded(test, new HashMap<String, Metric>());
         mTestListener.testRunEnded(0L, new HashMap<String, Metric>());
-        EasyMock.verify(mMockListener, mMockDevice, mMockReceiver);
+        EasyMock.verify(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
         // Ensure the callback went through
         assertTrue(mCollector.mOnTestStartCalled);
         assertTrue(mCollector.mOnTestFailCalled);
@@ -132,9 +140,9 @@
     @Test
     public void testCollect_noRuns() throws Exception {
         // If there was no runs, nothing should be done.
-        EasyMock.replay(mMockListener, mMockDevice, mMockReceiver);
+        EasyMock.replay(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
         mTestListener = mCollector.init(mContext, mMockListener);
-        EasyMock.verify(mMockListener, mMockDevice, mMockReceiver);
+        EasyMock.verify(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
         assertFalse(mCollector.mOnTestStartCalled);
         assertFalse(mCollector.mOnTestFailCalled);
     }
@@ -187,7 +195,7 @@
                 EasyMock.eq(LogDataType.LOGCAT),
                 EasyMock.anyObject());
 
-        EasyMock.replay(mMockListener, mMockDevice, mMockReceiver);
+        EasyMock.replay(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
         mTestListener = mCollector.init(mContext, mMockListener);
         mTestListener.testRunStarted("runName", 1);
         mTestListener.testStarted(test);
@@ -200,7 +208,7 @@
         mTestListener.testFailed(test2, "I failed");
         mTestListener.testEnded(test2, new HashMap<String, Metric>());
         mTestListener.testRunEnded(0L, new HashMap<String, Metric>());
-        EasyMock.verify(mMockListener, mMockDevice, mMockReceiver);
+        EasyMock.verify(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
         // Ensure the callback went through
         assertTrue(mCollector.mOnTestStartCalled);
         assertTrue(mCollector.mOnTestFailCalled);
diff --git a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
index 485f295..3aabd79 100644
--- a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
@@ -16,6 +16,8 @@
 
 package com.android.tradefed.device.metric;
 
+import static org.junit.Assert.assertTrue;
+
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
@@ -69,7 +71,6 @@
         OptionSetter setter = new OptionSetter(mPerfettoMetricCollector);
         setter.setOptionValue("pull-pattern-keys", "perfettofile");
         HashMap<String, Metric> currentMetrics = new HashMap<>();
-        currentMetrics.put("perfettofile", TfMetricProtoUtil.stringToMetric("/data/trace.pb"));
 
         Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/trace.pb")))
                 .thenReturn(new File("trace"));
@@ -78,8 +79,8 @@
         mPerfettoMetricCollector.testStarted(testDesc);
         mPerfettoMetricCollector.testEnded(testDesc, currentMetrics);
 
-        Mockito.verify(mMockListener)
-                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+        assertTrue("Trace duration available but not expected.",
+                currentMetrics.size() == 0);
     }
 
     @Test
@@ -106,6 +107,44 @@
         Mockito.verify(mPerfettoMetricCollector).runHostCommand(Mockito.any());
         Mockito.verify(mMockListener)
                 .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+        assertTrue("Expected two metrics that includes success status",
+                currentMetrics.get("perfetto_trace_extractor_status").getMeasurements()
+                        .getSingleString().equals("1"));
+        assertTrue("Trace duration metrics not available but expected.",
+                currentMetrics.get("perfetto_trace_extractor_runtime").getMeasurements()
+                        .getSingleDouble() >= 0);
+    }
+
+    @Test
+    public void testScriptFailureStatus() throws Exception {
+
+        OptionSetter setter = new OptionSetter(mPerfettoMetricCollector);
+        setter.setOptionValue("pull-pattern-keys", "perfettofile");
+        setter.setOptionValue("perfetto-binary-path", "trx");
+        HashMap<String, Metric> currentMetrics = new HashMap<>();
+        currentMetrics.put("perfettofile", TfMetricProtoUtil.stringToMetric("/data/trace.pb"));
+        Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/trace.pb")))
+                .thenReturn(new File("trace"));
+
+        TestDescription testDesc = new TestDescription("xyz", "abc");
+        CommandResult cr = new CommandResult();
+        cr.setStatus(CommandStatus.FAILED);
+        cr.setStdout("abc:efg");
+
+        Mockito.doReturn(cr).when(mPerfettoMetricCollector).runHostCommand(Mockito.any());
+
+        mPerfettoMetricCollector.testStarted(testDesc);
+        mPerfettoMetricCollector.testEnded(testDesc, currentMetrics);
+
+        Mockito.verify(mPerfettoMetricCollector).runHostCommand(Mockito.any());
+        Mockito.verify(mMockListener)
+                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+        assertTrue("Expected two metrics that includes failure status",
+                currentMetrics.get("perfetto_trace_extractor_status").getMeasurements()
+                        .getSingleString().equals("0"));
+        assertTrue("Trace duration metrics not available but expected.",
+                currentMetrics.get("perfetto_trace_extractor_runtime").getMeasurements()
+                        .getSingleDouble() >= 0);
     }
 
     @Test
diff --git a/tests/src/com/android/tradefed/device/metric/RebootReasonCollectorTest.java b/tests/src/com/android/tradefed/device/metric/RebootReasonCollectorTest.java
index da81ba0..34aed61 100644
--- a/tests/src/com/android/tradefed/device/metric/RebootReasonCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/RebootReasonCollectorTest.java
@@ -144,8 +144,21 @@
                                                                         .getMeasurements()
                                                                         .getSingleString())
                                                         .equals(1));
+        boolean totalCountCorrect =
+                runMetrics
+                        .entrySet()
+                        .stream()
+                        .anyMatch(
+                                entry ->
+                                        entry.getKey().equals(RebootReasonCollector.COUNT_KEY)
+                                                && Integer.valueOf(
+                                                                entry.getValue()
+                                                                        .getMeasurements()
+                                                                        .getSingleString())
+                                                        .equals(3));
         Assert.assertTrue(reason1CountCorrect);
         Assert.assertTrue(reason2CountCorrect);
+        Assert.assertTrue(totalCountCorrect);
     }
 
     /** Test that the collector makes the correct callbacks when testing multiple devices. */
@@ -184,6 +197,8 @@
         ITestDevice testDevice1 = mockTestDevice(DEVICE_SERIAL_1);
         ITestDevice testDevice2 = mockTestDevice(DEVICE_SERIAL_2);
         doReturn(Arrays.asList(testDevice1, testDevice2)).when(mContext).getDevices();
+        doReturn(DEVICE_SERIAL_1).when(mContext).getDeviceName(testDevice1);
+        doReturn(DEVICE_SERIAL_2).when(mContext).getDeviceName(testDevice2);
         doReturn(CONFIG_ID_1).when(mCollector).pushStatsConfig(eq(testDevice1), any(List.class));
         doReturn(CONFIG_ID_2).when(mCollector).pushStatsConfig(eq(testDevice2), any(List.class));
         doReturn(Arrays.asList(mockBootEventMetric("bootloader_reason", "system_reason")))
@@ -214,6 +229,19 @@
                                                                         .getMeasurements()
                                                                         .getSingleString())
                                                         .equals(1));
+        boolean device1TotalCountCorrect =
+                runMetrics
+                        .entrySet()
+                        .stream()
+                        .anyMatch(
+                                entry ->
+                                        entry.getKey().contains(RebootReasonCollector.COUNT_KEY)
+                                                && entry.getKey().contains(DEVICE_SERIAL_1)
+                                                && Integer.valueOf(
+                                                                entry.getValue()
+                                                                        .getMeasurements()
+                                                                        .getSingleString())
+                                                        .equals(1));
         boolean device2CountCorrect =
                 runMetrics
                         .entrySet()
@@ -223,19 +251,34 @@
                                         entry.getKey().contains(RebootReasonCollector.METRIC_PREFIX)
                                                 && entry.getKey().contains("bootloader_reason")
                                                 && entry.getKey().contains("system_reason")
-                                                && entry.getKey().contains(DEVICE_SERIAL_1)
+                                                && entry.getKey().contains(DEVICE_SERIAL_2)
+                                                && Integer.valueOf(
+                                                                entry.getValue()
+                                                                        .getMeasurements()
+                                                                        .getSingleString())
+                                                        .equals(1));
+        boolean device2TotalCountCorrect =
+                runMetrics
+                        .entrySet()
+                        .stream()
+                        .anyMatch(
+                                entry ->
+                                        entry.getKey().contains(RebootReasonCollector.COUNT_KEY)
+                                                && entry.getKey().contains(DEVICE_SERIAL_2)
                                                 && Integer.valueOf(
                                                                 entry.getValue()
                                                                         .getMeasurements()
                                                                         .getSingleString())
                                                         .equals(1));
         Assert.assertTrue(device1CountCorrect);
+        Assert.assertTrue(device1TotalCountCorrect);
         Assert.assertTrue(device2CountCorrect);
+        Assert.assertTrue(device2TotalCountCorrect);
     }
 
-    /** Test that no metrics are added when no metrics are received. */
+    /** Test that only a count is added when no reboots were recorded. */
     @Test
-    public void testNoMetrics() throws Exception {
+    public void testCountOnlyWhenNoReboots() throws Exception {
         ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
         when(mContext.getDevices()).thenReturn(Arrays.asList(testDevice));
         doReturn(CONFIG_ID_1)
@@ -255,6 +298,11 @@
                         .keySet()
                         .stream()
                         .noneMatch(key -> key.startsWith(RebootReasonCollector.METRIC_PREFIX)));
+        Assert.assertTrue(
+                runMetrics
+                        .keySet()
+                        .stream()
+                        .anyMatch(key -> key.contains(RebootReasonCollector.COUNT_KEY)));
     }
 
     private ITestDevice mockTestDevice(String serial) {
diff --git a/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java
index 06092b7..683a466 100644
--- a/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.device.metric;
 
+import com.android.ddmlib.IDevice;
 import com.android.tradefed.config.ConfigurationDef;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
@@ -50,7 +51,7 @@
         mCollector = new ScreenshotOnFailureCollector();
         mContext = new InvocationContext();
         mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
-        mTestListener = mCollector.init(mContext, mMockListener);
+        EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(EasyMock.createMock(IDevice.class));
         EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("serial");
     }
 
@@ -72,6 +73,7 @@
                 EasyMock.anyObject());
 
         EasyMock.replay(mMockListener, mMockDevice);
+        mTestListener = mCollector.init(mContext, mMockListener);
         mTestListener.testStarted(test);
         mTestListener.testFailed(test, "I failed");
         mTestListener.testEnded(test, new HashMap<String, Metric>());
diff --git a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
index 5384a65..8623025 100644
--- a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
@@ -35,6 +35,7 @@
 import com.android.tradefed.guice.InvocationScope;
 import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
 import com.android.tradefed.log.ILogRegistry;
+import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.ILogSaver;
 import com.android.tradefed.result.ITestInvocationListener;
@@ -179,7 +180,8 @@
                             public boolean shardConfig(
                                     IConfiguration config,
                                     IInvocationContext context,
-                                    IRescheduler rescheduler) {
+                                    IRescheduler rescheduler,
+                                    ITestLogger logger) {
                                 // Ensure that sharding is not called against a sandbox
                                 // configuration run
                                 throw new RuntimeException("Should not be called.");
@@ -308,7 +310,7 @@
         // Device early preInvocationSetup was called and even if no tests run we still call tear
         // down
         Mockito.verify(mMockDevice).preInvocationSetup(any(), any());
-        Mockito.verify(mMockDevice).postInvocationTearDown();
+        Mockito.verify(mMockDevice).postInvocationTearDown(null);
     }
 
     /**
@@ -378,6 +380,6 @@
         // Device early preInvocationSetup was called and even if no tests run we still call tear
         // down
         Mockito.verify(mMockDevice).preInvocationSetup(any(), any());
-        Mockito.verify(mMockDevice).postInvocationTearDown();
+        Mockito.verify(mMockDevice).postInvocationTearDown(exception);
     }
 }
diff --git a/tests/src/com/android/tradefed/invoker/ShardListenerTest.java b/tests/src/com/android/tradefed/invoker/ShardListenerTest.java
index c835038..a8127b1 100644
--- a/tests/src/com/android/tradefed/invoker/ShardListenerTest.java
+++ b/tests/src/com/android/tradefed/invoker/ShardListenerTest.java
@@ -78,6 +78,27 @@
         EasyMock.verify(mMockListener, mMockDevice);
     }
 
+    /** Test that we can replay events even if invocationEnded hasn't be called yet. */
+    @Test
+    public void testPlayRuns() {
+        mMockListener.invocationStarted(mContext);
+        mMockListener.testRunStarted("run1", 1);
+        TestDescription tid = new TestDescription("class1", "name1");
+        mMockListener.testStarted(tid, 0l);
+        mMockListener.testEnded(tid, 0l, new HashMap<String, Metric>());
+        mMockListener.testRunEnded(0l, new HashMap<String, Metric>());
+        // mMockListener.invocationEnded(0l); On purpose not calling invocationEnded.
+
+        EasyMock.replay(mMockListener, mMockDevice);
+        mShardListener.invocationStarted(mContext);
+        mShardListener.testRunStarted("run1", 1);
+        mShardListener.testStarted(tid, 0l);
+        mShardListener.testEnded(tid, 0l, new HashMap<String, Metric>());
+        mShardListener.testRunEnded(0l, new HashMap<String, Metric>());
+        // mShardListener.invocationEnded(0l); On purpose not calling invocationEnded.
+        EasyMock.verify(mMockListener, mMockDevice);
+    }
+
     /** Ensure that replaying a log without a run (no tests ran) still works. */
     @Test
     public void testLogWithoutRun() {
@@ -195,11 +216,43 @@
         // Log association to re-associate file to the run.
         mockListener.logAssociation("run-file", runFile);
         mockListener.testRunEnded(0l, new HashMap<String, Metric>());
+
+        // The log not associated to the run are replay at invocation level.
+        mockListener.testLog(
+                EasyMock.eq("host_log_of_shard"),
+                EasyMock.eq(LogDataType.TEXT),
+                EasyMock.anyObject());
+        LogFile invocFile = new LogFile("path", "url", false, LogDataType.TEXT, 0L);
+        EasyMock.expect(
+                        mMockSaver.saveLogData(
+                                EasyMock.eq("host_log_of_shard"),
+                                EasyMock.eq(LogDataType.TEXT),
+                                EasyMock.anyObject()))
+                .andReturn(invocFile);
+        mockListener.testLogSaved(
+                EasyMock.eq("host_log_of_shard"),
+                EasyMock.eq(LogDataType.TEXT),
+                EasyMock.anyObject(),
+                EasyMock.eq(invocFile));
+        mockListener.logAssociation("host_log_of_shard", invocFile);
         mockListener.invocationEnded(0l);
         EasyMock.expect(mockListener.getSummary()).andReturn(null);
 
+        // TODO: Fix the name of end_host_log for each shard
+        EasyMock.expect(
+                        mMockSaver.saveLogData(
+                                EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+                                EasyMock.eq(LogDataType.TEXT),
+                                EasyMock.anyObject()))
+                .andReturn(invocFile);
         mMockSaver.invocationEnded(0L);
-        EasyMock.expectLastCall().times(2);
+        EasyMock.expect(
+                        mMockSaver.saveLogData(
+                                EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+                                EasyMock.eq(LogDataType.TEXT),
+                                EasyMock.anyObject()))
+                .andReturn(invocFile);
+        mMockSaver.invocationEnded(0L);
 
         EasyMock.replay(mockListener, mMockSaver, mMockDevice);
         // Setup of sharding
@@ -223,6 +276,10 @@
                 new ByteArrayInputStreamSource("test file".getBytes()));
         shardedInvocation.testEnded(tid, 0l, new HashMap<String, Metric>());
         shardedInvocation.testRunEnded(0l, new HashMap<String, Metric>());
+        shardedInvocation.testLog(
+                "host_log_of_shard",
+                LogDataType.TEXT,
+                new ByteArrayInputStreamSource("test".getBytes()));
         shardedInvocation.invocationEnded(0L);
 
         EasyMock.verify(mockListener, mMockSaver, mMockDevice);
diff --git a/tests/src/com/android/tradefed/invoker/ShardMasterResultForwarderTest.java b/tests/src/com/android/tradefed/invoker/ShardMasterResultForwarderTest.java
index d85671c..b62dd38 100644
--- a/tests/src/com/android/tradefed/invoker/ShardMasterResultForwarderTest.java
+++ b/tests/src/com/android/tradefed/invoker/ShardMasterResultForwarderTest.java
@@ -168,7 +168,7 @@
         invocationLogger.invocationEnded(500L);
 
         // Log saver only saved the file once.
-        Mockito.verify(mMockLogSaver, times(1))
+        Mockito.verify(mMockLogSaver, times(2))
                 .saveLogData(Mockito.any(), Mockito.any(), Mockito.any());
         Mockito.verify(mMockLogListener, times(1)).invocationStarted(Mockito.eq(main));
         Mockito.verify(mMockLogListener, times(1))
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index 89e6887..c9a22e5 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -170,6 +170,7 @@
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
         EasyMock.expect(mMockConfig.getTests()).andStubReturn(new ArrayList<>());
+        mMockConfig.resolveDynamicOptions();
         mMockConfig.cleanDynamicOptionFiles();
         IBuildInfo build1 = new BuildInfo();
         EasyMock.expect(mProvider1.getBuild()).andReturn(build1);
@@ -190,6 +191,12 @@
                         mMockLogSaver.saveLogData(
                                 EasyMock.anyObject(), EasyMock.anyObject(), EasyMock.anyObject()))
                 .andReturn(new LogFile("", "", LogDataType.TEXT));
+        EasyMock.expect(
+                        mMockLogSaver.saveLogData(
+                                EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
+                .andReturn(new LogFile("", "", LogDataType.TEXT));
         mMockTestListener.invocationEnded(EasyMock.anyLong());
         EasyMock.expect(mMockTestListener.getSummary()).andReturn(null);
         mMockLogSaver.invocationEnded(EasyMock.anyLong());
@@ -250,6 +257,7 @@
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
         EasyMock.expect(mMockConfig.getTests()).andStubReturn(new ArrayList<>());
+        mMockConfig.resolveDynamicOptions();
         mMockConfig.cleanDynamicOptionFiles();
 
         mMockTestListener.invocationStarted(mContext);
@@ -261,6 +269,12 @@
                         mMockLogSaver.saveLogData(
                                 EasyMock.anyObject(), EasyMock.anyObject(), EasyMock.anyObject()))
                 .andReturn(new LogFile("", "", LogDataType.TEXT));
+        EasyMock.expect(
+                        mMockLogSaver.saveLogData(
+                                EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
+                .andReturn(new LogFile("", "", LogDataType.TEXT));
         mMockTestListener.invocationEnded(EasyMock.anyLong());
         EasyMock.expect(mMockTestListener.getSummary()).andReturn(null);
         mMockLogSaver.invocationEnded(EasyMock.anyLong());
@@ -330,6 +344,7 @@
         EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
         EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
         EasyMock.expect(mMockConfig.getTests()).andStubReturn(new ArrayList<>());
+        mMockConfig.resolveDynamicOptions();
         mMockConfig.cleanDynamicOptionFiles();
 
         mMockTestListener.invocationStarted(mContext);
@@ -341,6 +356,12 @@
                         mMockLogSaver.saveLogData(
                                 EasyMock.anyObject(), EasyMock.anyObject(), EasyMock.anyObject()))
                 .andReturn(new LogFile("", "", LogDataType.TEXT));
+        EasyMock.expect(
+                        mMockLogSaver.saveLogData(
+                                EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+                                EasyMock.anyObject(),
+                                EasyMock.anyObject()))
+                .andReturn(new LogFile("", "", LogDataType.TEXT));
         mMockTestListener.invocationEnded(EasyMock.anyLong());
         EasyMock.expect(mMockTestListener.getSummary()).andReturn(null);
         mMockLogSaver.invocationEnded(EasyMock.anyLong());
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index c6ea271..38bca14 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -46,7 +46,6 @@
 import com.android.tradefed.device.DeviceAllocationState;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.IDeviceRecovery;
-import com.android.tradefed.device.INativeDevice;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
 import com.android.tradefed.device.StubDevice;
@@ -93,10 +92,10 @@
 import org.easymock.Capture;
 import org.easymock.EasyMock;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
 
 import java.io.File;
 import java.io.IOException;
@@ -155,6 +154,15 @@
     private IRescheduler mockRescheduler;
     private DeviceDescriptor mFakeDescriptor;
 
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        try {
+            GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
+        } catch (IllegalStateException e) {
+            // Avoid exception in case of multi-init
+        }
+    }
+
     @Before
     public void setUp() throws Exception {
 
@@ -212,10 +220,16 @@
         mMockDevice.preInvocationSetup(
                 (IBuildInfo) EasyMock.anyObject(), EasyMock.<List<IBuildInfo>>anyObject());
         EasyMock.expectLastCall().anyTimes();
-        mMockDevice.postInvocationTearDown();
-        EasyMock.expectLastCall().anyTimes();
-        mFakeDescriptor = new DeviceDescriptor(SERIAL, false, DeviceAllocationState.Available,
-                "unknown", "unknown", "unknown", "unknown", "unknown");
+        mFakeDescriptor =
+                new DeviceDescriptor(
+                        SERIAL,
+                        false,
+                        DeviceAllocationState.Available,
+                        "unknown",
+                        "unknown",
+                        "unknown",
+                        "unknown",
+                        "unknown");
         EasyMock.expect(mMockDevice.getDeviceDescriptor()).andStubReturn(mFakeDescriptor);
 
         EasyMock.expect(mMockBuildInfo.getBuildId()).andStubReturn("1");
@@ -229,9 +243,13 @@
 
         // always expect logger initialization and cleanup calls
         mMockLogRegistry.registerLogger(mMockLogger);
+        EasyMock.expectLastCall().times(2);
         mMockLogger.init();
+        EasyMock.expectLastCall().times(2);
         mMockLogger.closeLog();
+        EasyMock.expectLastCall().times(2);
         mMockLogRegistry.unregisterLogger();
+        EasyMock.expectLastCall().times(2);
         mUriCapture = new Capture<List<TestSummary>>();
 
         mStubInvocationMetadata = new InvocationContext();
@@ -351,6 +369,11 @@
 
         setupMockFailureListeners(exception);
         setupInvoke();
+        EasyMock.reset(mMockLogger, mMockLogRegistry);
+        mMockLogRegistry.registerLogger(mMockLogger);
+        mMockLogger.init();
+        mMockLogger.closeLog();
+        mMockLogRegistry.unregisterLogger();
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         CommandOptions cmdOptions = new CommandOptions();
         final String expectedTestTag = "TEST_TAG";
@@ -380,6 +403,12 @@
         setupInvoke();
         setupMockFailureListenersAny(new BuildRetrievalError("No build found to test."), true);
 
+        EasyMock.reset(mMockLogger, mMockLogRegistry);
+        mMockLogRegistry.registerLogger(mMockLogger);
+        mMockLogger.init();
+        mMockLogger.closeLog();
+        mMockLogRegistry.unregisterLogger();
+
         IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
         mStubConfiguration.setTest(test);
         EasyMock.expect(mMockLogger.getLog()).andReturn(EMPTY_STREAM_SOURCE);
@@ -408,13 +437,20 @@
         IRetriableTest test = EasyMock.createMock(IRetriableTest.class);
         EasyMock.expect(test.isRetriable()).andReturn(Boolean.TRUE);
 
+        setupInvoke();
+
+        EasyMock.reset(mMockLogger, mMockLogRegistry);
+        mMockLogRegistry.registerLogger(mMockLogger);
+        mMockLogger.init();
+        mMockLogger.closeLog();
+        mMockLogRegistry.unregisterLogger();
+
         EasyMock.expect(mockRescheduler.rescheduleCommand()).andReturn(EasyMock.anyBoolean());
         mStubConfiguration.setTest(test);
         mStubConfiguration.getCommandOptions().setLoopMode(false);
         mMockLogRegistry.dumpToGlobalLog(mMockLogger);
         EasyMock.expectLastCall().times(1);
 
-        setupInvoke();
         setupMockFailureListenersAny(new BuildRetrievalError("No build found to test."), true);
         Capture<IBuildInfo> captured = new Capture<>();
         mMockBuildProvider.cleanUp(EasyMock.capture(captured));
@@ -548,6 +584,9 @@
         setupMockFailureListeners(exception);
         EasyMock.expect(mMockDevice.getBugreport()).andReturn(EMPTY_STREAM_SOURCE);
         setupInvokeWithBuild();
+
+        mMockDevice.postInvocationTearDown(exception);
+
         replayMocks(test);
         EasyMock.replay(mockRescheduler);
         mTestInvocation.invoke(mStubInvocationMetadata, mStubConfiguration, mockRescheduler);
@@ -611,6 +650,12 @@
                                 EasyMock.eq(LogDataType.TEXT),
                                 (InputStream) EasyMock.anyObject()))
                 .andReturn(new LogFile(PATH, URL, LogDataType.TEXT));
+        EasyMock.expect(
+                        mMockLogSaver.saveLogData(
+                                EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+                                EasyMock.eq(LogDataType.TEXT),
+                                (InputStream) EasyMock.anyObject()))
+                .andReturn(new LogFile(PATH, URL, LogDataType.TEXT));
         resumeListener.testLog(
                 EasyMock.startsWith(LOGCAT_NAME_SETUP),
                 EasyMock.eq(LogDataType.LOGCAT),
@@ -704,6 +749,7 @@
         EasyMock.expectLastCall().times(3);
         mMockDevice.clearLastConnectedWifiNetwork();
         mMockDevice.stopLogcat();
+        mMockDevice.postInvocationTearDown(null);
         EasyMock.replay(mockRescheduler, resumeListener, resumableTest, mMockPreparer,
                 mMockBuildProvider, mMockLogger, mMockLogSaver, mMockDevice, mMockBuildInfo);
 
@@ -736,6 +782,10 @@
         IRescheduler mockRescheduler = EasyMock.createMock(IRescheduler.class);
         EasyMock.expect(mockRescheduler.rescheduleCommand()).andReturn(EasyMock.anyBoolean());
         mMockBuildProvider.buildNotTested(mMockBuildInfo);
+
+        mMockDevice.postInvocationTearDown(exception);
+        EasyMock.expectLastCall().anyTimes();
+
         setupMockFailureListeners(exception);
         setupNormalInvoke(test);
         EasyMock.replay(mockRescheduler);
@@ -1120,6 +1170,11 @@
         mMockSummaryListener.invocationStarted(mStubInvocationMetadata);
         EasyMock.expect(mMockSummaryListener.getSummary()).andReturn(null);
 
+        if (throwable == null) {
+            mMockDevice.postInvocationTearDown(null);
+            EasyMock.expectLastCall().anyTimes();
+        }
+
         if (!(throwable instanceof BuildRetrievalError)) {
             EasyMock.expect(
                             mMockLogSaver.saveLogData(
@@ -1218,6 +1273,12 @@
                 EasyMock.eq(LogDataType.TEXT), (InputStreamSource)EasyMock.anyObject());
         mMockSummaryListener.testLog(EasyMock.eq(TestInvocation.TRADEFED_LOG_NAME),
                 EasyMock.eq(LogDataType.TEXT), (InputStreamSource)EasyMock.anyObject());
+        EasyMock.expect(
+                        mMockLogSaver.saveLogData(
+                                EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+                                EasyMock.eq(LogDataType.TEXT),
+                                (InputStream) EasyMock.anyObject()))
+                .andReturn(new LogFile(PATH, URL, LogDataType.TEXT));
 
         // invocationEnded, getSummary (mMockTestListener)
         mMockTestListener.invocationEnded(EasyMock.anyLong());
@@ -1235,12 +1296,6 @@
      */
     @Test
     public void testInvoke_shardableTest_legacy() throws Throwable {
-        try {
-            GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
-        } catch (IllegalStateException e) {
-            // Avoid exception in case of multi-init
-        }
-
         String command = "empty --test-tag t";
         String[] commandLine = {"empty", "--test-tag", "t"};
         int shardCount = 2;
@@ -1259,6 +1314,11 @@
                 .andReturn(new StubKeyStoreFactory())
                 .times(2);
         setupInvoke();
+        EasyMock.reset(mMockLogger, mMockLogRegistry);
+        mMockLogRegistry.registerLogger(mMockLogger);
+        mMockLogger.init();
+        mMockLogger.closeLog();
+        mMockLogRegistry.unregisterLogger();
         mMockLogSaver.invocationStarted(mStubInvocationMetadata);
         mMockLogSaver.invocationEnded(0L);
         setupNShardInvocation(shardCount, command);
@@ -1488,76 +1548,6 @@
     }
 
     /**
-     * Test {@link INativeDevice#preInvocationSetup(IBuildInfo, List)} is called when command option
-     * skip-pre-device-setup is not set.
-     */
-    @Test
-    public void testNotSkipPreDeviceSetup() throws Throwable {
-        IInvocationContext context = new InvocationContext();
-        ITestDevice device1 = EasyMock.createMock(ITestDevice.class);
-        IDevice idevice = Mockito.mock(IDevice.class);
-        context.addAllocatedDevice("DEFAULT_DEVICE", device1);
-        IBuildInfo testResourceBuildInfo = new BuildInfo();
-        testResourceBuildInfo.setTestResourceBuild(true);
-        context.addDeviceBuildInfo("test-resource", testResourceBuildInfo);
-        List<IBuildInfo> testResourceBuildInfos = new ArrayList<>();
-        testResourceBuildInfos.add(testResourceBuildInfo);
-        EasyMock.expect(device1.getSerialNumber()).andReturn("serial1").anyTimes();
-        EasyMock.expect(device1.getIDevice()).andReturn(idevice).anyTimes();
-
-        device1.preInvocationSetup(
-                (IBuildInfo) EasyMock.anyObject(), EasyMock.eq(testResourceBuildInfos));
-        EasyMock.expectLastCall().once();
-
-        CommandOptions commandOption = new CommandOptions();
-        OptionSetter setter = new OptionSetter(commandOption);
-        setter.setOptionValue("skip-pre-device-setup", "false");
-        mStubConfiguration.setCommandOptions(commandOption);
-        // Not expect isTearDownDisabled.
-        ITestInvocationListener listener = EasyMock.createStrictMock(ITestInvocationListener.class);
-        EasyMock.replay(device1, listener, mMockPreparer);
-        new InvocationExecution()
-                .runDevicePreInvocationSetup(context, mStubConfiguration, listener);
-        EasyMock.verify(device1, listener, mMockPreparer);
-
-    }
-
-    /**
-     * Test {@link INativeDevice#preInvocationSetup(IBuildInfo info)} is not called when command
-     * option skip-pre-device-setup is set.
-     */
-    @Test
-    public void testSkipPreDeviceSetup() throws Throwable {
-        IInvocationContext context = new InvocationContext();
-        ITestDevice device1 = EasyMock.createMock(ITestDevice.class);
-        IDevice idevice = Mockito.mock(IDevice.class);
-        context.addAllocatedDevice("DEFAULT_DEVICE", device1);
-        EasyMock.expect(device1.getSerialNumber()).andReturn("serial1").anyTimes();
-        EasyMock.expect(device1.getIDevice()).andReturn(idevice).anyTimes();
-        EasyMock.expect(device1.getLogcat()).andReturn(EMPTY_STREAM_SOURCE).times(1);
-        device1.clearLogcat();
-        EasyMock.expectLastCall().once();
-
-        CommandOptions commandOption = new CommandOptions();
-        OptionSetter setter = new OptionSetter(commandOption);
-        setter.setOptionValue("skip-pre-device-setup", "true");
-        mStubConfiguration.setCommandOptions(commandOption);
-
-        EasyMock.expect(mMockPreparer.isDisabled()).andReturn(true);
-        // Not expect isTearDownDisabled
-
-        ITestInvocationListener listener = EasyMock.createStrictMock(ITestInvocationListener.class);
-        listener.testLog(
-                EasyMock.startsWith(LOGCAT_NAME_SETUP),
-                EasyMock.eq(LogDataType.LOGCAT),
-                (InputStreamSource) EasyMock.anyObject());
-
-        EasyMock.replay(device1, listener, mMockPreparer);
-        new InvocationExecution().doSetup(context, mStubConfiguration, listener);
-        EasyMock.verify(device1, listener, mMockPreparer);
-    }
-
-    /**
      * Test when a {@link IDeviceBuildInfo} is passing through we do not attempt to add any external
      * directories when there is none coming from environment.
      */
diff --git a/tests/src/com/android/tradefed/invoker/logger/InvocationMetricLoggerTest.java b/tests/src/com/android/tradefed/invoker/logger/InvocationMetricLoggerTest.java
new file mode 100644
index 0000000..c7eead0
--- /dev/null
+++ b/tests/src/com/android/tradefed/invoker/logger/InvocationMetricLoggerTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.invoker.logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+import java.util.UUID;
+
+/** Unit tests for {@link InvocationMetricLogger}. */
+@RunWith(JUnit4.class)
+public class InvocationMetricLoggerTest {
+
+    @Test
+    public void testLogMetrics() throws Exception {
+        Map<String, String> result = logMetric(InvocationMetricKey.FETCH_BUILD, "TEST");
+        assertEquals("TEST", result.get(InvocationMetricKey.FETCH_BUILD.toString()));
+        // Ensure that it wasn't added in current ThreadGroup
+        assertNull(
+                InvocationMetricLogger.getInvocationMetrics()
+                        .get(InvocationMetricKey.FETCH_BUILD.toString()));
+    }
+
+    private Map<String, String> logMetric(InvocationMetricKey key, String value) throws Exception {
+        String uuid = UUID.randomUUID().toString();
+        ThreadGroup testGroup = new ThreadGroup("unit-test-group-" + uuid);
+        TestRunnable runnable = new TestRunnable(key, value);
+        Thread testThread = new Thread(testGroup, runnable);
+        testThread.setName("InvocationMetricLoggerTest-test-thread");
+        testThread.setDaemon(true);
+        testThread.start();
+        testThread.join(10000);
+        return runnable.getResultMap();
+    }
+
+    private class TestRunnable implements Runnable {
+
+        private InvocationMetricKey mKey;
+        private String mValue;
+        private Map<String, String> mResultMap;
+
+        public TestRunnable(InvocationMetricKey key, String value) {
+            mKey = key;
+            mValue = value;
+        }
+
+        @Override
+        public void run() {
+            InvocationMetricLogger.addInvocationMetrics(mKey, mValue);
+            mResultMap = InvocationMetricLogger.getInvocationMetrics();
+        }
+
+        public Map<String, String> getResultMap() {
+            return mResultMap;
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/invoker/shard/ShardHelperTest.java b/tests/src/com/android/tradefed/invoker/shard/ShardHelperTest.java
index 58d0bac..f4c0e5f 100644
--- a/tests/src/com/android/tradefed/invoker/shard/ShardHelperTest.java
+++ b/tests/src/com/android/tradefed/invoker/shard/ShardHelperTest.java
@@ -135,7 +135,7 @@
         setter.setOptionValue("num-shards", "5");
         mConfig.setTest(test);
         assertEquals(1, mConfig.getTests().size());
-        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
         // Ensure that we did split 1 tests per shard rescheduled.
         Mockito.verify(mRescheduler, Mockito.times(3))
                 .scheduleConfig(
@@ -161,7 +161,7 @@
         setter.setOptionValue("num-shards", "5");
         mConfig.setTest(test);
         assertEquals(1, mConfig.getTests().size());
-        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
         // Ensure that we did split 1 tests per shard rescheduled.
         Mockito.verify(mRescheduler, Mockito.times(5))
                 .scheduleConfig(
@@ -193,7 +193,7 @@
         setter.setOptionValue("num-shards", "5");
         mConfig.setTest(test);
         assertEquals(1, mConfig.getTests().size());
-        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
         // We only reschedule 5 times and not 10 like --shard-count because there is not enough
         // tests to put at least 1 test per shard. So there is no point in rescheduling on new
         // devices.
@@ -238,7 +238,7 @@
             setter.setOptionValue("num-shards", "5");
             mConfig.setTest(test);
             assertEquals(1, mConfig.getTests().size());
-            assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+            assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
             // Ensure that we did split 1 tests per shard rescheduled.
             Mockito.verify(mRescheduler, Mockito.times(3))
                     .scheduleConfig(
@@ -285,7 +285,7 @@
             setter.setOptionValue("num-shards", "5");
             mConfig.setTest(test);
             assertEquals(1, mConfig.getTests().size());
-            assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+            assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
             // Ensure that we did split 1 tests per shard rescheduled.
             Mockito.verify(mRescheduler, Mockito.times(3))
                     .scheduleConfig(
@@ -359,7 +359,7 @@
         doReturn(true).when(mockClient).isAvailable();
         doReturn(SuccessTestCase.class.getName()).when(mockClient).fetchKey("test");
 
-        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
         // Ensure that we did split 1 tests per shard rescheduled.
         Mockito.verify(mRescheduler, Mockito.times(2))
                 .scheduleConfig(
@@ -420,7 +420,7 @@
         doReturn(SuccessTestCase.class.getName()).when(mockClient).fetchKey("test");
         doThrow(new RuntimeException()).when(mockClient).fetchKey("test");
         try {
-            mHelper.shardConfig(mConfig, mContext, mRescheduler);
+            mHelper.shardConfig(mConfig, mContext, mRescheduler, null);
             fail("Should have thrown an exception.");
         } catch (RuntimeException expected) {
             // expected
@@ -452,7 +452,7 @@
         tests.add(new TokenTestClass());
         mConfig.setTests(tests);
         assertEquals(2, mConfig.getTests().size());
-        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
         // Ensure that we did split 1 tests per shard rescheduled.
         Mockito.verify(mRescheduler, Mockito.times(3))
                 .scheduleConfig(
diff --git a/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java b/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
index 0f162d4..00d098e 100644
--- a/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
+++ b/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
@@ -92,7 +92,7 @@
         setter.setOptionValue("num-shards", "5");
         mConfig.setTest(test);
         assertEquals(1, mConfig.getTests().size());
-        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+        assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
         // Ensure that we did split 1 tests per shard rescheduled.
         Mockito.verify(mRescheduler, Mockito.times(5))
                 .scheduleConfig(
@@ -121,7 +121,7 @@
         mConfig.setTest(test);
         assertEquals(1, mConfig.getTests().size());
         // We do not shard, we are relying on the current invocation to run.
-        assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+        assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
         // Rescheduled is NOT called because we use the current invocation to run the index.
         Mockito.verify(mRescheduler, Mockito.times(0)).scheduleConfig(Mockito.any());
         assertEquals(1, mConfig.getTests().size());
@@ -152,7 +152,7 @@
         mConfig.setTest(test);
         assertEquals(1, mConfig.getTests().size());
         // We do not shard, we are relying on the current invocation to run.
-        assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+        assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
         // Rescheduled is NOT called because we use the current invocation to run the index.
         Mockito.verify(mRescheduler, Mockito.times(0)).scheduleConfig(Mockito.any());
         assertEquals(1, mConfig.getTests().size());
@@ -183,7 +183,7 @@
         mConfig.setTest(test);
         assertEquals(1, mConfig.getTests().size());
         // We do not shard, we are relying on the current invocation to run.
-        assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+        assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
         // Rescheduled is NOT called because we use the current invocation to run the index.
         Mockito.verify(mRescheduler, Mockito.times(0)).scheduleConfig(Mockito.any());
         // We have no tests to put in shard-index 1 so it's empty.
@@ -249,7 +249,7 @@
         mConfig.setCommandOptions(options);
         mConfig.setCommandLine(new String[] {"empty"});
         mConfig.setTests(test);
-        mHelper.shardConfig(mConfig, mContext, mRescheduler);
+        mHelper.shardConfig(mConfig, mContext, mRescheduler, null);
         return mConfig.getTests();
     }
 
@@ -314,7 +314,7 @@
     @Test
     public void testShardSuite() throws Exception {
         //mConfig
-        mHelper.shardConfig(mConfig, mContext, mRescheduler);
+        mHelper.shardConfig(mConfig, mContext, mRescheduler, null);
     }
 
     /**
@@ -365,7 +365,7 @@
         setter.setOptionValue("shard-index", Integer.toString(0));
         mConfig.setCommandOptions(options);
         mConfig.setTest(test);
-        mHelper.shardConfig(mConfig, mContext, mRescheduler);
+        mHelper.shardConfig(mConfig, mContext, mRescheduler, null);
 
         List<IRemoteTest> res = mConfig.getTests();
         assertEquals(1, res.size());
diff --git a/tests/src/com/android/tradefed/log/HistoryLoggerTest.java b/tests/src/com/android/tradefed/log/HistoryLoggerTest.java
index 65687d1..dd48251 100644
--- a/tests/src/com/android/tradefed/log/HistoryLoggerTest.java
+++ b/tests/src/com/android/tradefed/log/HistoryLoggerTest.java
@@ -22,7 +22,6 @@
 import org.junit.Assert;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -52,7 +51,7 @@
         mLogger =
                 new HistoryLogger() {
                     @Override
-                    void writeToLog(String outMessage) throws IOException {
+                    protected void writeToLog(String outMessage) {
                         Assert.assertEquals("INVOCATION_END: {\"test\":\"value\"}\n", outMessage);
                         mWasCalled = true;
                     }
@@ -72,7 +71,7 @@
         mLogger =
                 new HistoryLogger() {
                     @Override
-                    void writeToLog(String outMessage) throws IOException {
+                    protected void writeToLog(String outMessage) {
                         Assert.assertEquals("INVOCATION_END: {}\n", outMessage);
                         mWasCalled = true;
                     }
diff --git a/tests/src/com/android/tradefed/log/LogRegistryTest.java b/tests/src/com/android/tradefed/log/LogRegistryTest.java
index 5ddba9c..b43ff87 100644
--- a/tests/src/com/android/tradefed/log/LogRegistryTest.java
+++ b/tests/src/com/android/tradefed/log/LogRegistryTest.java
@@ -18,28 +18,23 @@
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
 
+import junit.framework.TestCase;
+
 import org.easymock.EasyMock;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
-import java.util.List;
-
-/** Unit tests for {@link LogRegistry}. */
-@RunWith(JUnit4.class)
-public class LogRegistryTest {
+/**
+ * Unit tests for {@link LogRegistry}.
+ */
+public class LogRegistryTest extends TestCase {
 
     private static final String LOG_TAG = "LogRegistryTest";
-    private static final long MAX_HISTORY_SIZE = 5;
 
     private LogRegistry mLogRegistry;
     private ThreadGroup mStubThreadGroup;
 
-    @Before
-    public void setUp() throws Exception {
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
         mStubThreadGroup = new ThreadGroup("LogRegistryTest");
         mLogRegistry =
                 new LogRegistry() {
@@ -54,29 +49,24 @@
                     public void saveGlobalLog() {
                         // empty on purpose, avoid leaving logs that we can't clean.
                     }
-
-                    @Override
-                    long getMaxHistorySize() {
-                        return MAX_HISTORY_SIZE;
-                    }
                 };
     }
 
-    @After
-    public void tearDown() throws Exception {
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
         mLogRegistry.closeAndRemoveAllLogs();
     }
 
     /**
      * Tests that {@link LogRegistry#getLogger} returns the logger that was previously registered.
      */
-    @Test
     public void testGetLogger() {
         StdoutLogger stdoutLogger = new StdoutLogger();
         mLogRegistry.registerLogger(stdoutLogger);
 
         ILeveledLogOutput returnedLogger = mLogRegistry.getLogger();
-        Assert.assertEquals(stdoutLogger, returnedLogger);
+        assertEquals(stdoutLogger, returnedLogger);
         mLogRegistry.unregisterLogger();
     }
 
@@ -84,7 +74,6 @@
      * Tests that {@link LogRegistry#printLog} calls into the underlying logger's printLog method
      * when the logging level is appropriate for printing.
      */
-    @Test
     public void testPrintLog_sameLogLevel() {
         String testMessage = "This is a test message.";
         ILeveledLogOutput mockLogger = EasyMock.createMock(ILeveledLogOutput.class);
@@ -102,7 +91,6 @@
      * Tests that {@link LogRegistry#printLog} does not call into the underlying logger's printLog
      * method when the logging level is lower than necessary to log.
      */
-    @Test
     public void testPrintLog_lowerLogLevel() {
         String testMessage = "This is a test message.";
         ILeveledLogOutput mockLogger = EasyMock.createMock(ILeveledLogOutput.class);
@@ -117,10 +105,9 @@
     }
 
     /**
-     * Tests for ensuring new threads spawned without an explicit ThreadGroup will inherit the same
-     * logger as the parent's logger.
+     * Tests for ensuring new threads spawned without an explicit ThreadGroup will inherit the
+     * same logger as the parent's logger.
      */
-    @Test
     public void testThreadedLogging() {
         final String testMessage = "Another test message!";
         final ILeveledLogOutput mockLogger = EasyMock.createMock(ILeveledLogOutput.class);
@@ -144,7 +131,7 @@
                     secondThread.join();  // threaded, but force serialization for testing
                 }
                 catch (InterruptedException ie) {
-                    Assert.fail("Thread was unexpectedly interrupted.");
+                    fail("Thread was unexpectedly interrupted.");
                 }
                 finally {
                     mLogRegistry.unregisterLogger();
@@ -169,35 +156,7 @@
             firstThread.join();  // threaded, but force serialization for testing
         }
         catch (InterruptedException ie) {
-            Assert.fail("Thread was unexpectedly interrupted.");
+            fail("Thread was unexpectedly interrupted.");
         }
     }
-
-    /** Tests {@link LogRegistry#getLogEntriesAfter}. */
-    @Test
-    public void testGetLogEntriesAfter() {
-        mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, "message1");
-        mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, "message2");
-        List<LogEntry> logs = mLogRegistry.getLogEntriesAfter(null);
-        Assert.assertEquals(2, logs.size());
-        Assert.assertEquals("message1", logs.get(0).getMessage());
-        Assert.assertEquals("message2", logs.get(1).getMessage());
-        mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, "message3");
-        mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, "message4");
-        logs = mLogRegistry.getLogEntriesAfter(logs.get(1));
-        Assert.assertEquals(2, logs.size());
-        Assert.assertEquals("message3", logs.get(0).getMessage());
-        Assert.assertEquals("message4", logs.get(1).getMessage());
-    }
-
-    /** Tests {@link LogRegistry#getLogEntriesAfter} will not store more than limit. */
-    @Test
-    public void testGetLogEntriesAfter_exceedLimit() {
-        for (int i = 0; i < MAX_HISTORY_SIZE * 2; ++i) {
-            mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, String.format("message-%d", i));
-        }
-        List<LogEntry> logs = mLogRegistry.getLogEntriesAfter(null);
-        Assert.assertEquals(MAX_HISTORY_SIZE, logs.size());
-        Assert.assertEquals("message-9", logs.get(logs.size() - 1).getMessage());
-    }
 }
diff --git a/tests/src/com/android/tradefed/log/LogUtilFuncTest.java b/tests/src/com/android/tradefed/log/LogUtilFuncTest.java
index 136d23c..44d3a75 100644
--- a/tests/src/com/android/tradefed/log/LogUtilFuncTest.java
+++ b/tests/src/com/android/tradefed/log/LogUtilFuncTest.java
@@ -18,14 +18,10 @@
 
 import com.android.ddmlib.Log;
 import com.android.ddmlib.Log.LogLevel;
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.IGlobalConfiguration;
 import com.android.tradefed.log.LogUtil.CLog;
 
 import junit.framework.TestCase;
 
-import org.easymock.EasyMock;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -36,14 +32,6 @@
 public class LogUtilFuncTest extends TestCase {
     private static final String CLASS_NAME = "LogUtilFuncTest";
     private static final String STRING = "hallo!";
-    private ITerribleFailureHandler mMockWtfHandler;
-    private IGlobalConfiguration mMockGlobalConfig;
-
-    @Override
-    protected void setUp() throws Exception, ConfigurationException {
-        mMockWtfHandler = EasyMock.createMock(ITerribleFailureHandler.class);
-        mMockGlobalConfig = EasyMock.createMock(IGlobalConfiguration.class);
-    }
 
     public void testCLog_v() {
         Log.v(CLASS_NAME, "this is the real Log.v");
@@ -76,47 +64,6 @@
     }
 
     /**
-     * Verify that all variants of calling CLog.wtf() results in a wtf handler being called
-     */
-    public void testCLog_wtf() {
-        // force CLog.wtf to use mock version of Global Config instead,
-        // and set getWtfHandler() to return a mock wtf handler
-        CLog.setGlobalConfigInstance(mMockGlobalConfig);
-        EasyMock.expect(mMockGlobalConfig.getWtfHandler()).andReturn(mMockWtfHandler).anyTimes();
-        EasyMock.replay(mMockGlobalConfig);
-
-        // expect onTerribleFailure to be called once per CLog.wtf() call
-        EasyMock.expect(mMockWtfHandler.onTerribleFailure(EasyMock.<String> anyObject(),
-                EasyMock.<Throwable> anyObject())).andReturn(true).times(4);
-        EasyMock.replay(mMockWtfHandler);
-
-        CLog.wtf("this is CLog.wtf");
-        CLog.wtf(new Throwable("this is CLog.wtf as a throwable"));
-        CLog.wtf("this is CLog.wtf with a format string: %s has length %d",
-                STRING, STRING.length());
-        CLog.wtf("this is CLog.wtf with a throwable", new Throwable("this is my throwable"));
-        EasyMock.verify(mMockWtfHandler);
-    }
-
-    /**
-     * Verify scenario where no wtf handler has been configured when CLog.wtf() is called
-     */
-    public void testCLog_wtf_wtfHandlerNotSet() {
-        // force CLog.wtf to use mock version of Global Config instead,
-        // and set getWtfHandler() to return null (simulating no wtf handler being set)
-        CLog.setGlobalConfigInstance(mMockGlobalConfig);
-        EasyMock.expect(mMockGlobalConfig.getWtfHandler()).andReturn(null);
-        EasyMock.replay(mMockGlobalConfig);
-
-        // by not setting any expect on mMockWtfHandler,
-        // EasyMock will verify that 0 calls are made to wtf handler
-        EasyMock.replay(mMockWtfHandler);
-
-        CLog.wtf("this is CLog.wtf without any handler set");
-        EasyMock.verify(mMockWtfHandler);
-    }
-
-    /**
      * Verify that getClassName can get the desired class name from the stack trace.
      */
     public void testCLog_getClassName() {
diff --git a/tests/src/com/android/tradefed/log/SimpleFileLoggerTest.java b/tests/src/com/android/tradefed/log/SimpleFileLoggerTest.java
new file mode 100644
index 0000000..b6ae74c
--- /dev/null
+++ b/tests/src/com/android/tradefed/log/SimpleFileLoggerTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.log;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.junit.Assert.assertTrue;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Unit tests for {@link SimpleFileLogger}. */
+@RunWith(JUnit4.class)
+public class SimpleFileLoggerTest {
+
+    private static final String LOG_TAG = SimpleFileLoggerTest.class.getSimpleName();
+
+    private File mLogFile;
+    private SimpleFileLogger mLogger;
+
+    @Before
+    public void setUp() throws IOException, ConfigurationException {
+        mLogFile = Files.createTempFile(LOG_TAG, null).toFile();
+        mLogger = new SimpleFileLogger();
+        OptionSetter setter = new OptionSetter(mLogger);
+        setter.setOptionValue("path", mLogFile.getAbsolutePath());
+    }
+
+    @After
+    public void tearDown() {
+        if (mLogger != null) {
+            mLogger.closeLog();
+        }
+        FileUtil.deleteFile(mLogFile);
+    }
+
+    @Test
+    public void testPrintLog() throws IOException {
+        mLogger.init();
+        mLogger.printLog(LogLevel.DEBUG, LOG_TAG, "debug");
+        mLogger.printLog(LogLevel.INFO, LOG_TAG, "info");
+        mLogger.printLog(LogLevel.WARN, LOG_TAG, "warn");
+        mLogger.printLog(LogLevel.ERROR, LOG_TAG, "error");
+
+        List<String> lines = readLines(new FileInputStream(mLogFile));
+        assertEquals(4, lines.size());
+        assertThat(lines.get(0), endsWith(String.format("D/%s: %s", LOG_TAG, "debug")));
+        assertThat(lines.get(1), endsWith(String.format("I/%s: %s", LOG_TAG, "info")));
+        assertThat(lines.get(2), endsWith(String.format("W/%s: %s", LOG_TAG, "warn")));
+        assertThat(lines.get(3), endsWith(String.format("E/%s: %s", LOG_TAG, "error")));
+    }
+
+    @Test
+    public void testGetLog() throws IOException {
+        mLogger.init();
+        mLogger.printLog(LogLevel.INFO, LOG_TAG, "hello world");
+        // getting the log is equivalent to reading the file
+        List<String> expected = readLines(new FileInputStream(mLogFile));
+        List<String> actual = readLines(mLogger.getLog().createInputStream());
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void testInit() throws IOException {
+        // logging is ignored if not initialized
+        mLogger.printLog(LogLevel.INFO, LOG_TAG, "hello world");
+        List<String> lines = readLines(new FileInputStream(mLogFile));
+        assertTrue(lines.isEmpty());
+    }
+
+    @Test
+    public void testCloseLog() throws IOException {
+        mLogger.init();
+        mLogger.closeLog();
+        // logging is ignored if closed
+        mLogger.printLog(LogLevel.INFO, LOG_TAG, "hello world");
+        List<String> lines = readLines(new FileInputStream(mLogFile));
+        assertTrue(lines.isEmpty());
+    }
+
+    @Test
+    public void testClone() throws IOException {
+        mLogger.init();
+        mLogger.printLog(LogLevel.DEBUG, LOG_TAG, "original");
+
+        // clone will append to same log file
+        SimpleFileLogger clone = mLogger.clone();
+        try {
+            clone.init();
+            clone.printLog(LogLevel.DEBUG, LOG_TAG, "clone");
+        } finally {
+            clone.closeLog();
+        }
+
+        List<String> lines = readLines(new FileInputStream(mLogFile));
+        assertEquals(2, lines.size());
+        assertThat(lines.get(0), endsWith("original"));
+        assertThat(lines.get(1), endsWith("clone"));
+    }
+
+    // Read input stream as a list of strings.
+    private static List<String> readLines(InputStream is) {
+        return new BufferedReader(new InputStreamReader(is)).lines().collect(Collectors.toList());
+    }
+}
diff --git a/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java b/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java
index 384b04a..8b37194 100644
--- a/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java
+++ b/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java
@@ -45,6 +45,7 @@
     private static final String STATS_KEY_VAR = "var";
     private static final String STATS_KEY_STDEV = "stdev";
     private static final String STATS_KEY_MEDIAN = "median";
+    private static final String STATS_KEY_TOTAL = "total";
     // Separator for final upload
     private static final String STATS_KEY_SEPARATOR = "-";
 
@@ -71,6 +72,7 @@
         singularDoubleStats.put(STATS_KEY_VAR, "0.54");
         singularDoubleStats.put(STATS_KEY_STDEV, "0.73");
         singularDoubleStats.put(STATS_KEY_MEDIAN, "2.00");
+        singularDoubleStats.put(STATS_KEY_TOTAL, "6.00");
 
         // Construct ListMultimap of multiple iterations of test metrics.
         // Stores processed metrics which is overwitten with every test; this is consistent with
@@ -146,6 +148,16 @@
                         .build()
                         .getMeasurements()
                         .getSingleString());
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, singularDoubleKey, STATS_KEY_TOTAL)));
+        Assert.assertEquals(
+                singularDoubleStats.get(STATS_KEY_TOTAL),
+                processedMetrics
+                        .get(String.join(STATS_KEY_SEPARATOR, singularDoubleKey, STATS_KEY_TOTAL))
+                        .build()
+                        .getMeasurements()
+                        .getSingleString());
     }
 
     /** Test correct aggregation of list double metrics. */
@@ -162,6 +174,7 @@
         listDoubleStats.put(STATS_KEY_VAR, "0.36");
         listDoubleStats.put(STATS_KEY_STDEV, "0.60");
         listDoubleStats.put(STATS_KEY_MEDIAN, "2.05");
+        listDoubleStats.put(STATS_KEY_TOTAL, "12.10");
 
         // Stores processed metrics which is overwitten with every test; this is consistent with
         // the current reporting behavior. We only test the correctness on the final metrics values.
@@ -233,6 +246,16 @@
                         .build()
                         .getMeasurements()
                         .getSingleString());
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        listDoubleKey + STATS_KEY_SEPARATOR + STATS_KEY_TOTAL));
+        Assert.assertEquals(
+                listDoubleStats.get(STATS_KEY_TOTAL),
+                processedMetrics
+                        .get(listDoubleKey + STATS_KEY_SEPARATOR + STATS_KEY_TOTAL)
+                        .build()
+                        .getMeasurements()
+                        .getSingleString());
     }
 
 
@@ -274,6 +297,9 @@
         Assert.assertFalse(
                 processedMetrics.containsKey(
                         String.join(STATS_KEY_SEPARATOR, nonNumericKey, STATS_KEY_MEDIAN)));
+        Assert.assertFalse(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, nonNumericKey, STATS_KEY_TOTAL)));
     }
 
     /** Test empty result. */
@@ -312,6 +338,9 @@
         Assert.assertFalse(
                 processedMetrics.containsKey(
                         String.join(STATS_KEY_SEPARATOR, emptyResultKey, STATS_KEY_MEDIAN)));
+        Assert.assertFalse(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, emptyResultKey, STATS_KEY_TOTAL)));
     }
 
     /** Test single run. */
@@ -392,6 +421,16 @@
                         .build()
                         .getMeasurements()
                         .getSingleString());
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, singleRunKey, STATS_KEY_TOTAL)));
+        Assert.assertEquals(
+                singleRunVal,
+                processedMetrics
+                        .get(String.join(STATS_KEY_SEPARATOR, singleRunKey, STATS_KEY_TOTAL))
+                        .build()
+                        .getMeasurements()
+                        .getSingleString());
     }
 
 
@@ -430,6 +469,9 @@
         Assert.assertTrue(
                 processedMetrics.containsKey(
                         String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_MEDIAN)));
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_TOTAL)));
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/result/LegacySubprocessResultsReporterTest.java b/tests/src/com/android/tradefed/result/LegacySubprocessResultsReporterTest.java
index 5c4854f..0b18e11 100644
--- a/tests/src/com/android/tradefed/result/LegacySubprocessResultsReporterTest.java
+++ b/tests/src/com/android/tradefed/result/LegacySubprocessResultsReporterTest.java
@@ -105,7 +105,8 @@
             Map<String, String> map = new HashMap<>();
             map.put("key1", "value1");
             map.put("key2", "value2");
-            mMockListener.testRunStarted("test run", 2);
+            mMockListener.testRunStarted(
+                    EasyMock.eq("test run"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
             mMockListener.testRunEnded(50, TfMetricProtoUtil.upgradeConvert(map));
             EasyMock.replay(mMockListener);
             mReporter.testRunStarted("test run", 2);
diff --git a/tests/src/com/android/tradefed/result/SubprocessResultsReporterTest.java b/tests/src/com/android/tradefed/result/SubprocessResultsReporterTest.java
index 5fe32ca..1c2ea0d 100644
--- a/tests/src/com/android/tradefed/result/SubprocessResultsReporterTest.java
+++ b/tests/src/com/android/tradefed/result/SubprocessResultsReporterTest.java
@@ -69,13 +69,15 @@
         File tmpReportFile = FileUtil.createTempFile("subprocess-reporter", "unittest");
         try {
             setter.setOptionValue("subprocess-report-file", tmpReportFile.getAbsolutePath());
-            mReporter.testRunStarted("TEST", 5);
+            mReporter.testRunStarted("TEST", 5, 1, 500);
             mReporter.testRunEnded(100, new HashMap<String, Metric>());
             String content = FileUtil.readStringFromFile(tmpReportFile);
-            assertEquals(
-                    "TEST_RUN_STARTED {\"testCount\":5,\"runName\":\"TEST\",\"runAttempt\":0}\n"
-                            + "TEST_RUN_ENDED {\"time\":100}\n",
-                    content);
+            assertTrue(content.contains("TEST_RUN_STARTED"));
+            assertTrue(content.contains("\"testCount\":5"));
+            assertTrue(content.contains("\"runName\":\"TEST\""));
+            assertTrue(content.contains("\"start_time\":500"));
+            assertTrue(content.contains("\"runAttempt\":1"));
+            assertTrue(content.contains("TEST_RUN_ENDED {\"time\":100}\n"));
         } finally {
             FileUtil.deleteFile(tmpReportFile);
         }
diff --git a/tests/src/com/android/tradefed/result/TestResultTest.java b/tests/src/com/android/tradefed/result/TestResultTest.java
index 404c071..cb971e2 100644
--- a/tests/src/com/android/tradefed/result/TestResultTest.java
+++ b/tests/src/com/android/tradefed/result/TestResultTest.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.tradefed.testtype.retry.MergeStrategy;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/tests/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParserTest.java b/tests/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParserTest.java
new file mode 100644
index 0000000..f154fc5
--- /dev/null
+++ b/tests/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParserTest.java
@@ -0,0 +1,1096 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result.ddmlib;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.commands.am.InstrumentationData.ResultsBundle;
+import com.android.commands.am.InstrumentationData.ResultsBundleEntry;
+import com.android.commands.am.InstrumentationData.Session;
+import com.android.commands.am.InstrumentationData.SessionStatus;
+import com.android.commands.am.InstrumentationData.SessionStatusCode;
+import com.android.commands.am.InstrumentationData.TestStatus;
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.InstrumentationResultParser;
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/** Unit tests for {@link InstrumentationResultProtoParser}. */
+@RunWith(JUnit4.class)
+public class InstrumentationResultProtoParserTest {
+
+    private InstrumentationResultProtoParser mParser;
+    private ITestRunListener mMockListener;
+
+    private static final String RUN_KEY = "testing";
+    private static final String CLASS_NAME_1 = "class_1";
+    private static final String METHOD_NAME_1 = "method_1";
+    private static final String CLASS_NAME_2 = "class_2";
+    private static final String METHOD_NAME_2 = "method_2";
+    private static final String TEST_FAILURE_MESSAGE_1 = "java.lang.AssertionError: No App";
+    private static final String RUN_FAILURE_MESSAGE = "Unable to find instrumentation info:";
+    private static final String TEST_COMPLETED_STATUS_1 = "Expected 2 tests, received 0";
+    private static final String TEST_COMPLETED_STATUS_2 = "Expected 2 tests, received 1";
+    private static final String INCOMPLETE_TEST_ERR_MSG_PREFIX = "Test failed to run"
+            + " to completion";
+    private static final String INCOMPLETE_RUN_ERR_MSG_PREFIX = "Test run failed to complete";
+    private static final String FATAL_EXCEPTION_MSG = "Fatal exception when running tests";
+
+    private File protoTestFile = null;
+
+    @Before
+    public void setUp() {
+        List<ITestRunListener> runListeners = new ArrayList<>();
+        mMockListener = EasyMock.createStrictMock(ITestRunListener.class);
+        runListeners.add(mMockListener);
+        mParser = new InstrumentationResultProtoParser(RUN_KEY, runListeners);
+    }
+
+    // Sample one test success instrumentation proto file in a test run.
+
+    // result_code: 1
+    // results {
+    // entries {
+    // key: "class"
+    // value_string: "android.platform.test.scenario.clock.OpenAppMicrobenchmark"
+    // }
+    // entries {
+    // key: "current"
+    // value_int: 1
+    // }
+    // entries {
+    // key: "id"
+    // value_string: "AndroidJUnitRunner"
+    // }
+    // entries {
+    // key: "numtests"
+    // value_int: 1
+    // }
+    // entries {
+    // key: "stream"
+    // value_string: "\nandroid.platform.test.scenario.clock.OpenAppMicrobenchmark:"
+    // }
+    // entries {
+    // key: "test"
+    // value_string: "testOpen"
+    // }
+    // }
+    // result_code: 2
+    // results {
+    // entries {
+    // key: "cold_startup_com.google.android.deskclock"
+    // value_string: "626"
+    // }
+    // }
+    //
+    // results {
+    // entries {
+    // key: "class"
+    // value_string: "android.platform.test.scenario.clock.OpenAppMicrobenchmark"
+    // }
+    // entries {
+    // key: "current"
+    // value_int: 1
+    // }
+    // entries {
+    // key: "id"
+    // value_string: "AndroidJUnitRunner"
+    // }
+    // entries {
+    // key: "numtests"
+    // value_int: 1
+    // }
+    // entries {
+    // key: "stream"
+    // value_string: "."
+    // }
+    // entries {
+    // key: "test"
+    // value_string: "testOpen"
+    // }
+    // }
+    //
+    // result_code: -1
+    // results {
+    // entries {
+    // key: "stream"
+    // value_string: "\n\nTime: 27.013\n\nOK (1 test)\n\n"
+    // }
+    // entries {
+    // key: "total_cpu_usage"
+    // value_string: "39584"
+    // }
+    // }
+
+    /**
+     * Test for the null input instrumentation results proto file.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testNullProtoFile() throws IOException {
+        protoTestFile = null;
+        mMockListener.testRunStarted(RUN_KEY, 0);
+        mMockListener.testRunFailed(EasyMock
+                .eq(InstrumentationResultProtoParser.NO_TEST_RESULTS_FILE));
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+    }
+
+    /**
+     * Test for the empty input instrumentation results proto file.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testEmptyProtoFile() throws IOException {
+        protoTestFile = File.createTempFile("tmp", ".pb");
+        mMockListener.testRunStarted(RUN_KEY, 0);
+        mMockListener.testRunFailed(EasyMock
+                .eq(InstrumentationResultProtoParser.NO_TEST_RESULTS_MSG));
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+    }
+
+    /**
+     * Test for the invalid input instrumentation results proto file.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testInvalidResultsProtoFile() throws IOException {
+        protoTestFile = File.createTempFile("tmp", ".pb");
+        FileOutputStream fout = new FileOutputStream(protoTestFile);
+        fout.write(65);
+        fout.close();
+        mMockListener.testRunStarted(RUN_KEY, 0);
+        mMockListener.testRunFailed(EasyMock
+                .eq(InstrumentationResultProtoParser.INVALID_TEST_RESULTS_FILE));
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+    }
+
+    /**
+     * Test for the no test results in input instrumentation results proto file.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testNoTestResults() throws IOException {
+
+        protoTestFile = buildNoTestResultsProtoFile();
+
+        mMockListener.testRunStarted(RUN_KEY, 0);
+        mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+    }
+
+    /**
+     * Test for one test success results in input instrumentation results proto file.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testOneTestSuccessWithMetrics() throws IOException {
+        protoTestFile = buildSingleTestMetricSuccessProtoFile();
+
+        TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        Capture<Map<String, String>> captureTestMetrics = new Capture<Map<String, String>>();
+
+        mMockListener.testRunStarted(RUN_KEY, 1);
+        mMockListener.testStarted(td);
+        mMockListener.testEnded(EasyMock.eq(td), EasyMock.capture(captureTestMetrics));
+        mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+
+        // Verify the test metrics
+        assertEquals("626",
+                captureTestMetrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureTestMetrics.getValue()
+                        .get("metric_key2"));
+    }
+
+    /**
+     * Test for one test success result with multiple listeners in instrumentation results proto
+     * file.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testOneTestSuccessWithMultipleListeners() throws IOException {
+
+        List<ITestRunListener> runListeners = new ArrayList<>();
+        ITestRunListener mMockListener1 = EasyMock.createStrictMock(ITestRunListener.class);
+        ITestRunListener mMockListener2 = EasyMock.createStrictMock(ITestRunListener.class);
+        runListeners.add(mMockListener1);
+        runListeners.add(mMockListener2);
+
+        mParser = new InstrumentationResultProtoParser(RUN_KEY, runListeners);
+
+        protoTestFile = buildSingleTestMetricSuccessProtoFile();
+
+        TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        Capture<Map<String, String>> captureListener1Metrics = new Capture<Map<String, String>>();
+        Capture<Map<String, String>> captureListener2Metrics = new Capture<Map<String, String>>();
+
+        mMockListener1.testRunStarted(RUN_KEY, 1);
+        mMockListener1.testStarted(td);
+        mMockListener1.testEnded(EasyMock.eq(td), EasyMock.capture(captureListener1Metrics));
+        mMockListener1.testRunEnded(27013, Collections.emptyMap());
+
+        mMockListener2.testRunStarted(RUN_KEY, 1);
+        mMockListener2.testStarted(td);
+        mMockListener2.testEnded(EasyMock.eq(td), EasyMock.capture(captureListener2Metrics));
+        mMockListener2.testRunEnded(27013, Collections.emptyMap());
+
+        EasyMock.replay(mMockListener1);
+        EasyMock.replay(mMockListener2);
+        mParser.processProtoFile(protoTestFile);
+        EasyMock.verify(mMockListener1);
+        EasyMock.verify(mMockListener2);
+
+        // Verify the test metrics
+        assertEquals("626",
+                captureListener1Metrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureListener1Metrics.getValue()
+                        .get("metric_key2"));
+
+        // Verify the test metrics
+        assertEquals("626",
+                captureListener2Metrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureListener2Metrics.getValue()
+                        .get("metric_key2"));
+    }
+
+    /**
+     * Test for test run with the metrics.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testOneRunSuccessWithMetrics() throws IOException {
+        protoTestFile = buildRunMetricSuccessProtoFile();
+
+        TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        Capture<Map<String, String>> captureRunMetrics = new Capture<Map<String, String>>();
+        mMockListener.testRunStarted(RUN_KEY, 1);
+        mMockListener.testStarted(td);
+        mMockListener.testEnded(td, Collections.emptyMap());
+        mMockListener.testRunEnded(EasyMock.eq(27013L), EasyMock.capture(captureRunMetrics));
+
+        processProtoAndVerify(protoTestFile);
+
+        // Verify run metrics
+        assertEquals("39584",
+                captureRunMetrics.getValue().get("run_metric_key"));
+    }
+
+    /**
+     * Test for test metrics and test run metrics in instrumentation proto file.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testOneTestAndRunSuccessWithMetrics() throws IOException {
+        protoTestFile = buildTestAndRunMetricSuccessProtoFile();
+
+        TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        Capture<Map<String, String>> captureTestMetrics = new Capture<Map<String, String>>();
+        Capture<Map<String, String>> captureRunMetrics = new Capture<Map<String, String>>();
+        mMockListener.testRunStarted(RUN_KEY, 1);
+        mMockListener.testStarted(td);
+        mMockListener.testEnded(EasyMock.eq(td), EasyMock.capture(captureTestMetrics));
+        mMockListener.testRunEnded(EasyMock.eq(27013L), EasyMock.capture(captureRunMetrics));
+
+        processProtoAndVerify(protoTestFile);
+
+        // Verify the test metrics
+        assertEquals("626",
+                captureTestMetrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureTestMetrics.getValue()
+                        .get("metric_key2"));
+
+        // Verify run metrics
+        assertEquals("39584",
+                captureRunMetrics.getValue().get("run_metric_key"));
+    }
+
+    /**
+     * Test for multiple test success with metrics.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testMultipleTestSuccessWithMetrics() throws IOException {
+        protoTestFile = buildMultipleTestAndRunMetricSuccessProtoFile();
+
+        TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        TestIdentifier td2 = new TestIdentifier(CLASS_NAME_2, METHOD_NAME_2);
+
+        Capture<Map<String, String>> captureTest1Metrics = new Capture<Map<String, String>>();
+        Capture<Map<String, String>> captureTest2Metrics = new Capture<Map<String, String>>();
+        Capture<Map<String, String>> captureRunMetrics = new Capture<Map<String, String>>();
+
+        mMockListener.testRunStarted(RUN_KEY, 2);
+        mMockListener.testStarted(td1);
+        mMockListener.testEnded(EasyMock.eq(td1), EasyMock.capture(captureTest1Metrics));
+        mMockListener.testStarted(td2);
+        mMockListener.testEnded(EasyMock.eq(td2), EasyMock.capture(captureTest2Metrics));
+        mMockListener.testRunEnded(EasyMock.eq(27013L), EasyMock.capture(captureRunMetrics));
+
+        processProtoAndVerify(protoTestFile);
+
+        // Verify the test1 and test2 metrics
+        assertEquals("626",
+                captureTest1Metrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureTest1Metrics.getValue()
+                        .get("metric_key2"));
+        assertEquals("626",
+                captureTest2Metrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureTest2Metrics.getValue()
+                        .get("metric_key2"));
+
+        // Verify run metrics
+        assertEquals("39584",
+                captureRunMetrics.getValue().get("run_metric_key"));
+    }
+
+    /**
+     * Test for one test failure.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testOneTestFailure() throws IOException {
+        protoTestFile = buildSingleTestFailureProtoFile();
+
+        TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        Capture<Map<String, String>> captureTestMetrics = new Capture<Map<String, String>>();
+
+        mMockListener.testRunStarted(RUN_KEY, 1);
+        mMockListener.testStarted(td);
+        mMockListener.testFailed(EasyMock.eq(td), EasyMock.eq(TEST_FAILURE_MESSAGE_1));
+        mMockListener.testEnded(EasyMock.eq(td), EasyMock.capture(captureTestMetrics));
+        mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+
+        // Verify the test metrics
+        assertEquals("626",
+                captureTestMetrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureTestMetrics.getValue()
+                        .get("metric_key2"));
+    }
+
+    /**
+     * Test for one test pass and one test failure.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testOneTestPassOneTestFailure() throws IOException {
+        protoTestFile = buildOneTestPassOneTestFailProtoFile();
+
+        TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        TestIdentifier td2 = new TestIdentifier(CLASS_NAME_2, METHOD_NAME_2);
+
+        Capture<Map<String, String>> captureTest1Metrics = new Capture<Map<String, String>>();
+
+        mMockListener.testRunStarted(RUN_KEY, 2);
+        mMockListener.testStarted(td1);
+        mMockListener.testEnded(EasyMock.eq(td1), EasyMock.capture(captureTest1Metrics));
+
+        mMockListener.testStarted(td2);
+        mMockListener.testFailed(EasyMock.eq(td2), EasyMock.eq(TEST_FAILURE_MESSAGE_1));
+        mMockListener.testEnded(td2, Collections.emptyMap());
+
+        mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+
+        // Verify the test metrics
+        assertEquals("626",
+                captureTest1Metrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureTest1Metrics.getValue()
+                        .get("metric_key2"));
+    }
+
+    /**
+     * Test for all tests incomplete in a test run.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testAllTestsIncomplete() throws IOException {
+        protoTestFile = buildTestsIncompleteProtoFile();
+        Capture<String> testOutputErrorMessage = new Capture<>();
+        Capture<String> runOutputErrorMessage = new Capture<>();
+
+        TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        mMockListener.testRunStarted(RUN_KEY, 2);
+        mMockListener.testStarted(td1);
+        mMockListener.testFailed(EasyMock.eq(td1), EasyMock.capture(testOutputErrorMessage));
+        mMockListener.testEnded(td1, Collections.emptyMap());
+        mMockListener.testRunFailed(EasyMock.capture(runOutputErrorMessage));
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+
+        assertTrue(testOutputErrorMessage.toString().contains(
+                INCOMPLETE_TEST_ERR_MSG_PREFIX));
+        assertTrue(testOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_1));
+        assertTrue(runOutputErrorMessage.toString().contains(
+                INCOMPLETE_RUN_ERR_MSG_PREFIX));
+        assertTrue(runOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_1));
+    }
+
+    /**
+     * Test for one test complete and another test partial status.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testPartialTestsIncomplete() throws IOException {
+        protoTestFile = buildPartialTestsIncompleteProtoFile();
+
+        Capture<String> testOutputErrorMessage = new Capture<>();
+        Capture<String> runOutputErrorMessage = new Capture<>();
+        TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        TestIdentifier td2 = new TestIdentifier(CLASS_NAME_2, METHOD_NAME_2);
+        Capture<Map<String, String>> captureTest1Metrics = new Capture<Map<String, String>>();
+
+        mMockListener.testRunStarted(RUN_KEY, 2);
+        mMockListener.testStarted(td1);
+        mMockListener.testEnded(EasyMock.eq(td1), EasyMock.capture(captureTest1Metrics));
+        mMockListener.testStarted(td2);
+        mMockListener.testFailed(EasyMock.eq(td2), EasyMock.capture(testOutputErrorMessage));
+        mMockListener.testEnded(td2, Collections.emptyMap());
+        mMockListener.testRunFailed(EasyMock.capture(runOutputErrorMessage));
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+
+        assertEquals("626",
+                captureTest1Metrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureTest1Metrics.getValue()
+                        .get("metric_key2"));
+        assertTrue(testOutputErrorMessage.toString().contains(
+                INCOMPLETE_TEST_ERR_MSG_PREFIX));
+        assertTrue(testOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_2));
+        assertTrue(runOutputErrorMessage.toString().contains(
+                INCOMPLETE_RUN_ERR_MSG_PREFIX));
+        assertTrue(runOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_2));
+    }
+
+    /**
+     * Test 1 test completed, 1 test not started from two expected tests in a test run.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testOneTestNotStarted() throws IOException {
+        protoTestFile = buildOneTestNotStarted();
+        Capture<String> runOutputErrorMessage = new Capture<>();
+        TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        Capture<Map<String, String>> captureTest1Metrics = new Capture<Map<String, String>>();
+
+        mMockListener.testRunStarted(RUN_KEY, 2);
+        mMockListener.testStarted(td1);
+        mMockListener.testEnded(EasyMock.eq(td1), EasyMock.capture(captureTest1Metrics));
+        mMockListener.testRunFailed(EasyMock.capture(runOutputErrorMessage));
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+
+        assertEquals("626",
+                captureTest1Metrics.getValue().get("metric_key1"));
+        assertEquals("1",
+                captureTest1Metrics.getValue()
+                        .get("metric_key2"));
+        assertTrue(runOutputErrorMessage.toString().contains(
+                INCOMPLETE_RUN_ERR_MSG_PREFIX));
+        assertTrue(runOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_2));
+    }
+
+    /**
+     * Test for time stamp missing when the time stamp parsing is enforced.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testTimeStampMissing() throws IOException {
+        // we enforce the time stamp
+        mParser.setEnforceTimeStamp(true);
+        protoTestFile = buildInvalidTimeStampResultsProto(false);
+
+        mMockListener.testRunStarted(RUN_KEY, 0);
+        mMockListener.testRunFailed(InstrumentationResultParser.INVALID_OUTPUT_ERR_MSG);
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+    }
+
+    /**
+     * Test for no time stamp parsing error when the time stamp parsing is not enforced.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testTimeStampMissingNotEnforced() throws IOException {
+        protoTestFile = buildInvalidTimeStampResultsProto(false);
+
+        mMockListener.testRunStarted(RUN_KEY, 0);
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+    }
+
+    /**
+     * Test for time stamp missing with the stack message when the time stamp parsing is enforced.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testMissingTimeStampWithStack() throws IOException {
+        mParser.setEnforceTimeStamp(true);
+        protoTestFile = buildInvalidTimeStampResultsProto(true);
+
+        Capture<String> capture = new Capture<>();
+        mMockListener.testRunStarted(RUN_KEY, 0);
+        mMockListener.testRunFailed(EasyMock.capture(capture));
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+
+        String failure = capture.getValue();
+        assertTrue(failure.startsWith(InstrumentationResultParser.INVALID_OUTPUT_ERR_MSG));
+        assertTrue(failure.contains(FATAL_EXCEPTION_MSG));
+    }
+
+    /**
+     * Tests parsing the fatal error output of an instrumentation invoked with "-e log true". Since
+     * it is log only, it will not report directly the failure, but the stream should still be
+     * populated.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testDirectFailure() throws IOException {
+
+        mParser.setEnforceTimeStamp(true);
+        protoTestFile = buildValidTimeStampWithFatalExceptionResultsProto();
+
+        Capture<String> capture = new Capture<>();
+        mMockListener.testRunStarted(RUN_KEY, 0);
+        mMockListener.testRunFailed(EasyMock.capture(capture));
+        mMockListener.testRunEnded(0, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+
+        String failure = capture.getValue();
+        assertTrue(failure.contains("java.lang.RuntimeException: it failed super fast."));
+    }
+
+    /**
+     * Tests for ignore test status from the proto output.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testIgnoreProtoResult() throws IOException {
+
+        mParser.setEnforceTimeStamp(true);
+        protoTestFile = buildTestIgnoredResultsProto();
+
+        mMockListener.testRunStarted(RUN_KEY, 1);
+        TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        mMockListener.testStarted(td1);
+        mMockListener.testIgnored(td1);
+        mMockListener.testEnded(td1, Collections.emptyMap());
+        mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+        processProtoAndVerify(protoTestFile);
+    }
+
+    /**
+     * Tests for assumption failure test status from the proto output.
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testAssumptionProtoResult() throws IOException {
+        mParser.setEnforceTimeStamp(true);
+        protoTestFile = buildTestAssumptionResultsProto();
+
+        mMockListener.testRunStarted(RUN_KEY, 1);
+        TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+        mMockListener.testStarted(td1);
+        mMockListener.testAssumptionFailure(EasyMock.eq(td1),
+                EasyMock.startsWith(
+                        "org.junit.AssumptionViolatedException:"
+                                + " got: <false>, expected: is <true>"));
+        mMockListener.testEnded(td1, Collections.emptyMap());
+        mMockListener.testRunEnded(27013, Collections.emptyMap());
+        processProtoAndVerify(protoTestFile);
+
+    }
+
+    @After
+    public void tearDown() {
+        if (protoTestFile != null && protoTestFile.exists()) {
+            protoTestFile.delete();
+        }
+    }
+
+    private void processProtoAndVerify(File protoTestFile) throws IOException {
+        EasyMock.replay(mMockListener);
+        mParser.processProtoFile(protoTestFile);
+        EasyMock.verify(mMockListener);
+    }
+
+    private File buildNoTestResultsProtoFile() throws IOException {
+        Session sessionProto = Session.newBuilder()
+                .setSessionStatus(getSessionStatusProto(false, false)).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildSingleTestMetricSuccessProtoFile() throws IOException {
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+        // Test Metric
+        testStatusList.add(getTestStatusProto(true));
+        // Test End
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, false, false));
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+                .setSessionStatus(getSessionStatusProto(false, false)).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildRunMetricSuccessProtoFile() throws IOException {
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+        // Test status without metrics.
+        testStatusList.add(getTestStatusProto(false));
+        // Test End.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, false, false));
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+                .setSessionStatus(getSessionStatusProto(true, false)).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildTestAndRunMetricSuccessProtoFile() throws IOException {
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+        // Test status without metrics.
+        testStatusList.add(getTestStatusProto(true));
+        // Test End.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, false, false));
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+                .setSessionStatus(getSessionStatusProto(true, false)).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildMultipleTestAndRunMetricSuccessProtoFile() throws IOException {
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+        // Test status without metrics.
+        testStatusList.add(getTestStatusProto(true));
+        // Test End.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, false, false));
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, true, false));
+        // Test status without metrics.
+        testStatusList.add(getTestStatusProto(true));
+        // Test End.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, false, false));
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+                .setSessionStatus(getSessionStatusProto(true, false)).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildSingleTestFailureProtoFile() throws IOException {
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+        // Test status without metrics.
+        testStatusList.add(getTestStatusProto(true));
+        // Test End.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, false, true));
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+                .setSessionStatus(getSessionStatusProto(false, false)).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildOneTestPassOneTestFailProtoFile() throws IOException {
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+        // Test status without metrics.
+        testStatusList.add(getTestStatusProto(true));
+        // Test End.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, false, false));
+        testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, true, false));
+        // Test status without metrics.
+        testStatusList.add(getTestStatusProto(false));
+        // Test End.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, false, true));
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+                .setSessionStatus(getSessionStatusProto(false, false)).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildTestsIncompleteProtoFile() throws IOException {
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+                .build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+
+    }
+
+    private File buildPartialTestsIncompleteProtoFile() throws IOException {
+
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+        // Test status without metrics.
+        testStatusList.add(getTestStatusProto(true));
+        // Test End.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, false, false));
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, true, false));
+
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildOneTestNotStarted() throws IOException {
+
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+        // Test status without metrics.
+        testStatusList.add(getTestStatusProto(true));
+        // Test End.
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, false, false));
+
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildInvalidTimeStampResultsProto(boolean isWithStack) throws IOException {
+
+        List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+
+        if (isWithStack) {
+            entryList.add(ResultsBundleEntry.newBuilder()
+                    .setKey("stream")
+                    .setValueString(FATAL_EXCEPTION_MSG
+                            + " java.lang.IllegalArgumentException: Ambiguous arguments: "
+                            + "cannot provide both test package and test class(es) to run")
+                    .build());
+        } else {
+            entryList.add(ResultsBundleEntry.newBuilder()
+                    .setKey("stream").setValueString("")
+                    .build());
+        }
+
+        SessionStatus sessionStatus = SessionStatus.newBuilder().setResultCode(-1)
+                .setStatusCode(SessionStatusCode.SESSION_FINISHED)
+                .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+                .build();
+
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder()
+                .setSessionStatus(sessionStatus).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildValidTimeStampWithFatalExceptionResultsProto() throws IOException {
+        List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+
+        entryList.add(ResultsBundleEntry.newBuilder()
+                .setKey("stream")
+                .setValueString(FATAL_EXCEPTION_MSG
+                        + "Time: 0 \n"
+                        + "1) Fatal exception when running tests"
+                        + "java.lang.RuntimeException: it failed super fast."
+                        + "at stackstack"
+                )
+                .build());
+
+        SessionStatus sessionStatus = SessionStatus.newBuilder().setResultCode(-1)
+                .setStatusCode(SessionStatusCode.SESSION_FINISHED)
+                .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+                .build();
+
+        // Session with metrics.
+        Session sessionProto = Session.newBuilder()
+                .setSessionStatus(sessionStatus).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildTestIgnoredResultsProto() throws IOException {
+
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+        // Test start
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+
+        // Test ignore status result.
+        List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("class").setValueString(CLASS_NAME_1)
+                .build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("current").setValueInt(1)
+                .build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("id")
+                .setValueString("AndroidJUnitRunner").build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("numtests").setValueInt(1)
+                .build());
+
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("test").setValueString(METHOD_NAME_1)
+                .build());
+
+        testStatusList.add(TestStatus.newBuilder().setResultCode(-3)
+                .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+                .build());
+
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+                .setSessionStatus(getSessionStatusProto(false, false)).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    private File buildTestAssumptionResultsProto() throws IOException {
+
+        List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+
+        // Test start
+        testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+
+        // Test ignore status result.
+        List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("class").setValueString(CLASS_NAME_1)
+                .build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("current").setValueInt(1)
+                .build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("id")
+                .setValueString("AndroidJUnitRunner").build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("numtests").setValueInt(1)
+                .build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("test").setValueString(METHOD_NAME_1)
+                .build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("stack").setValueString(
+                "org.junit.AssumptionViolatedException: got: <false>, expected: is <true>")
+                .build());
+        testStatusList.add(TestStatus.newBuilder().setResultCode(-4)
+                .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+                .build());
+
+        Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+                .setSessionStatus(getSessionStatusProto(false, false)).build();
+        File protoFile = File.createTempFile("tmp", ".pb");
+        sessionProto.writeTo(new FileOutputStream(protoFile));
+        return protoFile;
+    }
+
+    /**
+     * Add test status proto message based on the args supplied to this method.
+     *
+     * @param className class name where the test method is.
+     * @param methodName method name currently running.
+     * @param current current number of the test.
+     * @param numTests total number of test.
+     * @param isStart true is if it is start of the test otherwise treated as end of the test.
+     * @param isFailure true if the test if failed.
+     * @return
+     */
+    private TestStatus getTestInfoProto(String className, String methodName, int current,
+            int numTests, boolean isStart, boolean isFailure) {
+        List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("class").setValueString(className)
+                .build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("current").setValueInt(current)
+                .build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("id")
+                .setValueString("AndroidJUnitRunner").build());
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("numtests").setValueInt(numTests)
+                .build());
+
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("test").setValueString(methodName)
+                .build());
+
+        if (isFailure) {
+            entryList.add(ResultsBundleEntry.newBuilder().setKey("stack")
+                    .setValueString(TEST_FAILURE_MESSAGE_1)
+                    .build());
+            entryList.add(ResultsBundleEntry.newBuilder().setKey("stream")
+                    .setValueString(TEST_FAILURE_MESSAGE_1)
+                    .build());
+            // Test failure will have result code "-2"
+            return TestStatus.newBuilder().setResultCode(-2)
+                    .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+                    .build();
+        }
+
+        entryList.add(ResultsBundleEntry.newBuilder().setKey("stream").setValueString("\nabc:")
+                .build());
+
+        if (isStart) {
+            // Test start will have result code 1.
+            return TestStatus.newBuilder().setResultCode(1)
+                    .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+                    .build();
+        }
+
+        return TestStatus.newBuilder()
+                .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build()).build();
+    }
+
+    /**
+     * Add test status with the metrics in the proto result file.
+     *
+     * @param isWithMetrics if false metric will be ignored.
+     * @return
+     */
+    private TestStatus getTestStatusProto(boolean isWithMetrics) {
+        List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+        if (isWithMetrics) {
+            entryList.add(ResultsBundleEntry.newBuilder()
+                    .setKey("metric_key1").setValueString("626")
+                    .build());
+            entryList.add(ResultsBundleEntry.newBuilder()
+                    .setKey("metric_key2").setValueString("1")
+                    .build());
+        }
+
+        // Metric status will be in progress
+        return TestStatus.newBuilder().setResultCode(2)
+                .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+                .build();
+    }
+
+    /**
+     * Add session status message in the proto result file based on the args supplied to this
+     * method.
+     *
+     * @param isWithMetrics is true then add metrics to the session message.
+     * @param isFailure is true then failure message will be added to the final message.
+     * @return
+     */
+    private SessionStatus getSessionStatusProto(boolean isWithMetrics, boolean isFailure) {
+        List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+
+        if (isFailure) {
+            entryList.add(ResultsBundleEntry.newBuilder()
+                    .setKey("Error").setValueString(RUN_FAILURE_MESSAGE)
+                    .build());
+            entryList.add(ResultsBundleEntry.newBuilder()
+                    .setKey("id").setValueString("ActivityManagerService")
+                    .build());
+            return SessionStatus.newBuilder().setResultCode(-1)
+                    .setStatusCode(SessionStatusCode.SESSION_FINISHED)
+                    .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+                    .build();
+
+        }
+        entryList.add(ResultsBundleEntry.newBuilder()
+                .setKey("stream").setValueString("\n\nTime: 27.013\n\nOK (1 test)\n\n")
+                .build());
+
+        if (isWithMetrics) {
+            entryList.add(ResultsBundleEntry.newBuilder()
+                    .setKey("run_metric_key").setValueString("39584")
+                    .build());
+        }
+
+
+
+        return SessionStatus.newBuilder().setResultCode(-1)
+                .setStatusCode(SessionStatusCode.SESSION_FINISHED)
+                .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+                .build();
+    }
+}
diff --git a/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java b/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java
index db48f97..31e71d2 100644
--- a/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.fail;
 
 import com.android.tradefed.config.ConfigurationDescriptor;
+import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.proto.InvocationContext.Context;
@@ -93,9 +94,10 @@
 
     @Test
     public void testWriteResults_periodic() throws Exception {
+        OptionSetter setter = new OptionSetter(mReporter);
+        setter.setOptionValue("periodic-proto-writing", "true");
+        setter.setOptionValue("proto-output-file", mOutput.getAbsolutePath());
         TestDescription test1 = new TestDescription("class1", "test1");
-        mReporter.setPeriodicWriting(true);
-        mReporter.setFileOutput(mOutput);
         IInvocationContext context = new InvocationContext();
         context.setConfigurationDescriptor(new ConfigurationDescriptor());
         context.addInvocationAttribute("test", "test");
diff --git a/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java b/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java
index c82a4a9..1bcbe79 100644
--- a/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java
@@ -91,12 +91,14 @@
         mReporter.testModuleStarted(createModuleContext("arm32 module1"));
         mReporter.testModuleEnded();
         // Invocation ends
+        mReporter.invocationFailed(new NullPointerException());
         mReporter.invocationEnded(500L);
 
         //  ------ Verify that everything was populated ------
         assertNotNull(mFinalRecord.getTestRecordId());
         assertNotNull(mFinalRecord.getStartTime().getSeconds());
         assertNotNull(mFinalRecord.getEndTime().getSeconds());
+        assertNotNull(mFinalRecord.getDebugInfo());
 
         // The invocation has 2 modules
         assertEquals(2, mFinalRecord.getChildrenCount());
diff --git a/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java b/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java
index c142c05..dee9e5d 100644
--- a/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java
+++ b/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java
@@ -16,11 +16,11 @@
 package com.android.tradefed.suite.checker;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
+import com.android.tradefed.util.ProcessInfo;
 
 import org.easymock.EasyMock;
 import org.junit.Before;
@@ -28,6 +28,9 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /** Unit tests for {@link SystemServerStatusChecker} */
 @RunWith(JUnit4.class)
 public class SystemServerStatusCheckerTest {
@@ -38,6 +41,7 @@
     @Before
     public void setUp() {
         mMockDevice = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn("SERIAL");
         mChecker =
                 new SystemServerStatusChecker() {
                     @Override
@@ -47,11 +51,11 @@
                 };
     }
 
-    /** Test that system checker pass if the pid of system checker does not change. */
+    /** Test that system checker pass if system_server didn't restart. */
     @Test
-    public void testPidRemainUnchanged() throws Exception {
-        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
-                .andReturn("914")
+    public void testSystemServerProcessNotRestarted() throws Exception {
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+                .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L))
                 .times(2);
         EasyMock.replay(mMockDevice);
         assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
@@ -59,13 +63,48 @@
         EasyMock.verify(mMockDevice);
     }
 
-    /** Test that system checker fail if the pid of system checker does change. */
+    /** Test that system checker fail if system_server crashed and didn't come back. */
     @Test
-    public void testPidChanged() throws Exception {
-        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
-                .andReturn("914\n");
-        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
-                .andReturn("1024\n");
+    public void testSystemServerProcessCrashed() throws Exception {
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+                .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L));
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server"))).andReturn(null);
+        EasyMock.replay(mMockDevice);
+        assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
+        StatusCheckerResult result = mChecker.postExecutionCheck(mMockDevice);
+        assertEquals(CheckStatus.FAILED, result.getStatus());
+        assertTrue(result.isBugreportNeeded());
+        EasyMock.verify(mMockDevice);
+    }
+
+    /** Test that system checker fail if system_server restarted without device reboot. */
+    @Test
+    public void testSystemServerProcessRestartedWithoutDeviceReboot() throws Exception {
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+                .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L));
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+                .andReturn(new ProcessInfo("system", 1024, "system_server", 1559096000L));
+        EasyMock.expect(mMockDevice.getBootHistorySince(EasyMock.eq(1559091922L)))
+                .andReturn(new HashMap<Long, String>());
+        EasyMock.replay(mMockDevice);
+        assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
+        StatusCheckerResult result = mChecker.postExecutionCheck(mMockDevice);
+        assertEquals(CheckStatus.FAILED, result.getStatus());
+        assertTrue(result.isBugreportNeeded());
+        EasyMock.verify(mMockDevice);
+    }
+
+    /** Test that system checker fail if system_server restarted with device reboot. */
+    @Test
+    public void testSystemServerProcessRestartedWithUnintentionalDeviceReboot() throws Exception {
+        Map<Long, String> history = new HashMap<Long, String>();
+        history.put(1559095000L, "kernel_panic");
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+                .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L));
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+                .andReturn(new ProcessInfo("system", 1024, "system_server", 1559096000L));
+        EasyMock.expect(mMockDevice.getBootHistorySince(EasyMock.eq(1559091922L)))
+                .andReturn(history);
         EasyMock.expect(mMockDevice.getLastExpectedRebootTimeMillis()).andReturn(200L);
         EasyMock.replay(mMockDevice);
         assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
@@ -76,47 +115,38 @@
     }
 
     /**
-     * Test that if the pid changed but there was a Tradefed reboot, we still fail the checker just
-     * in case, but don't collect a bugreport.
+     * Test that if the pid changed but there was a Tradefed reboot, we still not fail the checker.
      */
     @Test
-    public void testPidChanged_tfReboot() throws Exception {
-        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
-                .andReturn("914\n");
-        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
-                .andReturn("1024\n");
-        // TF reboot was done
+    public void testSystemServerProcessRestartedWithIntentionalDeviceReboot() throws Exception {
+        Map<Long, String> history = new HashMap<Long, String>();
+        history.put(1559095000L, "reboot");
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+                .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L));
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+                .andReturn(new ProcessInfo("system", 1024, "system_server", 1559096000L));
+        EasyMock.expect(mMockDevice.getBootHistorySince(EasyMock.eq(1559091922L)))
+                .andReturn(history);
+        // TF reboot was triggered by host
         EasyMock.expect(mMockDevice.getLastExpectedRebootTimeMillis()).andReturn(600L);
         EasyMock.replay(mMockDevice);
         assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
         StatusCheckerResult result = mChecker.postExecutionCheck(mMockDevice);
-        assertEquals(CheckStatus.FAILED, result.getStatus());
-        assertFalse(result.isBugreportNeeded());
-        EasyMock.verify(mMockDevice);
-    }
-
-    /** Test that if the format of the pid is unexpected, we skip the system checker. */
-    @Test
-    public void testFailToGetPid() throws Exception {
-        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
-                .andReturn("not found\n");
-        EasyMock.replay(mMockDevice);
-        assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
-        assertEquals(CheckStatus.SUCCESS, mChecker.postExecutionCheck(mMockDevice).getStatus());
+        assertEquals(CheckStatus.SUCCESS, result.getStatus());
         EasyMock.verify(mMockDevice);
     }
 
     /**
-     * Test that if the pid output is null, we fail the current preExecution but skip post
-     * execution.
+     * Test that if fail to get system_server process at preExecutionCheck, we skip the
+     * system_server check in postExecution.
      */
     @Test
-    public void testPid_null() throws Exception {
-        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
-                .andReturn(null);
+    public void testFailToGetSystemServerProcess() throws Exception {
+        EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server"))).andReturn(null);
         EasyMock.replay(mMockDevice);
         assertEquals(CheckStatus.FAILED, mChecker.preExecutionCheck(mMockDevice).getStatus());
         assertEquals(CheckStatus.SUCCESS, mChecker.postExecutionCheck(mMockDevice).getStatus());
         EasyMock.verify(mMockDevice);
     }
+
 }
diff --git a/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java b/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
index b013373..c6f0ca5 100644
--- a/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
+++ b/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
@@ -15,17 +15,16 @@
  */
 package com.android.tradefed.suite.checker;
 
-import com.android.tradefed.suite.checker.UserChecker.DeviceUserState;
-
 import java.util.Arrays;
-import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Map;
+import java.util.HashMap;
 
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
 import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
 
 import org.junit.Test;
@@ -35,7 +34,12 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+
 
 /** Unit tests for {@link UserChecker} */
 @RunWith(JUnit4.class)
@@ -45,191 +49,306 @@
         UserChecker checker = new UserChecker();
 
         ITestDevice preDevice =
-                mockDeviceUserState(
-                        /* userIds=        */ new Integer[] {0},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
+                mockDeviceUserState(/* currentUser=  */ 0, /* userIds= */ new Integer[] {0});
         assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
 
         ITestDevice postDevice =
-                mockDeviceUserState(
-                        /* userIds=        */ new Integer[] {0},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
+                mockDeviceUserState(/* currentUser=  */ 0, /* userIds= */ new Integer[] {0});
         assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
     }
 
     @Test
+    public void testSwitchIsSuccess() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "system");
+        ITestDevice preDevice =
+                mockDeviceUserState(
+                        /* currentUser=  */ 10,
+                        /* userIds=        */ new Integer[] {0, 10},
+                        /* flags=        */ new Integer[] {0, 0},
+                        /* isRunning= */ new Boolean[] {true, true});
+        when(preDevice.switchUser(0)).thenReturn(true);
+
+        assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+        verify(preDevice, never()).createUser(any(), anyBoolean(), anyBoolean());
+        verify(preDevice, times(1)).switchUser(0);
+
+        ITestDevice postDevice =
+                mockDeviceUserState(
+                        /* currentUser=  */ 0,
+                        /* userIds=        */ new Integer[] {0, 10},
+                        /* flags=        */ new Integer[] {0, 0},
+                        /* isRunning= */ new Boolean[] {true, false});
+        assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
+        verify(postDevice, never()).createUser(any(), anyBoolean(), anyBoolean());
+        verify(postDevice, never()).switchUser(anyInt());
+    }
+
+    @Test
+    public void testCreateIsSuccess() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "secondary");
+        ITestDevice preDevice =
+                mockDeviceUserState(
+                        /* currentUser=  */ 0,
+                        /* userIds=        */ new Integer[] {0},
+                        /* flags=        */ new Integer[] {0},
+                        /* isRunning= */ new Boolean[] {true});
+        when(preDevice.createUser("Tfsecondary", false, false)).thenReturn(10);
+        when(preDevice.switchUser(10)).thenReturn(true);
+
+        assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+        verify(preDevice, times(1)).createUser("Tfsecondary", false, false);
+        verify(preDevice, times(1)).switchUser(10);
+
+        ITestDevice postDevice =
+                mockDeviceUserState(
+                        /* currentUser=  */ 10,
+                        /* userIds=        */ new Integer[] {0, 10},
+                        /* flags=        */ new Integer[] {0, 0},
+                        /* isRunning= */ new Boolean[] {true, true});
+        assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
+        verify(postDevice, never()).removeUser(anyInt());
+        verify(postDevice, never()).switchUser(anyInt());
+    }
+
+    @Test
+    public void testCreateCleanup() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "secondary");
+        mOptionSetter.setOptionValue("user-cleanup", "true");
+        ITestDevice preDevice =
+                mockDeviceUserState(
+                        /* currentUser=  */ 0,
+                        /* userIds=        */ new Integer[] {0},
+                        /* flags=        */ new Integer[] {0},
+                        /* isRunning= */ new Boolean[] {true});
+        when(preDevice.createUser("Tfsecondary", false, false)).thenReturn(10);
+        when(preDevice.switchUser(10)).thenReturn(true);
+
+        assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+        verify(preDevice, times(1)).createUser("Tfsecondary", false, false);
+        verify(preDevice, times(1)).switchUser(10);
+
+        ITestDevice postDevice =
+                mockDeviceUserState(
+                        /* currentUser=  */ 10,
+                        /* userIds=        */ new Integer[] {0, 10},
+                        /* flags=        */ new Integer[] {0, 0},
+                        /* isRunning= */ new Boolean[] {true, true});
+        when(postDevice.switchUser(0)).thenReturn(true);
+        when(postDevice.removeUser(10)).thenReturn(true);
+        assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
+        verify(postDevice, times(1)).switchUser(0);
+        verify(postDevice, times(1)).removeUser(10);
+    }
+
+    @Test
+    public void testCreateCleanup_cleanupFail() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "secondary");
+        mOptionSetter.setOptionValue("user-cleanup", "true");
+        ITestDevice preDevice =
+                mockDeviceUserState(
+                        /* currentUser=  */ 0,
+                        /* userIds=        */ new Integer[] {0},
+                        /* flags=        */ new Integer[] {0},
+                        /* isRunning= */ new Boolean[] {true});
+        when(preDevice.createUser("Tfsecondary", false, false)).thenReturn(10);
+        when(preDevice.switchUser(10)).thenReturn(true);
+
+        assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+        verify(preDevice, times(1)).createUser("Tfsecondary", false, false);
+        verify(preDevice, times(1)).switchUser(10);
+
+        ITestDevice postDevice =
+                mockDeviceUserState(
+                        /* currentUser=  */ 10,
+                        /* userIds=        */ new Integer[] {0, 10},
+                        /* flags=        */ new Integer[] {0, 0},
+                        /* isRunning= */ new Boolean[] {true, true});
+        when(postDevice.switchUser(0)).thenReturn(false);
+        when(postDevice.removeUser(10)).thenReturn(false);
+        StatusCheckerResult result = checker.postExecutionCheck(postDevice);
+        verify(postDevice, times(1)).switchUser(0);
+        verify(postDevice, times(1)).removeUser(10);
+        assertEquals(CheckStatus.FAILED, result.getStatus());
+        assertTrue(
+                result.getErrorMessage()
+                        .contains("Failed to switch back to previous current user 0"));
+        assertTrue(result.getErrorMessage().contains("Failed to remove new user 10"));
+    }
+
+    @Test
     /** Returns FAILED in the precessense of errors */
     public void testAllErrorsIsFailed() throws Exception {
         UserChecker checker = new UserChecker();
 
         ITestDevice preDevice =
                 mockDeviceUserState(
+                        /* currentUser=  */ 10,
                         /* userIds=        */ new Integer[] {0, 10, 11},
-                        /* runningUsers= */ new Integer[] {0, 10},
-                        /* currentUser=  */ 10);
+                        /* flags=        */ new Integer[] {0, 0, 0},
+                        /* isRunning= */ new Boolean[] {true, true, false});
         assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
 
         // User12 created, User11 deleted, User10 stopped, currentUser changed
         ITestDevice postDevice =
                 mockDeviceUserState(
-                        /* userIds=        */ new Integer[] {0, 10, 12},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
+                        /* currentUser=  */ 0,
+                        /* userIds= */ new Integer[] {0, 10, 12},
+                        /* flags=        */ new Integer[] {0, 0, 0},
+                        /* isRunning= */ new Boolean[] {true, false, false});
         assertEquals(CheckStatus.FAILED, checker.postExecutionCheck(postDevice).getStatus());
     }
 
     @Test
-    public void testSwitchToExistingOrCreateUserType() throws Exception {
+    public void testSwitchToSystem() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "system");
+        ITestDevice device =
+                mockDeviceUserState(/* currentUser=  */ 10, /* userIds= */ new Integer[] {0, 10});
+
+        when(device.switchUser(0)).thenReturn(true);
+
+        StatusCheckerResult result = checker.preExecutionCheck(device);
+        assertEquals(CheckStatus.SUCCESS, result.getStatus());
+        verify(device, never()).createUser(any(), anyBoolean(), anyBoolean());
+        verify(device, times(1)).switchUser(0);
+    }
+
+    @Test
+    public void testSwitchToSecondary() throws Exception {
         UserChecker checker = new UserChecker();
         OptionSetter mOptionSetter = new OptionSetter(checker);
         mOptionSetter.setOptionValue("user-type", "secondary");
         ITestDevice device =
-                mockDeviceUserState(
-                        /* userIds=        */ new Integer[] {0},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
+                mockDeviceUserState(/* currentUser=  */ 0, /* userIds= */ new Integer[] {0, 10});
 
-        when(device.getCurrentUser()).thenReturn(0);
-        mockListUsers(device, new Integer[] {0});
-        when(device.createUserNoThrow(UserChecker.DEFAULT_NAME)).thenReturn(10);
-        when(device.getCurrentUser()).thenReturn(0);
-        mockListUsers(device, new Integer[] {0, 10});
-        when(device.isUserSecondary(10)).thenReturn(true);
         when(device.switchUser(10)).thenReturn(true);
 
         StatusCheckerResult result = checker.preExecutionCheck(device);
         assertEquals(CheckStatus.SUCCESS, result.getStatus());
+        verify(device, never()).createUser(any(), anyBoolean(), anyBoolean());
         verify(device, times(1)).switchUser(10);
     }
 
     @Test
-    public void testSwitchToSecondaryUserCreateNewFail() throws Exception {
+    public void testSwitchToSecondary_fail() throws Exception {
         UserChecker checker = new UserChecker();
         OptionSetter mOptionSetter = new OptionSetter(checker);
         mOptionSetter.setOptionValue("user-type", "secondary");
         ITestDevice device =
-                mockDeviceUserState(
-                        /* userIds=        */ new Integer[] {0},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
+                mockDeviceUserState(/* currentUser=  */ 0, /* userIds= */ new Integer[] {0, 10});
 
-        when(device.getCurrentUser()).thenReturn(0);
-        mockListUsers(device, new Integer[] {0});
-        when(device.createUserNoThrow(UserChecker.DEFAULT_NAME)).thenReturn(-1);
+        when(device.switchUser(10)).thenReturn(false);
 
         StatusCheckerResult result = checker.preExecutionCheck(device);
         assertEquals(CheckStatus.FAILED, result.getStatus());
-        verify(device, times(1)).createUserNoThrow(UserChecker.DEFAULT_NAME);
+        verify(device, never()).createUser(any(), anyBoolean(), anyBoolean());
+        verify(device, times(1)).switchUser(10);
     }
 
     @Test
-    public void testFindRemovedUsers() throws Exception {
-        DeviceUserState preState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0, 10},
-                        /* runningUsers= */ new Integer[] {0, 10},
-                        /* currentUser=  */ 0);
-        DeviceUserState postState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
+    public void testSwitchToGuest() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "guest");
+        ITestDevice device =
+                mockDeviceUserState(
+                        /* currentUser=  */ 0,
+                        /* userIds= */ new Integer[] {0, 10},
+                        /* flags=        */ new Integer[] {0, UserInfo.FLAG_GUEST},
+                        /* isRunning= */ new Boolean[] {true, false});
 
-        assertArrayEquals(new Integer[] {10}, preState.findRemovedUsers(postState).toArray());
+        when(device.switchUser(10)).thenReturn(true);
+
+        StatusCheckerResult result = checker.preExecutionCheck(device);
+        assertEquals(CheckStatus.SUCCESS, result.getStatus());
+        verify(device, never()).createUser(any(), anyBoolean(), anyBoolean());
+        verify(device, times(1)).switchUser(10);
     }
 
     @Test
-    public void testFindAddedUsers() throws Exception {
-        DeviceUserState preState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
-        DeviceUserState postState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0, 10},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
+    public void testCreateSecondary() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "secondary");
+        ITestDevice device =
+                mockDeviceUserState(/* currentUser=  */ 0, /* userIds= */ new Integer[] {0});
 
-        assertArrayEquals(new Integer[] {10}, preState.findAddedUsers(postState).toArray());
+        when(device.createUser("Tfsecondary", false, false)).thenReturn(10);
+        when(device.switchUser(10)).thenReturn(true);
+
+        StatusCheckerResult result = checker.preExecutionCheck(device);
+        assertEquals(CheckStatus.SUCCESS, result.getStatus());
+        verify(device, times(1)).createUser("Tfsecondary", false, false);
+        verify(device, times(1)).switchUser(10);
     }
 
     @Test
-    public void testCurrentUserChanged() throws Exception {
-        DeviceUserState preState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0, 10},
-                        /* runningUsers= */ new Integer[] {0, 10},
-                        /* currentUser=  */ 10);
-        DeviceUserState postState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0, 10},
-                        /* runningUsers= */ new Integer[] {0, 10},
-                        /* currentUser=  */ 0);
+    public void testCreateGuest() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "guest");
+        ITestDevice device =
+                mockDeviceUserState(/* currentUser=  */ 0, /* userIds= */ new Integer[] {0});
 
-        assertEquals(true, preState.currentUserChanged(postState));
+        when(device.createUser("Tfguest", /* guest= */ true, /* ephemeral= */ false))
+                .thenReturn(10);
+        when(device.switchUser(10)).thenReturn(true);
+
+        StatusCheckerResult result = checker.preExecutionCheck(device);
+        assertEquals(CheckStatus.SUCCESS, result.getStatus());
+        verify(device, times(1)).createUser("Tfguest", /* guest= */ true, /* ephemeral= */ false);
+        verify(device, times(1)).switchUser(10);
     }
 
-    @Test
-    public void testfindStartedUsers() throws Exception {
-        DeviceUserState preState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0, 10},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
-        DeviceUserState postState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0, 10},
-                        /* runningUsers= */ new Integer[] {0, 10},
-                        /* currentUser=  */ 0);
+    // // TEST HELPERS
 
-        assertArrayEquals(new Integer[] {10}, preState.findStartedUsers(postState).toArray());
-        assertArrayEquals(new Integer[] {}, preState.findStoppedUsers(postState).toArray());
-    }
+    /** Return a device with the user state calls mocked. */
+    private ITestDevice mockDeviceUserState(int currentUser, Integer[] userIds) throws Exception {
+        Integer[] flags = new Integer[userIds.length];
+        Arrays.fill(flags, 0);
 
-    @Test
-    public void testFindStopedUsers() throws Exception {
-        DeviceUserState preState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0, 10},
-                        /* runningUsers= */ new Integer[] {0, 10},
-                        /* currentUser=  */ 0);
-        DeviceUserState postState =
-                getMockedUserState(
-                        /* userIds=        */ new Integer[] {0, 10},
-                        /* runningUsers= */ new Integer[] {0},
-                        /* currentUser=  */ 0);
+        Boolean[] isRunning = new Boolean[userIds.length];
+        Arrays.fill(isRunning, false);
+        isRunning[0] = true;
 
-        assertArrayEquals(new Integer[] {}, preState.findStartedUsers(postState).toArray());
-        assertArrayEquals(new Integer[] {10}, preState.findStoppedUsers(postState).toArray());
-    }
-
-    // TEST HELPERS
-
-    /** Return an instantiated DeviceUserState which was mocked. */
-    private DeviceUserState getMockedUserState(
-            Integer[] userIds, Integer[] runningUsers, int currentUser) throws Exception {
-        ITestDevice device = mockDeviceUserState(userIds, runningUsers, currentUser);
-        return new UserChecker.DeviceUserState(device);
+        return mockDeviceUserState(currentUser, userIds, flags, isRunning);
     }
 
     /** Return a device with the user state calls mocked. */
     private ITestDevice mockDeviceUserState(
-            Integer[] userIds, Integer[] runningUsers, int currentUser) throws Exception {
-        HashSet<Integer> runningUsersSet = new HashSet<Integer>(Arrays.asList(runningUsers));
+            int currentUser, Integer[] userIds, Integer[] flags, Boolean[] isRunning)
+            throws Exception {
         ITestDevice device = mock(ITestDevice.class);
+
         when(device.getCurrentUser()).thenReturn(currentUser);
-        mockListUsers(device, userIds);
-        for (int userId : userIds) {
-            when(device.isUserRunning(userId)).thenReturn(runningUsersSet.contains(userId));
-        }
+        mockListUsersInfo(device, userIds, flags, isRunning);
 
         return device;
     }
 
-    private void mockListUsers(ITestDevice device, Integer[] userIds) throws Exception {
-        when(device.listUsers()).thenReturn(new ArrayList<Integer>(Arrays.asList(userIds)));
+    private void mockListUsersInfo(
+            ITestDevice device, Integer[] userIds, Integer[] flags, Boolean[] isRunning)
+            throws Exception {
+        Map<Integer, UserInfo> result = new HashMap<>();
+        for (int i = 0; i < userIds.length; i++) {
+            int userId = userIds[i];
+            result.put(
+                    userId,
+                    new UserInfo(
+                            /* userId= */ userId,
+                            /* userName= */ "usr" + userId,
+                            /* flag= */ flags[i],
+                            /* isRunning= */ isRunning[i]));
+        }
+        when(device.getUserInfos()).thenReturn(result);
     }
 }
diff --git a/tests/src/com/android/tradefed/targetprep/AppSetupTest.java b/tests/src/com/android/tradefed/targetprep/AppSetupTest.java
index 8e5e5ab..870fadf 100644
--- a/tests/src/com/android/tradefed/targetprep/AppSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/AppSetupTest.java
@@ -21,7 +21,6 @@
 import static org.mockito.Mockito.times;
 
 import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.build.IAppBuildInfo;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.build.VersionedFile;
 import com.android.tradefed.config.OptionSetter;
@@ -30,20 +29,22 @@
 import com.android.tradefed.util.AaptParser;
 import com.android.tradefed.util.FileUtil;
 
-import java.io.File;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
 import org.easymock.EasyMock;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
 
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
 /**
  * Unit Tests for {@link AppSetup}.
  */
@@ -53,17 +54,28 @@
     private static final String SERIAL = "serial";
     private AppSetup mAppSetup;
     private ITestDevice mMockDevice;
-    private IAppBuildInfo mMockBuildInfo;
+    private IBuildInfo mMockBuildInfo;
     private AaptParser mMockAaptParser;
+    private List<VersionedFile> mApps;
 
     @Before
-    public void setUp() {
+    public void setUp() throws IOException {
         mAppSetup = new AppSetup();
         mMockDevice = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn(SERIAL);
         EasyMock.expect(mMockDevice.getDeviceDescriptor()).andStubReturn(null);
-        mMockBuildInfo = EasyMock.createMock(IAppBuildInfo.class);
+        mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
         mMockAaptParser = Mockito.mock(AaptParser.class);
+        mApps = new ArrayList<>();
+        File tmpFile = FileUtil.createTempFile("versioned", ".test");
+        mApps.add(new VersionedFile(tmpFile, "1"));
+    }
+
+    @After
+    public void tearDown() {
+        for (VersionedFile f : mApps) {
+            FileUtil.deleteFile(f.getFile());
+        }
     }
 
     private void replayMocks() {
@@ -75,18 +87,14 @@
     }
 
     /**
-     * Test for {@link AppSetup#setUp(ITestDevice, IBuildInfo)} when the IBuildInfo is not an
-     * instance of {@link IAppBuildInfo}.
+     * Test for {@link AppSetup#setUp(ITestDevice, IBuildInfo)} when the IBuildInfo doesn't contain
+     * any apps.
      */
     @Test
-    public void testSetup_notIAppBuildInfo() throws Exception {
+    public void testSetup_notApps() throws Exception {
         replayMocks();
-        try {
-            mAppSetup.setUp(mMockDevice, new BuildInfo());
-            fail("Should have thrown an exception.");
-        } catch (IllegalArgumentException expected) {
-            assertEquals("Provided buildInfo is not a AppBuildInfo", expected.getMessage());
-        }
+        // Inop setup
+        mAppSetup.setUp(mMockDevice, new BuildInfo());
         verifyMocks();
     }
 
@@ -274,6 +282,7 @@
      */
     @Test
     public void testSetup_executePostInstall() throws Exception {
+        EasyMock.expect(mMockBuildInfo.getAppPackageFiles()).andReturn(mApps);
         final String fakeCmd = "fake command";
         OptionSetter setter = new OptionSetter(mAppSetup);
         setter.setOptionValue("install", "false");
@@ -292,6 +301,7 @@
      */
     @Test
     public void testSetup_uninstallAll_noPackage() throws Exception {
+        EasyMock.expect(mMockBuildInfo.getAppPackageFiles()).andReturn(mApps);
         OptionSetter setter = new OptionSetter(mAppSetup);
         setter.setOptionValue("install", "false");
         setter.setOptionValue("uninstall-all", "true");
@@ -308,6 +318,7 @@
      */
     @Test
     public void testSetup_uninstallAll() throws Exception {
+        EasyMock.expect(mMockBuildInfo.getAppPackageFiles()).andReturn(mApps);
         OptionSetter setter = new OptionSetter(mAppSetup);
         setter.setOptionValue("install", "false");
         setter.setOptionValue("uninstall-all", "true");
@@ -326,6 +337,7 @@
      */
     @Test
     public void testSetup_uninstallAll_fails() throws Exception {
+        EasyMock.expect(mMockBuildInfo.getAppPackageFiles()).andReturn(mApps);
         OptionSetter setter = new OptionSetter(mAppSetup);
         setter.setOptionValue("install", "false");
         setter.setOptionValue("uninstall-all", "true");
diff --git a/tests/src/com/android/tradefed/targetprep/CreateUserPreparerTest.java b/tests/src/com/android/tradefed/targetprep/CreateUserPreparerTest.java
index b96d6bd..a7cfaef 100644
--- a/tests/src/com/android/tradefed/targetprep/CreateUserPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/CreateUserPreparerTest.java
@@ -48,6 +48,7 @@
         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);
         mPreparer.setUp(mMockDevice, null);
 
         doReturn(true).when(mMockDevice).removeUser(5);
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java b/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
index 42618f2..03b5aa1 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
@@ -115,6 +115,7 @@
     @Test
     public void testSetup() throws Exception {
         doSetupExpectations();
+        mMockFlasher.setShouldFlashRamdisk(false);
         // report flashing success in normal case
         EasyMock.expect(mMockFlasher.getSystemFlashingStatus())
             .andReturn(CommandStatus.SUCCESS).anyTimes();
@@ -161,6 +162,21 @@
         }
     }
 
+    /**
+     * Test {@link DeviceFlashPreparer#setUp(ITestDevice, IBuildInfo)} when ramdisk flashing is
+     * required via parameter but not provided in build info
+     */
+    @Test
+    public void testSetUp_noRamdisk() throws Exception {
+        try {
+            mDeviceFlashPreparer.setShouldFlashRamdisk(true);
+            mDeviceFlashPreparer.setUp(mMockDevice, mMockBuildInfo);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
     /** Test {@link DeviceFlashPreparer#setUp(ITestDevice, IBuildInfo)} when build does not boot. */
     @Test
     public void testSetup_buildError() throws Exception {
@@ -168,6 +184,7 @@
         mMockFlasher.overrideDeviceOptions(mMockDevice);
         mMockFlasher.setForceSystemFlash(false);
         mMockFlasher.setDataWipeSkipList(Arrays.asList(new String[]{}));
+        mMockFlasher.setShouldFlashRamdisk(false);
         mMockFlasher.flash(mMockDevice, mMockBuildInfo);
         mMockFlasher.setWipeTimeout(EasyMock.anyLong());
         mMockDevice.waitForDeviceOnline();
@@ -211,6 +228,7 @@
         mMockFlasher.overrideDeviceOptions(mMockDevice);
         mMockFlasher.setForceSystemFlash(false);
         mMockFlasher.setDataWipeSkipList(Arrays.asList(new String[]{}));
+        mMockFlasher.setShouldFlashRamdisk(false);
         mMockFlasher.flash(mMockDevice, mMockBuildInfo);
         EasyMock.expectLastCall().andThrow(new DeviceNotAvailableException());
         mMockFlasher.setWipeTimeout(EasyMock.anyLong());
@@ -236,6 +254,7 @@
     @Test
     public void testSetup_flashSkipped() throws Exception {
         doSetupExpectations();
+        mMockFlasher.setShouldFlashRamdisk(false);
         // report flashing status as null (for not flashing system partitions)
         EasyMock.expect(mMockFlasher.getSystemFlashingStatus()).andReturn(null).anyTimes();
         EasyMock.replay(mMockFlasher, mMockDevice);
@@ -243,4 +262,25 @@
         EasyMock.verify(mMockFlasher, mMockDevice);
         assertFalse("should not report flashing metrics in normal case", mFlashingMetricsReported);
     }
+
+    /**
+     * Verifies that the ramdisk flashing parameter is passed down to the device flasher
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetup_flashRamdisk() throws Exception {
+        mDeviceFlashPreparer.setShouldFlashRamdisk(true);
+        mMockBuildInfo.setRamdiskFile(new File("foo"), "0");
+        doSetupExpectations();
+        // report flashing success in normal case
+        EasyMock.expect(mMockFlasher.getSystemFlashingStatus())
+                .andReturn(CommandStatus.SUCCESS)
+                .anyTimes();
+        mMockFlasher.setShouldFlashRamdisk(true);
+        EasyMock.expectLastCall();
+        EasyMock.replay(mMockFlasher, mMockDevice);
+        mDeviceFlashPreparer.setUp(mMockDevice, mMockBuildInfo);
+        EasyMock.verify(mMockFlasher, mMockDevice);
+    }
 }
diff --git a/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java b/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java
index 3f228d3..215f623 100644
--- a/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java
+++ b/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java
@@ -590,6 +590,90 @@
     }
 
     /**
+     * Test the fastboot flashing with ramdisk interaction flow
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testFlashingSystemWithRamdisk() throws Exception {
+        final String buildId = "systemBuildId";
+        mFlasher.setShouldFlashRamdisk(true);
+        IDeviceBuildInfo mockBuild = EasyMock.createMock(IDeviceBuildInfo.class);
+        EasyMock.expect(mockBuild.getDeviceBuildId()).andReturn(buildId);
+        File deviceImage = FileUtil.createTempFile("fakeDeviceImage", "");
+        File ramdisk = FileUtil.createTempFile("fakeRamdisk", "");
+        EasyMock.expect(mockBuild.getRamdiskFile()).andReturn(ramdisk);
+        try {
+            EasyMock.expect(mockBuild.getDeviceImageFile()).andStubReturn(deviceImage);
+            CommandResult res = new CommandResult(CommandStatus.SUCCESS);
+            res.setStderr("flashing");
+            EasyMock.expect(
+                            mMockDevice.executeLongFastbootCommand(
+                                    EasyMock.eq("--skip-reboot"),
+                                    EasyMock.eq("update"),
+                                    EasyMock.eq(deviceImage.getAbsolutePath())))
+                    .andReturn(res);
+            EasyMock.expect(
+                            mMockDevice.executeLongFastbootCommand(
+                                    EasyMock.eq("flash"),
+                                    EasyMock.eq("boot"),
+                                    EasyMock.eq(ramdisk.getAbsolutePath())))
+                    .andReturn(res);
+            mMockDevice.reboot();
+            EasyMock.expectLastCall();
+            EasyMock.replay(mMockDevice, mockBuild);
+            assertTrue(mFlasher.checkAndFlashSystem(mMockDevice, buildId, null, mockBuild));
+            EasyMock.verify(mMockDevice, mockBuild);
+            assertEquals(
+                    "system flashing status should be \"SUCCESS\"",
+                    CommandStatus.SUCCESS,
+                    mFlasher.getSystemFlashingStatus());
+        } finally {
+            FileUtil.deleteFile(deviceImage);
+            FileUtil.deleteFile(ramdisk);
+        }
+    }
+
+    /**
+     * Test that ramdisk is still flashed even system partition flashing is skipped
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSkipFlashingSystemWithRamdisk() throws Exception {
+        final String buildId = "systemBuildId";
+        final String buildFlavor = "systemBuildFlavor";
+        mFlasher.setShouldFlashRamdisk(true);
+        IDeviceBuildInfo mockBuild = EasyMock.createMock(IDeviceBuildInfo.class);
+        File ramdisk = FileUtil.createTempFile("fakeRamdisk", "");
+        EasyMock.expect(mockBuild.getRamdiskFile()).andReturn(ramdisk);
+        try {
+            EasyMock.expect(mockBuild.getDeviceBuildId()).andReturn(buildId);
+            EasyMock.expect(mockBuild.getBuildFlavor()).andReturn(buildFlavor);
+            mMockDevice.rebootUntilOnline();
+            EasyMock.expectLastCall();
+            CommandResult res = new CommandResult(CommandStatus.SUCCESS);
+            res.setStderr("flashing");
+            EasyMock.expect(
+                            mMockDevice.executeLongFastbootCommand(
+                                    EasyMock.eq("flash"),
+                                    EasyMock.eq("boot"),
+                                    EasyMock.eq(ramdisk.getAbsolutePath())))
+                    .andReturn(res);
+            mMockDevice.reboot();
+            EasyMock.expectLastCall();
+            EasyMock.replay(mMockDevice, mockBuild);
+            assertFalse(mFlasher.checkAndFlashSystem(mMockDevice, buildId, buildFlavor, mockBuild));
+            EasyMock.verify(mMockDevice, mockBuild);
+            assertNull(
+                    "system flash status should be null when partitions are not flashed",
+                    mFlasher.getSystemFlashingStatus());
+        } finally {
+            FileUtil.deleteFile(ramdisk);
+        }
+    }
+
+    /**
      * Test {@link FastbootDeviceFlasher#checkAndFlashSystem(ITestDevice, String, String,
      * IDeviceBuildInfo)} with flash options
      */
diff --git a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
index 869577a..4cde810 100644
--- a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
@@ -55,12 +55,16 @@
     private BundletoolUtil mMockBundletoolUtil;
     private File mFakeApex;
     private File mFakeApk;
+    private File mFakeApk2;
+    private File mFakePersistentApk;
     private File mFakeApkApks;
     private File mFakeApexApks;
     private File mBundletoolJar;
     private OptionSetter mSetter;
     private static final String APEX_PACKAGE_NAME = "com.android.FAKE_APEX_PACKAGE_NAME";
     private static final String APK_PACKAGE_NAME = "com.android.FAKE_APK_PACKAGE_NAME";
+    private static final String APK2_PACKAGE_NAME = "com.android.FAKE_APK2_PACKAGE_NAME";
+    private static final String PERSISTENT_APK_PACKAGE_NAME = "com.android.PERSISTENT_PACKAGE_NAME";
     private static final String SPLIT_APEX_PACKAGE_NAME =
             "com.android.SPLIT_FAKE_APEX_PACKAGE_NAME";
     private static final String SPLIT_APK_PACKAGE_NAME =
@@ -69,17 +73,22 @@
     private static final long APEX_VERSION = 1;
     private static final String APEX_NAME = "fakeApex.apex";
     private static final String APK_NAME = "fakeApk.apk";
+    private static final String APK2_NAME = "fakeSecondApk.apk";
+    private static final String PERSISTENT_APK_NAME = "fakePersistentApk.apk";
     private static final String SPLIT_APEX_APKS_NAME = "fakeApex.apks";
     private static final String SPLIT_APK__APKS_NAME = "fakeApk.apks";
     private static final String BUNDLETOOL_JAR_NAME = "bundletool.jar";
     private static final String APEX_DATA_DIR = "/data/apex/active/";
     private static final String STAGING_DATA_DIR = "/data/app-staging/";
     private static final String SESSION_DATA_DIR = "/data/apex/sessions/";
+    private static final String APEX_STAGING_WAIT_TIME = "10";
 
     @Before
     public void setUp() throws Exception {
         mFakeApex = FileUtil.createTempFile("fakeApex", ".apex");
         mFakeApk = FileUtil.createTempFile("fakeApk", ".apk");
+        mFakeApk2 = FileUtil.createTempFile("fakeSecondApk", ".apk");
+        mFakePersistentApk = FileUtil.createTempFile("fakePersistentApk", ".apk");
         mMockDevice = EasyMock.createMock(ITestDevice.class);
         mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
         mMockBundletoolUtil = Mockito.mock(BundletoolUtil.class);
@@ -111,7 +120,13 @@
                             return mFakeApex;
                         }
                         if (appFileName.endsWith(".apk")) {
-                            return mFakeApk;
+                            if (appFileName.contains("Second")) {
+                                return mFakeApk2;
+                            } else if (appFileName.contains("Persistent")) {
+                                return mFakePersistentApk;
+                            } else {
+                                return mFakeApk;
+                            }
                         }
                         if (appFileName.endsWith(".apks")) {
                             if (appFileName.contains("Apex")) {
@@ -133,12 +148,18 @@
                         if (testAppFile.getName().endsWith(".apex")) {
                             return APEX_PACKAGE_NAME;
                         }
-                        if (testAppFile.getName().endsWith(".apk") &&
-                            !testAppFile.getName().contains("Split")) {
-                            return APK_PACKAGE_NAME;
+                        if (testAppFile.getName().endsWith(".apk")
+                                && !testAppFile.getName().contains("Split")) {
+                            if (testAppFile.getName().contains("Second")) {
+                                return APK2_PACKAGE_NAME;
+                            } else if (testAppFile.getName().contains("Persistent")) {
+                                return PERSISTENT_APK_PACKAGE_NAME;
+                            } else {
+                                return APK_PACKAGE_NAME;
+                            }
                         }
-                        if (testAppFile.getName().endsWith(".apk") &&
-                            testAppFile.getName().contains("Split")) {
+                        if (testAppFile.getName().endsWith(".apk")
+                                && testAppFile.getName().contains("Split")) {
                             return SPLIT_APK_PACKAGE_NAME;
                         }
                         return null;
@@ -155,16 +176,29 @@
                         }
                         return apexInfo;
                     }
+
+                    @Override
+                    protected boolean isPersistentApk(
+                            String filename, ITestDevice device, IBuildInfo buildInfo)
+                            throws TargetSetupError {
+                        if (filename.contains("Persistent")) {
+                            return true;
+                        }
+                        return false;
+                    }
                 };
 
         mSetter = new OptionSetter(mInstallApexModuleTargetPreparer);
         mSetter.setOptionValue("cleanup-apks", "true");
+        mSetter.setOptionValue("apex-staging-wait-time", APEX_STAGING_WAIT_TIME);
     }
 
     @After
     public void tearDown() throws Exception {
         FileUtil.deleteFile(mFakeApex);
         FileUtil.deleteFile(mFakeApk);
+        FileUtil.deleteFile(mFakeApk2);
+        FileUtil.deleteFile(mFakePersistentApk);
     }
 
     @Test
@@ -183,7 +217,7 @@
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
         mMockDevice.reboot();
         EasyMock.expectLastCall();
-        mockSuccessfulInstallPackageAndReboot();
+        mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
         EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -201,7 +235,7 @@
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
-        mockSuccessfulInstallPackageAndReboot();
+        mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
         EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -226,7 +260,7 @@
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
         mMockDevice.reboot();
         EasyMock.expectLastCall();
-        mockSuccessfulInstallPackageAndReboot();
+        mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
         EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -252,7 +286,7 @@
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
         mMockDevice.reboot();
         EasyMock.expectLastCall();
-        mockSuccessfulInstallPackageAndReboot();
+        mockSuccessfulInstallPackageAndReboot(mFakeApex);
         EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<ApexInfo>());
 
         try {
@@ -284,7 +318,7 @@
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
         mMockDevice.reboot();
         EasyMock.expectLastCall();
-        mockSuccessfulInstallPackageAndReboot();
+        mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME_TO_FAIL", 1));
         EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -319,8 +353,11 @@
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
         mMockDevice.reboot();
         EasyMock.expectLastCall();
-        EasyMock.expect(mMockDevice.installPackage((File) EasyMock.anyObject(), EasyMock.eq(true)))
-                .andReturn(null)
+        List<String> trainInstallCmd = new ArrayList<>();
+        trainInstallCmd.add("install-multi-package");
+        trainInstallCmd.add(mFakeApk.getAbsolutePath());
+        EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                .andReturn("Success")
                 .once();
         EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
 
@@ -331,6 +368,75 @@
     }
 
     @Test
+    public void testSetupAndTearDown_InstallMultipleApk() throws Exception {
+        mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(APK2_NAME);
+        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        CommandResult res = new CommandResult();
+        res.setStdout("test.apex");
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
+        mMockDevice.reboot();
+        EasyMock.expectLastCall();
+        List<File> apks = new ArrayList<>();
+        apks.add(mFakeApk);
+        apks.add(mFakeApk2);
+        mockSuccessfulInstallMultiApkWithoutReboot(apks);
+        EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
+        EasyMock.expect(mMockDevice.uninstallPackage(APK2_PACKAGE_NAME)).andReturn(null).once();
+
+        EasyMock.replay(mMockBuildInfo, mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mMockDevice, mMockBuildInfo);
+        mInstallApexModuleTargetPreparer.tearDown(mMockDevice, mMockBuildInfo, null);
+        EasyMock.verify(mMockBuildInfo, mMockDevice);
+    }
+
+    @Test
+    public void testSetupAndTearDown_InstallMultipleApkContainingPersistentApk() throws Exception {
+        mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(APK2_NAME);
+        mInstallApexModuleTargetPreparer.addTestFileName(PERSISTENT_APK_NAME);
+        mMockDevice.deleteFile(APEX_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
+        EasyMock.expectLastCall().times(1);
+        CommandResult res = new CommandResult();
+        res.setStdout("test.apex");
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
+        EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
+        mMockDevice.reboot();
+        EasyMock.expectLastCall();
+        List<String> trainInstallCmd = new ArrayList<>();
+        trainInstallCmd.add("install-multi-package");
+        trainInstallCmd.add("--staged");
+        trainInstallCmd.add(mFakeApk.getAbsolutePath());
+        trainInstallCmd.add(mFakeApk2.getAbsolutePath());
+        trainInstallCmd.add(mFakePersistentApk.getAbsolutePath());
+        EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                .andReturn("Success")
+                .once();
+        mMockDevice.reboot();
+        EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
+        EasyMock.expect(mMockDevice.uninstallPackage(APK2_PACKAGE_NAME)).andReturn(null).once();
+        EasyMock.expect(mMockDevice.uninstallPackage(PERSISTENT_APK_PACKAGE_NAME))
+                .andReturn(null)
+                .once();
+        EasyMock.replay(mMockBuildInfo, mMockDevice);
+        mInstallApexModuleTargetPreparer.setUp(mMockDevice, mMockBuildInfo);
+        mInstallApexModuleTargetPreparer.tearDown(mMockDevice, mMockBuildInfo, null);
+        EasyMock.verify(mMockBuildInfo, mMockDevice);
+    }
+
+    @Test
     public void testSetupAndTearDown_ApkAndApks() throws Exception {
         mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
         mInstallApexModuleTargetPreparer.addTestFileName(SPLIT_APK__APKS_NAME);;
@@ -434,7 +540,7 @@
         EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
         mMockDevice.reboot();
         EasyMock.expectLastCall();
-        mockSuccessfulInstallPackageAndReboot();
+        mockSuccessfulInstallPackageAndReboot(mFakeApex);
         Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
         activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
         EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -608,14 +714,36 @@
         }
     }
 
-    private void mockSuccessfulInstallPackageAndReboot() throws Exception {
-        EasyMock.expect(mMockDevice.installPackage((File) EasyMock.anyObject(), EasyMock.eq(true)))
-                .andReturn(null)
+    /** Test that teardown without setup does not cause a NPE. */
+    @Test
+    public void testTearDown() throws Exception {
+        EasyMock.replay(mMockBuildInfo, mMockDevice);
+        mInstallApexModuleTargetPreparer.tearDown(mMockDevice, mMockBuildInfo, null);
+        EasyMock.verify(mMockBuildInfo, mMockDevice);
+    }
+
+    private void mockSuccessfulInstallPackageAndReboot(File f) throws Exception {
+        List<String> trainInstallCmd = new ArrayList<>();
+        trainInstallCmd.add("install-multi-package");
+        trainInstallCmd.add(f.getAbsolutePath());
+        EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                .andReturn("Success")
                 .once();
         mMockDevice.reboot();
         EasyMock.expectLastCall().once();
     }
 
+    private void mockSuccessfulInstallMultiApkWithoutReboot(List<File> apks) throws Exception {
+        List<String> trainInstallCmd = new ArrayList<>();
+        trainInstallCmd.add("install-multi-package");
+        for (File apk : apks) {
+            trainInstallCmd.add(apk.getAbsolutePath());
+        }
+        EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+                .andReturn("Success")
+                .once();
+    }
+
     private void mockSuccessfulInstallMultiPackageAndReboot() throws Exception {
         List<String> trainInstallCmd = new ArrayList<>();
         trainInstallCmd.add("install-multi-package");
diff --git a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
index b5c9a20..3817c98 100644
--- a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.android.tradefed.build.BuildInfo;
@@ -74,7 +73,7 @@
 
     @Test
     public void testLocalNoExist() throws Exception {
-        mOptionSetter.setOptionValue("push", "/noexist->/data/");
+        mOptionSetter.setOptionValue("push-file", "/noexist", "/data/");
         mOptionSetter.setOptionValue("post-push", "ls /");
         EasyMock.replay(mMockDevice);
         try {
@@ -89,7 +88,7 @@
 
     @Test
     public void testRemoteNoExist() throws Exception {
-        mOptionSetter.setOptionValue("push", "/bin/sh->/noexist/");
+        mOptionSetter.setOptionValue("push-file", "/bin/sh", "/noexist/");
         mOptionSetter.setOptionValue("post-push", "ls /");
         // expect a pushFile() call and return false (failed)
         EasyMock.expect(
@@ -118,7 +117,7 @@
             File testFile = new File(testsDir, "perf_test");
             testFile.createNewFile();
             info.setFile("perf_test", testFile, "v1");
-            mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/");
+            mOptionSetter.setOptionValue("push-file", "perf_test", "/data/local/tmp/");
             // expect a pushFile() to be done with the appended file name.
             EasyMock.expect(
                             mMockDevice.pushFile(
@@ -141,7 +140,7 @@
             File testFile = new File(testsDir, "perf_test");
             testFile.mkdir();
             info.setFile("perf_test", testFile, "v1");
-            mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/");
+            mOptionSetter.setOptionValue("push-file", "perf_test", "/data/local/tmp/");
             EasyMock.expect(mMockDevice.doesFileExist("/data/local/tmp/")).andReturn(true);
             EasyMock.expect(mMockDevice.isDirectory("/data/local/tmp/")).andReturn(true);
             // expect a pushFile() to be done with the appended file name.
@@ -168,7 +167,7 @@
             File testFile = new File(testsDir, "perf_test");
             testFile.mkdir();
             info.setFile("perf_test", testFile, "v1");
-            mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/file");
+            mOptionSetter.setOptionValue("push-file", "perf_test", "/data/local/tmp/file");
             EasyMock.expect(mMockDevice.doesFileExist("/data/local/tmp/file")).andReturn(true);
             EasyMock.expect(mMockDevice.isDirectory("/data/local/tmp/file")).andReturn(false);
             EasyMock.replay(mMockDevice);
@@ -185,11 +184,11 @@
     }
 
     /**
-     * Test pushing a file to remote dir. The 'push' contract allows to push the file to a named
-     * directory.
+     * Test pushing a file to remote dir. If there are multiple files push to the same place, the
+     * latest win.
      */
     @Test
-    public void testRemotePush_conflict() throws Exception {
+    public void testRemotePush_override() throws Exception {
         BuildInfo info = new BuildInfo();
         File testsDir = FileUtil.createTempDir("tests_dir");
         try {
@@ -199,27 +198,50 @@
             testFile2.createNewFile();
             info.setFile("perf_test", testFile, "v1");
             info.setFile("perf_test2", testFile2, "v1");
-            mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/perf_test");
-            mOptionSetter.setOptionValue("push", "perf_test2->/data/local/tmp/perf_test");
+            mOptionSetter.setOptionValue("push-file", "perf_test", "/data/local/tmp/perf_test");
+            mOptionSetter.setOptionValue("push-file", "perf_test2", "/data/local/tmp/perf_test");
             EasyMock.expect(mMockDevice.isDirectory(EasyMock.anyObject())).andStubReturn(false);
-            // expect a pushFile() to be done with the appended file name.
-            EasyMock.expect(
-                            mMockDevice.pushFile(
-                                    EasyMock.eq(testFile),
-                                    EasyMock.eq("/data/local/tmp/perf_test")))
-                    .andReturn(Boolean.TRUE);
+            // the latest config win.
             EasyMock.expect(
                             mMockDevice.pushFile(
                                     EasyMock.eq(testFile2),
                                     EasyMock.eq("/data/local/tmp/perf_test")))
                     .andReturn(Boolean.TRUE);
             EasyMock.replay(mMockDevice);
-            try {
-                mPreparer.setUp(mMockDevice, info);
-                fail("Should have thrown an exception.");
-            } catch (TargetSetupError expected) {
-                assertTrue(expected.getMessage().contains("We pushed two files to the "));
-            }
+            mPreparer.setUp(mMockDevice, info);
+            EasyMock.verify(mMockDevice);
+        } finally {
+            FileUtil.recursiveDelete(testsDir);
+        }
+    }
+
+    /**
+     * Test pushing a file to remote dir. If both push and push-file push to the same remote file,
+     * the push-file win.
+     */
+    @Test
+    public void testPushFileAndPush_override() throws Exception {
+        BuildInfo info = new BuildInfo();
+        File testsDir = FileUtil.createTempDir("tests_dir");
+        try {
+            File testFile = new File(testsDir, "perf_test");
+            testFile.createNewFile();
+            File testFile2 = new File(testsDir, "perf_test2");
+            testFile2.createNewFile();
+            info.setFile("perf_test", testFile, "v1");
+            info.setFile("perf_test2", testFile2, "v1");
+
+            mOptionSetter.setOptionValue("push-file", "perf_test2", "/data/local/tmp/perf_test");
+            mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/perf_test");
+            EasyMock.expect(mMockDevice.isDirectory(EasyMock.anyObject())).andStubReturn(false);
+            // the latest config win.
+            EasyMock.expect(
+                            mMockDevice.pushFile(
+                                    EasyMock.eq(testFile2),
+                                    EasyMock.eq("/data/local/tmp/perf_test")))
+                    .andReturn(Boolean.TRUE);
+            EasyMock.replay(mMockDevice);
+            mPreparer.setUp(mMockDevice, info);
             EasyMock.verify(mMockDevice);
         } finally {
             FileUtil.recursiveDelete(testsDir);
@@ -599,6 +621,45 @@
     }
 
     /**
+     * Ensure that in case we don't find the module directory. We fallback to top level match first
+     * and not first found.
+     */
+    @Test
+    public void testPush_moduleName_noMatch() throws Exception {
+        mOptionSetter.setOptionValue("push", "lib64->/data/local/tmp/lib");
+        mPreparer.setAbi(new Abi("x86_64", "64"));
+
+        mPreparer.setInvocationContext(createModuleWithName("CtsBionicTestCases"));
+        IDeviceBuildInfo info = new DeviceBuildInfo();
+        File tmpFolder = FileUtil.createTempDir("push-file-tests-dir");
+        try {
+            File beforeName = new File(tmpFolder, "lib64");
+            FileUtil.mkdirsRWX(beforeName);
+            File libX86File = new File(tmpFolder, "DATA/lib64");
+            FileUtil.mkdirsRWX(libX86File);
+            info.setFile(BuildInfoFileKey.TESTDIR_IMAGE, tmpFolder, "v1");
+            EasyMock.expect(mMockDevice.doesFileExist("/data/local/tmp/lib")).andReturn(false);
+            EasyMock.expect(mMockDevice.executeShellCommand("mkdir -p \"/data/local/tmp/lib\""))
+                    .andReturn("");
+            Capture<Set<String>> capture = new Capture<>();
+            EasyMock.expect(
+                            mMockDevice.pushDir(
+                                    EasyMock.eq(new File(tmpFolder, "lib64")),
+                                    EasyMock.eq("/data/local/tmp/lib"),
+                                    EasyMock.capture(capture)))
+                    .andReturn(true);
+            EasyMock.replay(mMockDevice);
+            mPreparer.setUp(mMockDevice, info);
+            EasyMock.verify(mMockDevice);
+            // The x86 folder was not filtered
+            Set<String> capValue = capture.getValue();
+            assertFalse(capValue.contains("x86_64"));
+        } finally {
+            FileUtil.recursiveDelete(tmpFolder);
+        }
+    }
+
+    /**
      * Test that if a binary name is repeating in another directory that is searched first we don't
      * use it and do prioritize the module name directory.
      */
diff --git a/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java
index 70187a2..c5bbb06 100644
--- a/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java
@@ -27,6 +27,7 @@
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -35,6 +36,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Map;
+import java.util.HashMap;
+
 /** Unit tests for {@link SwitchUserTargetPreparer}. */
 @RunWith(JUnit4.class)
 public class SwitchUserTargetPreparerTest {
@@ -53,26 +57,97 @@
     }
 
     @Test
-    public void testSetUpRunAsPrimary_ifAlreadyInPrimary_switchToPrimary()
+    public void testSetUpRunAsPrimary_ifAlreadyInPrimary_noSwitch()
             throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
-        // setup
-        mockUsers(/* primaryUserId= */ 11, /* currentUserId= */ 11);
         mOptionSetter.setOptionValue("user-type", "primary");
 
+        // setup
+        when(mMockDevice.getCurrentUser()).thenReturn(11);
+        mockListUsersInfo(
+                mMockDevice,
+                /* userIds= */ new Integer[] {0, 11},
+                /* flags= */ new Integer[] {0, UserInfo.FLAG_PRIMARY});
+
+
         // act
         mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
 
         // assert
-        verify(mMockDevice, times(1)).switchUser(11);
+        verify(mMockDevice, never()).switchUser(anyInt());
     }
 
     @Test
-    public void testSetUpRunAsSystem_ifAlreadyInSystem_switchToSystem()
+    public void testSetUpRunAsSystem_ifAlreadyInSystem_noSwitch()
             throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
-        // setup
-        mockUsers(/* primaryUserId= */ 11, /* currentUserId= */ USER_SYSTEM);
         mOptionSetter.setOptionValue("user-type", "system");
 
+        // setup
+        when(mMockDevice.getCurrentUser()).thenReturn(0);
+        mockListUsersInfo(
+                mMockDevice,
+                /* userIds= */ new Integer[] {0, 11},
+                /* flags= */ new Integer[] {0, UserInfo.FLAG_PRIMARY});
+
+        // act
+        mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
+
+        // assert
+        verify(mMockDevice, never()).switchUser(0);
+    }
+
+    @Test
+    public void testSetUpRunAsPrimary_ifNotInPrimary_switchToPrimary()
+            throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
+        mOptionSetter.setOptionValue("user-type", "primary");
+
+        // setup
+        when(mMockDevice.getCurrentUser()).thenReturn(11);
+        mockListUsersInfo(
+                mMockDevice,
+                /* userIds= */ new Integer[] {0, 10, 11},
+                /* flags= */ new Integer[] {0, UserInfo.FLAG_PRIMARY, 0});
+        when(mMockDevice.switchUser(10)).thenReturn(true);
+
+        // act
+        mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
+
+        // assert
+        verify(mMockDevice, times(1)).switchUser(10);
+    }
+
+    @Test
+    public void testSetUpRunAsGuest_ifNotInGuest_switchToGuest()
+            throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
+        mOptionSetter.setOptionValue("user-type", "guest");
+
+        // setup
+        when(mMockDevice.getCurrentUser()).thenReturn(11);
+        mockListUsersInfo(
+                mMockDevice,
+                /* userIds= */ new Integer[] {0, 10, 11},
+                /* flags= */ new Integer[] {0, UserInfo.FLAG_GUEST, 0});
+        when(mMockDevice.switchUser(10)).thenReturn(true);
+
+        // act
+        mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
+
+        // assert
+        verify(mMockDevice, times(1)).switchUser(10);
+    }
+
+    @Test
+    public void testSetUpRunAsSystem_ifNotInSystem_switchToSystem()
+            throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
+        mOptionSetter.setOptionValue("user-type", "system");
+
+        // setup
+        when(mMockDevice.getCurrentUser()).thenReturn(10);
+        mockListUsersInfo(
+                mMockDevice,
+                /* userIds= */ new Integer[] {0, 10},
+                /* flags= */ new Integer[] {0, 0});
+        when(mMockDevice.switchUser(0)).thenReturn(true);
+
         // act
         mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
 
@@ -81,39 +156,18 @@
     }
 
     @Test
-    public void testSetUpRunAsPrimary_ifNotInPrimary_switchToPrimary()
-            throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
-        // setup
-        mockUsers(/* primaryUserId= */ 10, /* currentUserId= */ 11);
-        mOptionSetter.setOptionValue("user-type", "primary");
-
-        // act
-        mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
-
-        // assert it switches to primary in setUp
-        verify(mMockDevice, times(1)).switchUser(10);
-    }
-
-    @Test
-    public void testSetUpRunAsSystem_ifNotInSystem_switchToSystem()
-            throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
-        // setup
-        mockUsers(/* primaryUserId= */ 10, /* currentUserId= */ 11);
-        mOptionSetter.setOptionValue("user-type", "system");
-
-        // act
-        mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
-
-        // assert it switches to primary in setUp
-        verify(mMockDevice, times(1)).switchUser(USER_SYSTEM);
-    }
-
-    @Test
     public void testTearDown_ifStartedInSecondary_switchesBackToSecondary()
             throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
+        mOptionSetter.setOptionValue("user-type", "system");
+
         // setup
-        mockUsers(/* primaryUserId= */ 0, /* currentUserId= */ 10);
-        mOptionSetter.setOptionValue("user-type", "primary");
+        when(mMockDevice.getCurrentUser()).thenReturn(10);
+        mockListUsersInfo(
+                mMockDevice,
+                /* userIds= */ new Integer[] {0, 10},
+                /* flags= */ new Integer[] {0, 0});
+        when(mMockDevice.switchUser(0)).thenReturn(true);
+        when(mMockDevice.switchUser(10)).thenReturn(true);
 
         // first switches to primary
         mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
@@ -122,13 +176,18 @@
         // then switches back to secondary
         mSwitchUserTargetPreparer.tearDown(mMockDevice, /* buildInfo= */ null, null);
         verify(mMockDevice, times(1)).switchUser(10);
+
     }
 
     @Test
     public void testSetUp_ifNoSwitchToSpecified_noUserSwitch()
             throws DeviceNotAvailableException, TargetSetupError {
         // setup
-        mockUsers(/* primaryUserId= */ 0, /* currentUserId= */ 10);
+        when(mMockDevice.getCurrentUser()).thenReturn(10);
+        mockListUsersInfo(
+                mMockDevice,
+                /* userIds= */ new Integer[] {0, 10},
+                /* flags= */ new Integer[] {0, 0});
 
         // act
         mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
@@ -140,9 +199,14 @@
     @Test
     public void testSetUp_ifSwitchFails_throwsTargetSetupError()
             throws DeviceNotAvailableException, ConfigurationException {
-        // setup
-        mockUsers(/* primaryUserId= */ 0, /* currentUserId= */ 11);
         mOptionSetter.setOptionValue("user-type", "primary");
+
+        // setup
+        when(mMockDevice.getCurrentUser()).thenReturn(10);
+        mockListUsersInfo(
+                mMockDevice,
+                /* userIds= */ new Integer[] {0, 10},
+                /* flags= */ new Integer[] {UserInfo.FLAG_PRIMARY, 0});
         when(mMockDevice.switchUser(0)).thenReturn(false);
 
         // act
@@ -157,7 +221,24 @@
     private void mockUsers(int primaryUserId, int currentUserId)
             throws DeviceNotAvailableException {
         when(mMockDevice.getCurrentUser()).thenReturn(currentUserId);
+
         when(mMockDevice.getPrimaryUserId()).thenReturn(primaryUserId);
         when(mMockDevice.switchUser(anyInt())).thenReturn(true);
     }
+
+    private void mockListUsersInfo(ITestDevice device, Integer[] userIds, Integer[] flags)
+            throws DeviceNotAvailableException {
+        Map<Integer, UserInfo> result = new HashMap<>();
+        for (int i = 0; i < userIds.length; i++) {
+            int userId = userIds[i];
+            result.put(
+                    userId,
+                    new UserInfo(
+                            /* userId= */ userId,
+                            /* userName= */ "usr" + userId,
+                            /* flag= */ flags[i],
+                            /* isRunning= */ false));
+        }
+        when(device.getUserInfos()).thenReturn(result);
+    }
 }
diff --git a/tests/src/com/android/tradefed/targetprep/app/NoApkTestSkipperTest.java b/tests/src/com/android/tradefed/targetprep/app/NoApkTestSkipperTest.java
index 3171040..d022139 100644
--- a/tests/src/com/android/tradefed/targetprep/app/NoApkTestSkipperTest.java
+++ b/tests/src/com/android/tradefed/targetprep/app/NoApkTestSkipperTest.java
@@ -20,7 +20,6 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tradefed.build.AppBuildInfo;
-import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.device.ITestDevice;
@@ -52,11 +51,6 @@
     }
 
     @Test
-    public void testNotAppBuild() throws Exception {
-        mSkipper.setUp(mMockDevice, new BuildInfo());
-    }
-
-    @Test
     public void testApksPresent() throws Exception {
         mAppBuildInfo.addAppPackageFile(new File("fakepackage"), "v2");
         mSkipper.setUp(mMockDevice, mAppBuildInfo);
diff --git a/tests/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparerTest.java b/tests/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparerTest.java
new file mode 100644
index 0000000..324d5e5
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparerTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep.multi;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.doAnswer;
+
+import com.android.tradefed.build.DeviceBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.device.CollectingOutputReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import org.junit.After;
+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.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/** Unit tests for {@link com.android.tradefed.targetprep.multi.DynamicSystemPreparer}. */
+@RunWith(JUnit4.class)
+public class DynamicSystemPreparerTest {
+    // Input build info.
+    private static final String SYSTEM_IMAGE_NAME = "system.img";
+
+    private IInvocationContext mMockContext;
+    private IDeviceBuildInfo mSystemBuild;
+    private ITestDevice mMockDevice;
+    private File mSystemImageZip;
+    // The object under test.
+    private DynamicSystemPreparer mPreparer;
+
+    @Before
+    public void setUp() throws IOException {
+        mMockContext = Mockito.mock(InvocationContext.class);
+
+        mMockDevice = Mockito.mock(ITestDevice.class);
+        ITestDevice mockSystem = Mockito.mock(ITestDevice.class);
+        mSystemImageZip = createImageZip(SYSTEM_IMAGE_NAME);
+        mSystemBuild = createDeviceBuildInfo(mSystemImageZip);
+
+        Mockito.when(mMockContext.getDevice("device")).thenReturn(mMockDevice);
+        Mockito.when(mMockContext.getDevice("system")).thenReturn(mockSystem);
+        Mockito.when(mMockContext.getBuildInfo(mockSystem)).thenReturn(mSystemBuild);
+
+        mPreparer = new DynamicSystemPreparer();
+    }
+
+    @After
+    public void tearDown() {
+        if (mSystemBuild != null) {
+            mSystemBuild.cleanUp();
+            mSystemBuild = null;
+        }
+        FileUtil.deleteFile(mSystemImageZip);
+    }
+
+    private IDeviceBuildInfo createDeviceBuildInfo(File imageZip) {
+        IDeviceBuildInfo buildInfo = new DeviceBuildInfo();
+        buildInfo.setDeviceImageFile(imageZip, "");
+        return buildInfo;
+    }
+
+    private File createImageDir(String... fileNames) throws IOException {
+        File tempDir = FileUtil.createTempDir("createImageDir");
+        for (String fileName : fileNames) {
+            new File(tempDir, fileName).createNewFile();
+        }
+        return tempDir;
+    }
+
+    private File createImageZip(String... fileNames) throws IOException {
+        File tempDir = null;
+        try {
+            tempDir = createImageDir(fileNames);
+
+            ArrayList<File> tempFiles = new ArrayList<File>(fileNames.length);
+            for (String fileName : fileNames) {
+                tempFiles.add(new File(tempDir, fileName));
+            }
+
+            return ZipUtil.createZip(tempFiles);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    @Test
+    public void testSetUp()
+            throws TargetSetupError, BuildError, DeviceNotAvailableException, IOException {
+
+        File systemgz = new File("system.raw.gz");
+        Mockito.when(mMockDevice.pushFile(systemgz, "/sdcard/system.raw.gz"))
+                .thenReturn(Boolean.TRUE);
+        doAnswer(
+                        new Answer<Object>() {
+                            @Override
+                            public Object answer(InvocationOnMock invocation) {
+                                byte[] outputBytes = "running".getBytes();
+                                ((CollectingOutputReceiver) invocation.getArguments()[1])
+                                        .addOutput(outputBytes, 0, outputBytes.length);
+                                return null;
+                            }
+                        })
+                .when(mMockDevice)
+                .executeShellCommand(
+                        matches("gsi_tool status"), any(CollectingOutputReceiver.class));
+        CommandResult res = new CommandResult();
+        res.setStdout("");
+        res.setStatus(CommandStatus.SUCCESS);
+        Mockito.when(mMockDevice.executeShellV2Command("gsi_tool enable")).thenReturn(res);
+        mPreparer.setUp(mMockContext);
+    }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java b/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java
new file mode 100644
index 0000000..3664ea4
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep.multi;
+
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.build.DeviceBuildInfo;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.targetprep.multi.MixImageZipPreparer.InputStreamFactory;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.ZipUtil;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link MixImageZipPreparer}. */
+@RunWith(JUnit4.class)
+public class MixImageZipPreparerTest {
+    // Input build info.
+    private static final String VENDOR_IMAGE_NAME = "vendor.img";
+    private static final String SYSTEM_IMAGE_NAME = "system.img";
+    private static final String VBMETA_IMAGE_NAME = "vbmeta.img";
+    private static final String SYSTEM_BUILD_FLAVOR = "system_flavor";
+    private static final String SYSTEM_BUILD_ID = "123456";
+    private static final String DEVICE_LABEL = "device";
+    private static final String SYSTEM_LABEL = "system";
+    private static final String RESOURCE_LABEL = "resource";
+
+    // The strings written to temporary image files.
+    private static final String DEVICE_CONTENT = "device content";
+    private static final String SYSTEM_CONTENT = "system content";
+    private static final String RESOURCE_CONTENT = "resource content";
+
+    private IInvocationContext mMockContext;
+    private IDeviceBuildInfo mDeviceBuild;
+    private IDeviceBuildInfo mSystemBuild;
+    private IBuildInfo mResourceBuild;
+    private File mDeviceImageZip;
+    private File mSystemImageZip;
+    private File mResourceDir;
+
+    // The object under test.
+    private MixImageZipPreparer mPreparer;
+
+    private static class ByteArrayInputStreamFactory implements InputStreamFactory {
+        private final byte[] mData;
+        private List<InputStream> createdInputStreams;
+
+        private ByteArrayInputStreamFactory(String data) {
+            mData = data.getBytes();
+            createdInputStreams = new ArrayList<InputStream>();
+        }
+
+        @Override
+        public InputStream createInputStream() throws IOException {
+            InputStream stream = Mockito.spy(new ByteArrayInputStream(mData));
+            createdInputStreams.add(stream);
+            return stream;
+        }
+
+        @Override
+        public long getSize() {
+            return mData.length;
+        }
+
+        @Override
+        public long getCrc32() throws IOException {
+            // calculateCrc32 closes the stream.
+            return StreamUtil.calculateCrc32(createInputStream());
+        }
+    }
+
+    private void setUpPreparer() throws IOException {
+        mMockContext = Mockito.mock(InvocationContext.class);
+
+        ITestDevice mockDevice = Mockito.mock(ITestDevice.class);
+        ITestDevice mockSystem = Mockito.mock(ITestDevice.class);
+        mDeviceImageZip =
+                createImageZip(
+                        DEVICE_CONTENT, VENDOR_IMAGE_NAME, SYSTEM_IMAGE_NAME, VBMETA_IMAGE_NAME);
+        mSystemImageZip = createImageZip(SYSTEM_CONTENT, SYSTEM_IMAGE_NAME);
+        mDeviceBuild = createDeviceBuildInfo("device_flavor", "device_build_id", mDeviceImageZip);
+        mSystemBuild = createDeviceBuildInfo(SYSTEM_BUILD_FLAVOR, SYSTEM_BUILD_ID, mSystemImageZip);
+
+        Mockito.when(mMockContext.getDevice(DEVICE_LABEL)).thenReturn(mockDevice);
+        Mockito.when(mMockContext.getBuildInfo(mockDevice)).thenReturn(mDeviceBuild);
+        Mockito.when(mMockContext.getDevice(SYSTEM_LABEL)).thenReturn(mockSystem);
+        Mockito.when(mMockContext.getBuildInfo(mockSystem)).thenReturn(mSystemBuild);
+
+        mPreparer = new MixImageZipPreparer();
+        mPreparer.addSystemFileName(SYSTEM_IMAGE_NAME);
+    }
+
+    private void setUpResource() throws IOException {
+        ITestDevice mockResource = Mockito.mock(ITestDevice.class);
+        mResourceDir = createImageDir(RESOURCE_CONTENT, VBMETA_IMAGE_NAME);
+        mResourceBuild = createBuildInfo(mResourceDir);
+
+        Mockito.when(mMockContext.getDevice(RESOURCE_LABEL)).thenReturn(mockResource);
+        Mockito.when(mMockContext.getBuildInfo(mockResource)).thenReturn(mResourceBuild);
+
+        mPreparer.addResourceFileName(VBMETA_IMAGE_NAME);
+    }
+
+    @After
+    public void tearDown() {
+        if (mDeviceBuild != null) {
+            mDeviceBuild.cleanUp();
+            mDeviceBuild = null;
+        }
+        if (mSystemBuild != null) {
+            mSystemBuild.cleanUp();
+            mSystemBuild = null;
+        }
+        if (mResourceBuild != null) {
+            mResourceBuild.cleanUp();
+            mResourceBuild = null;
+        }
+        if (mDeviceImageZip != null) {
+            mDeviceImageZip.delete();
+            mDeviceImageZip = null;
+        }
+        if (mSystemImageZip != null) {
+            mSystemImageZip.delete();
+            mSystemImageZip = null;
+        }
+        if (mResourceDir != null) {
+            FileUtil.recursiveDelete(mResourceDir);
+            mResourceDir = null;
+        }
+    }
+
+    private IDeviceBuildInfo createDeviceBuildInfo(
+            String buildFlavor, String buildId, File imageZip) {
+        IDeviceBuildInfo buildInfo = new DeviceBuildInfo();
+        buildInfo.setBuildFlavor(buildFlavor);
+        buildInfo.setBuildId(buildId);
+        buildInfo.setDeviceImageFile(imageZip, buildId);
+        return buildInfo;
+    }
+
+    private IBuildInfo createBuildInfo(File rootDir) {
+        BuildInfo buildInfo = new BuildInfo();
+        for (File file : rootDir.listFiles()) {
+            buildInfo.setFile(file.getName(), file, "0");
+        }
+        return buildInfo;
+    }
+
+    private File createImageDir(String content, String... fileNames) throws IOException {
+        File tempDir = FileUtil.createTempDir("createImageDir");
+        for (String fileName : fileNames) {
+            try (FileWriter writer = new FileWriter(new File(tempDir, fileName))) {
+                writer.write(content);
+            }
+        }
+        return tempDir;
+    }
+
+    private File createImageZip(String content, String... fileNames) throws IOException {
+        // = new ArrayList<File>(fileNames.length);
+        File tempDir = null;
+        try {
+            tempDir = createImageDir(content, fileNames);
+
+            ArrayList<File> tempFiles = new ArrayList<File>(fileNames.length);
+            for (String fileName : fileNames) {
+                tempFiles.add(new File(tempDir, fileName));
+            }
+
+            return ZipUtil.createZip(tempFiles);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    private void verifyImage(String content, File dir, String fileName)
+            throws FileNotFoundException, IOException {
+        try (FileReader reader = new FileReader(new File(dir, fileName))) {
+            char[] buffer = new char[content.length()];
+            reader.read(buffer);
+            Assert.assertEquals(content, new String(buffer));
+            Assert.assertTrue("Image contains extra content.", reader.read() < 0);
+        }
+    }
+
+    private void verifyImageZip(File imageZip) throws FileNotFoundException, IOException {
+        File mixedImageDir = ZipUtil.extractZipToTemp(imageZip, "verifyImageZip");
+        try {
+            verifyImage(DEVICE_CONTENT, mixedImageDir, VENDOR_IMAGE_NAME);
+            verifyImage(SYSTEM_CONTENT, mixedImageDir, SYSTEM_IMAGE_NAME);
+            if (mResourceBuild != null) {
+                verifyImage(RESOURCE_CONTENT, mixedImageDir, VBMETA_IMAGE_NAME);
+            }
+        } finally {
+            FileUtil.recursiveDelete(mixedImageDir);
+        }
+    }
+
+    private void runPreparerTest()
+            throws TargetSetupError, BuildError, DeviceNotAvailableException, ZipException,
+                    IOException {
+        mPreparer.setUp(mMockContext);
+
+        ArgumentCaptor<IBuildInfo> argument = ArgumentCaptor.forClass(IBuildInfo.class);
+        Mockito.verify(mMockContext)
+                .addDeviceBuildInfo(Mockito.eq(DEVICE_LABEL), argument.capture());
+        IDeviceBuildInfo addedBuildInfo = ((IDeviceBuildInfo) argument.getValue());
+        try {
+            Assert.assertFalse("Device build is not cleaned up.", mDeviceImageZip.exists());
+            mDeviceImageZip = null;
+            mDeviceBuild = null;
+
+            Assert.assertEquals(SYSTEM_BUILD_FLAVOR, addedBuildInfo.getBuildFlavor());
+            Assert.assertEquals(SYSTEM_BUILD_ID, addedBuildInfo.getDeviceBuildId());
+            verifyImageZip(addedBuildInfo.getDeviceImageFile());
+        } finally {
+            addedBuildInfo.cleanUp();
+        }
+    }
+
+    /**
+     * Test that the mixed {@link IDeviceBuildInfo} contains the resource file and works with
+     * non-default compression level.
+     */
+    @Test
+    public void testSetUpWithResource()
+            throws TargetSetupError, BuildError, DeviceNotAvailableException, IOException {
+        setUpPreparer();
+        setUpResource();
+        mPreparer.setCompressionLevel(0);
+        runPreparerTest();
+    }
+
+    /**
+     * Test that the mixed {@link IDeviceBuildInfo} contains the system build's image, build flavor,
+     * and build id.
+     */
+    @Test
+    public void testSetUpWithSystem()
+            throws TargetSetupError, BuildError, DeviceNotAvailableException, IOException {
+        setUpPreparer();
+        runPreparerTest();
+    }
+
+    private void runCreateZipTest(int compressionLevel) throws IOException {
+        Map<String, ByteArrayInputStreamFactory> data =
+                new HashMap<String, ByteArrayInputStreamFactory>();
+        data.put("entry1", new ByteArrayInputStreamFactory("abcabcabcabcabcabc"));
+        data.put("entry2", new ByteArrayInputStreamFactory("01230123012301230123"));
+
+        File file = null;
+        ZipFile zipFile = null;
+        try {
+            file = MixImageZipPreparer.createZip(data, compressionLevel);
+            zipFile = new ZipFile(file);
+
+            Assert.assertEquals(data.size(), zipFile.stream().count());
+            for (Map.Entry<String, ByteArrayInputStreamFactory> entry : data.entrySet()) {
+                ByteArrayInputStreamFactory expected = entry.getValue();
+                ZipEntry actual = zipFile.getEntry(entry.getKey());
+                Assert.assertEquals(expected.getSize(), actual.getSize());
+                Assert.assertEquals(expected.getCrc32(), actual.getCrc());
+                if (compressionLevel == Deflater.NO_COMPRESSION) {
+                    Assert.assertEquals(expected.getSize(), actual.getCompressedSize());
+                } else {
+                    Assert.assertTrue(expected.getSize() > actual.getCompressedSize());
+                }
+
+                for (InputStream stream : expected.createdInputStreams) {
+                    Mockito.verify(stream).close();
+                }
+            }
+        } finally {
+            ZipUtil.closeZip(zipFile);
+            FileUtil.deleteFile(file);
+        }
+    }
+
+    /** Verify createZip with default compression level. */
+    @Test
+    public void testCreateZip() throws IOException {
+        runCreateZipTest(Deflater.DEFAULT_COMPRESSION);
+    }
+
+    /** Verify createZip with no compression. */
+    @Test
+    public void testCreateZipWithNoCompression() throws IOException {
+        runCreateZipTest(Deflater.NO_COMPRESSION);
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java b/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java
index 82ef7c9..3e691b8 100644
--- a/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java
@@ -68,7 +68,7 @@
                             new DynamicRemoteFileResolver() {
                                 @Override
                                 protected IRemoteFileResolver getResolver(String protocol) {
-                                    if (protocol.equals(GcsRemoteFileResolver.PROTOCOL)) {
+                                    if (GcsRemoteFileResolver.PROTOCOL.equals(protocol)) {
                                         IRemoteFileResolver mockResolver =
                                                 Mockito.mock(IRemoteFileResolver.class);
                                         try {
@@ -76,6 +76,7 @@
                                                     .when(mockResolver)
                                                     .resolveRemoteFiles(
                                                             Mockito.eq(FAKE_REMOTE_FILE_PATH),
+                                                            Mockito.any(),
                                                             Mockito.any());
                                             return mockResolver;
                                         } catch (ConfigurationException e) {
diff --git a/tests/src/com/android/tradefed/testtype/GTestBaseTest.java b/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
index fa34c71..27a816f 100644
--- a/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
@@ -195,7 +195,7 @@
     public void testCoverage_addsCodeCoverageListener() throws ConfigurationException {
         GTestBase gTestBase = new GTestBaseImpl();
         mSetter = new OptionSetter(gTestBase);
-        mSetter.setOptionValue("native-coverage", "true");
+        mSetter.setOptionValue("coverage", "true");
 
         ITestInvocationListener listener =
                 gTestBase.addNativeCoverageListenerIfEnabled(mMockTestDevice, mMockListener);
@@ -208,7 +208,7 @@
     public void testNoCoverage_doesNotAddCodeCoverageListener() throws ConfigurationException {
         GTestBase gTestBase = new GTestBaseImpl();
         mSetter = new OptionSetter(gTestBase);
-        mSetter.setOptionValue("native-coverage", "false");
+        mSetter.setOptionValue("coverage", "false");
 
         ITestInvocationListener listener =
                 gTestBase.addNativeCoverageListenerIfEnabled(mMockTestDevice, mMockListener);
diff --git a/tests/src/com/android/tradefed/testtype/GTestTest.java b/tests/src/com/android/tradefed/testtype/GTestTest.java
index f47190f..56a2b6f 100644
--- a/tests/src/com/android/tradefed/testtype/GTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestTest.java
@@ -36,6 +36,8 @@
 import org.junit.runners.JUnit4;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 
@@ -140,6 +142,48 @@
 
         String[] files = new String[] {"test1", "test2"};
         EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
+        mMockITestDevice.executeShellCommand(
+                EasyMock.contains(test1),
+                EasyMock.same(mMockReceiver),
+                EasyMock.anyLong(),
+                (TimeUnit) EasyMock.anyObject(),
+                EasyMock.anyInt());
+        mMockITestDevice.executeShellCommand(
+                EasyMock.contains(test2),
+                EasyMock.same(mMockReceiver),
+                EasyMock.anyLong(),
+                (TimeUnit) EasyMock.anyObject(),
+                EasyMock.anyInt());
+
+        replayMocks();
+
+        mGTest.run(mMockInvocationListener);
+        verifyMocks();
+    }
+
+    /** Test the run method without clearing coverage before running the tests. */
+    @Test
+    public void testRunNoCoverageClear() throws Exception {
+        mSetter.setOptionValue("coverage-clear-before-test", "false");
+
+        final String nativeTestPath = GTest.DEFAULT_NATIVETEST_PATH;
+        final String test1 = "test1";
+        final String test2 = "test2";
+        final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
+        final String testPath2 = String.format("%s/%s", nativeTestPath, test2);
+
+        MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1, test2);
+        EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+        // report the file as executable
+        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
+        // report the file as executable
+        EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
+
+        String[] files = new String[] {"test1", "test2"};
+        EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
         mMockITestDevice.executeShellCommand(EasyMock.contains(test1),
                 EasyMock.same(mMockReceiver), EasyMock.anyLong(),
                 (TimeUnit)EasyMock.anyObject(), EasyMock.anyInt());
@@ -426,6 +470,121 @@
         verifyMocks();
     }
 
+    /** Test cross-process coverage dump for all native processes */
+    @Test
+    public void testNativeCoverageAllProcesses() throws Exception {
+        mSetter.setOptionValue("coverage", "true");
+        mSetter.setOptionValue("coverage-flush", "true");
+
+        final String nativeTestPath = GTest.DEFAULT_NATIVETEST_PATH;
+        final String test1 = "test1";
+        final String test2 = "test2";
+        final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
+        final String testPath2 = String.format("%s/%s", nativeTestPath, test2);
+
+        MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1, test2);
+        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
+        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+        EasyMock.expect(mMockITestDevice.executeShellCommand("rm -rf /data/misc/trace/*"))
+                .andReturn("");
+        EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+        // report the file as executable
+        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
+        // report the file as executable
+        EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
+
+        String[] files = new String[] {"test1", "test2"};
+        EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
+        mMockITestDevice.executeShellCommand(
+                EasyMock.contains(test1),
+                EasyMock.same(mMockReceiver),
+                EasyMock.anyLong(),
+                (TimeUnit) EasyMock.anyObject(),
+                EasyMock.anyInt());
+        mMockITestDevice.executeShellCommand(
+                EasyMock.contains(test2),
+                EasyMock.same(mMockReceiver),
+                EasyMock.anyLong(),
+                (TimeUnit) EasyMock.anyObject(),
+                EasyMock.anyInt());
+
+        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
+
+        replayMocks();
+
+        mGTest.run(mMockInvocationListener);
+        verifyMocks();
+    }
+
+    /** Test cross-process coverage dump for specific processes */
+    @Test
+    public void testNativeCoverageSpecificProcesses() throws Exception {
+        mSetter.setOptionValue("coverage", "true");
+        mSetter.setOptionValue("coverage-flush", "true");
+
+        final List<String> processNames = new ArrayList<>();
+        processNames.add("init");
+        processNames.add("surfaceflinger");
+
+        mGTest.setCoverageProcesses(processNames);
+
+        final String nativeTestPath = GTest.DEFAULT_NATIVETEST_PATH;
+        final String test1 = "test1";
+        final String test2 = "test2";
+        final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
+        final String testPath2 = String.format("%s/%s", nativeTestPath, test2);
+
+        MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1, test2);
+        // Get the pids to flush coverage data.
+        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(0))).andReturn("1");
+        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(1))).andReturn("1000");
+        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 1 1000")).andReturn("");
+
+        // Clear the coverage data.
+        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+        EasyMock.expect(mMockITestDevice.executeShellCommand("rm -rf /data/misc/trace/*"))
+                .andReturn("");
+        EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+        // report the file as executable
+        EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+        EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
+        // report the file as executable
+        EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
+
+        String[] files = new String[] {"test1", "test2"};
+        EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
+        mMockITestDevice.executeShellCommand(
+                EasyMock.contains(test1),
+                EasyMock.same(mMockReceiver),
+                EasyMock.anyLong(),
+                (TimeUnit) EasyMock.anyObject(),
+                EasyMock.anyInt());
+        mMockITestDevice.executeShellCommand(
+                EasyMock.contains(test2),
+                EasyMock.same(mMockReceiver),
+                EasyMock.anyLong(),
+                (TimeUnit) EasyMock.anyObject(),
+                EasyMock.anyInt());
+
+        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(0))).andReturn("1");
+        EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(1))).andReturn("1000");
+        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 1 1000")).andReturn("");
+
+        replayMocks();
+
+        mGTest.run(mMockInvocationListener);
+        verifyMocks();
+    }
+
     @Test
     public void testGetFileName() {
         String expected = "bar";
diff --git a/tests/src/com/android/tradefed/testtype/HostTestTest.java b/tests/src/com/android/tradefed/testtype/HostTestTest.java
index a5d8387..0b63d72 100644
--- a/tests/src/com/android/tradefed/testtype/HostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/HostTestTest.java
@@ -504,7 +504,7 @@
                             new DynamicRemoteFileResolver() {
                                 @Override
                                 protected IRemoteFileResolver getResolver(String protocol) {
-                                    if (protocol.equals(GcsRemoteFileResolver.PROTOCOL)) {
+                                    if (GcsRemoteFileResolver.PROTOCOL.equals(protocol)) {
                                         return mRemoteFileResolver;
                                     }
                                     return null;
@@ -672,7 +672,8 @@
     public void testRun_junit3TestSuite_dynamicOptions() throws Exception {
         doReturn(new File("/downloaded/somewhere"))
                 .when(mMockResolver)
-                .resolveRemoteFiles(Mockito.eq(FAKE_REMOTE_FILE_PATH), Mockito.any());
+                .resolveRemoteFiles(
+                        Mockito.eq(FAKE_REMOTE_FILE_PATH), Mockito.any(), Mockito.any());
         mHostTest.setClassName(DynamicTestCase.class.getName());
         TestDescription test1 = new TestDescription(DynamicTestCase.class.getName(), "testPass");
         mListener.testRunStarted((String) EasyMock.anyObject(), EasyMock.eq(1));
diff --git a/tests/src/com/android/tradefed/testtype/InstalledInstrumentationsTestTest.java b/tests/src/com/android/tradefed/testtype/InstalledInstrumentationsTestTest.java
index 9f955cb..cd318f0 100644
--- a/tests/src/com/android/tradefed/testtype/InstalledInstrumentationsTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstalledInstrumentationsTestTest.java
@@ -23,10 +23,8 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.metric.BaseDeviceMetricCollector;
 import com.android.tradefed.device.metric.IMetricCollector;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 
-import org.easymock.Capture;
 import org.easymock.EasyMock;
 import org.easymock.IAnswer;
 import org.junit.Before;
@@ -36,7 +34,6 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
 
 /** Unit tests for {@link InstalledInstrumentationsTest}. */
@@ -71,12 +68,8 @@
         injectShellResponse(String.format(INSTR_OUTPUT_FORMAT, TEST_PKG, TEST_RUNNER,
                 TEST_COVERAGE_TARGET), 1);
 
-        mMockListener.testRunStarted(TEST_PKG, 0);
-        Capture<HashMap<String, Metric>> captureMetrics = new Capture<>();
-        mMockListener.testRunEnded(EasyMock.anyLong(), EasyMock.capture(captureMetrics));
         ArgsOptionParser p = new ArgsOptionParser(mInstalledInstrTest);
         p.parse("--size", "small", "--force-abi", ABI);
-        mInstalledInstrTest.setSendCoverage(true);
         EasyMock.replay(mMockTestDevice, mMockListener);
         mInstalledInstrTest.run(mMockListener);
         assertEquals(1, mMockInstrumentationTests.size());
@@ -84,13 +77,6 @@
         assertEquals(mMockListener, mockInstrumentationTest.getListener());
         assertEquals(TEST_PKG, mockInstrumentationTest.getPackageName());
         assertEquals(TEST_RUNNER, mockInstrumentationTest.getRunnerName());
-        assertEquals(
-                TEST_COVERAGE_TARGET,
-                captureMetrics
-                        .getValue()
-                        .get(InstalledInstrumentationsTest.COVERAGE_TARGET_KEY)
-                        .getMeasurements()
-                        .getSingleString());
         assertEquals("small", mockInstrumentationTest.getTestSize());
         assertEquals(ABI, mockInstrumentationTest.getForceAbi());
 
@@ -156,12 +142,8 @@
         injectShellResponse(
                 String.format(INSTR_OUTPUT_FORMAT, TEST_PKG, TEST_RUNNER, TEST_COVERAGE_TARGET), 1);
 
-        mMockListener.testRunStarted(TEST_PKG, 0);
-        Capture<HashMap<String, Metric>> captureMetrics = new Capture<>();
-        mMockListener.testRunEnded(EasyMock.anyLong(), EasyMock.capture(captureMetrics));
         ArgsOptionParser p = new ArgsOptionParser(mInstalledInstrTest);
         p.parse("--size", "small", "--force-abi", ABI);
-        mInstalledInstrTest.setSendCoverage(true);
         List<IMetricCollector> collectors = new ArrayList<>();
         collectors.add(new BaseDeviceMetricCollector());
         mInstalledInstrTest.setMetricCollectors(collectors);
@@ -172,13 +154,6 @@
         assertEquals(mMockListener, mockInstrumentationTest.getListener());
         assertEquals(TEST_PKG, mockInstrumentationTest.getPackageName());
         assertEquals(TEST_RUNNER, mockInstrumentationTest.getRunnerName());
-        assertEquals(
-                TEST_COVERAGE_TARGET,
-                captureMetrics
-                        .getValue()
-                        .get(InstalledInstrumentationsTest.COVERAGE_TARGET_KEY)
-                        .getMeasurements()
-                        .getSingleString());
         assertEquals("small", mockInstrumentationTest.getTestSize());
         assertEquals(ABI, mockInstrumentationTest.getForceAbi());
         assertEquals(1, mockInstrumentationTest.getCollectors().size());
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
index f00cab1..e91fb88 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
@@ -307,6 +307,7 @@
                     }
                 };
         setRunTestExpectations(secdondSerialRunAnswer);
+        mMockTestDevice.waitForDeviceAvailable();
 
         mInstrumentationFileTest = new InstrumentationFileTest(mMockITest, testsList, true, -1) {
             @Override
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
index c5d0f27..aaef451 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.ArgumentMatchers.anyCollectionOf;
 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;
@@ -42,6 +43,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.metric.IMetricCollector;
+import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.CollectingTestListener;
@@ -530,6 +532,8 @@
                 .when(mMockTestDevice)
                 .runInstrumentationTests(
                         any(IRemoteAndroidTestRunner.class), any(ITestLifeCycleReceiver.class));
+        doReturn(true).when(mMockTestDevice).enableAdbRoot();
+        doReturn("").when(mMockTestDevice).executeShellCommand(anyString());
 
         mInstrumentationTest.run(mMockListener);
 
@@ -922,6 +926,9 @@
         ITestInvocationListener listener =
                 mInstrumentationTest.addJavaCoverageListenerIfEnabled(mMockListener);
         assertThat(listener).isInstanceOf(JavaCodeCoverageListener.class);
+
+        listener = mInstrumentationTest.addNativeCoverageListenerIfEnabled(mMockListener);
+        assertThat(listener).isInstanceOf(NativeCodeCoverageListener.class);
     }
 
     @Test
@@ -931,6 +938,9 @@
         ITestInvocationListener listener =
                 mInstrumentationTest.addJavaCoverageListenerIfEnabled(mMockListener);
         assertThat(listener).isSameAs(mMockListener);
+
+        listener = mInstrumentationTest.addNativeCoverageListenerIfEnabled(mMockListener);
+        assertThat(listener).isSameAs(mMockListener);
     }
 
     /** Test normal run scenario when {@link IMetricCollector} are specified. */
@@ -955,10 +965,13 @@
 
         List<IMetricCollector> collectors = new ArrayList<>();
         CalledMetricCollector calledCollector = new CalledMetricCollector();
+        calledCollector.mName = "called";
         CalledMetricCollector notCalledCollector = new CalledMetricCollector();
         notCalledCollector.setDisable(true);
+        notCalledCollector.mName = "not-called";
         collectors.add(notCalledCollector);
         collectors.add(calledCollector);
+        mInstrumentationTest.setInvocationContext(new InvocationContext());
         mInstrumentationTest.setMetricCollectors(collectors);
         mInstrumentationTest.run(mMockListener);
 
@@ -968,16 +981,26 @@
         inOrder.verify(mInstrumentationTest).setRunnerArgs(runner.capture());
         inOrder.verify(mMockTestDevice, times(2))
                 .runInstrumentationTests(eq(runner.getValue()), any(ITestLifeCycleReceiver.class));
-
         inOrder.verify(mMockListener).testRunStarted(TEST_PACKAGE_VALUE, 2);
         inOrder.verify(mMockListener).testStarted(eq(TEST1), anyLong());
-        inOrder.verify(mMockListener).testEnded(eq(TEST1), anyLong(), eq(EMPTY_STRING_MAP));
+        ArgumentCaptor<HashMap<String, Metric>> testCapture1 =
+                ArgumentCaptor.forClass(HashMap.class);
+        inOrder.verify(mMockListener).testEnded(eq(TEST1), anyLong(), testCapture1.capture());
+        HashMap<String, Metric> test1Metric = testCapture1.getValue();
+        assertTrue(test1Metric.containsKey("called"));
+        assertFalse(test1Metric.containsKey("not-called"));
         inOrder.verify(mMockListener).testStarted(eq(TEST2), anyLong());
-        inOrder.verify(mMockListener).testEnded(eq(TEST2), anyLong(), eq(EMPTY_STRING_MAP));
-        inOrder.verify(mMockListener).testRunEnded(1, EMPTY_STRING_MAP);
-
-        assertTrue(calledCollector.wasCalled);
-        assertFalse(notCalledCollector.wasCalled);
+        ArgumentCaptor<HashMap<String, Metric>> testCapture2 =
+                ArgumentCaptor.forClass(HashMap.class);
+        inOrder.verify(mMockListener).testEnded(eq(TEST2), anyLong(), testCapture2.capture());
+        HashMap<String, Metric> test2Metric = testCapture2.getValue();
+        assertTrue(test2Metric.containsKey("called"));
+        assertFalse(test2Metric.containsKey("not-called"));
+        ArgumentCaptor<HashMap<String, Metric>> runCapture = ArgumentCaptor.forClass(HashMap.class);
+        inOrder.verify(mMockListener).testRunEnded(anyLong(), runCapture.capture());
+        HashMap<String, Metric> runMetric = runCapture.getValue();
+        assertTrue(runMetric.containsKey("called"));
+        assertFalse(runMetric.containsKey("not-called"));
     }
 
     private static class FakeTestRunner extends RemoteAndroidTestRunner {
diff --git a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java b/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
index dd65aca..98d88eb 100644
--- a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
+++ b/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
@@ -59,6 +59,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.StringJoiner;
+import java.util.zip.ZipFile;
 
 /** Unit tests for {@link NativeCodeCoverageListener}. */
 @RunWith(JUnit4.class)
@@ -90,10 +91,12 @@
     @Test
     public void test_logsCoverageZip() throws DeviceNotAvailableException, IOException {
         // Setup mocks to write the coverage measurement to the file.
+        doReturn(true).when(mMockDevice).enableAdbRoot();
         doReturn(
                         new StringJoiner("\n")
-                                .add("/data/misc/trace/path/to/coverage.gcda")
-                                .add("/data/misc/trace/path/to/.hidden/coverage2.gcda")
+                                .add("/data/misc/trace/proc/self/cwd/out/path/to/coverage.gcda")
+                                .add(
+                                        "/data/misc/trace/proc/self/cwd/out/path/to/.hidden/coverage2.gcda")
                                 .toString())
                 .when(mMockDevice)
                 .executeShellCommand(anyString());
@@ -135,9 +138,32 @@
     }
 
     @Test
+    public void testNoCoverageFiles_logsEmptyZip() throws DeviceNotAvailableException, IOException {
+        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn("").when(mMockDevice).executeShellCommand(anyString());
+
+        // Simulate a test run.
+        mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
+        Map<String, String> metric = new HashMap<>();
+        mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+
+        // Verify testLog(..) was called with an empty zip.
+        List<ByteString> logs = mFakeListener.getLogs();
+        assertThat(logs).hasSize(1);
+        File outputZip = folder.newFile("empty_coverage.zip");
+        try (OutputStream out = new FileOutputStream(outputZip)) {
+            logs.get(0).writeTo(out);
+        }
+
+        ZipFile loggedZip = new ZipFile(outputZip);
+        assertThat(loggedZip.size()).isEqualTo(0);
+    }
+
+    @Test
     public void testFailure_unableToPullFile() throws DeviceNotAvailableException {
         // Setup mocks.
-        doReturn("/data/misc/trace/some/path/to/coverage.gcda\n")
+        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn("/data/misc/trace/proc/self/cwd/out/some/path/to/coverage.gcda\n")
                 .when(mMockDevice)
                 .executeShellCommand(anyString());
         doReturn(false).when(mMockDevice).pullFile(anyString(), any());
diff --git a/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java b/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java
deleted file mode 100644
index 7424240..0000000
--- a/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java
+++ /dev/null
@@ -1,381 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import com.android.ddmlib.IDevice;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.build.IFolderBuildInfo;
-import com.android.tradefed.command.CommandOptions;
-import com.android.tradefed.config.GlobalConfiguration;
-import com.android.tradefed.config.IConfiguration;
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.device.NullDevice;
-import com.android.tradefed.device.StubDevice;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
-import com.android.tradefed.util.IRunUtil;
-import com.android.tradefed.util.IRunUtil.EnvPriority;
-
-import org.easymock.EasyMock;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-/** Unit tests for {@link VersionedTfLauncher}. */
-@RunWith(JUnit4.class)
-public class VersionedTfLauncherTest {
-
-    private static final String FAKE_SERIAL = "FAKE_SERIAL";
-    private static final String CONFIG_NAME = "FAKE_CONFIG";
-    private static final String TF_COMMAND_LINE_TEMPLATE = "--template:map";
-    private static final String TF_COMMAND_LINE_TEST = "test=tf/fake";
-    // Test option value with empty spaces should be parsed correctly.
-    private static final String TF_COMMAND_LINE_OPTION = "--option";
-    private static final String TF_COMMAND_LINE_OPTION_VALUE = "value1 value2";
-    private static final String TF_COMMAND_LINE_OPTION_VALUE_QUOTED =
-            ("\\\"" + TF_COMMAND_LINE_OPTION_VALUE + "\\\"");
-    private static final String TF_COMMAND_LINE =
-            (TF_COMMAND_LINE_TEMPLATE + " " + TF_COMMAND_LINE_TEST + " " + TF_COMMAND_LINE_OPTION +
-             " " + TF_COMMAND_LINE_OPTION_VALUE_QUOTED);
-    private static final String ADDITIONAL_TEST_ZIP = "/tmp/tests.zip";
-
-    private VersionedTfLauncher mVersionedTfLauncher;
-    private ITestInvocationListener mMockListener;
-    private IRunUtil mMockRunUtil;
-    private ITestDevice mMockTestDevice;
-    private IDevice mMockIDevice;
-    private IFolderBuildInfo mMockBuildInfo;
-    private IConfiguration mMockConfig;
-
-    @Before
-    public void setUp() throws Exception {
-        mMockListener = EasyMock.createMock(ITestInvocationListener.class);
-        mMockRunUtil = EasyMock.createMock(IRunUtil.class);
-        mMockBuildInfo = EasyMock.createMock(IFolderBuildInfo.class);
-        mMockTestDevice = EasyMock.createMock(ITestDevice.class);
-        mMockConfig = EasyMock.createMock(IConfiguration.class);
-
-        mVersionedTfLauncher = new VersionedTfLauncher();
-        mVersionedTfLauncher.setRunUtil(mMockRunUtil);
-        mVersionedTfLauncher.setBuild(mMockBuildInfo);
-        mVersionedTfLauncher.setEventStreaming(false);
-        mVersionedTfLauncher.setConfiguration(mMockConfig);
-
-        OptionSetter setter = new OptionSetter(mVersionedTfLauncher);
-        setter.setOptionValue("config-name", CONFIG_NAME);
-        setter.setOptionValue("tf-command-line", TF_COMMAND_LINE);
-        setter.setOptionValue("inject-invocation-data", "true");
-
-        try {
-            GlobalConfiguration.createGlobalConfiguration(new String[] {});
-        } catch (IllegalStateException e) {
-            // ignore re-init.
-        }
-    }
-
-    /**
-     * Test {@link VersionedTfLauncher#run(ITestInvocationListener)} for test with a single device
-     */
-    @Test
-    public void testRun_singleDevice() {
-        mMockIDevice = EasyMock.createMock(IDevice.class);
-
-        CommandResult cr = new CommandResult(CommandStatus.SUCCESS);
-        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
-        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
-        mMockRunUtil.unsetEnvVariable(SubprocessTfLauncher.ANDROID_SERIAL_VAR);
-        mMockRunUtil.setEnvVariablePriority(EnvPriority.SET);
-        mMockRunUtil.setEnvVariable(
-                EasyMock.eq(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE),
-                (String) EasyMock.anyObject());
-
-        EasyMock.expect(
-                        mMockRunUtil.runTimedCmd(
-                                EasyMock.anyLong(),
-                                (FileOutputStream) EasyMock.anyObject(),
-                                (FileOutputStream) EasyMock.anyObject(),
-                                EasyMock.eq("java"),
-                                (String) EasyMock.anyObject(),
-                                EasyMock.eq("--add-opens=java.base/java.nio=ALL-UNNAMED"),
-                                EasyMock.eq("-cp"),
-                                (String) EasyMock.anyObject(),
-                                EasyMock.eq("com.android.tradefed.command.CommandRunner"),
-                                EasyMock.eq(CONFIG_NAME),
-                                EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
-                                EasyMock.eq(TF_COMMAND_LINE_TEST),
-                                EasyMock.eq(TF_COMMAND_LINE_OPTION),
-                                EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
-                                EasyMock.eq("--serial"),
-                                EasyMock.eq(FAKE_SERIAL),
-                                EasyMock.eq("--additional-tests-zip"),
-                                EasyMock.eq(ADDITIONAL_TEST_ZIP),
-                                EasyMock.eq("--" + CommandOptions.INVOCATION_DATA),
-                                EasyMock.eq(SubprocessTfLauncher.SUBPROCESS_TAG_NAME),
-                                EasyMock.eq("true"),
-                                EasyMock.eq("--subprocess-report-file"),
-                                (String) EasyMock.anyObject()))
-                .andReturn(cr);
-        Map<ITestDevice, IBuildInfo> deviceInfos = new HashMap<ITestDevice, IBuildInfo>();
-        deviceInfos.put(mMockTestDevice, null);
-        mVersionedTfLauncher.setDeviceInfos(deviceInfos);
-        EasyMock.expect(mMockBuildInfo.getRootDir()).andReturn(new File(""));
-        EasyMock.expect(mMockBuildInfo.getBuildId()).andReturn("FAKEID").times(2);
-        EasyMock.expect(mMockBuildInfo.getFile("general-tests.zip"))
-                .andReturn(new File(ADDITIONAL_TEST_ZIP));
-        mMockBuildInfo.addBuildAttribute(SubprocessTfLauncher.PARENT_PROC_TAG_NAME, "true");
-        EasyMock.expect(mMockTestDevice.getIDevice()).andReturn(mMockIDevice).times(2);
-        EasyMock.expect(mMockTestDevice.getSerialNumber()).andReturn(FAKE_SERIAL).times(1);
-        mMockListener.testLog((String)EasyMock.anyObject(), (LogDataType)EasyMock.anyObject(),
-                (FileInputStreamSource)EasyMock.anyObject());
-        EasyMock.expectLastCall().times(3);
-        mMockListener.testRunStarted("StdErr", 1);
-        mMockListener.testStarted((TestDescription) EasyMock.anyObject());
-        mMockListener.testEnded(
-                (TestDescription) EasyMock.anyObject(), EasyMock.eq(new HashMap<String, Metric>()));
-        mMockListener.testRunEnded(0, new HashMap<String, Metric>());
-
-        EasyMock.expect(mMockConfig.getCommandOptions()).andReturn(new CommandOptions());
-        EasyMock.replay(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
-        mVersionedTfLauncher.run(mMockListener);
-        EasyMock.verify(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
-    }
-
-    /**
-     * Test {@link VersionedTfLauncher#run(ITestInvocationListener)} for test with a null device
-     */
-    @Test
-    public void testRun_nullDevice() {
-        mMockIDevice = new NullDevice("null-device-1");
-
-        CommandResult cr = new CommandResult(CommandStatus.SUCCESS);
-        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
-        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
-        mMockRunUtil.unsetEnvVariable(SubprocessTfLauncher.ANDROID_SERIAL_VAR);
-        mMockRunUtil.setEnvVariablePriority(EnvPriority.SET);
-        mMockRunUtil.setEnvVariable(
-                EasyMock.eq(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE),
-                (String) EasyMock.anyObject());
-
-        EasyMock.expect(
-                        mMockRunUtil.runTimedCmd(
-                                EasyMock.anyLong(),
-                                (FileOutputStream) EasyMock.anyObject(),
-                                (FileOutputStream) EasyMock.anyObject(),
-                                EasyMock.eq("java"),
-                                (String) EasyMock.anyObject(),
-                                EasyMock.eq("--add-opens=java.base/java.nio=ALL-UNNAMED"),
-                                EasyMock.eq("-cp"),
-                                (String) EasyMock.anyObject(),
-                                EasyMock.eq("com.android.tradefed.command.CommandRunner"),
-                                EasyMock.eq(CONFIG_NAME),
-                                EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
-                                EasyMock.eq(TF_COMMAND_LINE_TEST),
-                                EasyMock.eq(TF_COMMAND_LINE_OPTION),
-                                EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
-                                EasyMock.eq("--null-device"),
-                                EasyMock.eq("--" + CommandOptions.INVOCATION_DATA),
-                                EasyMock.eq(SubprocessTfLauncher.SUBPROCESS_TAG_NAME),
-                                EasyMock.eq("true"),
-                                EasyMock.eq("--subprocess-report-file"),
-                                (String) EasyMock.anyObject()))
-                .andReturn(cr);
-        Map<ITestDevice, IBuildInfo> deviceInfos = new HashMap<ITestDevice, IBuildInfo>();
-        deviceInfos.put(mMockTestDevice, null);
-        mVersionedTfLauncher.setDeviceInfos(deviceInfos);
-        EasyMock.expect(mMockBuildInfo.getRootDir()).andReturn(new File(""));
-        EasyMock.expect(mMockBuildInfo.getBuildId()).andReturn("FAKEID").times(2);
-        EasyMock.expect(mMockBuildInfo.getFile("general-tests.zip")).andReturn(null);
-        mMockBuildInfo.addBuildAttribute(SubprocessTfLauncher.PARENT_PROC_TAG_NAME, "true");
-        EasyMock.expect(mMockTestDevice.getIDevice()).andReturn(mMockIDevice).times(1);
-        mMockListener.testLog((String)EasyMock.anyObject(), (LogDataType)EasyMock.anyObject(),
-                (FileInputStreamSource)EasyMock.anyObject());
-        EasyMock.expectLastCall().times(3);
-        mMockListener.testRunStarted("StdErr", 1);
-        mMockListener.testStarted((TestDescription) EasyMock.anyObject());
-        mMockListener.testEnded(
-                (TestDescription) EasyMock.anyObject(), EasyMock.eq(new HashMap<String, Metric>()));
-        mMockListener.testRunEnded(0, new HashMap<String, Metric>());
-
-        EasyMock.expect(mMockConfig.getCommandOptions()).andReturn(new CommandOptions());
-        EasyMock.replay(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
-        mVersionedTfLauncher.run(mMockListener);
-        EasyMock.verify(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
-    }
-
-    /** Test {@link VersionedTfLauncher#run(ITestInvocationListener)} for test with a StubDevice. */
-    @Test
-    public void testRun_DeviceNoPreSetup() {
-        CommandResult cr = new CommandResult(CommandStatus.SUCCESS);
-        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
-        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
-        mMockRunUtil.unsetEnvVariable(SubprocessTfLauncher.ANDROID_SERIAL_VAR);
-        mMockRunUtil.setEnvVariablePriority(EnvPriority.SET);
-        mMockRunUtil.setEnvVariable(
-                EasyMock.eq(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE),
-                (String) EasyMock.anyObject());
-
-        EasyMock.expect(
-                        mMockRunUtil.runTimedCmd(
-                                EasyMock.anyLong(),
-                                (FileOutputStream) EasyMock.anyObject(),
-                                (FileOutputStream) EasyMock.anyObject(),
-                                EasyMock.eq("java"),
-                                (String) EasyMock.anyObject(),
-                                EasyMock.eq("--add-opens=java.base/java.nio=ALL-UNNAMED"),
-                                EasyMock.eq("-cp"),
-                                (String) EasyMock.anyObject(),
-                                EasyMock.eq("com.android.tradefed.command.CommandRunner"),
-                                EasyMock.eq(CONFIG_NAME),
-                                EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
-                                EasyMock.eq(TF_COMMAND_LINE_TEST),
-                                EasyMock.eq(TF_COMMAND_LINE_OPTION),
-                                EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
-                                EasyMock.eq("--additional-tests-zip"),
-                                EasyMock.eq(ADDITIONAL_TEST_ZIP),
-                                EasyMock.eq("--" + CommandOptions.INVOCATION_DATA),
-                                EasyMock.eq(SubprocessTfLauncher.SUBPROCESS_TAG_NAME),
-                                EasyMock.eq("true"),
-                                EasyMock.eq("--subprocess-report-file"),
-                                (String) EasyMock.anyObject()))
-                .andReturn(cr);
-        Map<ITestDevice, IBuildInfo> deviceInfos = new HashMap<ITestDevice, IBuildInfo>();
-        deviceInfos.put(mMockTestDevice, null);
-        mVersionedTfLauncher.setDeviceInfos(deviceInfos);
-        EasyMock.expect(mMockBuildInfo.getRootDir()).andReturn(new File(""));
-        EasyMock.expect(mMockBuildInfo.getBuildId()).andReturn("FAKEID").times(2);
-        EasyMock.expect(mMockBuildInfo.getFile("general-tests.zip"))
-                .andReturn(new File(ADDITIONAL_TEST_ZIP));
-        mMockBuildInfo.addBuildAttribute(SubprocessTfLauncher.PARENT_PROC_TAG_NAME, "true");
-        EasyMock.expect(mMockTestDevice.getIDevice()).andReturn(new StubDevice("serial1")).times(2);
-        mMockListener.testLog(
-                (String) EasyMock.anyObject(),
-                (LogDataType) EasyMock.anyObject(),
-                (FileInputStreamSource) EasyMock.anyObject());
-        EasyMock.expectLastCall().times(3);
-        mMockListener.testRunStarted("StdErr", 1);
-        mMockListener.testStarted((TestDescription) EasyMock.anyObject());
-        mMockListener.testEnded(
-                (TestDescription) EasyMock.anyObject(), EasyMock.eq(new HashMap<String, Metric>()));
-        mMockListener.testRunEnded(0, new HashMap<String, Metric>());
-
-        EasyMock.expect(mMockConfig.getCommandOptions()).andReturn(new CommandOptions());
-        EasyMock.replay(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
-        mVersionedTfLauncher.run(mMockListener);
-        EasyMock.verify(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
-    }
-
-    /**
-     * Test that when a test is sharded, the instance of the implementation is used and options are
-     * passed to the shard test.
-     */
-    @Test
-    public void testGetTestShard() {
-        Collection<IRemoteTest> tests = mVersionedTfLauncher.split(2);
-        assertEquals(2, tests.size());
-        Iterator<IRemoteTest> ite = tests.iterator();
-        IRemoteTest firstTest = ite.next();
-        assertTrue(firstTest instanceof VersionedTfLauncher);
-        IRemoteTest secondTest = ite.next();
-        assertTrue(secondTest instanceof VersionedTfLauncher);
-
-        VersionedTfLauncher shardedTest = (VersionedTfLauncher) secondTest;
-
-        shardedTest.setRunUtil(mMockRunUtil);
-        shardedTest.setBuild(mMockBuildInfo);
-        shardedTest.setEventStreaming(false);
-        shardedTest.setConfiguration(mMockConfig);
-
-        mMockIDevice = EasyMock.createMock(IDevice.class);
-
-        CommandResult cr = new CommandResult(CommandStatus.SUCCESS);
-        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
-        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
-        mMockRunUtil.unsetEnvVariable(SubprocessTfLauncher.ANDROID_SERIAL_VAR);
-        mMockRunUtil.setEnvVariablePriority(EnvPriority.SET);
-        mMockRunUtil.setEnvVariable(
-                EasyMock.eq(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE),
-                (String) EasyMock.anyObject());
-
-        EasyMock.expect(
-                        mMockRunUtil.runTimedCmd(
-                                EasyMock.anyLong(),
-                                (FileOutputStream) EasyMock.anyObject(),
-                                (FileOutputStream) EasyMock.anyObject(),
-                                EasyMock.eq("java"),
-                                (String) EasyMock.anyObject(),
-                                EasyMock.eq("--add-opens=java.base/java.nio=ALL-UNNAMED"),
-                                EasyMock.eq("-cp"),
-                                (String) EasyMock.anyObject(),
-                                EasyMock.eq("com.android.tradefed.command.CommandRunner"),
-                                EasyMock.eq(CONFIG_NAME),
-                                EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
-                                EasyMock.eq(TF_COMMAND_LINE_TEST),
-                                EasyMock.eq(TF_COMMAND_LINE_OPTION),
-                                EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
-                                EasyMock.eq("--serial"),
-                                EasyMock.eq(FAKE_SERIAL),
-                                EasyMock.eq("--shard-count"),
-                                EasyMock.eq("2"),
-                                EasyMock.eq("--shard-index"),
-                                EasyMock.eq("1"),
-                                EasyMock.eq("--" + CommandOptions.INVOCATION_DATA),
-                                EasyMock.eq(SubprocessTfLauncher.SUBPROCESS_TAG_NAME),
-                                EasyMock.eq("true"),
-                                EasyMock.eq("--subprocess-report-file"),
-                                (String) EasyMock.anyObject()))
-                .andReturn(cr);
-        Map<ITestDevice, IBuildInfo> deviceInfos = new HashMap<ITestDevice, IBuildInfo>();
-        deviceInfos.put(mMockTestDevice, null);
-        shardedTest.setDeviceInfos(deviceInfos);
-        EasyMock.expect(mMockBuildInfo.getRootDir()).andReturn(new File(""));
-        EasyMock.expect(mMockBuildInfo.getBuildId()).andReturn("FAKEID").times(2);
-        EasyMock.expect(mMockBuildInfo.getFile("general-tests.zip")).andReturn(null);
-        mMockBuildInfo.addBuildAttribute(SubprocessTfLauncher.PARENT_PROC_TAG_NAME, "true");
-        EasyMock.expect(mMockTestDevice.getIDevice()).andReturn(mMockIDevice).times(2);
-        EasyMock.expect(mMockTestDevice.getSerialNumber()).andReturn(FAKE_SERIAL).times(1);
-        mMockListener.testLog(
-                (String) EasyMock.anyObject(),
-                (LogDataType) EasyMock.anyObject(),
-                (FileInputStreamSource) EasyMock.anyObject());
-        EasyMock.expectLastCall().times(3);
-        mMockListener.testRunStarted("StdErr", 1);
-        mMockListener.testStarted((TestDescription) EasyMock.anyObject());
-        mMockListener.testEnded(
-                (TestDescription) EasyMock.anyObject(), EasyMock.eq(new HashMap<String, Metric>()));
-        mMockListener.testRunEnded(0, new HashMap<String, Metric>());
-        EasyMock.expect(mMockConfig.getCommandOptions()).andReturn(new CommandOptions());
-        EasyMock.replay(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
-        shardedTest.run(mMockListener);
-        EasyMock.verify(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
-    }
-}
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
index 246aeed..cc64b50 100644
--- a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
@@ -19,6 +19,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.times;
@@ -41,8 +42,11 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 import java.io.File;
+import java.io.OutputStream;
 import java.util.HashMap;
 
 /** Unit tests for {@link ExecutableHostTest}. */
@@ -101,7 +105,45 @@
             CommandResult result = new CommandResult(CommandStatus.SUCCESS);
             doReturn(result)
                     .when(mMockRunUtil)
-                    .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+                    .runTimedCmd(
+                            Mockito.anyLong(),
+                            (OutputStream) Mockito.any(),
+                            Mockito.any(),
+                            Mockito.eq(tmpBinary.getAbsolutePath()));
+
+            mExecutableTest.run(mMockListener);
+
+            verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(1));
+            verify(mMockListener, Mockito.times(0)).testRunFailed(any());
+            verify(mMockListener, Mockito.times(0)).testFailed(any(), any());
+            verify(mMockListener, Mockito.times(1))
+                    .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
+        } finally {
+            FileUtil.recursiveDelete(tmpBinary);
+        }
+    }
+
+    @Test
+    public void testRunHostExecutable_relativePath() throws Exception {
+        File tmpBinary = FileUtil.createTempFile("test-executable", "");
+        try {
+            OptionSetter setter = new OptionSetter(mExecutableTest);
+            setter.setOptionValue("binary", tmpBinary.getAbsolutePath());
+            setter.setOptionValue("relative-path-execution", "true");
+
+            CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+            doReturn(result)
+                    .when(mMockRunUtil)
+                    .runTimedCmd(
+                            Mockito.anyLong(),
+                            (OutputStream) Mockito.any(),
+                            Mockito.any(),
+                            Mockito.eq("bash"),
+                            Mockito.eq("-c"),
+                            Mockito.eq(
+                                    String.format(
+                                            "pushd %s; ./%s;",
+                                            tmpBinary.getParent(), tmpBinary.getName())));
 
             mExecutableTest.run(mMockListener);
 
@@ -125,7 +167,11 @@
             CommandResult result = new CommandResult(CommandStatus.SUCCESS);
             doReturn(result)
                     .when(mMockRunUtil)
-                    .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+                    .runTimedCmd(
+                            Mockito.anyLong(),
+                            (OutputStream) Mockito.any(),
+                            Mockito.any(),
+                            Mockito.eq(tmpBinary.getAbsolutePath()));
 
             doThrow(new DeviceNotAvailableException()).when(mMockDevice).waitForDeviceAvailable();
             try {
@@ -165,7 +211,11 @@
             CommandResult result = new CommandResult(CommandStatus.SUCCESS);
             doReturn(result)
                     .when(mMockRunUtil)
-                    .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+                    .runTimedCmd(
+                            Mockito.anyLong(),
+                            (OutputStream) Mockito.any(),
+                            Mockito.any(),
+                            Mockito.eq(tmpBinary.getAbsolutePath()));
 
             mExecutableTest.run(mMockListener);
 
@@ -222,9 +272,24 @@
             CommandResult result = new CommandResult(CommandStatus.FAILED);
             result.setExitCode(5);
             result.setStdout("stdout");
-            doReturn(result)
+
+            doAnswer(
+                            new Answer<CommandResult>() {
+
+                                @Override
+                                public CommandResult answer(InvocationOnMock invocation)
+                                        throws Throwable {
+                                    OutputStream outputStream = invocation.getArgument(1);
+                                    outputStream.write("stdout".getBytes());
+                                    return result;
+                                }
+                            })
                     .when(mMockRunUtil)
-                    .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+                    .runTimedCmd(
+                            Mockito.anyLong(),
+                            (OutputStream) Mockito.any(),
+                            Mockito.any(),
+                            Mockito.eq(tmpBinary.getAbsolutePath()));
 
             mExecutableTest.run(mMockListener);
 
diff --git a/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java b/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java
index 4d98090..9f91e7e 100644
--- a/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java
+++ b/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.tradefed.testtype.junit4;
 
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -44,6 +46,7 @@
 import com.android.tradefed.util.ListInstrumentationParser;
 
 import org.easymock.EasyMock;
+import org.easymock.IAnswer;
 import org.junit.Assert;
 import org.junit.AssumptionViolatedException;
 import org.junit.Before;
@@ -187,6 +190,57 @@
         EasyMock.verify(mMockBuild, mMockDevice);
     }
 
+    /** Test that we carry the assumption failure messages. */
+    @Test
+    public void testRunDeviceTests_assumptionFailure() throws Exception {
+        TestableHostJUnit4Test test = new TestableHostJUnit4Test();
+        test.setDevice(mMockDevice);
+        test.setBuild(mMockBuild);
+        test.setInvocationContext(mMockContext);
+        mMockDevice.executeShellCommand(
+                EasyMock.eq("pm list instrumentation"), EasyMock.anyObject());
+        EasyMock.expect(mMockDevice.getIDevice()).andReturn(new StubDevice("serial"));
+        EasyMock.expect(
+                        mMockDevice.runInstrumentationTests(
+                                (IRemoteAndroidTestRunner) EasyMock.anyObject(),
+                                EasyMock.<Collection<ITestLifeCycleReceiver>>anyObject()))
+                .andAnswer(
+                        new IAnswer<Boolean>() {
+                            @SuppressWarnings("unchecked")
+                            @Override
+                            public Boolean answer() throws Throwable {
+                                Collection<ITestLifeCycleReceiver> receivers =
+                                        (Collection<ITestLifeCycleReceiver>)
+                                                getCurrentArguments()[1];
+                                for (ITestLifeCycleReceiver i : receivers) {
+                                    i.testRunStarted("runName", 2);
+                                    i.testStarted(new TestDescription("class", "test1"));
+                                    i.testAssumptionFailure(
+                                            new TestDescription("class", "test1"), "assumpFail");
+                                    i.testEnded(
+                                            new TestDescription("class", "test1"),
+                                            new HashMap<String, Metric>());
+
+                                    i.testStarted(new TestDescription("class", "test2"));
+                                    i.testAssumptionFailure(
+                                            new TestDescription("class", "test2"), "assumpFail2");
+                                    i.testEnded(
+                                            new TestDescription("class", "test2"),
+                                            new HashMap<String, Metric>());
+                                }
+                                return true;
+                            }
+                        });
+        EasyMock.replay(mMockBuild, mMockDevice);
+        try {
+            test.runDeviceTests("com.package", "testClass");
+            fail("Should have thrown an Assume exception.");
+        } catch (AssumptionViolatedException e) {
+            assertEquals("assumpFail\n\nassumpFail2", e.getMessage());
+        }
+        EasyMock.verify(mMockBuild, mMockDevice);
+    }
+
     /** Test that when running an instrumentation, the abi is properly passed. */
     @Test
     public void testRunDeviceTests_abi() throws Exception {
diff --git a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
index 6fea16b..181154d 100644
--- a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
@@ -97,7 +97,11 @@
                             mMockRunUtil.runTimedCmd(
                                     EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
                     .andReturn(res);
-            mMockListener.testRunStarted(binary.getName(), 5);
+            mMockListener.testRunStarted(
+                    EasyMock.eq(binary.getName()),
+                    EasyMock.eq(5),
+                    EasyMock.eq(0),
+                    EasyMock.anyLong());
             mMockListener.testLog(
                     EasyMock.eq(binary.getName() + "-stderr"),
                     EasyMock.eq(LogDataType.TEXT),
@@ -137,7 +141,11 @@
                             mMockRunUtil.runTimedCmd(
                                     EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
                     .andReturn(res);
-            mMockListener.testRunStarted(binary.getName(), 5);
+            mMockListener.testRunStarted(
+                    EasyMock.eq(binary.getName()),
+                    EasyMock.eq(5),
+                    EasyMock.eq(0),
+                    EasyMock.anyLong());
             mMockListener.testLog(
                     EasyMock.eq(binary.getName() + "-stderr"),
                     EasyMock.eq(LogDataType.TEXT),
@@ -212,7 +220,11 @@
                             mMockRunUtil.runTimedCmd(
                                     EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
                     .andReturn(res);
-            mMockListener.testRunStarted(binary.getName(), 5);
+            mMockListener.testRunStarted(
+                    EasyMock.eq(binary.getName()),
+                    EasyMock.eq(5),
+                    EasyMock.eq(0),
+                    EasyMock.anyLong());
             mMockListener.testLog(
                     EasyMock.eq(binary.getName() + "-stderr"),
                     EasyMock.eq(LogDataType.TEXT),
diff --git a/tests/src/com/android/tradefed/testtype/retry/ResultAggregatorTest.java b/tests/src/com/android/tradefed/testtype/retry/ResultAggregatorTest.java
new file mode 100644
index 0000000..f71c4e4
--- /dev/null
+++ b/tests/src/com/android/tradefed/testtype/retry/ResultAggregatorTest.java
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.config.ConfigurationDef;
+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.ILogSaver;
+import com.android.tradefed.result.ILogSaverListener;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.retry.ISupportGranularResults;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+/** Unit tests for {@link ResultAggregator}. */
+@RunWith(JUnit4.class)
+public class ResultAggregatorTest {
+
+    private ResultAggregator mAggregator;
+    private ILogSaverListener mAggListener;
+    private ITestDetailedReceiver mDetailedListener;
+    private IInvocationContext mInvocationContext;
+    private IInvocationContext mModuleContext;
+
+    private interface ITestDetailedReceiver
+            extends ISupportGranularResults, ITestInvocationListener, ILogSaverListener {}
+
+    @Before
+    public void setUp() {
+        mAggListener = EasyMock.createMock(ILogSaverListener.class);
+        mDetailedListener = EasyMock.createMock(ITestDetailedReceiver.class);
+        mInvocationContext = new InvocationContext();
+        mInvocationContext.addDeviceBuildInfo(
+                ConfigurationDef.DEFAULT_DEVICE_NAME, new BuildInfo());
+        mModuleContext = new InvocationContext();
+    }
+
+    @Test
+    public void testForwarding() {
+        TestDescription test1 = new TestDescription("classname", "test1");
+        TestDescription test2 = new TestDescription("classname", "test2");
+        ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+        EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+        // Invocation level
+        mAggListener.setLogSaver(logger);
+        mAggListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+        mDetailedListener.setLogSaver(logger);
+        mDetailedListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+        mAggListener.testModuleStarted(mModuleContext);
+        mDetailedListener.testModuleStarted(mModuleContext);
+
+        // Detailed receives the breakdown
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testFailed(test2, "I failed. retry me.");
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunFailed("run fail");
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        // Aggregated listeners receives the aggregated results
+        mAggListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggListener.testModuleEnded();
+        mDetailedListener.testModuleEnded();
+        mAggListener.invocationEnded(500L);
+        mDetailedListener.invocationEnded(500L);
+
+        EasyMock.replay(mAggListener, mDetailedListener);
+        mAggregator =
+                new ResultAggregator(
+                        Arrays.asList(mAggListener, mDetailedListener),
+                        RetryStrategy.RETRY_ANY_FAILURE);
+        mAggregator.setLogSaver(logger);
+        mAggregator.invocationStarted(mInvocationContext);
+        mAggregator.testModuleStarted(mModuleContext);
+        // Attempt 1
+        mAggregator.testRunStarted("run1", 2, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testStarted(test2);
+        mAggregator.testFailed(test2, "I failed. retry me.");
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunFailed("run fail");
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        // Attempt 2
+        mAggregator.testRunStarted("run1", 2, 1);
+        mAggregator.testStarted(test2);
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.testModuleEnded();
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+    }
+
+    @Test
+    public void testForwarding_noModules() {
+        TestDescription test1 = new TestDescription("classname", "test1");
+        TestDescription test2 = new TestDescription("classname", "test2");
+        ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+        EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+        // Invocation level
+        mAggListener.setLogSaver(logger);
+        mAggListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+        mDetailedListener.setLogSaver(logger);
+        mDetailedListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+        // Detailed receives the breakdown
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testFailed(test2, "I failed. retry me.");
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        // Aggregated listeners receives the aggregated results
+        mAggListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testRunEnded(900L, new HashMap<String, Metric>());
+
+        mAggListener.invocationEnded(500L);
+        mDetailedListener.invocationEnded(500L);
+
+        EasyMock.replay(mAggListener, mDetailedListener);
+        mAggregator =
+                new ResultAggregator(
+                        Arrays.asList(mAggListener, mDetailedListener),
+                        RetryStrategy.RETRY_ANY_FAILURE);
+        mAggregator.setLogSaver(logger);
+        mAggregator.invocationStarted(mInvocationContext);
+        // Attempt 1
+        mAggregator.testRunStarted("run1", 2, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testStarted(test2);
+        mAggregator.testFailed(test2, "I failed. retry me.");
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        // Attempt 2
+        mAggregator.testRunStarted("run1", 2, 1);
+        mAggregator.testStarted(test2);
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+    }
+
+    /** Test aggregation of results coming from a module first then from a simple test run. */
+    @Test
+    public void testForwarding_module_noModule() {
+        TestDescription test1 = new TestDescription("classname", "test1");
+        TestDescription test2 = new TestDescription("classname", "test2");
+        ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+        EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+        // Invocation level
+        mAggListener.setLogSaver(logger);
+        mAggListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+        mDetailedListener.setLogSaver(logger);
+        mDetailedListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+        mAggListener.testModuleStarted(mModuleContext);
+        mDetailedListener.testModuleStarted(mModuleContext);
+
+        // Detailed receives the breakdown
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testFailed(test2, "I failed. retry me.");
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        // Aggregated listeners receives the aggregated results
+        mAggListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggListener.testModuleEnded();
+        mDetailedListener.testModuleEnded();
+
+        // Detailed receives the breakdown for non-module
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testFailed(test1, "I failed. retry me.");
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        // Aggregated listeners receives the aggregated results for non-module
+        mAggListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testRunEnded(900L, new HashMap<String, Metric>());
+
+        mAggListener.invocationEnded(500L);
+        mDetailedListener.invocationEnded(500L);
+
+        EasyMock.replay(mAggListener, mDetailedListener);
+        mAggregator =
+                new ResultAggregator(
+                        Arrays.asList(mAggListener, mDetailedListener),
+                        RetryStrategy.RETRY_ANY_FAILURE);
+        mAggregator.setLogSaver(logger);
+        mAggregator.invocationStarted(mInvocationContext);
+        mAggregator.testModuleStarted(mModuleContext);
+        // Attempt 1
+        mAggregator.testRunStarted("run1", 2, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testStarted(test2);
+        mAggregator.testFailed(test2, "I failed. retry me.");
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        // Attempt 2
+        mAggregator.testRunStarted("run1", 2, 1);
+        mAggregator.testStarted(test2);
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        mAggregator.testModuleEnded();
+
+        // New run that is not a module
+        mAggregator.testRunStarted("run2", 1, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.testFailed(test1, "I failed. retry me.");
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.testRunStarted("run2", 1, 1);
+        mAggregator.testStarted(test1);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+    }
+
+    /** Test aggregation of results coming from a simple test run first then from a module. */
+    @Test
+    public void testForwarding_noModule_module() {
+        TestDescription test1 = new TestDescription("classname", "test1");
+        TestDescription test2 = new TestDescription("classname", "test2");
+        ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+        EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+        // Invocation level
+        mAggListener.setLogSaver(logger);
+        mAggListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+        mDetailedListener.setLogSaver(logger);
+        mDetailedListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+        mAggListener.testModuleStarted(mModuleContext);
+        mDetailedListener.testModuleStarted(mModuleContext);
+
+        // Detailed receives the breakdown
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testFailed(test2, "I failed. retry me.");
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        // Aggregated listeners receives the aggregated results
+        mAggListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggListener.testModuleEnded();
+        mDetailedListener.testModuleEnded();
+
+        // Detailed receives the breakdown for non-module
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testFailed(test1, "I failed. retry me.");
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        // Aggregated listeners receives the aggregated results for non-module
+        mAggListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testRunEnded(900L, new HashMap<String, Metric>());
+
+        mAggListener.invocationEnded(500L);
+        mDetailedListener.invocationEnded(500L);
+
+        EasyMock.replay(mAggListener, mDetailedListener);
+        mAggregator =
+                new ResultAggregator(
+                        Arrays.asList(mAggListener, mDetailedListener),
+                        RetryStrategy.RETRY_ANY_FAILURE);
+        mAggregator.setLogSaver(logger);
+        mAggregator.invocationStarted(mInvocationContext);
+        // First run that is not a module
+        mAggregator.testRunStarted("run2", 1, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.testFailed(test1, "I failed. retry me.");
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.testRunStarted("run2", 1, 1);
+        mAggregator.testStarted(test1);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        // Module start
+        mAggregator.testModuleStarted(mModuleContext);
+        // Attempt 1
+        mAggregator.testRunStarted("run1", 2, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testStarted(test2);
+        mAggregator.testFailed(test2, "I failed. retry me.");
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        // Attempt 2
+        mAggregator.testRunStarted("run1", 2, 1);
+        mAggregator.testStarted(test2);
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        mAggregator.testModuleEnded();
+
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+    }
+
+    /** Test when two modules follow each others. */
+    @Test
+    public void testForwarding_module_module() {
+        TestDescription test1 = new TestDescription("classname", "test1");
+        TestDescription test2 = new TestDescription("classname", "test2");
+        ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+        EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+        // Invocation level
+        mAggListener.setLogSaver(logger);
+        mAggListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+        mDetailedListener.setLogSaver(logger);
+        mDetailedListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+        mAggListener.testModuleStarted(mModuleContext);
+        mDetailedListener.testModuleStarted(mModuleContext);
+
+        // Detailed receives the breakdown
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testFailed(test2, "I failed. retry me.");
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        // Aggregated listeners receives the aggregated results
+        mAggListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test2),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggListener.testModuleEnded();
+        mDetailedListener.testModuleEnded();
+
+        // second module
+        mAggListener.testModuleStarted(mModuleContext);
+        mDetailedListener.testModuleStarted(mModuleContext);
+
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testFailed(test1, "I failed. retry me.");
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(1), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        // Aggregated listeners receives the aggregated results for non-module
+        mAggListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+        mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mAggListener.testEnded(
+                EasyMock.eq(test1),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggListener.testModuleEnded();
+        mDetailedListener.testModuleEnded();
+
+        mAggListener.invocationEnded(500L);
+        mDetailedListener.invocationEnded(500L);
+
+        EasyMock.replay(mAggListener, mDetailedListener);
+        mAggregator =
+                new ResultAggregator(
+                        Arrays.asList(mAggListener, mDetailedListener),
+                        RetryStrategy.RETRY_ANY_FAILURE);
+        mAggregator.setLogSaver(logger);
+        mAggregator.invocationStarted(mInvocationContext);
+
+        // Module 1 starts
+        mAggregator.testModuleStarted(mModuleContext);
+        // Attempt 1
+        mAggregator.testRunStarted("run1", 2, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testStarted(test2);
+        mAggregator.testFailed(test2, "I failed. retry me.");
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        // Attempt 2
+        mAggregator.testRunStarted("run1", 2, 1);
+        mAggregator.testStarted(test2);
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        mAggregator.testModuleEnded();
+
+        // Module 2 starts
+        mAggregator.testModuleStarted(mModuleContext);
+        mAggregator.testRunStarted("run2", 1, 0);
+        mAggregator.testStarted(test1);
+        mAggregator.testFailed(test1, "I failed. retry me.");
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.testRunStarted("run2", 1, 1);
+        mAggregator.testStarted(test1);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        mAggregator.testModuleEnded();
+
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
index df7bfd2..659391e 100644
--- a/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
@@ -30,6 +30,7 @@
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.testtype.Abi;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IRemoteTest;
@@ -286,6 +287,29 @@
         assertNull(mRunner.split(2));
     }
 
+    /** Ensure that during sharding we don't attempt to log the filter files. */
+    @Test
+    public void testSplit_withFilters() throws Exception {
+        OptionSetter setter = new OptionSetter(mRunner);
+        setter.setOptionValue("suite-config-prefix", "suite");
+        setter.setOptionValue("run-suite-tag", "example-suite");
+        Set<String> excludeModule = new HashSet<>();
+        for (int i = 0; i < 25; i++) {
+            excludeModule.add("arm64-v8a suite/load-filter-test" + i);
+        }
+        mRunner.setExcludeFilter(excludeModule);
+        ITestLogger logger = EasyMock.createMock(ITestLogger.class);
+        mRunner.setTestLogger(logger);
+
+        EasyMock.replay(logger);
+        Collection<IRemoteTest> tests = mRunner.split(2);
+        assertEquals(4, tests.size());
+        for (IRemoteTest test : tests) {
+            assertTrue(test instanceof BaseTestSuite);
+        }
+        EasyMock.verify(logger);
+    }
+
     /**
      * Test for {@link BaseTestSuite#loadTests()} that when a test config supports IAbiReceiver,
      * multiple instances of the config are queued up.
@@ -648,4 +672,51 @@
         }
         EasyMock.verify(mockDevice);
     }
+
+    /** Test that loading the option parameterization is gated by the option. */
+    @Test
+    public void testLoadTests_optionalParameterizedModule() throws Exception {
+        ITestDevice mockDevice = EasyMock.createMock(ITestDevice.class);
+        mRunner.setDevice(mockDevice);
+        OptionSetter setter = new OptionSetter(mRunner);
+        setter.setOptionValue("suite-config-prefix", "suite");
+        setter.setOptionValue("run-suite-tag", "example-suite-parameters");
+        setter.setOptionValue("enable-parameterized-modules", "true");
+        setter.setOptionValue("enable-optional-parameterization", "true");
+        setter.setOptionValue(
+                "test-arg",
+                "com.android.tradefed.testtype.suite.TestSuiteStub:"
+                        + "exclude-annotation:android.platform.test.annotations.AppModeInstant");
+        EasyMock.replay(mockDevice);
+        LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+        assertEquals(4, configMap.size());
+        assertTrue(configMap.containsKey("arm64-v8a suite/stub-parameterized"));
+        assertTrue(configMap.containsKey("arm64-v8a suite/stub-parameterized[instant]"));
+        assertTrue(configMap.containsKey("arm64-v8a suite/stub-parameterized[secondary_user]"));
+        assertTrue(configMap.containsKey("armeabi-v7a suite/stub-parameterized"));
+        EasyMock.verify(mockDevice);
+    }
+
+    /** Test that we can explicitly request the option parameterization type. */
+    @Test
+    public void testLoadTests_optionalParameterizedModule_filter() throws Exception {
+        ITestDevice mockDevice = EasyMock.createMock(ITestDevice.class);
+        mRunner.setDevice(mockDevice);
+        OptionSetter setter = new OptionSetter(mRunner);
+        setter.setOptionValue("suite-config-prefix", "suite");
+        setter.setOptionValue("run-suite-tag", "example-suite-parameters");
+        setter.setOptionValue("enable-parameterized-modules", "true");
+        setter.setOptionValue("enable-optional-parameterization", "true");
+        setter.setOptionValue("module-parameter", "SECONDARY_USER");
+        setter.setOptionValue(
+                "test-arg",
+                "com.android.tradefed.testtype.suite.TestSuiteStub:"
+                        + "exclude-annotation:android.platform.test.annotations.AppModeInstant");
+        EasyMock.replay(mockDevice);
+        LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+        // Only the secondary_user requested is created
+        assertEquals(1, configMap.size());
+        assertTrue(configMap.containsKey("arm64-v8a suite/stub-parameterized[secondary_user]"));
+        EasyMock.verify(mockDevice);
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
index c941afd..b64498b 100644
--- a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
@@ -15,14 +15,12 @@
  */
 package com.android.tradefed.testtype.suite;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.*;
 
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.ITestDevice;
@@ -30,6 +28,7 @@
 import com.android.tradefed.device.metric.DeviceMetricData;
 import com.android.tradefed.device.metric.IMetricCollector;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.FileSystemLogSaver;
@@ -39,7 +38,8 @@
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
-import com.android.tradefed.testtype.suite.ITestSuite.RetryStrategy;
+import com.android.tradefed.testtype.retry.RetryStatistics;
+import com.android.tradefed.testtype.retry.RetryStrategy;
 
 import org.easymock.EasyMock;
 import org.junit.Before;
@@ -245,14 +245,7 @@
     private GranularRetriableTestWrapper createGranularTestWrapper(
             IRemoteTest test, int maxRunCount, List<IMetricCollector> collectors) {
         GranularRetriableTestWrapper granularTestWrapper =
-                new GranularRetriableTestWrapper(test, null, null, null, maxRunCount) {
-                    @Override
-                    List<IMetricCollector> cloneCollectors(
-                            List<IMetricCollector> originalCollectors) {
-                        // For testing purpose, avoid cloning.
-                        return originalCollectors;
-                    }
-                };
+                new GranularRetriableTestWrapper(test, null, null, null, maxRunCount);
         granularTestWrapper.setModuleId("test module");
         granularTestWrapper.setMarkTestsSkipped(false);
         granularTestWrapper.setMetricCollectors(collectors);
@@ -282,7 +275,7 @@
                 .run(Mockito.any(ITestInvocationListener.class));
 
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(mockTest, 1);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         try {
             granularTestWrapper.run(new CollectingTestListener());
             fail("Should have thrown an exception.");
@@ -334,7 +327,7 @@
         int maxRunCount = 5;
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
         // Verify the test runs several times but under the same run name
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -370,8 +363,9 @@
         }
 
         // Since tests stay failed, we have two failure in our monitoring.
-        assertEquals(0, granularTestWrapper.getRetrySuccess());
-        assertEquals(2, granularTestWrapper.getRetryFailed());
+        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        assertEquals(0, stats.mRetrySuccess);
+        assertEquals(2, stats.mRetryFailure);
     }
 
     /** Test when a test becomes pass after failing */
@@ -393,7 +387,7 @@
         int maxRunCount = 5;
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
         // Verify the test runs several times but under the same run name
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -429,8 +423,9 @@
         }
 
         // One success since one test recover, one test never recover so one failure
-        assertEquals(1, granularTestWrapper.getRetrySuccess());
-        assertEquals(1, granularTestWrapper.getRetryFailed());
+        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        assertEquals(1, stats.mRetrySuccess);
+        assertEquals(1, stats.mRetryFailure);
     }
 
     /** Test when all tests become pass, we stop intra-module retry early. */
@@ -453,7 +448,7 @@
         int maxRunCount = 5;
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
         // Verify the test runs several times but under the same run name
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -516,9 +511,10 @@
                         .getTestResults()
                         .containsKey(fakeTestCase2));
 
-        // One success since one test recover, one test never recover so one failure
-        assertEquals(2, granularTestWrapper.getRetrySuccess());
-        assertEquals(0, granularTestWrapper.getRetryFailed());
+        // One success since one test recover, one test never recover so one failure\
+        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        assertEquals(2, stats.mRetrySuccess);
+        assertEquals(0, stats.mRetryFailure);
     }
 
     /**
@@ -539,7 +535,7 @@
         int maxRunCount = 3;
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
 
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -579,7 +575,7 @@
 
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
         // Two runs.
         assertEquals(2, granularTestWrapper.getTestRunResultCollected().size());
@@ -620,7 +616,7 @@
         FakeTest test = new FakeTest(testCases);
         test.setRunFailure("I failed!");
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 3);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_RUN_FAILURE);
+        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
 
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -636,8 +632,9 @@
         }
 
         // No Test cases tracking since it was a run retry.
-        assertEquals(0, granularTestWrapper.getRetrySuccess());
-        assertEquals(0, granularTestWrapper.getRetryFailed());
+        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        assertEquals(0, stats.mRetrySuccess);
+        assertEquals(0, stats.mRetryFailure);
     }
 
     /**
@@ -654,10 +651,8 @@
         FakeTest test = new FakeTest(testCases);
         test.setRunFailure("I failed!");
         test.setClearRunFailure(3);
-        // Failed test cases do not affect retry of runs
-        test.addFailedTestCase(fakeTestCase1);
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 7);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_RUN_FAILURE);
+        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
 
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -677,8 +672,9 @@
         assertEquals(2, lastRes.getNumCompleteTests());
 
         // No Test cases tracking since it was a run retry.
-        assertEquals(0, granularTestWrapper.getRetrySuccess());
-        assertEquals(0, granularTestWrapper.getRetryFailed());
+        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        assertEquals(0, stats.mRetrySuccess);
+        assertEquals(0, stats.mRetryFailure);
     }
 
     /** Test the retry with iterations, it doesn't require any failure to rerun. */
@@ -707,8 +703,9 @@
         }
 
         // No Test cases tracking since it was a run retry.
-        assertEquals(0, granularTestWrapper.getRetrySuccess());
-        assertEquals(0, granularTestWrapper.getRetryFailed());
+        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        assertEquals(0, stats.mRetrySuccess);
+        assertEquals(0, stats.mRetryFailure);
     }
 
     /** When re-running until failure, stop when failure is encountered. */
@@ -737,9 +734,9 @@
         // All tests cases are rerun each time.
         assertEquals(2, res.getNumCompleteTests());
 
-        // No Test cases tracking since it was a run retry.
-        assertEquals(0, granularTestWrapper.getRetrySuccess());
-        assertEquals(0, granularTestWrapper.getRetryFailed());
+        // No stats since no retry occurred.
+        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        assertNull(stats);
     }
 
     /**
@@ -752,8 +749,10 @@
         // Add a disabled collector to ensure it's never called
         CalledMetricCollector notCalledCollector = new CalledMetricCollector();
         notCalledCollector.setDisable(true);
+        notCalledCollector.mName = "not-called";
         collectors.add(notCalledCollector);
         CalledMetricCollector calledCollector = new CalledMetricCollector();
+        calledCollector.mName = "called";
         collectors.add(calledCollector);
 
         ArrayList<TestDescription> testCases = new ArrayList<>();
@@ -789,12 +788,16 @@
         // All tests cases are rerun each time.
         assertEquals(2, lastRes.getNumCompleteTests());
         assertEquals(1, lastRes.getFailedTests().size());
+        assertTrue(lastRes.getRunProtoMetrics().containsKey("called"));
+        assertFalse(lastRes.getRunProtoMetrics().containsKey("not-called"));
 
         lastRes = allResults.get(4);
         assertFalse(lastRes.isRunFailure());
         // The passed test does not rerun now that there is no run failure.
         assertEquals(1, lastRes.getNumCompleteTests());
         assertEquals(1, lastRes.getFailedTests().size());
+        assertTrue(lastRes.getRunProtoMetrics().containsKey("called"));
+        assertFalse(lastRes.getRunProtoMetrics().containsKey("not-called"));
 
         lastRes = allResults.get(5);
         assertFalse(lastRes.isRunFailure());
@@ -802,14 +805,13 @@
         assertEquals(1, lastRes.getNumCompleteTests());
         // The failed test final pass
         assertEquals(0, lastRes.getFailedTests().size());
+        assertTrue(lastRes.getRunProtoMetrics().containsKey("called"));
+        assertFalse(lastRes.getRunProtoMetrics().containsKey("not-called"));
 
-        // No Test cases tracking since it was a run retry.
-        assertEquals(1, granularTestWrapper.getRetrySuccess());
-        assertEquals(0, granularTestWrapper.getRetryFailed());
-
-        // Ensure that the disabled collector was not called, and enabled one was called
-        assertFalse(notCalledCollector.wasCalled);
-        assertTrue(calledCollector.wasCalled);
+        // Check that failure are cleared
+        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        assertEquals(1, stats.mRetrySuccess);
+        assertEquals(0, stats.mRetryFailure);
     }
 
     /**
@@ -862,28 +864,41 @@
     /** Collector that track if it was called or not */
     public static class CalledMetricCollector extends BaseDeviceMetricCollector {
 
-        public boolean wasCalled = false;
+        @Option(name = "name")
+        public String mName;
 
         @Override
         public void onTestRunStart(DeviceMetricData runData) {
-            wasCalled = true;
+            runData.addMetric(
+                    mName,
+                    Metric.newBuilder()
+                            .setMeasurements(Measurements.newBuilder().setSingleString(mName)));
         }
 
         @Override
         public void onTestRunEnd(
                 DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
-            wasCalled = true;
+            runData.addMetric(
+                    mName,
+                    Metric.newBuilder()
+                            .setMeasurements(Measurements.newBuilder().setSingleString(mName)));
         }
 
         @Override
         public void onTestStart(DeviceMetricData testData) {
-            wasCalled = true;
+            testData.addMetric(
+                    mName,
+                    Metric.newBuilder()
+                            .setMeasurements(Measurements.newBuilder().setSingleString(mName)));
         }
 
         @Override
         public void onTestEnd(
                 DeviceMetricData testData, final Map<String, Metric> currentTestCaseMetrics) {
-            wasCalled = true;
+            testData.addMetric(
+                    mName,
+                    Metric.newBuilder()
+                            .setMeasurements(Measurements.newBuilder().setSingleString(mName)));
         }
     }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteIntegrationTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteIntegrationTest.java
index 11e2593..b7c8742 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteIntegrationTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteIntegrationTest.java
@@ -504,7 +504,7 @@
         mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockBuildInfo);
 
         StrictShardHelper helper = new StrictShardHelper();
-        helper.shardConfig(config, mContext, new TestShardRescheduler());
+        helper.shardConfig(config, mContext, new TestShardRescheduler(), null);
 
         assertEquals(2, mListener.getTotalModules());
         assertEquals(2, mListener.getCompleteModules());
@@ -545,7 +545,7 @@
 
         StrictShardHelper helper = new StrictShardHelper();
         TestParallelShardRescheduler rescheduler = new TestParallelShardRescheduler();
-        helper.shardConfig(config, mContext, rescheduler);
+        helper.shardConfig(config, mContext, rescheduler, null);
         // Wait until all results are received, we expect 2 modules.
         while (mListener.getTotalModules() < 2) {
             for (Thread t : rescheduler.mRunning) {
@@ -591,7 +591,7 @@
         mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockBuildInfo);
 
         StrictShardHelper helper = new StrictShardHelper();
-        helper.shardConfig(config, mContext, null);
+        helper.shardConfig(config, mContext, null, null);
         // rescheduler is not called, execution is in the same invocation.
         new ResultForwarder(config.getTestInvocationListeners()).invocationStarted(mContext);
         for (IRemoteTest test : config.getTests()) {
@@ -670,7 +670,7 @@
         mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockBuildInfo);
 
         StrictShardHelper helper = new StrictShardHelper();
-        helper.shardConfig(config, mContext, null);
+        helper.shardConfig(config, mContext, null, null);
         // rescheduler is not called, execution is in the same invocation.
         new ResultForwarder(config.getTestInvocationListeners()).invocationStarted(mContext);
         for (IRemoteTest test : config.getTests()) {
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteMultiTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteMultiTest.java
index ae27598..1759f0a 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteMultiTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteMultiTest.java
@@ -116,6 +116,7 @@
         mMockDevice1 = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockDevice1.getSerialNumber()).andStubReturn("SERIAL1");
         mMockBuildInfo1 = EasyMock.createMock(IBuildInfo.class);
+        EasyMock.expect(mMockBuildInfo1.getRemoteFiles()).andReturn(null).once();
         mMockDevice2 = EasyMock.createMock(ITestDevice.class);
         EasyMock.expect(mMockDevice2.getSerialNumber()).andStubReturn("SERIAL2");
         mMockBuildInfo2 = EasyMock.createMock(IBuildInfo.class);
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
index 8374a80..62b3512 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.testtype.suite;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -22,12 +23,15 @@
 import static org.junit.Assert.fail;
 
 import com.android.ddmlib.IDevice;
+import com.android.tradefed.build.BuildInfo;
 import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
 import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.ConfigurationDef;
 import com.android.tradefed.config.ConfigurationDescriptor;
 import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.ConfigurationFactory;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionSetter;
@@ -77,7 +81,9 @@
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
 
+import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -143,6 +149,7 @@
                     config.setTargetPreparer(mPreparer);
                 }
                 config.setTest(new StubCollectingTest());
+                config.getConfigurationDescription().setModuleName(TEST_CONFIG_NAME);
                 testConfig.put(TEST_CONFIG_NAME, config);
 
                 for (int i = 1; i < mNumTests; i++) {
@@ -269,6 +276,7 @@
         EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn("SERIAL");
         EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(EasyMock.createMock(IDevice.class));
         mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
+        EasyMock.expect(mMockBuildInfo.getRemoteFiles()).andReturn(null).once();
         mMockSysChecker = EasyMock.createMock(ISystemStatusChecker.class);
         mMockLogSaver = EasyMock.createMock(ILogSaver.class);
         mStubMainConfiguration = new Configuration("stub", "stub");
@@ -778,10 +786,8 @@
         mMockListener.testRunStarted(
                 EasyMock.eq(TEST_CONFIG_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         EasyMock.expectLastCall().times(1);
-        mMockListener.testRunFailed(
-                "runtime"
-                        + TestRunResult.ERROR_DIVIDER
-                        + "Module test only ran 0 out of 1 expected tests.");
+        Capture<String> captured = new Capture<>();
+        mMockListener.testRunFailed(EasyMock.capture(captured));
         EasyMock.expect(
                         mMockDevice.logBugreport(
                                 EasyMock.eq("module-test-failure-SERIAL-bugreport"),
@@ -794,6 +800,12 @@
         replayMocks();
         mTestSuite.run(mMockListener);
         verifyMocks();
+        String exception = captured.getValue();
+        assertTrue(exception.contains("runtime"));
+        assertTrue(
+                exception.contains(
+                        TestRunResult.ERROR_DIVIDER
+                                + "Module test only ran 0 out of 1 expected tests."));
     }
 
     /**
@@ -804,6 +816,7 @@
     @Test
     public void testShardModules_notShardable() {
         mTestSuite = new TestSuiteImpl(5);
+        mTestSuite.setBuild(mMockBuildInfo);
         Collection<IRemoteTest> tests = mTestSuite.split(3);
         assertEquals(5, tests.size());
         for (IRemoteTest test : tests) {
@@ -827,6 +840,7 @@
         // default runtime hint is 0, it is only meant to be used for sharding.
         assertEquals(0l, mTestSuite.getRuntimeHint());
         mTestSuite = new TestSuiteImpl(5);
+        mTestSuite.setBuild(mMockBuildInfo);
         Collection<IRemoteTest> tests = mTestSuite.split(3);
         for (IRemoteTest test : tests) {
             assertTrue(test instanceof TestSuiteImpl);
@@ -1478,7 +1492,9 @@
         mTestSuite.setDevice(mMockDevice);
         mTestSuite.setBuild(mMockBuildInfo);
         mTestSuite.setConfiguration(mStubMainConfiguration);
-        mTestSuite.setMaxRunLimit(maxRunLimit);
+        mStubMainConfiguration.getCommandOptions().setMaxRetryCount(maxRunLimit);
+        OptionSetter cmdSetter = new OptionSetter(mStubMainConfiguration.getCommandOptions());
+        cmdSetter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
         mContext = new InvocationContext();
         mTestSuite.setInvocationContext(mContext);
         mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
@@ -1612,6 +1628,8 @@
     @Test
     public void testRandomizeTestModulesWithSameSeed() throws Exception {
         mTestSuite = new TestSuiteImpl(5);
+        mTestSuite.setBuild(mMockBuildInfo);
+
         LinkedHashMap<String, IConfiguration> testConfigs = mTestSuite.loadTests();
         List<ModuleDefinition> runModules = getRunModules(testConfigs);
         List<ModuleDefinition> runModules2 = getRunModules(testConfigs);
@@ -1632,6 +1650,8 @@
     @Test
     public void testRandomizeTestModulesWithDifferentSeed() throws Exception {
         mTestSuite = new TestSuiteImpl(5);
+        mTestSuite.setBuild(mMockBuildInfo);
+
         LinkedHashMap<String, IConfiguration> testConfigs = mTestSuite.loadTests();
         List<ModuleDefinition> runModules = getRunModules(testConfigs);
         List<ModuleDefinition> runModules2 = getRunModules(testConfigs);
@@ -1641,4 +1661,59 @@
         assertFalse(runModules.toString().equals(runModules2.toString()));
     }
 
+    /**
+     * Test for {@link ITestSuite#randomizeTestModules(List, long)} to make sure the random-seed
+     * be injected into BuildInfo correctly.
+     */
+    @Test
+    public void testSeedwhenRandomization() throws Exception {
+        IBuildInfo mMockInfo = new BuildInfo();
+        mTestSuite.setBuild(mMockInfo);
+
+        List<ModuleDefinition> runModules = getRunModules(mTestSuite.loadTests());
+        mTestSuite.randomizeTestModules(runModules, 123L);
+
+        String randomSeed = mMockInfo.getBuildAttributes().get(ITestSuite.RANDOM_SEED);
+        assertTrue(randomSeed.equals(String.valueOf(123L)));
+    }
+
+    /**
+     * Test for {@link ITestSuite#stageTestArtifacts(Set)} is called when test zip build artifact
+     * staging is delayed.
+     */
+    @Test
+    public void testStageTestArtifacts() throws Exception {
+        String remoteFilePath = "gs://module1/tests.zip";
+        List<String> files =
+                Arrays.asList("dir/test/test.config", "dir/test/file1", "module2/file1");
+        DynamicRemoteFileResolver dynamicResolver =
+                new DynamicRemoteFileResolver() {
+                    @Override
+                    public void resolvePartialDownloadZip(
+                            File destDir,
+                            String remoteFilePath,
+                            List<String> includeFilters,
+                            List<String> excludeFilters)
+                            throws ConfigurationException {
+                        assertEquals(new File("tests_dir"), destDir);
+                        assertEquals(remoteFilePath, remoteFilePath);
+                        assertArrayEquals(new String[] {"/test/"}, includeFilters.toArray());
+                        assertArrayEquals(new String[] {"[.]config$"}, excludeFilters.toArray());
+                    }
+                };
+        mTestSuite.setDynamicResolver(dynamicResolver);
+        IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+        EasyMock.expect(mockBuildInfo.getTestsDir()).andStubReturn(new File("tests_dir"));
+        EasyMock.expect(mockBuildInfo.getRemoteFiles())
+                .andReturn(new HashSet<File>(Arrays.asList(new File(remoteFilePath))))
+                .times(3);
+        mTestSuite.setBuild(mockBuildInfo);
+
+        List<ISystemStatusChecker> checkers = new ArrayList<ISystemStatusChecker>();
+        mTestSuite.setSystemStatusChecker(checkers);
+
+        EasyMock.replay(mockBuildInfo);
+        mTestSuite.run(mMockListener);
+        EasyMock.verify(mockBuildInfo);
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
index c71fea9..6bb0d10 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
@@ -57,7 +57,7 @@
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
-import com.android.tradefed.testtype.suite.ITestSuite.RetryStrategy;
+import com.android.tradefed.testtype.retry.RetryStrategy;
 import com.android.tradefed.testtype.suite.module.BaseModuleController;
 import com.android.tradefed.testtype.suite.module.IModuleController;
 import com.android.tradefed.testtype.suite.module.TestFailureModuleController;
@@ -413,7 +413,11 @@
                 EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
         // Exception thrown during tear down do not bubble up to invocation.
         EasyMock.expectLastCall().andThrow(new RuntimeException("teardown failed"));
-        mMockListener.testRunStarted(MODULE_NAME, testCount);
+        mMockListener.testRunStarted(
+                EasyMock.eq(MODULE_NAME),
+                EasyMock.eq(testCount),
+                EasyMock.eq(0),
+                EasyMock.anyLong());
         for (int i = 0; i < 1; i++) {
             mMockListener.testStarted((TestDescription) EasyMock.anyObject(), EasyMock.anyLong());
             mMockListener.testEnded(
@@ -557,7 +561,8 @@
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
         mMockCleaner.tearDown(EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo),
                 EasyMock.isNull());
-        mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+        mMockListener.testRunStarted(
+                EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -594,7 +599,8 @@
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
         mMockCleaner.tearDown(
                 EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
-        mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+        mMockListener.testRunStarted(
+                EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -628,7 +634,8 @@
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
         mMockCleaner.tearDown(
                 EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
-        mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+        mMockListener.testRunStarted(
+                EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -668,12 +675,14 @@
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
         mMockCleaner.tearDown(
                 EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
-        mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+        mMockListener.testRunStarted(
+                EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
         // Ensure that module listeners receive the callbacks too.
-        mockModuleListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+        mockModuleListener.testRunStarted(
+                EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         mockModuleListener.testRunFailed(EasyMock.contains(exceptionMessage));
         mockModuleListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -710,7 +719,8 @@
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
         mMockCleaner.tearDown(
                 EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
-        mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+        mMockListener.testRunStarted(
+                EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -892,7 +902,8 @@
         // Only one module
         assertEquals(1, mModule.getTestsResults().size());
         assertEquals(2, mModule.getTestsResults().get(0).getNumCompleteTests());
-        assertEquals("assert error", mModule.getTestsResults().get(0).getRunFailureMessage());
+        assertTrue(
+                mModule.getTestsResults().get(0).getRunFailureMessage().contains("assert error"));
         verifyMocks();
     }
 
@@ -1104,7 +1115,8 @@
                 EasyMock.eq(loggedFile));
         mMockLogSaverListener.logAssociation("testlogclass", loggedFile);
 
-        mMockLogSaverListener.testRunStarted(MODULE_NAME, 0);
+        mMockLogSaverListener.testRunStarted(
+                EasyMock.eq(MODULE_NAME), EasyMock.eq(0), EasyMock.eq(0), EasyMock.anyLong());
         mMockLogSaverListener.testRunEnded(
                 EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
 
@@ -1271,7 +1283,11 @@
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
 
-        mMockLogSaverListener.testRunStarted(MODULE_NAME, testCount);
+        mMockLogSaverListener.testRunStarted(
+                EasyMock.eq(MODULE_NAME),
+                EasyMock.eq(testCount),
+                EasyMock.eq(0),
+                EasyMock.anyLong());
         for (int i = 0; i < testCount; i++) {
             mMockLogSaverListener.testStarted(
                     (TestDescription) EasyMock.anyObject(), EasyMock.anyLong());
@@ -1379,7 +1395,7 @@
 
         mMockListener.testRunStarted(
                 EasyMock.eq("fakeName"), EasyMock.eq(0), EasyMock.eq(0), EasyMock.anyLong());
-        mMockListener.testRunFailed("early failure!");
+        mMockListener.testRunFailed(EasyMock.contains("early failure!"));
         mMockListener.testRunEnded(
                 EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
 
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleListenerTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleListenerTest.java
index 1e6a6b4..e00bbe3 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleListenerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleListenerTest.java
@@ -155,10 +155,10 @@
         int finalRunFailureAtAttempt =
                 Math.max(clearRun1FailureAtAttempt, clearRun2FailureAtAttempt);
         for (int attempt = 0; attempt < finalRunFailureAtAttempt; attempt++) {
-            assertTrue(mListener.hasRunCrashedAtAttempt(attempt));
+            assertTrue(hasRunCrashed(mListener.getTestRunForAttempts(attempt)));
         }
         for (int attempt = finalRunFailureAtAttempt; attempt < maxRunLimit; attempt++) {
-            assertFalse(mListener.hasRunCrashedAtAttempt(attempt));
+            assertFalse(hasRunCrashed(mListener.getTestRunForAttempts(attempt)));
         }
     }
 
@@ -191,4 +191,13 @@
                         + "test.apex did not report any run.",
                 results.get(1).getRunFailureMessage());
     }
+
+    private boolean hasRunCrashed(List<TestRunResult> results) {
+        for (TestRunResult run : results) {
+            if (run != null && run.isRunFailure()) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
index 4c7a8ba..b5bbd81 100644
--- a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
@@ -56,6 +56,12 @@
                     + "$TestInject\" />\n"
                     + "</configuration>";
 
+    private static final String TEST_INSTANT_CONFIG =
+            "<configuration description=\"Runs a stub tests part of some suite\">\n"
+                    + "    <option name=\"config-descriptor:metadata\" key=\"parameter\" value=\"instant_app\" />"
+                    + "    <test class=\"com.android.tradefed.testtype.suite.TestSuiteStub\" />\n"
+                    + "</configuration>";
+
     private SuiteModuleLoader mRepo;
     private File mTestsDir;
     private Set<IAbi> mAbis;
@@ -83,11 +89,19 @@
         FileUtil.writeToFile(TEST_CONFIG, module);
     }
 
+    private void createInstantModuleConfig(String moduleName) throws IOException {
+        File module = new File(mTestsDir, moduleName + SuiteModuleLoader.CONFIG_EXT);
+        FileUtil.writeToFile(TEST_INSTANT_CONFIG, module);
+    }
+
     @OptionClass(alias = "test-inject")
     public static class TestInject implements IRemoteTest {
         @Option(name = "simple-string")
         public String test = null;
 
+        @Option(name = "empty-string")
+        public String testEmpty = null;
+
         @Option(name = "alias-option")
         public String testAlias = null;
 
@@ -106,11 +120,14 @@
     public void testInjectConfigOptions_moduleArgs() throws Exception {
         List<String> moduleArgs = new ArrayList<>();
         moduleArgs.add("module1:simple-string:value1");
+        moduleArgs.add("module1:empty-string:"); // value is the empty string
 
         moduleArgs.add("module1:list-string:value2");
         moduleArgs.add("module1:list-string:value3");
         moduleArgs.add("module1:list-string:set-option:moreoption");
+        moduleArgs.add("module1:list-string:"); // value is the empty string
         moduleArgs.add("module1:map-string:set-option:=moreoption");
+        moduleArgs.add("module1:map-string:empty-option:="); // value is the empty string
 
         createModuleConfig("module1");
 
@@ -131,14 +148,17 @@
 
         TestInject checker = (TestInject) config.getTests().get(0);
         assertEquals("value1", checker.test);
+        assertEquals("", checker.testEmpty);
         // Check list
-        assertTrue(checker.testList.size() == 3);
+        assertTrue(checker.testList.size() == 4);
         assertTrue(checker.testList.contains("value2"));
         assertTrue(checker.testList.contains("value3"));
         assertTrue(checker.testList.contains("set-option:moreoption"));
+        assertTrue(checker.testList.contains(""));
         // Chech map
-        assertTrue(checker.testMap.size() == 1);
+        assertTrue(checker.testMap.size() == 2);
         assertEquals("moreoption", checker.testMap.get("set-option"));
+        assertEquals("", checker.testMap.get("empty-option"));
     }
 
     /** Test an end-to-end injection of --test-arg. */
@@ -150,6 +170,9 @@
                         + "simple-string:value1");
         testArgs.add(
                 "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
+                        + "empty-string:"); // value is the empty string
+        testArgs.add(
+                "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
                         + "list-string:value2");
         testArgs.add(
                 "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
@@ -159,7 +182,13 @@
                         + "list-string:set-option:moreoption");
         testArgs.add(
                 "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
+                        + "list-string:"); // value is the empty string
+        testArgs.add(
+                "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
                         + "map-string:set-option:=moreoption");
+        testArgs.add(
+                "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
+                        + "map-string:empty-option:="); // value is the empty string
 
         createModuleConfig("module1");
 
@@ -180,14 +209,17 @@
 
         TestInject checker = (TestInject) config.getTests().get(0);
         assertEquals("value1", checker.test);
+        assertEquals("", checker.testEmpty);
         // Check list
-        assertTrue(checker.testList.size() == 3);
+        assertTrue(checker.testList.size() == 4);
         assertTrue(checker.testList.contains("value2"));
         assertTrue(checker.testList.contains("value3"));
         assertTrue(checker.testList.contains("set-option:moreoption"));
+        assertTrue(checker.testList.contains(""));
         // Chech map
-        assertTrue(checker.testMap.size() == 1);
+        assertTrue(checker.testMap.size() == 2);
         assertEquals("moreoption", checker.testMap.get("set-option"));
+        assertEquals("", checker.testMap.get("empty-option"));
     }
 
     @Test
@@ -215,4 +247,44 @@
         TestInject checker = (TestInject) config.getTests().get(0);
         assertEquals("value1", checker.testAlias);
     }
+
+    /**
+     * Test that if the base module is excluded in full, the filters of parameterized modules are
+     * still populated with the proper filters.
+     */
+    @Test
+    public void testFilterParameterized() throws Exception {
+        Map<String, List<SuiteTestFilter>> excludeFilters = new LinkedHashMap<>();
+        createInstantModuleConfig("basemodule");
+        SuiteTestFilter fullFilter = SuiteTestFilter.createFrom("armeabi-v7a basemodule");
+        excludeFilters.put("armeabi-v7a basemodule", Arrays.asList(fullFilter));
+
+        SuiteTestFilter instantMethodFilter =
+                SuiteTestFilter.createFrom(
+                        "armeabi-v7a basemodule[instant] NativeDnsAsyncTest#Async_Cancel");
+        excludeFilters.put("armeabi-v7a basemodule[instant]", Arrays.asList(instantMethodFilter));
+
+        mRepo =
+                new SuiteModuleLoader(
+                        new LinkedHashMap<String, List<SuiteTestFilter>>(),
+                        excludeFilters,
+                        new ArrayList<>(),
+                        new ArrayList<>());
+        mRepo.setParameterizedModules(true);
+
+        List<String> patterns = new ArrayList<>();
+        patterns.add(".*.config");
+        patterns.add(".*.xml");
+        LinkedHashMap<String, IConfiguration> res =
+                mRepo.loadConfigsFromDirectory(
+                        Arrays.asList(mTestsDir), mAbis, null, null, patterns);
+        assertEquals(1, res.size());
+        // Full module was excluded completely
+        IConfiguration instantModule = res.get("armeabi-v7a basemodule[instant]");
+        assertNotNull(instantModule);
+        TestSuiteStub stubTest = (TestSuiteStub) instantModule.getTests().get(0);
+        assertEquals(1, stubTest.getExcludeFilters().size());
+        assertEquals(
+                "NativeDnsAsyncTest#Async_Cancel", stubTest.getExcludeFilters().iterator().next());
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index bbf22c4..cf4eba5 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -418,6 +418,7 @@
                     .andReturn(null);
             EasyMock.expect(mockBuildInfo.getTestsDir()).andReturn(new File("non-existing-dir"));
             EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+            EasyMock.expect(mockBuildInfo.getRemoteFiles()).andReturn(null).once();
 
             mRunner.setBuild(mockBuildInfo);
             EasyMock.replay(mockBuildInfo);
@@ -478,4 +479,111 @@
         assertTrue(configMap.containsKey(ABI_2 + " suite/stubAbi"));
         EasyMock.verify(mockDevice);
     }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#loadTests()} that when force-test-mapping-module is
+     * specified, tests would be filtered.
+     */
+    @Test
+    public void testLoadTestsWithModule() throws Exception {
+        File tempDir = null;
+        try {
+            OptionSetter setter = new OptionSetter(mRunner);
+            setter.setOptionValue("test-mapping-test-group", "postsubmit");
+            setter.setOptionValue("force-test-mapping-module", "suite/stub1");
+
+            tempDir = FileUtil.createTempDir("test_mapping");
+
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile =
+                    File.separator + TEST_DATA_DIR + File.separator + DISABLED_PRESUBMIT_TESTS;
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, DISABLED_PRESUBMIT_TESTS);
+
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+            List<File> filesToZip =
+                    Arrays.asList(srcDir, new File(tempDir, DISABLED_PRESUBMIT_TESTS));
+            File zipFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPINGS_ZIP).toFile();
+            ZipUtil.createZip(filesToZip, zipFile);
+
+            IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+            EasyMock.expect(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR))
+                    .andReturn(null);
+            EasyMock.expect(mockBuildInfo.getTestsDir()).andReturn(new File("non-existing-dir"));
+            EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+
+            mRunner.setBuild(mockBuildInfo);
+            EasyMock.replay(mockBuildInfo);
+
+            LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+            assertEquals(2, configMap.size());
+            assertTrue(configMap.containsKey(ABI_1 + " suite/stub1"));
+            assertTrue(configMap.containsKey(ABI_2 + " suite/stub1"));
+            EasyMock.verify(mockBuildInfo);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
+
+    /**
+     * Test for {@link TestMappingSuiteRunner#loadTests()} that when multi force-test-mapping-module
+     * are specified, tests would be filtered.
+     */
+    @Test
+    public void testLoadTestsWithMultiModules() throws Exception {
+        File tempDir = null;
+        try {
+            OptionSetter setter = new OptionSetter(mRunner);
+            setter.setOptionValue("test-mapping-test-group", "postsubmit");
+            setter.setOptionValue("force-test-mapping-module", "suite/stub1");
+            setter.setOptionValue("force-test-mapping-module", "suite/stub2");
+
+            tempDir = FileUtil.createTempDir("test_mapping");
+
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile =
+                File.separator + TEST_DATA_DIR + File.separator + DISABLED_PRESUBMIT_TESTS;
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, DISABLED_PRESUBMIT_TESTS);
+
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+            srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+            resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+            List<File> filesToZip =
+                Arrays.asList(srcDir, new File(tempDir, DISABLED_PRESUBMIT_TESTS));
+            File zipFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPINGS_ZIP).toFile();
+            ZipUtil.createZip(filesToZip, zipFile);
+
+            IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+            EasyMock.expect(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR))
+                .andReturn(null);
+            EasyMock.expect(mockBuildInfo.getTestsDir()).andReturn(new File("non-existing-dir"));
+            EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+
+            mRunner.setBuild(mockBuildInfo);
+            EasyMock.replay(mockBuildInfo);
+
+            LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+            assertEquals(4, configMap.size());
+            assertTrue(configMap.containsKey(ABI_1 + " suite/stub1"));
+            assertTrue(configMap.containsKey(ABI_1 + " suite/stub2"));
+            assertTrue(configMap.containsKey(ABI_2 + " suite/stub1"));
+            assertTrue(configMap.containsKey(ABI_2 + " suite/stub2"));
+            EasyMock.verify(mockBuildInfo);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java b/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java
index 58a3e03..4e7aac2 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java
@@ -86,6 +86,8 @@
             description = "The notAnnotation class name of the test name to run, can be repeated")
     private Set<String> mExcludeAnnotationFilter = new HashSet<>();
 
+    private Set<String> mExcludeFilters = new HashSet<>();
+
     /** Tests attempt. */
     private void testAttempt(ITestInvocationListener listener) throws DeviceNotAvailableException {
         listener.testRunStarted(mModule, 3);
@@ -224,10 +226,14 @@
     public void addAllIncludeFilters(Set<String> filters) {}
 
     @Override
-    public void addExcludeFilter(String filter) {}
+    public void addExcludeFilter(String filter) {
+        mExcludeFilters.add(filter);
+    }
 
     @Override
-    public void addAllExcludeFilters(Set<String> filters) {}
+    public void addAllExcludeFilters(Set<String> filters) {
+        mExcludeFilters.addAll(filters);
+    }
 
     @Override
     public void clearIncludeFilters() {}
@@ -239,11 +245,13 @@
 
     @Override
     public Set<String> getExcludeFilters() {
-        return new HashSet<>();
+        return mExcludeFilters;
     }
 
     @Override
-    public void clearExcludeFilters() {}
+    public void clearExcludeFilters() {
+        mExcludeFilters.clear();
+    }
 
     @Override
     public void addIncludeAnnotation(String annotation) {
diff --git a/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java b/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java
index ce47dc8..06c8b32 100644
--- a/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java
@@ -16,12 +16,15 @@
 package com.android.tradefed.testtype.suite.retry;
 
 import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log.LogLevel;
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.config.ConfigurationDef;
+import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.log.ILeveledLogOutput;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.TestDescription;
@@ -46,6 +49,8 @@
     private IInvocationContext mContext;
     private ITestDevice mMockDevice;
     private IDevice mMockIDevice;
+    private IConfiguration mMockConfig;
+    private ILeveledLogOutput mMockLogOutput;
 
     @Before
     public void setUp() throws Exception {
@@ -53,8 +58,16 @@
         mMockListener = EasyMock.createStrictMock(ITestInvocationListener.class);
         mMockDevice = EasyMock.createMock(ITestDevice.class);
         mMockIDevice = EasyMock.createMock(IDevice.class);
+        mMockConfig = EasyMock.createMock(IConfiguration.class);
+        mMockLogOutput = EasyMock.createMock(ILeveledLogOutput.class);
+        EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogOutput);
+        EasyMock.expect(mMockLogOutput.getLogLevel()).andReturn(LogLevel.VERBOSE);
+        mMockLogOutput.setLogLevel(LogLevel.WARN);
+        mMockLogOutput.setLogLevel(LogLevel.VERBOSE);
+
         mPlayer = new ResultsPlayer();
         mPlayer.setInvocationContext(mContext);
+        mPlayer.setConfiguration(mMockConfig);
         mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
 
         EasyMock.expect(mMockDevice.getIDevice()).andReturn(mMockIDevice);
@@ -82,9 +95,9 @@
                 EasyMock.eq(new HashMap<String, Metric>()));
         mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
 
-        EasyMock.replay(mMockListener, mMockDevice);
+        EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
         mPlayer.run(mMockListener);
-        EasyMock.verify(mMockListener, mMockDevice);
+        EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
     }
 
     /** Test that when replaying a module we properly replay all the results. */
@@ -128,9 +141,9 @@
         mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
         mMockListener.testModuleEnded();
 
-        EasyMock.replay(mMockListener, mMockDevice);
+        EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
         mPlayer.run(mMockListener);
-        EasyMock.verify(mMockListener, mMockDevice);
+        EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
     }
 
     /** Test that the replay of a single requested test case is working. */
@@ -155,9 +168,9 @@
                 EasyMock.eq(test), EasyMock.anyLong(), EasyMock.eq(new HashMap<String, Metric>()));
         mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
 
-        EasyMock.replay(mMockListener, mMockDevice);
+        EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
         mPlayer.run(mMockListener);
-        EasyMock.verify(mMockListener, mMockDevice);
+        EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
     }
 
     /** Test requesting several tests to re-run. */
@@ -198,9 +211,9 @@
 
         mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
 
-        EasyMock.replay(mMockListener, mMockDevice);
+        EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
         mPlayer.run(mMockListener);
-        EasyMock.verify(mMockListener, mMockDevice);
+        EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
     }
 
     private TestRunResult createTestRunResult(String runName, int testCount, int failCount) {
diff --git a/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java b/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
index 23d219c..380e75c 100644
--- a/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
@@ -35,6 +35,8 @@
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.proto.ProtoResultReporter;
 import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
+import com.android.tradefed.testtype.Abi;
+import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.suite.BaseTestSuite;
 
 import org.easymock.EasyMock;
@@ -345,6 +347,50 @@
         verify(mSuite).setExcludeFilter(excludeRun1);
     }
 
+    /**
+     * Test that if an exclude-filter is provided without abi, we are still able to exclude all the
+     * matching modules for all abis.
+     */
+    @Test
+    public void testReschedule_excludeFilters_abi() throws Exception {
+        OptionSetter setter = new OptionSetter(mTest);
+        // We specify to exclude "run1"
+        setter.setOptionValue(BaseTestSuite.EXCLUDE_FILTER_OPTION, "run1");
+        populateFakeResults(2, 2, 1, 0, 0, false, new Abi("armeabi-v7a", "32"));
+        mMockLoader.init();
+        EasyMock.expect(mMockLoader.getCommandLine()).andReturn("previous_command");
+        EasyMock.expect(mMockFactory.createConfigurationFromArgs(EasyMock.anyObject()))
+                .andReturn(mRescheduledConfiguration);
+        EasyMock.expect(mMockLoader.loadPreviousRecord()).andReturn(mFakeRecord);
+
+        mRescheduledConfiguration.setTests(EasyMock.anyObject());
+        EasyMock.expectLastCall().times(1);
+
+        EasyMock.expect(mMockRescheduler.scheduleConfig(mRescheduledConfiguration)).andReturn(true);
+        EasyMock.replay(
+                mMockRescheduler,
+                mMockLoader,
+                mMockFactory,
+                mRescheduledConfiguration,
+                mMockCommandOptions);
+        mTest.run(null);
+        EasyMock.verify(
+                mMockRescheduler,
+                mMockLoader,
+                mMockFactory,
+                mRescheduledConfiguration,
+                mMockCommandOptions);
+
+        Set<String> excludeRun0 = new HashSet<>();
+        // Run with the abi are excluded
+        excludeRun0.add("armeabi-v7a run0 test.class#testPass0");
+        verify(mSuite).setExcludeFilter(excludeRun0);
+        Set<String> excludeRun1 = new HashSet<>();
+        // Even if run1 had failed test cases, it was excluded so it's not running.
+        excludeRun1.add("armeabi-v7a run1");
+        verify(mSuite).setExcludeFilter(excludeRun1);
+    }
+
     /** Test rescheduling a configuration when no parameterized tests previously failed. */
     @Test
     public void testReschedule_parameterized_nofail() throws Exception {
@@ -452,6 +498,18 @@
             int assumpFailure,
             int parameterized,
             boolean failedParam) {
+        populateFakeResults(
+                numModule, numTests, failedTests, assumpFailure, parameterized, failedParam, null);
+    }
+
+    private void populateFakeResults(
+            int numModule,
+            int numTests,
+            int failedTests,
+            int assumpFailure,
+            int parameterized,
+            boolean failedParam,
+            IAbi abi) {
         ProtoResultReporter reporter =
                 new ProtoResultReporter() {
                     @Override
@@ -464,7 +522,11 @@
         context.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, new BuildInfo());
         reporter.invocationStarted(context);
         for (int i = 0; i < numModule; i++) {
-            reporter.testRunStarted("run" + i, numTests);
+            String runName = "run" + i;
+            if (abi != null) {
+                runName = abi.getName() + " " + runName;
+            }
+            reporter.testRunStarted(runName, numTests);
             for (int j = 0; j < numTests - failedTests - assumpFailure - parameterized; j++) {
                 TestDescription test = new TestDescription("test.class", "testPass" + j);
                 reporter.testStarted(test);
diff --git a/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsParserTest.java b/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsParserTest.java
deleted file mode 100644
index 61c74bd..0000000
--- a/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsParserTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
-
-import junit.framework.TestCase;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
-/**
- * Unit tests for {@link XmlDefsParser}.
- */
-public class XmlDefsParserTest extends TestCase {
-
-    static final String TEST_PKG = "testpkg";
-    static final String TEST_COVERAGE_TARGET = "testcoverage";
-
-    private static final String TEST_DATA =
-        "<test name=\"frameworks-core\" " +
-        "package=\"" + TEST_PKG + "\" " +
-        "continuous=\"true\" />";
-
-    private static final String NON_CONTINUOUS_TEST_DATA =
-        "<test name=\"frameworks-core\" " +
-        "package=\"" + TEST_PKG + "\" " +
-        "/>";
-
-    private static final String FALSE_CONTINUOUS_TEST_DATA =
-        "<test name=\"frameworks-core\" " +
-        "package=\"" + TEST_PKG + "\" " +
-        "continuous=\"false\" />";
-
-    static final String FULL_DATA =
-        "<test name=\"frameworks-core\" " +
-        "package=\"" + TEST_PKG + "\" " +
-        "class=\"class\" " +
-        "runner=\"runner\" " +
-        "continuous=\"true\" " +
-        "coverage_target=\"" + TEST_COVERAGE_TARGET + "\" />";
-
-    /**
-     * Simple test for parsing a single test definition.
-     */
-    public void testParseSingleDef() throws ParseException {
-        XmlDefsParser parser = new XmlDefsParser();
-        parser.parse(getStringAsStream(TEST_DATA));
-        assertEquals(1, parser.getTestDefs().size());
-        InstrumentationTestDef def = parser.getTestDefs().iterator().next();
-        assertEquals("frameworks-core", def.getName());
-        assertEquals(TEST_PKG, def.getPackage());
-        assertTrue(def.isContinuous());
-        assertNull(def.getClassName());
-        assertNull(def.getRunner());
-        assertNull(def.getCoverageTarget());
-    }
-
-    /**
-     * Test for parsing a test definition without continuous attribute.
-     */
-    public void testParseNonContinuous() throws ParseException {
-        XmlDefsParser parser = new XmlDefsParser();
-        parser.parse(getStringAsStream(NON_CONTINUOUS_TEST_DATA));
-        assertEquals(1, parser.getTestDefs().size());
-        InstrumentationTestDef def = parser.getTestDefs().iterator().next();
-        assertFalse(def.isContinuous());
-    }
-
-    /**
-     * Test for parsing a test definition with continuous attribute equal false.
-     */
-    public void testParseFaleContinuous() throws ParseException {
-        XmlDefsParser parser = new XmlDefsParser();
-        parser.parse(getStringAsStream(FALSE_CONTINUOUS_TEST_DATA));
-        assertEquals(1, parser.getTestDefs().size());
-        InstrumentationTestDef def = parser.getTestDefs().iterator().next();
-        assertFalse(def.isContinuous());
-    }
-
-    /**
-     * Simple test for parsing a single test definition with all attributes defined.
-     */
-    public void testParseFullDef() throws ParseException {
-        XmlDefsParser parser = new XmlDefsParser();
-        parser.parse(getStringAsStream(FULL_DATA));
-        assertEquals(1, parser.getTestDefs().size());
-        InstrumentationTestDef def = parser.getTestDefs().iterator().next();
-        assertEquals("frameworks-core", def.getName());
-        assertEquals(TEST_PKG, def.getPackage());
-        assertTrue(def.isContinuous());
-        assertEquals("class", def.getClassName());
-        assertEquals("runner", def.getRunner());
-        assertEquals(TEST_COVERAGE_TARGET, def.getCoverageTarget());
-    }
-
-    /**
-     * Test parsing non-xml data throws a ParseException
-     */
-    public void testParseException()  {
-        try {
-            XmlDefsParser parser = new XmlDefsParser();
-            parser.parse(getStringAsStream("ghgh"));
-            fail("ParseException not thrown");
-        } catch (ParseException e) {
-            // expected
-        }
-    }
-
-    private InputStream getStringAsStream(String input) {
-        return new ByteArrayInputStream(input.getBytes());
-    }
-}
diff --git a/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsTestTest.java b/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsTestTest.java
deleted file mode 100644
index 03466f6..0000000
--- a/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsTestTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.InstrumentationTest;
-import com.android.tradefed.testtype.MockInstrumentationTest;
-
-import junit.framework.TestCase;
-
-import org.easymock.Capture;
-import org.easymock.EasyMock;
-import org.easymock.IAnswer;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.HashMap;
-
-/**
- * Unit tests for {@link XmlDefsTest}.
- */
-public class XmlDefsTestTest extends TestCase {
-
-    private static final String TEST_PATH = "foopath";
-    private static final String TEST_DEF_DATA = XmlDefsParserTest.FULL_DATA;
-    private static final String TEST_PKG = XmlDefsParserTest.TEST_PKG;
-    private static final String TEST_COVERAGE_TARGET = XmlDefsParserTest.TEST_COVERAGE_TARGET;
-    private ITestDevice mMockTestDevice;
-    private ITestInvocationListener mMockListener;
-    private XmlDefsTest mXmlTest;
-    private MockInstrumentationTest mMockInstrumentationTest;
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mMockTestDevice = EasyMock.createMock(ITestDevice.class);
-        EasyMock.expect(mMockTestDevice.getSerialNumber()).andReturn("foo").anyTimes();
-        mMockListener = EasyMock.createMock(ITestInvocationListener.class);
-        mMockInstrumentationTest = new MockInstrumentationTest();
-
-        mXmlTest = new XmlDefsTest() {
-            @Override
-            InstrumentationTest createInstrumentationTest() {
-                return mMockInstrumentationTest;
-            }
-        };
-        mXmlTest.setDevice(mMockTestDevice);
-    }
-
-    /**
-     * Test the run normal case. Simple verification that expected data is passed along, etc.
-     */
-    public void testRun() throws DeviceNotAvailableException {
-        mXmlTest.addRemoteFilePath(TEST_PATH);
-
-        injectMockXmlData();
-        mMockListener.testRunStarted(TEST_PKG, 0);
-        Capture<HashMap<String, Metric>> captureMetrics = new Capture<HashMap<String, Metric>>();
-        mMockListener.testRunEnded(EasyMock.anyLong(), EasyMock.capture(captureMetrics));
-        EasyMock.replay(mMockTestDevice, mMockListener);
-        mXmlTest.run(mMockListener);
-        assertEquals(mMockListener, mMockInstrumentationTest.getListener());
-        assertEquals(TEST_PKG, mMockInstrumentationTest.getPackageName());
-        assertEquals(
-                TEST_COVERAGE_TARGET,
-                captureMetrics
-                        .getValue()
-                        .get(XmlDefsTest.COVERAGE_TARGET_KEY)
-                        .getMeasurements()
-                        .getSingleString());
-    }
-
-    private void injectMockXmlData() throws DeviceNotAvailableException {
-        // TODO: it would be nice to mock out the file objects, so this test wouldn't need to do
-        // IO
-        mMockTestDevice.pullFile(EasyMock.eq(TEST_PATH), (File)EasyMock.anyObject());
-        EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
-            @Override
-            public Object answer() throws Throwable {
-             // simulate the pull file by dumping data into local file
-                FileOutputStream outStream;
-                try {
-                    outStream = new FileOutputStream((File)EasyMock.getCurrentArguments()[1]);
-                    outStream.write(TEST_DEF_DATA.getBytes());
-                    outStream.close();
-                    return true;
-                } catch (IOException e) {
-                    fail(e.toString());
-                }
-                return false;
-            }
-        });
-    }
-
-    /**
-     * Test a run that was aborted then resumed
-     */
-    public void testRun_resume() throws DeviceNotAvailableException {
-        mXmlTest.addRemoteFilePath(TEST_PATH);
-        // turn off sending of coverage for simplicity
-        mXmlTest.setSendCoverage(false);
-        injectMockXmlData();
-        mMockInstrumentationTest.setException(new DeviceNotAvailableException());
-        EasyMock.replay(mMockTestDevice, mMockListener);
-        try {
-            mXmlTest.run(mMockListener);
-            fail("DeviceNotAvailableException not thrown");
-        } catch (DeviceNotAvailableException e) {
-            // expected
-        }
-        // verify InstrumentationTest.run was called
-        assertEquals(mMockListener, mMockInstrumentationTest.getListener());
-        mMockInstrumentationTest.setException(null);
-        mMockInstrumentationTest.clearListener();
-        // resume test run, on a different device
-        ITestDevice newTestDevice = EasyMock.createMock(ITestDevice.class);
-        mXmlTest.setDevice(newTestDevice);
-        mXmlTest.run(mMockListener);
-        // verify InstrumentationTest.run was called again, with same listener + different device
-        assertEquals(mMockListener, mMockInstrumentationTest.getListener());
-        assertEquals(newTestDevice, mMockInstrumentationTest.getDevice());
-    }
-
-    /**
-     * Test that IllegalArgumentException is thrown when attempting run without setting device.
-     */
-    public void testRun_noDevice() throws Exception {
-        mXmlTest.addRemoteFilePath(TEST_PATH);
-        mXmlTest.setDevice(null);
-        try {
-            mXmlTest.run(mMockListener);
-            fail("IllegalArgumentException not thrown");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-        assertNull(mMockInstrumentationTest.getPackageName());
-    }
-
-    /**
-     * Test that IllegalArgumentException is thrown when attempting run without setting any file
-     * paths.
-     */
-    public void testRun_noPath() throws Exception {
-        try {
-            mXmlTest.run(mMockListener);
-            fail("IllegalArgumentException not thrown");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-        assertNull(mMockInstrumentationTest.getPackageName());
-    }
-}
diff --git a/tests/src/com/android/tradefed/util/BundletoolUtilTest.java b/tests/src/com/android/tradefed/util/BundletoolUtilTest.java
index e288bca..c61cc46 100644
--- a/tests/src/com/android/tradefed/util/BundletoolUtilTest.java
+++ b/tests/src/com/android/tradefed/util/BundletoolUtilTest.java
@@ -23,6 +23,8 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.ITestDevice;
 
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.easymock.EasyMock;
 import org.junit.After;
 import org.junit.Before;
@@ -61,6 +63,11 @@
                     protected String getAdbPath() {
                         return "adb";
                     }
+
+                    @Override
+                    protected File getBundletoolFile() {
+                        return mBundletoolJar;
+                    }
                 };
     }
 
@@ -88,12 +95,17 @@
                                 (String) EasyMock.anyObject(),
                                 (String) EasyMock.anyObject(),
                                 (String) EasyMock.anyObject(),
+                                (String) EasyMock.anyObject(),
                                 (String) EasyMock.anyObject()))
                 .andReturn(res)
                 .once();
+        Path expectedSpecFilePath =
+                Paths.get(mBundletoolJar.getParentFile().getAbsolutePath(), "serial.json");
+
 
         EasyMock.replay(mMockDevice, mMockRuntil);
-        mBundletoolUtil.generateDeviceSpecFile(mMockDevice);
+        String actualSpecFilePath = mBundletoolUtil.generateDeviceSpecFile(mMockDevice);
+        assertEquals(expectedSpecFilePath.toString(), actualSpecFilePath);
         EasyMock.verify(mMockRuntil);
         EasyMock.verify(mMockDevice);
     }
diff --git a/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java b/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java
index 01662a1..4a9d110 100644
--- a/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java
+++ b/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java
@@ -17,9 +17,9 @@
 
 import static org.junit.Assert.assertEquals;
 
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
 import com.android.tradefed.config.ConfigurationDescriptor;
 import com.android.tradefed.config.ConfigurationDescriptor.LocalTestRunner;
+import com.android.tradefed.config.OptionDef;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.testtype.Abi;
 
@@ -103,4 +103,20 @@
                         + "atest module_name:class_name -- --abi arm",
                 instruction);
     }
+
+    /** Test when a parameterized module needs to be repro. */
+    @Test
+    public void testGetInstruction_withParameter() {
+        ConfigurationDescriptor configDescriptor = new ConfigurationDescriptor();
+        configDescriptor.setAbi(new Abi(ABI_NAME, "32"));
+        configDescriptor.setModuleName(OPTION_SOURCE);
+        configDescriptor.addMetadata(ConfigurationDescriptor.PARAMETER_KEY, "instant");
+        String instruction =
+                LocalRunInstructionBuilder.getInstruction(
+                        configDescriptor, LocalTestRunner.ATEST, null);
+        assertEquals(
+                "Run following command to try the test in a local setup:\n"
+                        + "atest module_name -- --abi arm --instant",
+                instruction);
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java b/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java
new file mode 100644
index 0000000..523655c
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.IllegalStateException;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public final class NativeCodeCoverageFlusherTest {
+
+    @Mock ITestDevice mMockDevice;
+
+    // Object under test
+    NativeCodeCoverageFlusher mFlusher;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mFlusher = new NativeCodeCoverageFlusher(mMockDevice);
+    }
+
+    @Test
+    public void testClearCoverageMeasurements_rmCommandCalled() throws DeviceNotAvailableException {
+        doReturn(true).when(mMockDevice).isAdbRoot();
+
+        mFlusher.clearCoverageMeasurements();
+
+        // Verify that the rm command was executed.
+        verify(mMockDevice).executeShellCommand("rm -rf /data/misc/trace/*");
+    }
+
+    @Test
+    public void testNoAdbRootClearCoverageMeasurements_noOp() throws DeviceNotAvailableException {
+        doReturn(false).when(mMockDevice).isAdbRoot();
+
+        try {
+            mFlusher.clearCoverageMeasurements();
+            fail("Should have thrown an exception");
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+
+        // Verify that no shell command was executed.
+        verify(mMockDevice, never()).executeShellCommand(anyString());
+    }
+
+    @Test
+    public void testFlushCoverageAllProcesses_flushAllCommandCalled()
+            throws DeviceNotAvailableException {
+        doReturn(true).when(mMockDevice).isAdbRoot();
+
+        mFlusher.forceCoverageFlush(ImmutableList.of());
+
+        // Verify that the flush command for all processes was called.
+        verify(mMockDevice).executeShellCommand("kill -37 -1");
+    }
+
+    @Test
+    public void testFlushCoverageSpecificProcesses_flushSpecificCommandCalled()
+            throws DeviceNotAvailableException {
+        List<String> processes = ImmutableList.of("mediaserver", "mediaextractor");
+
+        doReturn(true).when(mMockDevice).isAdbRoot();
+        doReturn("12").when(mMockDevice).getProcessPid(processes.get(0));
+        doReturn("789").when(mMockDevice).getProcessPid(processes.get(1));
+
+        mFlusher.forceCoverageFlush(processes);
+
+        // Verify that the flush command for the specific processes was called.
+        verify(mMockDevice).executeShellCommand("kill -37 12 789");
+    }
+
+    @Test
+    public void testNoAdbRootFlush_noOp() throws DeviceNotAvailableException {
+        doReturn(false).when(mMockDevice).isAdbRoot();
+
+        try {
+            mFlusher.forceCoverageFlush(ImmutableList.of("mediaserver"));
+            fail("Should have thrown an exception");
+        } catch (IllegalStateException e) {
+            // Expected
+        }
+
+        // Verify no shell commands or pid lookups were executed.
+        verify(mMockDevice, never()).executeShellCommand(anyString());
+        verify(mMockDevice, never()).getProcessPid(anyString());
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/StreamUtilTest.java b/tests/src/com/android/tradefed/util/StreamUtilTest.java
index f1a238c..f2e774c 100644
--- a/tests/src/com/android/tradefed/util/StreamUtilTest.java
+++ b/tests/src/com/android/tradefed/util/StreamUtilTest.java
@@ -23,8 +23,10 @@
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 
@@ -119,6 +121,20 @@
     }
 
     /**
+     * Verify that {@link com.android.tradefed.util.StreamUtil#calculateCrc32(InputStream)} works as
+     * expected.
+     *
+     * @throws IOException
+     */
+    public void testCalculateCrc32() throws IOException {
+        final String source = getLargeText();
+        final long crc32 = 3023941728L;
+        ByteArrayInputStream inputSource = new ByteArrayInputStream(source.getBytes());
+        long actualCrc32 = StreamUtil.calculateCrc32(inputSource);
+        assertEquals(crc32, actualCrc32);
+    }
+
+    /**
      * Verify that {@link com.android.tradefed.util.StreamUtil#calculateMd5(InputStream)} works as
      * expected.
      *
@@ -156,6 +172,46 @@
         assertEquals(text, baos.toString());
     }
 
+    /**
+     * Verify that {@link com.android.tradefed.util.StreamUtil#copyStreams(InputStream,
+     * OutputStream, int, int)} can copy partial content.
+     */
+    public void testCopyStreams_partialSuccess() throws Exception {
+        String text = getLargeText();
+        StringBuilder builder = new StringBuilder(33 * 1024);
+        // Create a string longer than StreamUtil.BUF_SIZE
+        while (builder.length() < 32 * 1024) {
+            builder.append(text);
+        }
+        ByteArrayInputStream bais = new ByteArrayInputStream(builder.toString().getBytes());
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        // Skip the first 1kB, and read longer than StreamUtil.BUF_SIZE
+        StreamUtil.copyStreams(bais, baos, 1024, 20 * 1024);
+        bais.close();
+        baos.close();
+        assertEquals(builder.toString().substring(1024, 21 * 1024), baos.toString());
+    }
+
+    /**
+     * Verify that {@link com.android.tradefed.util.StreamUtil#copyStreams(InputStream,
+     * OutputStream, int, int)} cannot copy partial content if requested size is larger than what's
+     * available.
+     */
+    public void testCopyStreams_partialFail() throws Exception {
+        ByteArrayInputStream bais = null;
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            String text = getLargeText();
+            bais = new ByteArrayInputStream(text.getBytes());
+            // Skip the first 1kB, and read longer than the size of text
+            StreamUtil.copyStreams(bais, baos, 10, text.length() + 1024);
+            fail("IOException should be thrown when reading too much data.");
+        } catch (IOException e) {
+            // Ignore expected error.
+        } finally {
+            StreamUtil.close(bais);
+        }
+    }
+
     public void testCopyStreamToWriter() throws Exception {
         String text = getLargeText();
         ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes());
@@ -169,6 +225,26 @@
     }
 
     /**
+     * Verify that {@link com.android.tradefed.util.StreamUtil#copyFileToStream(File, OutputStream)}
+     * works as expected.
+     *
+     * @throws IOException
+     */
+    public void testCopyFileToStream() throws IOException {
+        String text = getLargeText();
+        File file = File.createTempFile("testCopyFileToStream", ".txt");
+        try {
+            FileUtil.writeToFile(text, file);
+            try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
+                StreamUtil.copyFileToStream(file, outStream);
+                assertEquals(text, outStream.toString());
+            }
+        } finally {
+            file.delete();
+        }
+    }
+
+    /**
      * Returns a large chunk of text that's at least 16K in size
      */
     private String getLargeText() {
diff --git a/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java b/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
index ffec8e4..9d84bab 100644
--- a/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
+++ b/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
@@ -89,16 +89,20 @@
         String[] contents = readInFile(SUBPROC_OUTPUT_FILE_1);
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
-        mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+        mockRunListener.testRunStarted(
+                EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+                EasyMock.eq(4),
+                EasyMock.eq(0),
+                EasyMock.anyLong());
         mockRunListener.testStarted((TestDescription) EasyMock.anyObject(), EasyMock.anyLong());
         EasyMock.expectLastCall().times(4);
         mockRunListener.testEnded(
                 (TestDescription) EasyMock.anyObject(),
                 EasyMock.anyLong(),
-                (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.<HashMap<String, Metric>>anyObject());
         EasyMock.expectLastCall().times(4);
         mockRunListener.testRunEnded(
-                EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
         EasyMock.expectLastCall().times(1);
         mockRunListener.testIgnored((TestDescription) EasyMock.anyObject());
         EasyMock.expectLastCall();
@@ -128,18 +132,22 @@
         String[] contents =  readInFile(SUBPROC_OUTPUT_FILE_2);
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
-        mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+        mockRunListener.testRunStarted(
+                EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+                EasyMock.eq(4),
+                EasyMock.eq(0),
+                EasyMock.anyLong());
         mockRunListener.testStarted((TestDescription) EasyMock.anyObject(), EasyMock.anyLong());
         EasyMock.expectLastCall().times(4);
         mockRunListener.testEnded(
                 (TestDescription) EasyMock.anyObject(),
                 EasyMock.anyLong(),
-                (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.<HashMap<String, Metric>>anyObject());
         EasyMock.expectLastCall().times(3);
         mockRunListener.testRunFailed((String)EasyMock.anyObject());
         EasyMock.expectLastCall().times(1);
         mockRunListener.testRunEnded(
-                EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
         EasyMock.expectLastCall().times(1);
         mockRunListener.testIgnored((TestDescription) EasyMock.anyObject());
         EasyMock.expectLastCall();
@@ -165,11 +173,15 @@
     public void testParse_testNotStarted() throws Exception {
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
-        mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+        mockRunListener.testRunStarted(
+                EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+                EasyMock.eq(4),
+                EasyMock.eq(0),
+                EasyMock.anyLong());
         mockRunListener.testEnded(
                 (TestDescription) EasyMock.anyObject(),
                 EasyMock.anyLong(),
-                (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.<HashMap<String, Metric>>anyObject());
         EasyMock.expectLastCall().times(1);
         EasyMock.replay(mockRunListener);
         File tmp = FileUtil.createTempFile("sub", "unit");
@@ -200,11 +212,15 @@
     public void testParse_noTimeStamp() throws Exception {
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
-        mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+        mockRunListener.testRunStarted(
+                EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+                EasyMock.eq(4),
+                EasyMock.eq(0),
+                EasyMock.anyLong());
         mockRunListener.testStarted(EasyMock.anyObject());
         mockRunListener.testEnded(
                 (TestDescription) EasyMock.anyObject(),
-                (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.<HashMap<String, Metric>>anyObject());
         EasyMock.expectLastCall().times(1);
         EasyMock.replay(mockRunListener);
         File tmp = FileUtil.createTempFile("sub", "unit");
@@ -272,11 +288,15 @@
     public void testParser_receiveFromSocket() throws Exception {
         ITestInvocationListener mockRunListener =
                 EasyMock.createMock(ITestInvocationListener.class);
-        mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+        mockRunListener.testRunStarted(
+                EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+                EasyMock.eq(4),
+                EasyMock.eq(0),
+                EasyMock.anyLong());
         mockRunListener.testEnded(
                 (TestDescription) EasyMock.anyObject(),
                 EasyMock.anyLong(),
-                (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.<HashMap<String, Metric>>anyObject());
         EasyMock.expectLastCall().times(1);
         EasyMock.replay(mockRunListener);
         SubprocessTestResultsParser resultParser = null;
@@ -473,7 +493,8 @@
     public void testParse_logAssociation() throws Exception {
         ILogSaverListener mockRunListener = EasyMock.createMock(ILogSaverListener.class);
         Capture<LogFile> capture = new Capture<>();
-        mockRunListener.logAssociation(EasyMock.eq("dataname"), EasyMock.capture(capture));
+        mockRunListener.logAssociation(
+                EasyMock.eq("subprocess-dataname"), EasyMock.capture(capture));
         EasyMock.replay(mockRunListener);
         LogFile logFile = new LogFile("path", "url", LogDataType.TEXT);
         File serializedLogFile = null;
diff --git a/tests/src/com/android/tradefed/util/TarUtilTest.java b/tests/src/com/android/tradefed/util/TarUtilTest.java
index cc9c267..4870b4d 100644
--- a/tests/src/com/android/tradefed/util/TarUtilTest.java
+++ b/tests/src/com/android/tradefed/util/TarUtilTest.java
@@ -88,6 +88,24 @@
     }
 
     /**
+     * Test that {TarUtil#extractTarGzipToTemp(File, String)} can extract properly a tar.gz file.
+     */
+    @Test
+    public void testExtractTarGzipToTemp() throws Exception {
+        InputStream logTarGz = getClass().getResourceAsStream(EMMA_METADATA_RESOURCE_PATH);
+        File tarGzFile = FileUtil.createTempFile("extract_tar_gz_test", ".tar.gz");
+        File tempDir = null;
+        try {
+            FileUtil.writeToFile(logTarGz, tarGzFile);
+            tempDir = TarUtil.extractTarGzipToTemp(tarGzFile, "extract_tar_gz_test");
+            Assert.assertEquals(2, tempDir.list().length);
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+            FileUtil.deleteFile(tarGzFile);
+        }
+    }
+
+    /**
      * Test that {TarUtil#extractAndLog(ITestLogger, File, String)} can untar properly a tar file
      * and export its content.
      */
diff --git a/tests/src/com/android/tradefed/util/UserUtilTest.java b/tests/src/com/android/tradefed/util/UserUtilTest.java
deleted file mode 100644
index ee18375..0000000
--- a/tests/src/com/android/tradefed/util/UserUtilTest.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.util;
-
-import com.android.tradefed.util.UserUtil.UserType;
-
-import java.util.Arrays;
-import java.util.ArrayList;
-
-import static org.junit.Assert.fail;
-
-import com.android.tradefed.device.ITestDevice;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-
-/** Unit tests for {@link UserChecker} */
-@RunWith(JUnit4.class)
-public class UserUtilTest {
-    @Test
-    public void testSwitchToUserSystemSuccess() throws Exception {
-        int currentUser = 12;
-
-        ITestDevice device = mock(ITestDevice.class);
-        when(device.switchUser(UserUtil.USER_SYSTEM)).thenReturn(true);
-
-        UserUtil.switchToUserType(device, UserType.SYSTEM);
-        verify(device, times(1)).switchUser(UserUtil.USER_SYSTEM);
-    }
-
-    @Test
-    public void testSwitchToUserSystemFail() throws Exception {
-        int currentUser = 12;
-
-        ITestDevice device = mock(ITestDevice.class);
-        when(device.switchUser(UserUtil.USER_SYSTEM)).thenReturn(false);
-
-        try {
-            UserUtil.switchToUserType(device, UserType.SYSTEM);
-            fail();
-        } catch (UserUtil.UserSwitchFailedException _expected) {
-        }
-        verify(device, times(1)).switchUser(UserUtil.USER_SYSTEM);
-    }
-
-    @Test
-    public void testSwitchToSecondaryUserCurrent() throws Exception {
-        int currentUser = 10;
-
-        ITestDevice device = mock(ITestDevice.class);
-        when(device.getCurrentUser()).thenReturn(currentUser);
-        when(device.isUserSecondary(currentUser)).thenReturn(true);
-
-        UserUtil.switchToUserType(device, UserUtil.UserType.SECONDARY);
-        verify(device, never()).switchUser(currentUser);
-    }
-
-    @Test
-    public void testSwitchToSecondaryUserExists() throws Exception {
-        ITestDevice device = mock(ITestDevice.class);
-        when(device.getCurrentUser()).thenReturn(0);
-        mockListUsers(device, new Integer[] {0, 10});
-        when(device.isUserSecondary(10)).thenReturn(true);
-        when(device.switchUser(10)).thenReturn(true);
-
-        UserUtil.switchToUserType(device, UserUtil.UserType.SECONDARY);
-        verify(device, times(1)).switchUser(10);
-    }
-
-    @Test
-    /** Validate that invalid user types will be skipped as secondaries. */
-    public void testSwitchToSecondaryUserWithInvalid() throws Exception {
-        ITestDevice device = mock(ITestDevice.class);
-        when(device.getCurrentUser()).thenReturn(0);
-        mockListUsers(device, new Integer[] {0, 10, 11, 12});
-        when(device.isUserSecondary(10)).thenReturn(false);
-        when(device.isUserSecondary(11)).thenReturn(false);
-        when(device.isUserSecondary(12)).thenReturn(true);
-        when(device.switchUser(12)).thenReturn(true);
-
-        UserUtil.switchToUserType(device, UserUtil.UserType.SECONDARY);
-        verify(device, times(1)).switchUser(12);
-    }
-
-    @Test
-    public void testSwitchToPrimaryUserNonSystem() throws Exception {
-        ITestDevice device = mock(ITestDevice.class);
-        when(device.getCurrentUser()).thenReturn(0);
-        when(device.getPrimaryUserId()).thenReturn(10);
-        when(device.switchUser(10)).thenReturn(true);
-
-        UserUtil.switchToUserType(device, UserUtil.UserType.PRIMARY);
-        verify(device, times(1)).switchUser(10);
-    }
-
-    // Helpers
-
-    private void mockListUsers(ITestDevice device, Integer[] userIds) throws Exception {
-        when(device.listUsers()).thenReturn(new ArrayList<Integer>(Arrays.asList(userIds)));
-    }
-}
diff --git a/tests/src/com/android/tradefed/util/ZipUtilTest.java b/tests/src/com/android/tradefed/util/ZipUtilTest.java
index 4e4fb2f..51dc090 100644
--- a/tests/src/com/android/tradefed/util/ZipUtilTest.java
+++ b/tests/src/com/android/tradefed/util/ZipUtilTest.java
@@ -15,13 +15,24 @@
  */
 package com.android.tradefed.util;
 
+import com.android.tradefed.util.zip.CentralDirectoryInfo;
+import com.android.tradefed.util.zip.EndCentralDirectoryInfo;
+import com.android.tradefed.util.zip.LocalFileHeader;
+
 import junit.framework.TestCase;
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.zip.ZipFile;
 
@@ -191,6 +202,159 @@
         }
     }
 
+    public void testPartipUnzip() throws Exception {
+        File partialZipFile = null;
+        File tmpDir = null;
+        Set<PosixFilePermission> permissions;
+        try {
+            // The zip file is small, read the whole file and assume it's partial.
+            // This does not affect testing the behavior of partial unzipping.
+            partialZipFile = getTestDataFile("partial_zip");
+            EndCentralDirectoryInfo endCentralDirInfo = new EndCentralDirectoryInfo(partialZipFile);
+            List<CentralDirectoryInfo> zipEntries =
+                    ZipUtil.getZipCentralDirectoryInfos(
+                            partialZipFile,
+                            endCentralDirInfo,
+                            endCentralDirInfo.getCentralDirOffset());
+            // The zip file has 3 folders, 4 files.
+            assertEquals(7, zipEntries.size());
+
+            CentralDirectoryInfo zipEntry;
+            LocalFileHeader localFileHeader;
+            File targetFile;
+            tmpDir = FileUtil.createTempDir("partial_unzip");
+
+            // Unzip empty file
+            zipEntry =
+                    zipEntries
+                            .stream()
+                            .filter(e -> e.getFileName().equals("empty_file"))
+                            .findFirst()
+                            .get();
+            targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+            localFileHeader =
+                    new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+            ZipUtil.unzipPartialZipFile(
+                    partialZipFile,
+                    targetFile,
+                    zipEntry,
+                    localFileHeader,
+                    zipEntry.getLocalHeaderOffset());
+            // Verify file permissions - readonly - 644 rw-r--r--
+            permissions = Files.getPosixFilePermissions(targetFile.toPath());
+            assertEquals(PosixFilePermissions.fromString("rw-r--r--"), permissions);
+
+            // Unzip text file
+            zipEntry =
+                    zipEntries
+                            .stream()
+                            .filter(e -> e.getFileName().equals("large_text/file.txt"))
+                            .findFirst()
+                            .get();
+            targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+            localFileHeader =
+                    new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+            ZipUtil.unzipPartialZipFile(
+                    partialZipFile,
+                    targetFile,
+                    zipEntry,
+                    localFileHeader,
+                    zipEntry.getLocalHeaderOffset());
+            // Verify CRC
+            long crc = FileUtil.calculateCrc32(targetFile);
+            assertEquals(4146093769L, crc);
+            try (BufferedReader br = new BufferedReader(new FileReader(targetFile))) {
+                String line = br.readLine();
+                assertTrue(line.endsWith("this is a text file."));
+            } catch (IOException e) {
+                // fail if the file is corrupt in any way
+                fail("failed reading text file");
+            }
+            // Verify file permissions - read/write - 666 rw-rw-rw-
+            permissions = Files.getPosixFilePermissions(targetFile.toPath());
+            assertEquals(PosixFilePermissions.fromString("rw-rw-rw-"), permissions);
+
+            // Verify file permissions - executable - 755 rwxr-xr-x
+            zipEntry =
+                    zipEntries
+                            .stream()
+                            .filter(e -> e.getFileName().equals("executable/executable_file"))
+                            .findFirst()
+                            .get();
+            targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+            localFileHeader =
+                    new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+            ZipUtil.unzipPartialZipFile(
+                    partialZipFile,
+                    targetFile,
+                    zipEntry,
+                    localFileHeader,
+                    zipEntry.getLocalHeaderOffset());
+            permissions = Files.getPosixFilePermissions(targetFile.toPath());
+            assertEquals(PosixFilePermissions.fromString("rwxr-xr-x"), permissions);
+
+            // Verify file permissions - readonly - 444 r--r--r--
+            zipEntry =
+                    zipEntries
+                            .stream()
+                            .filter(e -> e.getFileName().equals("read_only/readonly_file"))
+                            .findFirst()
+                            .get();
+            targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+            localFileHeader =
+                    new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+            ZipUtil.unzipPartialZipFile(
+                    partialZipFile,
+                    targetFile,
+                    zipEntry,
+                    localFileHeader,
+                    zipEntry.getLocalHeaderOffset());
+            permissions = Files.getPosixFilePermissions(targetFile.toPath());
+            assertEquals(PosixFilePermissions.fromString("r--r--r--"), permissions);
+
+            // Verify folder permissions - readonly - 744 rwxr--r--
+            zipEntry =
+                    zipEntries
+                            .stream()
+                            .filter(e -> e.getFileName().equals("read_only/"))
+                            .findFirst()
+                            .get();
+            targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+            localFileHeader =
+                    new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+            ZipUtil.unzipPartialZipFile(
+                    partialZipFile,
+                    targetFile,
+                    zipEntry,
+                    localFileHeader,
+                    zipEntry.getLocalHeaderOffset());
+            permissions = Files.getPosixFilePermissions(targetFile.toPath());
+            assertEquals(PosixFilePermissions.fromString("rwxr--r--"), permissions);
+
+            // Verify folder permissions - read/write - 755 rwxr-xr-x
+            zipEntry =
+                    zipEntries
+                            .stream()
+                            .filter(e -> e.getFileName().equals("large_text/"))
+                            .findFirst()
+                            .get();
+            targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+            localFileHeader =
+                    new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+            ZipUtil.unzipPartialZipFile(
+                    partialZipFile,
+                    targetFile,
+                    zipEntry,
+                    localFileHeader,
+                    zipEntry.getLocalHeaderOffset());
+            permissions = Files.getPosixFilePermissions(targetFile.toPath());
+            assertEquals(PosixFilePermissions.fromString("rwxr-xr-x"), permissions);
+        } finally {
+            FileUtil.deleteFile(partialZipFile);
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
+
     // Helpers
     private File createTempDir(String prefix) throws IOException {
         return createTempDir(prefix, null);
diff --git a/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java b/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java
index 464780a..bd9b379 100644
--- a/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java
+++ b/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java
@@ -36,6 +36,7 @@
  */
 public class Sl4aClientTest {
 
+    private static final String DEVICE_SERIAL = "54321";
     private Sl4aClient mClient = null;
     private FakeSocketServerHelper mDeviceServer;
     private ITestDevice mMockDevice;
@@ -125,6 +126,7 @@
      */
     private void setupStartExpectation() throws DeviceNotAvailableException {
         final String cmd = String.format(Sl4aClient.SL4A_LAUNCH_CMD, mDeviceServer.getPort());
+        EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn(DEVICE_SERIAL);
         EasyMock.expect(mMockDevice.executeShellCommand(cmd))
                 .andReturn("");
         EasyMock.expect(mMockDevice.executeShellCommand(Sl4aClient.IS_SL4A_RUNNING_CMD))
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
index 56cfa8c..8c53c17 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
@@ -32,6 +32,8 @@
 
 import java.io.File;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
@@ -504,4 +506,35 @@
             FileUtil.recursiveDelete(tempDir);
         }
     }
+
+    /** Test for {@link TestMapping#removeComments()} for removing comments in TEST_MAPPING file. */
+    @Test
+    public void testRemoveComments() throws Exception {
+        String jsonString = getJsonStringByName("test_mapping_with_comments1");
+        String goldenString = getJsonStringByName("test_mapping_golden1");
+        assertEquals(TestMapping.removeComments(jsonString), goldenString);
+    }
+
+    /** Test for {@link TestMapping#removeComments()} for removing comments in TEST_MAPPING file. */
+    @Test
+    public void testRemoveComments2() throws Exception {
+        String jsonString = getJsonStringByName("test_mapping_with_comments2");
+        String goldenString = getJsonStringByName("test_mapping_golden2");
+        assertEquals(TestMapping.removeComments(jsonString), goldenString);
+    }
+
+    private String getJsonStringByName(String fileName) throws Exception  {
+        File tempDir = null;
+        try {
+            tempDir = FileUtil.createTempDir("test_mapping");
+            File srcDir = FileUtil.createTempDir("src", tempDir);
+            String srcFile = File.separator + TEST_DATA_DIR + File.separator + fileName;
+            InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+            FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+            Path file = Paths.get(srcDir.getAbsolutePath(), TEST_MAPPING);
+            return String.join("\n", Files.readAllLines(file, StandardCharsets.UTF_8));
+        } finally {
+            FileUtil.recursiveDelete(tempDir);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/xml/AndroidManifestWriterTest.java b/tests/src/com/android/tradefed/util/xml/AndroidManifestWriterTest.java
deleted file mode 100644
index 74488cb..0000000
--- a/tests/src/com/android/tradefed/util/xml/AndroidManifestWriterTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.util.xml;
-
-import com.android.tradefed.util.FileUtil;
-
-import junit.framework.TestCase;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Unit tests for {@link AndroidManifestWriter}.
- */
-public class AndroidManifestWriterTest extends TestCase {
-
-    private static final String TEST_SDK_VERSION = "99";
-
-    /**
-     * Success case test for {@link AndroidManifestWriter} when minSdkVersion is already set.
-     */
-    public void testSetMinSdkVersion() throws Exception {
-        assertMinSdkChange("AndroidManifest_usessdk.xml", "AndroidManifest_usessdk_result.xml");
-    }
-
-    /**
-     * Success case test for {@link AndroidManifestWriter} when minSdkVersion is not present in xml.
-     */
-    public void testSetMinSdkVersion_missing() throws Exception {
-        assertMinSdkChange("AndroidManifest_missing.xml", "AndroidManifest_missing_result.xml");
-    }
-
-    /**
-     * Negative case test for {@link AndroidManifestWriter} when xml is invalid.
-     */
-    public void testSetMinSdkVersion_invalid() throws IOException {
-        File manifest = extractTestXml("AndroidManifest_invalid.xml");
-        try {
-            assertNull(AndroidManifestWriter.parse(manifest.getAbsolutePath()));
-        } finally {
-            FileUtil.deleteFile(manifest);
-        }
-    }
-
-    private void assertMinSdkChange(String inputFileName, String resultFileName)
-            throws IOException {
-        File manifest = extractTestXml(inputFileName);
-        File expectedResultFile = extractTestXml(resultFileName);
-        try {
-            AndroidManifestWriter writer = AndroidManifestWriter.parse(manifest.getAbsolutePath());
-            assertNotNull(writer);
-            writer.setMinSdkVersion(TEST_SDK_VERSION);
-            assertTrue(String.format("File contents of %s and %s are not equal", inputFileName,
-                    resultFileName), FileUtil.compareFileContents(manifest, expectedResultFile));
-        } finally {
-            FileUtil.deleteFile(manifest);
-            FileUtil.deleteFile(expectedResultFile);
-        }
-    }
-
-    /**
-     * Helper method to extract a test data file from current jar, and store it as a file on local
-     * disk.
-     *
-     * @param fileName the base file name
-     * @return the {@link File}
-     * @throws IOException
-     */
-    private File extractTestXml(String fileName) throws IOException {
-        InputStream testStream = getClass().getResourceAsStream(File.separator + "xml" +
-                File.separator + fileName);
-        assertNotNull(testStream);
-        File tmpFile = FileUtil.createTempFile(fileName, ".xml");
-        FileUtil.writeToFile(testStream, tmpFile);
-        return tmpFile;
-    }
-}
diff --git a/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java b/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java
new file mode 100644
index 0000000..5cd860d
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util.zip;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Unit tests for {@link MergedZipEntryCollection} */
+@RunWith(JUnit4.class)
+public class MergedZipEntryCollectionTest {
+
+    private static final int COMPRESSED_SIZE = 2 * MergedZipEntryCollection.MAX_GAP;
+
+    /** Test that zip entries are merged into sections as expected due to small gap in size. */
+    @Test
+    public void testMergeZipEntries_smallGap() throws Exception {
+        List<CentralDirectoryInfo> entries = new ArrayList<>();
+        long startOffset = 10;
+        CentralDirectoryInfo info = createZipEntry(startOffset);
+        entries.add(info);
+        startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+        // Create a gap smaller than MAX_GAP.
+        startOffset += MergedZipEntryCollection.MAX_GAP / 2;
+        info = createZipEntry(startOffset);
+        entries.add(info);
+        startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+        List<MergedZipEntryCollection> collections =
+                MergedZipEntryCollection.CreateCollections(entries);
+        assertEquals(1, collections.size());
+        assertEquals(2, collections.get(0).getZipEntries().size());
+        assertEquals(10, collections.get(0).getStartOffset());
+        assertEquals(22598, collections.get(0).getEndOffset());
+    }
+
+    /** Test that zip entries are not merged due to large gap in size. */
+    @Test
+    public void testMergeZipEntries_largeGap() throws Exception {
+        List<CentralDirectoryInfo> entries = new ArrayList<>();
+        long startOffset = 10;
+        CentralDirectoryInfo info = createZipEntry(startOffset);
+        entries.add(info);
+        startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+        // Create a gap larger than MAX_GAP.
+        startOffset += MergedZipEntryCollection.MAX_GAP * 2;
+        info = createZipEntry(startOffset);
+        entries.add(info);
+        startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+        List<MergedZipEntryCollection> collections =
+                MergedZipEntryCollection.CreateCollections(entries);
+        assertEquals(2, collections.size());
+        assertEquals(1, collections.get(0).getZipEntries().size());
+        assertEquals(10, collections.get(0).getStartOffset());
+        assertEquals(10280, collections.get(0).getEndOffset());
+        assertEquals(18472, collections.get(1).getStartOffset());
+        assertEquals(28742, collections.get(1).getEndOffset());
+    }
+
+    /** Test that zip entries are merged into a single section due to a small gap in percentage. */
+    @Test
+    public void testMergeZipEntries_smallGapPercent() throws Exception {
+        List<CentralDirectoryInfo> entries = new ArrayList<>();
+        long startOffset = 10;
+        // Create enough entries so the gap size is greater than MAX_GAP.
+        for (int i = 0; i < 10; i++) {
+            CentralDirectoryInfo info = createZipEntry(startOffset);
+            entries.add(info);
+            startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+        }
+
+        // Create a gap smaller than MAX_GAP_PERCENTAGE
+        startOffset += (long) (startOffset * MergedZipEntryCollection.MAX_GAP_PERCENTAGE) - 1024;
+        CentralDirectoryInfo info = createZipEntry(startOffset);
+        entries.add(info);
+        startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+        List<MergedZipEntryCollection> collections =
+                MergedZipEntryCollection.CreateCollections(entries);
+        assertEquals(1, collections.size());
+        assertEquals(11, collections.get(0).getZipEntries().size());
+        assertEquals(10, collections.get(0).getStartOffset());
+        assertEquals(127362, collections.get(0).getEndOffset());
+    }
+
+    /** Test that zip entries are not merged due to a large gap in percentage. */
+    @Test
+    public void testMergeZipEntries_largeGapPercent() throws Exception {
+        List<CentralDirectoryInfo> entries = new ArrayList<>();
+        long startOffset = 10;
+        // Create enough entries so the gap size is greater than MAX_GAP.
+        for (int i = 0; i < 10; i++) {
+            CentralDirectoryInfo info = createZipEntry(startOffset);
+            entries.add(info);
+            startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+        }
+
+        // Create a gap larger than MAX_GAP_PERCENTAGE
+        startOffset += (long) (startOffset * MergedZipEntryCollection.MAX_GAP_PERCENTAGE * 3);
+        CentralDirectoryInfo info = createZipEntry(startOffset);
+        entries.add(info);
+        startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+        List<MergedZipEntryCollection> collections =
+                MergedZipEntryCollection.CreateCollections(entries);
+        assertEquals(2, collections.size());
+        assertEquals(10, collections.get(0).getZipEntries().size());
+        assertEquals(10, collections.get(0).getStartOffset());
+        assertEquals(102710, collections.get(0).getEndOffset());
+        assertEquals(1, collections.get(1).getZipEntries().size());
+        assertEquals(148929, collections.get(1).getStartOffset());
+        assertEquals(159199, collections.get(1).getEndOffset());
+    }
+
+    /** Create a {@link CentralDirectoryInfo} object for testing. */
+    private CentralDirectoryInfo createZipEntry(long startOffset) {
+        CentralDirectoryInfo info = new CentralDirectoryInfo();
+        info.setLocalHeaderOffset(startOffset);
+        info.setCompressedSize(COMPRESSED_SIZE);
+        return info;
+    }
+}
diff --git a/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java b/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
index 5edb2d4..f59dc1f 100644
--- a/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
+++ b/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
@@ -22,6 +22,7 @@
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
+import android.os.SystemClock;
 import android.util.Log;
 
 import org.apache.http.client.HttpClient;
@@ -88,12 +89,12 @@
             throw new WifiException(
                 String.format("Failed %s due to invalid timeout (%d ms)", description, timeout));
         }
-        long startTime = System.currentTimeMillis();
+        long startTime = SystemClock.uptimeMillis();
         long endTime = startTime + timeout;
         try {
-            while (System.currentTimeMillis() < endTime) {
+            while (SystemClock.uptimeMillis() < endTime) {
                 if (checker.call()) {
-                    long elapsed = System.currentTimeMillis() - startTime;
+                    long elapsed = SystemClock.uptimeMillis() - startTime;
                     Log.i(TAG, String.format(
                         "Time elapsed waiting for %s: %d ms", description, elapsed));
                     return elapsed;
diff --git a/verify.sh b/verify.sh
deleted file mode 100755
index 7484194..0000000
--- a/verify.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/bash
-
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# A helper script that launches Trade Federation's "Verify" entrypoint, to perform
-# standalone command file verification
-
-shdir=`dirname $0`/
-source "${shdir}/script_help.sh"
-# At this point, we're guaranteed to have the right Java version, and the following
-# env variables will be set, if appropriate:
-# JAVA_VERSION, RDBG_FLAG, TF_PATH, TRADEFED_OPTS
-
-
-# Note: must leave $RDBG_FLAG and $TRADEFED_OPTS unquoted so that they go away when unset
-java $RDBG_FLAG -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow $TRADEFED_OPTS \
-  -cp "${TF_PATH}" com.android.tradefed.command.Verify "$@"