Merge "Snap for 5798008 from 9b49d23b8165404d5d97554b05c0d96a66d6566f to sdk-release" into sdk-release
diff --git a/.classpath b/.classpath
index 95c2966..6c19af3 100644
--- a/.classpath
+++ b/.classpath
@@ -13,7 +13,6 @@
 	<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"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/loganalysis"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/platform-annotations"/>
@@ -41,5 +40,6 @@
 	<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="var" path="TRADEFED_ROOT/out/soong/.intermediates/platform_testing/libraries/health/runners/longevity/host/longevity-base-lib/linux_glibc_common/javac/longevity-base-lib.jar"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/Android.bp b/Android.bp
index 4b6d838..1d55ff1 100644
--- a/Android.bp
+++ b/Android.bp
@@ -114,10 +114,7 @@
         "junit-params",
         "kxml2-2.3.0",
         "libprotobuf-java-full",
-        "longevity-host-lib",
-        "perfetto_config-full",
         "platform-test-annotations",
-        "test-composers",
         "tf-remote-client",
         "tradefed-protos",
     ],
diff --git a/README.md b/README.md
index dbc3667..201d82b 100644
--- a/README.md
+++ b/README.md
@@ -17,3 +17,6 @@
 
 See more details about Tradefed Architecture at:
 https://source.android.com/devices/tech/test_infra/tradefed/architecture
+
+If you are a tests writer you should start looking in the test_framework/
+component which contains everything needed to write a tests in Tradefed.
diff --git a/atest/atest.py b/atest/atest.py
index 6baa28e..78fa306 100755
--- a/atest/atest.py
+++ b/atest/atest.py
@@ -32,10 +32,11 @@
 import time
 import platform
 
+from multiprocessing import Process
+
 import atest_arg_parser
 import atest_error
 import atest_execution_info
-import atest_metrics
 import atest_utils
 import bug_detector
 import cli_translator
@@ -49,6 +50,7 @@
 from metrics import metrics_base
 from metrics import metrics_utils
 from test_runners import regression_test_runner
+from tools import atest_tools
 
 EXPECTED_VARS = frozenset([
     constants.ANDROID_BUILD_TOP,
@@ -66,6 +68,12 @@
 TEST_COUNT = 'test_count'
 TEST_TYPE = 'test_type'
 
+# Tasks that must run in the build time but unable to build by soong.
+# (e.g subprocesses that invoke host commands.)
+EXTRA_TASKS = {
+    'index-targets': atest_tools.index_targets
+}
+
 
 def _parse_args(argv):
     """Parse command line arguments.
@@ -157,6 +165,8 @@
         extra_args[constants.POST_PATCH_ITERATIONS] = args.generate_new_metrics
     if args.instant:
         extra_args[constants.INSTANT] = args.instant
+    if args.secondary_user:
+        extra_args[constants.SECONDARY_USER] = args.secondary_user
     if args.host:
         extra_args[constants.HOST] = args.host
     if args.dry_run:
@@ -528,7 +538,6 @@
     args = _parse_args(argv)
     _configure_logging(args.verbose)
     _validate_args(args)
-    atest_metrics.log_start_event()
     metrics_utils.get_start_time()
     metrics.AtestStartEvent(
         command_line=' '.join(argv),
@@ -581,11 +590,20 @@
     # args.steps will be None if none of -bit set, else list of params set.
     steps = args.steps if args.steps else constants.ALL_STEPS
     if build_targets and constants.BUILD_STEP in steps:
+        if constants.TEST_STEP in steps:
+            # Run extra tasks with building deps concurrently.
+            # When only "-b" is given(without -t), will not index targets.
+            for task in EXTRA_TASKS.values():
+                proc = Process(target=task)
+                # Daemonlise proc so it terminates with the main process.
+                proc.daemon = True
+                proc.start()
         # Add module-info.json target to the list of build targets to keep the
         # file up to date.
         build_targets.add(mod_info.module_info_target)
         build_start = time.time()
-        success = atest_utils.build(build_targets, args.verbose)
+        success = atest_utils.build(build_targets, verbose=args.verbose,
+                                    env_vars=constants.ATEST_BUILD_ENV)
         metrics.BuildFinishEvent(
             duration=metrics_utils.convert_duration(time.time() - build_start),
             success=success,
diff --git a/atest/atest_arg_parser.py b/atest/atest_arg_parser.py
index 2c9711f..7571160 100644
--- a/atest/atest_arg_parser.py
+++ b/atest/atest_arg_parser.py
@@ -83,6 +83,8 @@
                                'if the module supports it. Note: running a test '
                                'that does not support instant with --instant '
                                'will result in nothing running.')
+        self.add_argument('--secondary-user', action='store_true',
+                          help='Run test with secondary user.')
         # Options related to Test Mapping
         self.add_argument('-p', '--test-mapping', action='store_true',
                           help='Run tests in TEST_MAPPING files.')
diff --git a/atest/atest_utils.py b/atest/atest_utils.py
index 983c935..2dc1b6b 100644
--- a/atest/atest_utils.py
+++ b/atest/atest_utils.py
@@ -59,6 +59,7 @@
                                'sample_test_cmd_result.json')
 TEST_INFO_CACHE_ROOT = os.path.join(os.path.expanduser('~'), '.atest',
                                     'info_cache')
+_DEFAULT_TERMINAL_WIDTH = 80
 
 def _capture_fail_section(full_log):
     """Return the error message from the build output.
@@ -97,9 +98,11 @@
                             stderr=subprocess.STDOUT, env=env_vars)
     sys.stdout.write('\n')
     # Determine the width of the terminal. We'll need to clear this many
-    # characters when carriage returning.
-    _, term_width = os.popen('stty size', 'r').read().split()
-    term_width = int(term_width)
+    # characters when carriage returning. Set default value as 80.
+    term_width = _DEFAULT_TERMINAL_WIDTH
+    stty_size = os.popen('stty size').read()
+    if stty_size:
+        term_width = int(stty_size.split()[1])
     white_space = " " * int(term_width)
     full_output = []
     while proc.poll() is None:
diff --git a/atest/atest_utils_unittest.py b/atest/atest_utils_unittest.py
index b41f113..f2c125c 100755
--- a/atest/atest_utils_unittest.py
+++ b/atest/atest_utils_unittest.py
@@ -42,10 +42,12 @@
 TEST_SUITE_A = 'FakeSuiteA'
 TEST_MODULE_CLASS_A = 'FAKE_MODULE_CLASS_A'
 TEST_INSTALL_LOC_A = set(['host', 'device'])
+TEST_FINDER_A = 'MODULE'
 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)
+TEST_INFO_A.test_finder = TEST_FINDER_A
 
 #pylint: disable=protected-access
 class AtestUtilsUnittests(unittest.TestCase):
@@ -362,10 +364,10 @@
         """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,
+        atest_utils.update_test_info_cache(test_reference, [TEST_INFO_A],
                                            test_cache_dir)
-        unittest_utils.assert_equal_testinfos(
-            self, TEST_INFO_A,
+        unittest_utils.assert_equal_testinfo_sets(
+            self, set([TEST_INFO_A]),
             atest_utils.load_test_info_cache(test_reference, test_cache_dir))
 
 if __name__ == "__main__":
diff --git a/atest/cli_translator.py b/atest/cli_translator.py
index b56e79a..97d7616 100644
--- a/atest/cli_translator.py
+++ b/atest/cli_translator.py
@@ -39,6 +39,7 @@
 
 TEST_MAPPING = 'TEST_MAPPING'
 FUZZY_FINDER = 'FUZZY'
+CACHE_FINDER = 'CACHE'
 
 # Pattern used to identify comments start with '//' or '#' in TEST_MAPPING.
 _COMMENTS_RE = re.compile(r'(?m)[\s\t]*(#|//).*|(\".*?\")')
@@ -98,18 +99,22 @@
             except atest_error.TestDiscoveryException as e:
                 find_test_err_msg = e
             if found_test_infos:
+                finder_info = finder.finder_info
                 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
+                    if finder_info != CACHE_FINDER:
+                        test_info.test_finder = finder_info
                     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))
+                if finder_info == CACHE_FINDER and test_infos:
+                    test_finders.append(list(test_infos)[0].test_finder)
                 test_finders.append(finder_info)
                 test_info_str = ','.join([str(x) for x in found_test_infos])
                 break
diff --git a/atest/constants_default.py b/atest/constants_default.py
index 676f993..f1b21f0 100644
--- a/atest/constants_default.py
+++ b/atest/constants_default.py
@@ -16,6 +16,7 @@
 Various globals used by atest.
 """
 
+import os
 import re
 
 MODE = 'DEFAULT'
@@ -45,6 +46,7 @@
 DRY_RUN = 'DRY_RUN'
 ANDROID_SERIAL = 'ANDROID_SERIAL'
 INSTANT = 'INSTANT'
+SECONDARY_USER = 'SECONDARY_USER'
 
 # Application exit codes.
 EXIT_CODE_SUCCESS = 0
@@ -66,8 +68,6 @@
 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'
@@ -168,3 +168,34 @@
 
 # ATest TF
 ATEST_TF_MODULE = 'atest-tradefed'
+
+# Build environment variable for each build on ATest
+# With SOONG_COLLECT_JAVA_DEPS enabled, out/soong/module_bp_java_deps.json will
+# be generated when make.
+ATEST_BUILD_ENV = {'SOONG_COLLECT_JAVA_DEPS':'true'}
+
+# Atest cache root and relative dirs/caches.
+INDEX_DIR = os.path.join(os.getenv(ANDROID_HOST_OUT, ''), 'indexes')
+LOCATE_CACHE = os.path.join(INDEX_DIR, 'mlocate.db')
+INT_INDEX = os.path.join(INDEX_DIR, 'integration.idx')
+CLASS_INDEX = os.path.join(INDEX_DIR, 'classes.idx')
+CC_CLASS_INDEX = os.path.join(INDEX_DIR, 'cc_classes.idx')
+PACKAGE_INDEX = os.path.join(INDEX_DIR, 'packages.idx')
+QCLASS_INDEX = os.path.join(INDEX_DIR, 'fqcn.idx')
+MODULE_INDEX = os.path.join(INDEX_DIR, 'modules.idx')
+
+# Regeular Expressions
+CC_EXT_RE = re.compile(r'.*\.(cc|cpp)$')
+JAVA_EXT_RE = re.compile(r'.*\.(java|kt)$')
+# e.g. /path/to/ccfile.cc: TEST_F(test_name, method_name){
+CC_OUTPUT_RE = re.compile(r'(?P<file_path>/.*):\s*TEST(_F|_P)?[ ]*\('
+                          r'(?P<test_name>\w+)\s*,\s*(?P<method_name>\w+)\)'
+                          r'\s*\{')
+CC_GREP_RE = r'^[ ]*TEST(_P|_F)?[ ]*\([[:alnum:]].*,'
+# e.g. /path/to/Javafile.java:package com.android.settings.accessibility
+# grab the path, Javafile(class) and com.android.settings.accessibility(package)
+CLASS_OUTPUT_RE = re.compile(r'(?P<java_path>.*/(?P<class>[A-Z]\w+)\.\w+)[:].*')
+QCLASS_OUTPUT_RE = re.compile(r'(?P<java_path>.*/(?P<class>[A-Z]\w+)\.\w+)'
+                              r'[:]\s*package\s+(?P<package>[^(;|\s)]+)\s*')
+PACKAGE_OUTPUT_RE = re.compile(r'(?P<java_path>.*)[:]\s*package\s+'
+                               r'(?P<package>[^(;|\s)]+)\s*')
diff --git a/atest/module_info.py b/atest/module_info.py
index 1cd911f..5e0104a 100644
--- a/atest/module_info.py
+++ b/atest/module_info.py
@@ -80,7 +80,8 @@
             logging.debug('Generating %s - this is required for '
                           'initial runs.', _MODULE_INFO)
             atest_utils.build([module_info_target],
-                              logging.getLogger().isEnabledFor(logging.DEBUG))
+                              verbose=logging.getLogger().isEnabledFor(logging.DEBUG),
+                              env_vars=constants.ATEST_BUILD_ENV)
         return module_info_target, module_file_path
 
     def _load_module_info_file(self, force_build, module_file):
diff --git a/atest/test_finder_handler.py b/atest/test_finder_handler.py
index 236f177..360c66e 100644
--- a/atest/test_finder_handler.py
+++ b/atest/test_finder_handler.py
@@ -36,18 +36,21 @@
 # Explanation of REFERENCE_TYPEs:
 # ----------------------------------
 # 0. MODULE: LOCAL_MODULE or LOCAL_PACKAGE_NAME value in Android.mk/Android.bp.
-# 1. MODULE_CLASS: Combo of MODULE and CLASS as "module:class".
-# 2. PACKAGE: package in java file. Same as file path to java file.
-# 3. MODULE_PACKAGE: Combo of MODULE and PACKAGE as "module:package".
-# 4. MODULE_FILE_PATH: File path to dir of tests or test itself.
-# 5. INTEGRATION_FILE_PATH: File path to config xml in one of the 4 integration
+# 1. CLASS: Names which the same with a ClassName.java/kt file.
+# 2. QUALIFIED_CLASS: String like "a.b.c.ClassName".
+# 3. MODULE_CLASS: Combo of MODULE and CLASS as "module:class".
+# 4. PACKAGE: Package in java file. Same as file path to java file.
+# 5. MODULE_PACKAGE: Combo of MODULE and PACKAGE as "module:package".
+# 6. MODULE_FILE_PATH: File path to dir of tests or test itself.
+# 7. INTEGRATION_FILE_PATH: File path to config xml in one of the 4 integration
 #                           config directories.
-# 6. INTEGRATION: xml file name in one of the 4 integration config directories.
-# 7. SUITE: Value of the "run-suite-tag" in xml config file in 4 config dirs.
+# 8. INTEGRATION: xml file name in one of the 4 integration config directories.
+# 9. SUITE: Value of the "run-suite-tag" in xml config file in 4 config dirs.
 #           Same as value of "test-suite-tag" in AndroidTest.xml files.
-# 8. CC_CLASS: Test case in cc file.
-# 9. SUITE_PLAN: Suite name such as cts.
-# 10. SUITE_PLAN_FILE_PATH: File path to config xml in the suite config directories.
+# 10. CC_CLASS: Test case in cc file.
+# 11. SUITE_PLAN: Suite name such as cts.
+# 12. SUITE_PLAN_FILE_PATH: File path to config xml in the suite config directories.
+# 13. CACHE: A pseudo type that runs cache_finder without finding test in real.
 _REFERENCE_TYPE = atest_enum.AtestEnum(['MODULE', 'CLASS', 'QUALIFIED_CLASS',
                                         'MODULE_CLASS', 'PACKAGE',
                                         'MODULE_PACKAGE', 'MODULE_FILE_PATH',
@@ -142,7 +145,7 @@
                 _REFERENCE_TYPE.MODULE_FILE_PATH,
                 _REFERENCE_TYPE.INTEGRATION,
                 _REFERENCE_TYPE.SUITE_PLAN_FILE_PATH,
-                # TODO: Comment in SUITE when it's supported
+                # TODO: Uncomment in SUITE when it's supported
                 # _REFERENCE_TYPE.SUITE
                ]
     if '.' in ref:
@@ -182,7 +185,7 @@
     # If this ever becomes not the case, then we need to include path below.
     return [_REFERENCE_TYPE.CACHE,
             _REFERENCE_TYPE.INTEGRATION,
-            # TODO: Comment in SUITE when it's supported
+            # TODO: Uncomment in SUITE when it's supported
             # _REFERENCE_TYPE.SUITE,
             _REFERENCE_TYPE.MODULE,
             _REFERENCE_TYPE.SUITE_PLAN,
diff --git a/atest/test_finders/cache_finder.py b/atest/test_finders/cache_finder.py
index 3937ef0..5b7bd07 100644
--- a/atest/test_finders/cache_finder.py
+++ b/atest/test_finders/cache_finder.py
@@ -18,6 +18,7 @@
 
 import atest_utils
 from test_finders import test_finder_base
+from test_finders import test_info
 
 class CacheFinder(test_finder_base.TestFinderBase):
     """Cache Finder class."""
@@ -26,6 +27,24 @@
     def __init__(self, **kwargs):
         super(CacheFinder, self).__init__()
 
+    def _is_latest_testinfos(self, test_infos):
+        """Check whether test_infos are up-to-date.
+
+        Args:
+            test_infos: A list of TestInfo.
+
+        Returns:
+            True if all keys in test_infos and TestInfo object are equal.
+            Otherwise, False.
+        """
+        sorted_base_ti = sorted(
+            vars(test_info.TestInfo(None, None, None)).keys())
+        for cached_test_info in test_infos:
+            sorted_cache_ti = sorted(vars(cached_test_info).keys())
+            if not sorted_cache_ti == sorted_base_ti:
+                return False
+        return True
+
     def find_test_by_cache(self, test_reference):
         """Find the matched test_infos in saved caches.
 
@@ -33,6 +52,10 @@
             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.
+            A list of TestInfo namedtuple if cache found and is in latest
+            TestInfo format, else None.
         """
-        return atest_utils.load_test_info_cache(test_reference)
+        test_infos = atest_utils.load_test_info_cache(test_reference)
+        if test_infos and self._is_latest_testinfos(test_infos):
+            return test_infos
+        return None
diff --git a/atest/test_finders/cache_finder_unittest.py b/atest/test_finders/cache_finder_unittest.py
index 92de278..7797ea3 100755
--- a/atest/test_finders/cache_finder_unittest.py
+++ b/atest/test_finders/cache_finder_unittest.py
@@ -36,19 +36,27 @@
     @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'
+        uncached_test = 'mytest1'
+        cached_test = 'hello_world_test'
+        uncached_test2 = 'mytest2'
         test_cache_root = os.path.join(uc.TEST_DATA_DIR, 'cache_root')
-        # Hit matched cache file, should return cached test infos.
+        # Hit matched cache file but no original_finder in it,
+        # should return None.
         mock_get_cache_path.return_value = os.path.join(
             test_cache_root,
             'cd66f9f5ad63b42d0d77a9334de6bb73.cache')
+        self.assertIsNone(self.cache_finder.find_test_by_cache(uncached_test))
+        # Hit matched cache file and original_finder is in it,
+        # should return cached test infos.
+        mock_get_cache_path.return_value = os.path.join(
+            test_cache_root,
+            '78ea54ef315f5613f7c11dd1a87f10c7.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))
+        self.assertIsNone(self.cache_finder.find_test_by_cache(uncached_test2))
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/atest/test_finders/module_finder.py b/atest/test_finders/module_finder.py
index 91b10be..d41e0c8 100644
--- a/atest/test_finders/module_finder.py
+++ b/atest/test_finders/module_finder.py
@@ -444,8 +444,7 @@
         else:
             search_dir = self.root_dir
         package_paths = test_finder_utils.run_find_cmd(
-            test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir,
-            package.replace('.', '/'))
+            test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir, package)
         # Package path will be the full path to the dir represented by package.
         if not package_paths:
             return None
diff --git a/atest/test_finders/module_finder_unittest.py b/atest/test_finders/module_finder_unittest.py
index 14041b0..1cb756f 100755
--- a/atest/test_finders/module_finder_unittest.py
+++ b/atest/test_finders/module_finder_unittest.py
@@ -290,6 +290,7 @@
         mock_checkoutput.return_value = ''
         self.assertIsNone(self.mod_finder.find_test_by_package_name('Not pkg'))
 
+    @mock.patch('os.path.isdir', return_value=False)
     @mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
                        return_value=False)
     @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
@@ -297,7 +298,7 @@
     @mock.patch('os.path.isfile', side_effect=unittest_utils.isfile_side_effect)
     #pylint: disable=unused-argument
     def test_find_test_by_module_and_package(self, _isfile, mock_checkoutput,
-                                             mock_build, _vts):
+                                             mock_build, _vts, _isdir):
         """Test find_test_by_module_and_package."""
         self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
         self.mod_finder.module_info.is_robolectric_test.return_value = False
@@ -308,7 +309,11 @@
                     constants.MODULE_CLASS: []}
         self.mod_finder.module_info.get_module_info.return_value = mod_info
         t_infos = self.mod_finder.find_test_by_module_and_package(MODULE_PACKAGE)
+        self.assertEqual(t_infos, None)
+        _isdir.return_value = True
+        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)
diff --git a/atest/test_finders/test_finder_utils.py b/atest/test_finders/test_finder_utils.py
index 63d9014..8635603 100644
--- a/atest/test_finders/test_finder_utils.py
+++ b/atest/test_finders/test_finder_utils.py
@@ -20,6 +20,7 @@
 import logging
 import multiprocessing
 import os
+import pickle
 import re
 import subprocess
 import time
@@ -36,16 +37,11 @@
 # assume the apk name is the build target.
 _APK_RE = re.compile(r'^[^/]+\.apk$', 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)
+_CC_CLASS_RE = re.compile(r'^[ ]*TEST(_F|_P)?[ ]*\(', 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'[ ]*\{')
+_CC_METHODS_PATTERN = r'^[ ]*TEST(_F|_P)?[ ]*\(.*,[ ]*({0})\).*'
 # 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)
@@ -86,7 +82,16 @@
     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"
+                                  r" egrep -sH '^[ ]*TEST(_F|_P)?[ ]*\({2}' || true"
+}
+
+# Map ref_type with its index file.
+FIND_INDEXES = {
+    FIND_REFERENCE_TYPE.CLASS: constants.CLASS_INDEX,
+    FIND_REFERENCE_TYPE.QUALIFIED_CLASS: constants.QCLASS_INDEX,
+    FIND_REFERENCE_TYPE.PACKAGE: constants.PACKAGE_INDEX,
+    FIND_REFERENCE_TYPE.INTEGRATION: constants.INT_INDEX,
+    FIND_REFERENCE_TYPE.CC_CLASS: constants.CC_CLASS_INDEX
 }
 
 # XML parsing related constants.
@@ -253,7 +258,7 @@
     /<some_root>/cts/tests/jank/src/android/jank/cts/ui/CtsDeviceJankUi.java
 
     Args:
-        output: A string output of a unix 'find' command.
+        output: A string or list output of a unix 'find' command.
         methods: A set of method names.
 
     Returns:
@@ -262,10 +267,11 @@
     if not output:
         return None
     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 isinstance(output, str):
+        output = output.splitlines()
+    for test in output:
+        # compare CC_OUTPUT_RE with output
+        match_obj = constants.CC_OUTPUT_RE.match(test)
         if match_obj:
             # cc/cpp
             fpath = match_obj.group('file_path')
@@ -404,15 +410,32 @@
 
     Return:
         A list of the path to the target.
+        If the search_dir is inexistent, None will be returned.
     """
-    prune_cond = _get_prune_cond_of_ignored_dirs()
-    find_cmd = FIND_CMDS[ref_type].format(search_dir, prune_cond, target)
-    start = time.time()
+    # If module_info.json is outdated, finding in the search_dir can result in
+    # raising exception. Return null immediately can guild users to run
+    # --rebuild-module-info to resolve the problem.
+    if not os.path.isdir(search_dir):
+        logging.debug('\'%s\' does not exist!', search_dir)
+        return None
     ref_name = FIND_REFERENCE_TYPE[ref_type]
-    logging.debug('Executing %s find cmd: %s', ref_name, find_cmd)
-    out = subprocess.check_output(find_cmd, shell=True)
+    start = time.time()
+    if os.path.isfile(FIND_INDEXES[ref_type]):
+        _dict, out = {}, None
+        with open(FIND_INDEXES[ref_type], 'rb') as index:
+            _dict = pickle.load(index)
+        if _dict.get(target):
+            logging.debug('Found %s in %s', target, FIND_INDEXES[ref_type])
+            out = [path for path in _dict.get(target) if search_dir in path]
+    else:
+        prune_cond = _get_prune_cond_of_ignored_dirs()
+        if '.' in target:
+            target = target.replace('.', '/')
+        find_cmd = FIND_CMDS[ref_type].format(search_dir, prune_cond, target)
+        logging.debug('Executing %s find cmd: %s', ref_name, find_cmd)
+        out = subprocess.check_output(find_cmd, shell=True)
+        logging.debug('%s find cmd out: %s', ref_name, out)
     logging.debug('%s find completed in %ss', ref_name, time.time() - start)
-    logging.debug('%s find cmd out: %s', ref_name, out)
     return extract_test_path(out, methods)
 
 
@@ -430,16 +453,12 @@
         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, methods)
-    if '.' in class_name:
-        find_target = class_name.replace('.', '/')
+    elif '.' in class_name:
         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, methods)
+    return run_find_cmd(ref_type, search_dir, class_name, methods)
 
 
 def is_equal_or_sub_dir(sub_dir, parent_dir):
diff --git a/atest/test_finders/test_finder_utils_unittest.py b/atest/test_finders/test_finder_utils_unittest.py
index 4c17678..c6015d7 100755
--- a/atest/test_finders/test_finder_utils_unittest.py
+++ b/atest/test_finders/test_finder_utils_unittest.py
@@ -433,26 +433,44 @@
         test_result = test_finder_utils.search_integration_dirs(INT_FILE_NAME, int_dirs)
         unittest_utils.assert_strict_equal(self, test_result, paths)
 
+    @mock.patch('os.path.isfile', return_value=False)
     @mock.patch('os.environ.get', return_value=uc.TEST_CONFIG_DATA_DIR)
     @mock.patch('__builtin__.raw_input', return_value='0')
-    def test_find_class_file(self, mock_input, _mock_env):
+    # pylint: disable=too-many-statements
+    def test_find_class_file(self, mock_input, _mock_env, _mock_isfile):
         """Test find_class_file."""
+        # 1. Java class(find).
         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.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.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
                                                                       uc.FIND_PATH_TESTCASE_JAVA))
-
         self.assertTrue(java_class in java_tmp_test_result)
         self.assertTrue(kt_class in java_tmp_test_result)
 
+        # 2. Java class(read index).
         del java_tmp_test_result[:]
         mock_input.return_value = '0'
+        _mock_isfile = True
+        test_finder_utils.FIND_INDEXES['CLASS'] = uc.CLASS_INDEX
+        java_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_TESTCASE_JAVA + '.java')
+        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.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
+                                                                      uc.FIND_PATH_TESTCASE_JAVA))
+        self.assertTrue(java_class in java_tmp_test_result)
+        self.assertTrue(kt_class in java_tmp_test_result)
+
+        # 3. Qualified Java class(find).
+        del java_tmp_test_result[:]
+        mock_input.return_value = '0'
+        _mock_isfile = False
         java_qualified_class = '{0}.{1}'.format(uc.FIND_PATH_FOLDER, uc.FIND_PATH_TESTCASE_JAVA)
         java_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
                                                                       java_qualified_class))
@@ -462,7 +480,23 @@
         self.assertTrue(java_class in java_tmp_test_result)
         self.assertTrue(kt_class in java_tmp_test_result)
 
+        # 4. Qualified Java class(read index).
+        del java_tmp_test_result[:]
+        mock_input.return_value = '0'
+        _mock_isfile = True
+        test_finder_utils.FIND_INDEXES['QUALIFIED_CLASS'] = uc.QCLASS_INDEX
+        java_qualified_class = '{0}.{1}'.format(uc.FIND_PATH_FOLDER, uc.FIND_PATH_TESTCASE_JAVA)
+        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.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)
+
+        # 5. CC class(find).
         cc_tmp_test_result = []
+        _mock_isfile = False
         mock_input.return_value = '0'
         cpp_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_FILENAME_CC + '.cpp')
         cc_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
@@ -473,7 +507,23 @@
         cc_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
                                                                     uc.FIND_PATH_TESTCASE_CC,
                                                                     True))
+        self.assertTrue(cpp_class in cc_tmp_test_result)
+        self.assertTrue(cc_class in cc_tmp_test_result)
 
+        # 6. CC class(read index).
+        del cc_tmp_test_result[:]
+        mock_input.return_value = '0'
+        _mock_isfile = True
+        test_finder_utils.FIND_INDEXES['CC_CLASS'] = uc.CC_CLASS_INDEX
+        cpp_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_FILENAME_CC + '.cpp')
+        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.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
+                                                                    uc.FIND_PATH_TESTCASE_CC,
+                                                                    True))
         self.assertTrue(cpp_class in cc_tmp_test_result)
         self.assertTrue(cc_class in cc_tmp_test_result)
 
diff --git a/atest/test_finders/test_info.py b/atest/test_finders/test_info.py
index 18e3dbf..2c24b3a 100644
--- a/atest/test_finders/test_info.py
+++ b/atest/test_finders/test_info.py
@@ -57,15 +57,18 @@
         # True if the test should run on host and require no device. The
         # attribute is only set through TEST_MAPPING file.
         self.host = False
+        # A string of test finder
+        self.test_finder = ''
 
     def __str__(self):
         host_info = (' - runs on host without device required.' if self.host
                      else '')
         return ('test_name: %s - test_runner:%s - build_targets:%s - data:%s - '
-                'suite:%s - module_class: %s - install_locations:%s%s' % (
+                'suite:%s - module_class: %s - install_locations:%s%s - '
+                'test_finder:%s' % (
                     self.test_name, self.test_runner, self.build_targets,
                     self.data, self.suite, self.module_class,
-                    self.install_locations, host_info))
+                    self.install_locations, host_info, self.test_finder))
 
     def get_supported_exec_mode(self):
         """Get the supported execution mode of the test.
diff --git a/atest/test_finders/tf_integration_finder_unittest.py b/atest/test_finders/tf_integration_finder_unittest.py
index a8b58cc..170da0c 100755
--- a/atest/test_finders/tf_integration_finder_unittest.py
+++ b/atest/test_finders/tf_integration_finder_unittest.py
@@ -63,13 +63,20 @@
                        return_value=uc.FULL_CLASS_NAME)
     @mock.patch('subprocess.check_output')
     @mock.patch('os.path.exists', return_value=True)
-    @mock.patch('os.path.isfile', return_value=True)
+    @mock.patch('os.path.isfile', return_value=False)
+    @mock.patch('os.path.isdir', return_value=False)
     #pylint: disable=unused-argument
-    def test_find_test_by_integration_name(self, _isfile, _path, mock_find,
+    def test_find_test_by_integration_name(self, _isdir, _isfile, _path, mock_find,
                                            _fcqn, _build):
-        """Test find_test_by_integration_name."""
+        """Test find_test_by_integration_name.
+
+        Note that _isfile is always False since we don't index integration tests.
+        """
         mock_find.return_value = os.path.join(uc.ROOT, uc.INT_DIR, uc.INT_NAME + '.xml')
         t_infos = self.tf_finder.find_test_by_integration_name(uc.INT_NAME)
+        self.assertEqual(len(t_infos), 0)
+        _isdir.return_value = True
+        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)
diff --git a/atest/test_runners/atest_tf_test_runner.py b/atest/test_runners/atest_tf_test_runner.py
index ffd1ac9..2a790e5 100644
--- a/atest/test_runners/atest_tf_test_runner.py
+++ b/atest/test_runners/atest_tf_test_runner.py
@@ -280,6 +280,7 @@
                 build_req.add(executable)
         return build_req
 
+    # pylint: disable=too-many-branches
     @staticmethod
     def _parse_extra_args(extra_args):
         """Convert the extra args into something tf can understand.
@@ -326,6 +327,12 @@
                 args_to_append.append('--module-parameter')
                 args_to_append.append('instant_app')
                 continue
+            if constants.SECONDARY_USER == arg:
+                args_to_append.append('--enable-parameterized-modules')
+                args_to_append.append('--enable-optional-parameterization')
+                args_to_append.append('--module-parameter')
+                args_to_append.append('secondary_user')
+                continue
             args_not_supported.append(arg)
         return args_to_append, args_not_supported
 
diff --git a/atest/test_runners/robolectric_test_runner.py b/atest/test_runners/robolectric_test_runner.py
index dacbc62..b10a6e8 100644
--- a/atest/test_runners/robolectric_test_runner.py
+++ b/atest/test_runners/robolectric_test_runner.py
@@ -162,6 +162,8 @@
         """
         buf = ''
         while True:
+            # Make sure that ATest gets content from current position.
+            communication_file.seek(0, 1)
             data = communication_file.read()
             buf += data
             reg = re.compile(r'(.|\n)*}\n\n')
diff --git a/atest/tools/atest_tools.py b/atest/tools/atest_tools.py
new file mode 100755
index 0000000..b333ff8
--- /dev/null
+++ b/atest/tools/atest_tools.py
@@ -0,0 +1,350 @@
+#!/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 tool functions.
+"""
+
+from __future__ import print_function
+
+import logging
+import os
+import pickle
+import shutil
+import subprocess
+import sys
+
+import constants
+import module_info
+
+MAC_UPDB_SRC = os.path.join(os.path.dirname(__file__), 'updatedb_darwin.sh')
+MAC_UPDB_DST = os.path.join(os.getenv(constants.ANDROID_HOST_OUT, ''), 'bin')
+UPDATEDB = 'updatedb'
+LOCATE = 'locate'
+SEARCH_TOP = os.getenv(constants.ANDROID_BUILD_TOP, '')
+MACOSX = 'Darwin'
+OSNAME = os.uname()[0]
+
+# The list was generated by command:
+# find `gettop` -type d -wholename `gettop`/out -prune  -o -type d -name '.*'
+# -print | awk -F/ '{{print $NF}}'| sort -u
+PRUNENAMES = ['.abc', '.appveyor', '.azure-pipelines',
+              '.bazelci', '.buildscript', '.ci', '.circleci', '.conan',
+              '.externalToolBuilders',
+              '.git', '.github', '.github-ci', '.google', '.gradle',
+              '.idea', '.intermediates',
+              '.jenkins',
+              '.kokoro',
+              '.libs_cffi_backend',
+              '.mvn',
+              '.prebuilt_info', '.private', '__pycache__',
+              '.repo',
+              '.semaphore', '.settings', '.static', '.svn',
+              '.test', '.travis', '.tx',
+              '.vscode']
+# Running locate + grep consumes tremendous amount of time in MacOS. Running it
+# with a physical script file can increase the performance.
+TMPRUN = '/tmp/._'
+
+def _mkdir_when_inexists(dirname):
+    if not os.path.isdir(dirname):
+        os.makedirs(dirname)
+
+def _install_updatedb():
+    """Install a customized updatedb for MacOS and ensure it is executable."""
+    _mkdir_when_inexists(MAC_UPDB_DST)
+    _mkdir_when_inexists(constants.INDEX_DIR)
+    if OSNAME == MACOSX:
+        shutil.copy2(MAC_UPDB_SRC, os.path.join(MAC_UPDB_DST, UPDATEDB))
+        os.chmod(os.path.join(MAC_UPDB_DST, UPDATEDB), 0755)
+
+def has_indexes():
+    """Detect if all index files are all available.
+
+    Returns:
+        True if indexes exist, False otherwise.
+    """
+    indexes = (constants.CLASS_INDEX, constants.QCLASS_INDEX,
+               constants.PACKAGE_INDEX, constants.CC_CLASS_INDEX)
+    for index in indexes:
+        if not os.path.isfile(index):
+            return False
+    return True
+
+def has_command(cmd):
+    """Detect if the command is available in PATH.
+
+    shutil.which('cmd') is only valid in Py3 so we need to customise it.
+
+    Args:
+        cmd: A string of the tested command.
+
+    Returns:
+        True if found, False otherwise."""
+    paths = os.getenv('PATH', '').split(':')
+    for path in paths:
+        if os.path.isfile(os.path.join(path, cmd)):
+            return True
+    return False
+
+def run_updatedb(search_root=SEARCH_TOP, output_cache=constants.LOCATE_CACHE,
+                 **kwargs):
+    """Run updatedb and generate cache in $ANDROID_HOST_OUT/indexes/mlocate.db
+
+    Args:
+        search_root: The path of the search root(-U).
+        output_cache: The filename of the updatedb cache(-o).
+        kwargs: (optional)
+            prunepaths: A list of paths unwanted to be searched(-e).
+            prunenames: A list of dirname that won't be cached(-n).
+    """
+    prunenames = kwargs.pop('prunenames', ' '.join(PRUNENAMES))
+    prunepaths = kwargs.pop('prunepaths', os.path.join(search_root, 'out'))
+    if kwargs:
+        raise TypeError('Unexpected **kwargs: %r' % kwargs)
+    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)
+
+    if not has_command(UPDATEDB):
+        return
+    logging.debug('Running updatedb... ')
+    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)
+    except (KeyboardInterrupt, SystemExit):
+        logging.error('Process interrupted or failure.')
+    except subprocess.CalledProcessError as err:
+        logging.error('Error executing: %s', updatedb_cmd)
+        if err.output:
+            logging.error(err.output)
+
+def _dump_index(dump_file, output, output_re, key, value):
+    """Dump indexed data with pickle.
+
+    Args:
+        dump_file: A string of absolute path of the index file.
+        output: A string generated by locate and grep.
+        output_re: An regex which is used for grouping patterns.
+        key: A string for dictionary key, e.g. classname, package, cc_class, etc.
+        value: A set of path.
+
+    The data structure will be like:
+    {
+      'Foo': {'/path/to/Foo.java', '/path2/to/Foo.kt'},
+      'Boo': {'/path3/to/Boo.java'}
+    }
+    """
+    _dict = {}
+    with open(dump_file, 'wb') as cache_file:
+        for entry in output.splitlines():
+            match = output_re.match(entry)
+            if match:
+                _dict.setdefault(match.group(key), set()).add(match.group(value))
+        try:
+            pickle.dump(_dict, cache_file, protocol=2)
+        except IOError:
+            os.remove(dump_file)
+            logging.error('Failed in dumping %s', dump_file)
+
+def _get_cc_result(locatedb=None):
+    """Search all testable cc/cpp and grep TEST(), TEST_F() or TEST_P().
+
+    Return:
+        A string object generated by subprocess.
+    """
+    if not locatedb:
+        locatedb = constants.LOCATE_CACHE
+    cc_grep_re = r'^\s*TEST(_P|_F)?\s*\([[:alnum:]]+,'
+    if OSNAME == MACOSX:
+        find_cmd = (r"locate -d {0} '*.cpp' '*.cc' | grep -i test "
+                    "| xargs egrep -sH '{1}' || true")
+    else:
+        find_cmd = (r"locate -d {0} / | egrep -i '/*.test.*\.(cc|cpp)$' "
+                    "| xargs egrep -sH '{1}' || true")
+    find_cc_cmd = find_cmd.format(locatedb, cc_grep_re)
+    logging.debug('Probing CC classes:\n %s', find_cc_cmd)
+    return subprocess.check_output('echo \"%s\" > %s; sh %s'
+                                   % (find_cc_cmd, TMPRUN, TMPRUN), shell=True)
+
+def _get_java_result(locatedb=None):
+    """Search all testable java/kt and grep package.
+
+    Return:
+        A string object generated by subprocess.
+    """
+    if not locatedb:
+        locatedb = constants.LOCATE_CACHE
+    package_grep_re = r'^\s*package\s+[a-z][[:alnum:]]+[^{]'
+    if OSNAME == MACOSX:
+        find_cmd = r"locate -d%s '*.java' '*.kt'|grep -i test" % locatedb
+    else:
+        find_cmd = r"locate -d%s / | egrep -i '/*.test.*\.(java|kt)$'" % locatedb
+    find_java_cmd = find_cmd + '| xargs egrep -sH \'%s\' || true' % package_grep_re
+    logging.debug('Probing Java classes:\n %s', find_java_cmd)
+    return subprocess.check_output('echo \"%s\" > %s; sh %s'
+                                   % (find_java_cmd, TMPRUN, TMPRUN), shell=True)
+
+def _index_testable_modules(index):
+    """Dump testable modules read by tab completion.
+
+    Args:
+        index: A string path of the index file.
+    """
+    logging.debug('indexing testable modules.')
+    testable_modules = module_info.ModuleInfo().get_testable_modules()
+    with open(index, 'wb') as cache:
+        try:
+            pickle.dump(testable_modules, cache, protocol=2)
+        except IOError:
+            os.remove(cache)
+            logging.error('Failed in dumping %s', cache)
+
+def _index_cc_classes(output, index):
+    """Index Java classes.
+
+    The data structure is like:
+    {
+      'FooTestCase': {'/path1/to/the/FooTestCase.java',
+                      '/path2/to/the/FooTestCase.kt'}
+    }
+
+    Args:
+        output: A string object generated by _get_cc_result().
+        index: A string path of the index file.
+    """
+    logging.debug('indexing CC classes.')
+    _dump_index(dump_file=index, output=output,
+                output_re=constants.CC_OUTPUT_RE,
+                key='test_name', value='file_path')
+
+def _index_java_classes(output, index):
+    """Index Java classes.
+    The data structure is like:
+    {
+        'FooTestCase': {'/path1/to/the/FooTestCase.java',
+                        '/path2/to/the/FooTestCase.kt'}
+    }
+
+    Args:
+        output: A string object generated by _get_java_result().
+        index: A string path of the index file.
+    """
+    logging.debug('indexing Java classes.')
+    _dump_index(dump_file=index, output=output,
+                output_re=constants.CLASS_OUTPUT_RE,
+                key='class', value='java_path')
+
+def _index_packages(output, index):
+    """Index Java packages.
+    The data structure is like:
+    {
+        'a.b.c.d': {'/path1/to/a/b/c/d/FooTestCase.java',
+                    '/path2/to/a/b/c/d/FooTestCase.kt'}
+    }
+
+    Args:
+        output: A string object generated by _get_java_result().
+        index: A string path of the index file.
+    """
+    logging.debug('indexing packages.')
+    _dump_index(dump_file=index,
+                output=output, output_re=constants.PACKAGE_OUTPUT_RE,
+                key='package', value='java_path')
+
+def _index_qualified_classes(output, index):
+    """Index Fully Qualified Java Classes(FQCN).
+    The data structure is like:
+    {
+        'a.b.c.d.FooTestCase': {'/path1/to/a/b/c/d/FooTestCase.java',
+                                '/path2/to/a/b/c/d/FooTestCase.kt'}
+    }
+
+    Args:
+        output: A string object generated by _get_java_result().
+        index: A string path of the index file.
+    """
+    logging.debug('indexing qualified classes.')
+    _dict = {}
+    with open(index, 'wb') as cache_file:
+        for entry in output.split('\n'):
+            match = constants.QCLASS_OUTPUT_RE.match(entry)
+            if match:
+                fqcn = match.group('package') + '.' + match.group('class')
+                _dict.setdefault(fqcn, set()).add(match.group('java_path'))
+        try:
+            pickle.dump(_dict, cache_file, protocol=2)
+        except (KeyboardInterrupt, SystemExit):
+            logging.error('Process interrupted or failure.')
+            os.remove(index)
+        except IOError:
+            logging.error('Failed in dumping %s', index)
+
+def index_targets(output_cache=constants.LOCATE_CACHE, **kwargs):
+    """The entrypoint of indexing targets.
+
+    Utilise mlocate database to index reference types of CLASS, CC_CLASS,
+    PACKAGE and QUALIFIED_CLASS. Testable module for tab completion is also
+    generated in this method.
+
+    Args:
+        output_cache: A file path of the updatedb cache(e.g. /path/to/mlocate.db).
+        kwargs: (optional)
+            class_index: A path string of the Java class index.
+            qclass_index: A path string of the qualified class index.
+            package_index: A path string of the package index.
+            cc_class_index: A path string of the CC class index.
+            module_index: A path string of the testable module index.
+            integration_index: A path string of the integration index.
+    """
+    class_index = kwargs.pop('class_index', constants.CLASS_INDEX)
+    qclass_index = kwargs.pop('qclass_index', constants.QCLASS_INDEX)
+    package_index = kwargs.pop('package_index', constants.PACKAGE_INDEX)
+    cc_class_index = kwargs.pop('cc_class_index', constants.CC_CLASS_INDEX)
+    module_index = kwargs.pop('module_index', constants.MODULE_INDEX)
+    # Uncomment below if we decide to support INTEGRATION.
+    #integration_index = kwargs.pop('integration_index', constants.INT_INDEX)
+    if kwargs:
+        raise TypeError('Unexpected **kwargs: %r' % kwargs)
+
+    # Step 0: generate mlocate database prior to indexing targets.
+    run_updatedb(SEARCH_TOP, constants.LOCATE_CACHE)
+    if not has_command(LOCATE):
+        return
+    # Step 1: generate output string for indexing targets.
+    logging.debug('Indexing targets... ')
+    cc_result = _get_cc_result(output_cache)
+    java_result = _get_java_result(output_cache)
+    # Step 2: index Java and CC classes.
+    _index_cc_classes(cc_result, cc_class_index)
+    _index_java_classes(java_result, class_index)
+    _index_qualified_classes(java_result, qclass_index)
+    _index_packages(java_result, package_index)
+    _index_testable_modules(module_index)
+    if os.path.isfile(TMPRUN):
+        os.remove(TMPRUN)
+
+if __name__ == '__main__':
+    if not os.getenv(constants.ANDROID_HOST_OUT, ''):
+        sys.exit()
+    index_targets()
diff --git a/atest/tools/atest_tools_unittest.py b/atest/tools/atest_tools_unittest.py
new file mode 100755
index 0000000..bd7ccf1
--- /dev/null
+++ b/atest/tools/atest_tools_unittest.py
@@ -0,0 +1,92 @@
+#!/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_tools."""
+
+import os
+import pickle
+import platform
+import subprocess
+import sys
+import unittest
+
+import atest_tools
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+# pylint: disable=wrong-import-position
+import unittest_constants as uc
+
+SEARCH_ROOT = uc.TEST_DATA_DIR
+PRUNEPATH = uc.TEST_CONFIG_DATA_DIR
+
+
+class AtestToolsUnittests(unittest.TestCase):
+    """"Unittest Class for atest_tools.py."""
+
+    def test_index_targets(self):
+        """Test method index_targets."""
+        if atest_tools.has_command('updatedb'):
+            atest_tools.run_updatedb(SEARCH_ROOT, uc.LOCATE_CACHE,
+                                     prunepaths=PRUNEPATH)
+            # test_config/ is excluded so that a.xml won't be found.
+            locate_cmd1 = ['locate', '-d', uc.LOCATE_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', uc.LOCATE_CACHE, 'module-info.json']
+            self.assertEqual(subprocess.call(locate_cmd2), 0)
+        else:
+            self.assertEqual(atest_tools.has_command('updatedb'), False)
+
+        if atest_tools.has_command('locate'):
+            atest_tools.index_targets(uc.LOCATE_CACHE,
+                                      class_index=uc.CLASS_INDEX,
+                                      qclass_index=uc.QCLASS_INDEX,
+                                      cc_class_index=uc.CC_CLASS_INDEX,
+                                      package_index=uc.PACKAGE_INDEX)
+            _dict = {}
+            # Test finding a Java class
+            with open(uc.CLASS_INDEX, 'rb') as _cache:
+                _dict = pickle.load(_cache)
+            self.assertIsNotNone(_dict.get('PathTesting'))
+            # Test finding a CC class
+            with open(uc.CC_CLASS_INDEX, 'rb') as _cache:
+                _dict = pickle.load(_cache)
+            self.assertIsNotNone(_dict.get('HelloWorldTest'))
+            # Test finding a package
+            with open(uc.PACKAGE_INDEX, 'rb') as _cache:
+                _dict = pickle.load(_cache)
+            self.assertIsNotNone(_dict.get('android.jank.cts.ui'))
+            # Test finding a fully qualified class name
+            with open(uc.QCLASS_INDEX, 'rb') as _cache:
+                _dict = pickle.load(_cache)
+            self.assertIsNotNone(_dict.get('android.jank.cts.ui.PathTesting'))
+            # Clean up.
+            targets_to_delete = (uc.LOCATE_CACHE,
+                                 uc.CLASS_INDEX,
+                                 uc.QCLASS_INDEX,
+                                 uc.CC_CLASS_INDEX,
+                                 uc.PACKAGE_INDEX)
+            for idx in targets_to_delete:
+                os.remove(idx)
+        else:
+            self.assertEqual(atest_tools.has_command('locate'), False)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/atest/tools/atest_updatedb.py b/atest/tools/atest_updatedb.py
deleted file mode 100755
index 6b0c8f8..0000000
--- a/atest/tools/atest_updatedb.py
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/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
deleted file mode 100755
index 923c5dc..0000000
--- a/atest/tools/atest_updatedb_unittest.py
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/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/unittest_constants.py b/atest/unittest_constants.py
index 03bf5c0..c757936 100644
--- a/atest/unittest_constants.py
+++ b/atest/unittest_constants.py
@@ -238,3 +238,10 @@
 FUZZY_MOD1 = 'Mod1'
 FUZZY_MOD2 = 'nod2'
 FUZZY_MOD3 = 'mod3mod3'
+
+LOCATE_CACHE = '/tmp/mcloate.db'
+CLASS_INDEX = '/tmp/classes.idx'
+QCLASS_INDEX = '/tmp/fqcn.idx'
+CC_CLASS_INDEX = '/tmp/cc_classes.idx'
+PACKAGE_INDEX = '/tmp/packages.idx'
+MODULE_INDEX = '/tmp/modules.idx'
diff --git a/atest/unittest_data/cache_root/78ea54ef315f5613f7c11dd1a87f10c7.cache b/atest/unittest_data/cache_root/78ea54ef315f5613f7c11dd1a87f10c7.cache
new file mode 100644
index 0000000..54e9794
--- /dev/null
+++ b/atest/unittest_data/cache_root/78ea54ef315f5613f7c11dd1a87f10c7.cache
@@ -0,0 +1,77 @@
+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'test_finder'
+p20
+S'MODULE'
+p21
+sS'build_targets'
+p22
+g0
+((lp23
+VMODULES-IN-platform_testing-tests-example-native
+p24
+atp25
+Rp26
+sS'host'
+p27
+I00
+sS'test_name'
+p28
+S'hello_world_test'
+p29
+sS'suite'
+p30
+NsS'data'
+p31
+(dp32
+S'rel_config'
+p33
+Vplatform_testing/tests/example/native/AndroidTest.xml
+p34
+sS'filter'
+p35
+c__builtin__
+frozenset
+p36
+((lp37
+tp38
+Rp39
+ssbatp40
+Rp41
+.
diff --git a/common_util/com/android/tradefed/util/RunUtil.java b/common_util/com/android/tradefed/util/RunUtil.java
index fd27843..a2aef47 100644
--- a/common_util/com/android/tradefed/util/RunUtil.java
+++ b/common_util/com/android/tradefed/util/RunUtil.java
@@ -299,7 +299,7 @@
     @Override
     public Process runCmdInBackground(final String... command) throws IOException  {
         final String fullCmd = Arrays.toString(command);
-        CLog.v("Running %s", fullCmd);
+        CLog.v("Running in background: %s", fullCmd);
         return createProcessBuilder(command).start();
     }
 
@@ -308,7 +308,7 @@
      */
     @Override
     public Process runCmdInBackground(final List<String> command) throws IOException  {
-        CLog.v("Running %s", command);
+        CLog.v("Running in background: %s", command);
         return createProcessBuilder(command).start();
     }
 
@@ -318,7 +318,7 @@
     @Override
     public Process runCmdInBackground(List<String> command, OutputStream output)
             throws IOException {
-        CLog.v("Running %s", command);
+        CLog.v("Running in background: %s", command);
         Process process = createProcessBuilder(command).start();
         inheritIO(
                 process.getInputStream(),
diff --git a/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
index 49ec5a7..55676b9 100644
--- a/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
+++ b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
@@ -220,4 +220,41 @@
         mExtraFieldLength = ByteArrayUtil.getInt(data, startOffset + 30, 2);
         mFileCommentLength = ByteArrayUtil.getInt(data, startOffset + 32, 2);
     }
+
+    @Override
+    public boolean equals(Object o) {
+        return this.toString().equals(o.toString());
+    }
+
+    @Override
+    public int hashCode() {
+        return this.toString().hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "Compression Method: %d\n"
+                        + "Crc: %d\n"
+                        + "Compressed Size: %d\n"
+                        + "Uncompressed Size: %d\n"
+                        + "Local Header Offset: %d\n"
+                        + "Internal File Attributes: %d\n"
+                        + "External File Attributes: %d\n"
+                        + "File Name: %s\n"
+                        + "File Name Length: %d\n"
+                        + "Extra Field Length: %d\n"
+                        + "File Comment Length: %d",
+                mCompressionMethod,
+                mCrc,
+                mCompressedSize,
+                mUncompressedSize,
+                mLocalHeaderOffset,
+                mInternalFileAttributes,
+                mExternalFileAttributes,
+                mFileName,
+                mFileNameLength,
+                mExtraFieldLength,
+                mFileCommentLength);
+    }
 }
diff --git a/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java b/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java
index ed1e8fc..0527f47 100644
--- a/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java
+++ b/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java
@@ -62,7 +62,7 @@
      *  @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(
+    public static List<MergedZipEntryCollection> createCollections(
             List<CentralDirectoryInfo> zipEntries) {
         if (zipEntries.size() == 0) {
             return new ArrayList<MergedZipEntryCollection>();
diff --git a/device_build_interfaces/README.md b/device_build_interfaces/README.md
new file mode 100644
index 0000000..b3508a4
--- /dev/null
+++ b/device_build_interfaces/README.md
@@ -0,0 +1,7 @@
+# Trade Federation Device and Build Interfaces Component
+
+A Tradefed component for our Device and Build interfaces
+and base classes.
+
+This directory should only contain classes related to our
+device and build handling.
diff --git a/device_build_interfaces/com/android/tradefed/build/IBuildInfo.java b/device_build_interfaces/com/android/tradefed/build/IBuildInfo.java
index 919bc31..8fd2026 100644
--- a/device_build_interfaces/com/android/tradefed/build/IBuildInfo.java
+++ b/device_build_interfaces/com/android/tradefed/build/IBuildInfo.java
@@ -305,4 +305,18 @@
     public default Set<File> getRemoteFiles() {
         return null;
     }
+
+    /**
+     * Stage a file that's part of remote files in the build info's root dir.
+     *
+     * <p>TODO(b/138416078): Remove this interface and its caller when modules required by a test
+     * can be properly built output to the test module's directory itself.
+     *
+     * @param fileName Name of the file to be located in remote files.
+     * @param workingDir a {@link File} object of the directory to stage the file.
+     * @return the {@link File} object of the file staged in local workingDir.
+     */
+    public default File stageRemoteFile(String fileName, File workingDir) {
+        return null;
+    }
 }
diff --git a/device_build_interfaces/com/android/tradefed/device/INativeDevice.java b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
index df7420a..5ea6f47 100644
--- a/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
@@ -1332,6 +1332,7 @@
      * Helper method runs the "pidof" and "stat" command and returns {@link ProcessInfo} object with
      * PID and process start time of the given process.
      *
+     * @param processName the proces name String.
      * @return ProcessInfo of given processName
      */
     public ProcessInfo getProcessByName(String processName) throws DeviceNotAvailableException;
@@ -1345,20 +1346,52 @@
 
     /**
      * 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.
+     * time since epoch from device and the time unit specified. The current device utcEpochTime in
+     * Millisecond can be obtained by method {@link #getDeviceDate}.
      *
+     * @param utcEpochTime the device time since Epoch.
+     * @param timeUnit the time unit <code>TimeUnit</code>.
      * @return Map of boot time (UTC time in second since Epoch) and boot reason
      */
-    public Map<Long, String> getBootHistorySince(long utcEpochTime)
+    public Map<Long, String> getBootHistorySince(long utcEpochTime, TimeUnit timeUnit)
             throws DeviceNotAvailableException;
 
-    /** Returns the pid of the service or null if something went wrong. */
+    /**
+     * Returns the pid of the service or null if something went wrong.
+     *
+     * @param process The proces name String.
+     */
     public String getProcessPid(String process) throws DeviceNotAvailableException;
 
     /**
+     * Helper method to check whether device soft-restarted since the UTC time since epoch from
+     * device and its {@link TimeUnit}. Soft-Restart refers to system_server restarted outside of a
+     * device hard reboot (for example: requested reboot). The current device utcEpochTime in
+     * Milliseccond can be obtained by method {@link #getDeviceDate}.
+     *
+     * @param utcEpochTime the device time in second since epoch.
+     * @param timeUnit the time unit <code>TimeUnit</code> for the given utcEpochTime.
+     * @return {@code true} if device soft-restarted
+     * @throws RuntimeException if device has abnormal boot reason
+     * @throws DeviceNotAvailableException
+     */
+    public boolean deviceSoftRestartedSince(long utcEpochTime, TimeUnit timeUnit)
+            throws DeviceNotAvailableException;
+
+    /**
+     * Helper method to check if device soft-restarted by comparing current system_server with
+     * previous system_server {@link ProcessInfo}. Use {@link #getProcessByName} to get {@link
+     * ProcessInfo}.
+     *
+     * @param prevSystemServerProcess the previous system_server process {@link ProcessInfo}.
+     * @return {@code true} if device soft-restarted
+     * @throws RuntimeException if device has abnormal boot reason
+     * @throws DeviceNotAvailableException
+     */
+    public boolean deviceSoftRestarted(ProcessInfo prevSystemServerProcess)
+            throws DeviceNotAvailableException;
+
+    /**
      * Log a message in the logcat of the device. This is a safe call that will not throw even if
      * the logging fails.
      *
diff --git a/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
index eb62635..d8c4cf0 100644
--- a/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
+++ b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
@@ -260,6 +260,18 @@
 
     // END ====================== Options Related to Virtual Devices ======================
 
+    // Option related to Remote Device only
+
+    public static final String REMOTE_TF_VERSION_OPTION = "remote-tf-version";
+
+    @Option(
+        name = REMOTE_TF_VERSION_OPTION,
+        description =
+                "The TF to push to the remote VM to drive the invocation. If null, current TF "
+                        + "will be pushed."
+    )
+    private File mRemoteTFVersion = null;
+
     /** Check whether adb root should be enabled on boot for this device */
     public boolean isEnableAdbRoot() {
         return mEnableAdbRoot;
@@ -637,6 +649,11 @@
         return mCrosPassword;
     }
 
+    /** The file pointing to the directory of the Tradefed version to be pushed to the remote. */
+    public File getRemoteTf() {
+        return mRemoteTFVersion;
+    }
+
     public static String getCreateCommandByInstanceType(InstanceType type) {
         switch (type) {
             case CHEEPS:
diff --git a/invocation_interfaces/Android.bp b/invocation_interfaces/Android.bp
index 5208e51..b034c6c 100644
--- a/invocation_interfaces/Android.bp
+++ b/invocation_interfaces/Android.bp
@@ -19,6 +19,7 @@
         "com/**/*.java",
     ],
     libs: [
+        "ddmlib-prebuilt",
         "guava",
         "tradefed-common-util",
         "tradefed-protos",
diff --git a/src/com/android/tradefed/result/ITestSummaryListener.java b/invocation_interfaces/com/android/tradefed/result/ITestSummaryListener.java
similarity index 100%
rename from src/com/android/tradefed/result/ITestSummaryListener.java
rename to invocation_interfaces/com/android/tradefed/result/ITestSummaryListener.java
diff --git a/src/com/android/tradefed/result/InvocationStatus.java b/invocation_interfaces/com/android/tradefed/result/InvocationStatus.java
similarity index 100%
rename from src/com/android/tradefed/result/InvocationStatus.java
rename to invocation_interfaces/com/android/tradefed/result/InvocationStatus.java
diff --git a/src/com/android/tradefed/result/InvocationSummaryHelper.java b/invocation_interfaces/com/android/tradefed/result/InvocationSummaryHelper.java
similarity index 100%
rename from src/com/android/tradefed/result/InvocationSummaryHelper.java
rename to invocation_interfaces/com/android/tradefed/result/InvocationSummaryHelper.java
diff --git a/src/com/android/tradefed/result/TestResult.java b/invocation_interfaces/com/android/tradefed/result/TestResult.java
similarity index 93%
rename from src/com/android/tradefed/result/TestResult.java
rename to invocation_interfaces/com/android/tradefed/result/TestResult.java
index 3d33df3..0a0141f 100644
--- a/src/com/android/tradefed/result/TestResult.java
+++ b/invocation_interfaces/com/android/tradefed/result/TestResult.java
@@ -16,8 +16,9 @@
 package com.android.tradefed.result;
 
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.testtype.retry.MergeStrategy;
+import com.android.tradefed.retry.MergeStrategy;
 
 import com.google.common.base.Joiner;
 
@@ -30,6 +31,8 @@
 
 /** Container for a result of a single test. */
 public class TestResult {
+    // Key that mark that an aggregation is hiding a failure.
+    public static final String IS_FLAKY = "is_flaky";
 
     private TestStatus mStatus;
     private String mStackTrace;
@@ -157,6 +160,14 @@
         return a == b || (a != null && a.equals(b));
     }
 
+    private void markFlaky() {
+        mProtoMetrics.put(
+                IS_FLAKY,
+                Metric.newBuilder()
+                        .setMeasurements(Measurements.newBuilder().setSingleString("true").build())
+                        .build());
+    }
+
     /**
      * Merge the attempts for a same test case based on the merging strategy.
      *
@@ -222,6 +233,9 @@
                 // We prioritize passing the test due to the merging strategy.
                 if (pass > 0) {
                     mergedResult.setStatus(TestStatus.PASSED);
+                    if (fail > 0) {
+                        mergedResult.markFlaky();
+                    }
                 } else if (fail == 0) {
                     if (ignored > 0) {
                         mergedResult.setStatus(TestStatus.IGNORED);
diff --git a/src/com/android/tradefed/result/TestRunResult.java b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
similarity index 99%
rename from src/com/android/tradefed/result/TestRunResult.java
rename to invocation_interfaces/com/android/tradefed/result/TestRunResult.java
index 6be64fc..6885abe 100644
--- a/src/com/android/tradefed/result/TestRunResult.java
+++ b/invocation_interfaces/com/android/tradefed/result/TestRunResult.java
@@ -18,7 +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.retry.MergeStrategy;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import com.google.common.base.Joiner;
diff --git a/src/com/android/tradefed/testtype/retry/MergeStrategy.java b/invocation_interfaces/com/android/tradefed/retry/MergeStrategy.java
similarity index 97%
rename from src/com/android/tradefed/testtype/retry/MergeStrategy.java
rename to invocation_interfaces/com/android/tradefed/retry/MergeStrategy.java
index d8c6869..37aa2da 100644
--- a/src/com/android/tradefed/testtype/retry/MergeStrategy.java
+++ b/invocation_interfaces/com/android/tradefed/retry/MergeStrategy.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tradefed.testtype.retry;
+package com.android.tradefed.retry;
 
 /** Describes how the results should be aggregated when multiple attempts are present. */
 public enum MergeStrategy {
diff --git a/src/com/android/tradefed/testtype/retry/RetryStrategy.java b/invocation_interfaces/com/android/tradefed/retry/RetryStrategy.java
similarity index 96%
rename from src/com/android/tradefed/testtype/retry/RetryStrategy.java
rename to invocation_interfaces/com/android/tradefed/retry/RetryStrategy.java
index dcce258..2b0beda 100644
--- a/src/com/android/tradefed/testtype/retry/RetryStrategy.java
+++ b/invocation_interfaces/com/android/tradefed/retry/RetryStrategy.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.tradefed.testtype.retry;
+package com.android.tradefed.retry;
 
 /** The Retry Strategy to be used when re-running some tests. */
 public enum RetryStrategy {
diff --git a/res/config/instrumentations.xml b/res/config/instrumentations.xml
index 88c7d50..7df6186 100644
--- a/res/config/instrumentations.xml
+++ b/res/config/instrumentations.xml
@@ -14,11 +14,16 @@
      limitations under the License.
 -->
 <configuration description="Runs all the Android instrumentation tests on an existing device">
+    <option name="compress-files" value="false" />
+
+    <logger class="com.android.tradefed.log.FileLogger">
+        <option name="log-level" value="verbose" />
+        <option name="log-level-display" value="debug" />
+    </logger>
 
     <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/BuildInfo.java b/src/com/android/tradefed/build/BuildInfo.java
index f1a92fd..414abe2 100644
--- a/src/com/android/tradefed/build/BuildInfo.java
+++ b/src/com/android/tradefed/build/BuildInfo.java
@@ -19,6 +19,8 @@
 import com.android.tradefed.build.proto.BuildInformation;
 import com.android.tradefed.build.proto.BuildInformation.BuildFile;
 import com.android.tradefed.build.proto.BuildInformation.KeyBuildFilePair;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.FileUtil;
@@ -698,7 +700,6 @@
         return null;
     }
 
-
     /** {@inheritDoc} */
     @Override
     public Set<File> getRemoteFiles() {
@@ -711,4 +712,25 @@
         }
         return remoteFiles;
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public File stageRemoteFile(String fileName, File workingDir) {
+        List<String> includeFilters = Arrays.asList(String.format("/%s$", fileName));
+        for (File file : getRemoteFiles()) {
+            try {
+                new DynamicRemoteFileResolver()
+                        .resolvePartialDownloadZip(
+                                workingDir, file.toString(), includeFilters, null);
+            } catch (ConfigurationException e) {
+                throw new RuntimeException(e);
+            }
+
+            File stagedFile = FileUtil.findFile(workingDir, fileName);
+            if (stagedFile != null) {
+                return stagedFile;
+            }
+        }
+        return null;
+    }
 }
diff --git a/src/com/android/tradefed/build/DeviceBuildDescriptor.java b/src/com/android/tradefed/build/DeviceBuildDescriptor.java
index 880e546..088114a 100644
--- a/src/com/android/tradefed/build/DeviceBuildDescriptor.java
+++ b/src/com/android/tradefed/build/DeviceBuildDescriptor.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.build;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceProperties;
 import com.android.tradefed.device.ITestDevice;
 
 /**
@@ -99,8 +100,11 @@
             throws DeviceNotAvailableException {
         b.addBuildAttribute(DEVICE_BUILD_ID, device.getBuildId());
         b.addBuildAttribute(DEVICE_BUILD_ALIAS, device.getBuildAlias());
-        String buildFlavor = String.format("%s-%s", device.getProperty("ro.product.name"),
-                device.getProperty("ro.build.type"));
+        String buildFlavor =
+                String.format(
+                        "%s-%s",
+                        device.getProperty(DeviceProperties.PRODUCT),
+                        device.getProperty(DeviceProperties.BUILD_TYPE));
         b.addBuildAttribute(DEVICE_BUILD_FLAVOR, buildFlavor);
         b.addBuildAttribute(DEVICE_DESC, generateDeviceDesc(device));
         b.addBuildAttribute(DEVICE_PRODUCT, generateDeviceProduct(device));
@@ -119,12 +123,15 @@
     public static String generateDeviceDesc(ITestDevice device)
             throws DeviceNotAvailableException {
         // brand is typically all lower case. Capitalize it
-        String brand =  device.getProperty("ro.product.brand");
+        String brand = device.getProperty(DeviceProperties.BRAND);
         if (brand.length() > 1) {
             brand = String.format("%s%s", brand.substring(0, 1).toUpperCase(), brand.substring(1));
         }
-        return String.format("%s %s %s", brand, device.getProperty("ro.product.model"),
-                device.getProperty("ro.build.version.release"));
+        return String.format(
+                "%s %s %s",
+                brand,
+                device.getProperty("ro.product.model"),
+                device.getProperty(DeviceProperties.RELEASE_VERSION));
     }
 
     /**
diff --git a/src/com/android/tradefed/build/FileDownloadCache.java b/src/com/android/tradefed/build/FileDownloadCache.java
index 2f06bcd..88fd416 100644
--- a/src/com/android/tradefed/build/FileDownloadCache.java
+++ b/src/com/android/tradefed/build/FileDownloadCache.java
@@ -108,22 +108,30 @@
                         mCacheRoot.getAbsolutePath()));
             }
         } else {
-            Log.d(LOG_TAG, String.format("Building file cache from contents at %s",
-                    mCacheRoot.getAbsolutePath()));
-            // create an unsorted list of all the files in mCacheRoot. Need to create list first
-            // rather than inserting in Map directly because Maps cannot be sorted
-            List<FilePair> cacheEntryList = new LinkedList<FilePair>();
-            addFiles(mCacheRoot, new Stack<String>(), cacheEntryList);
-            // now sort them based on file timestamp, to get them in LRU order
-            Collections.sort(cacheEntryList, new FileTimeComparator());
-            // now insert them into the map
-            for (FilePair cacheEntry : cacheEntryList) {
-                mCacheMap.put(cacheEntry.mRelPath, cacheEntry.mFile);
-                mCurrentCacheSize += cacheEntry.mFile.length();
-            }
-            // this would be an unusual situation, but check if current cache is already too big
-            if (mCurrentCacheSize > getMaxFileCacheSize()) {
-                incrementAndAdjustCache(0);
+            mCacheMapLock.lock();
+            try {
+                Log.d(
+                        LOG_TAG,
+                        String.format(
+                                "Building file cache from contents at %s",
+                                mCacheRoot.getAbsolutePath()));
+                // create an unsorted list of all the files in mCacheRoot. Need to create list first
+                // rather than inserting in Map directly because Maps cannot be sorted
+                List<FilePair> cacheEntryList = new LinkedList<FilePair>();
+                addFiles(mCacheRoot, new Stack<String>(), cacheEntryList);
+                // now sort them based on file timestamp, to get them in LRU order
+                Collections.sort(cacheEntryList, new FileTimeComparator());
+                // now insert them into the map
+                for (FilePair cacheEntry : cacheEntryList) {
+                    mCacheMap.put(cacheEntry.mRelPath, cacheEntry.mFile);
+                    mCurrentCacheSize += cacheEntry.mFile.length();
+                }
+                // this would be an unusual situation, but check if current cache is already too big
+                if (mCurrentCacheSize > getMaxFileCacheSize()) {
+                    incrementAndAdjustCache(0);
+                }
+            } finally {
+                mCacheMapLock.unlock();
             }
         }
     }
diff --git a/src/com/android/tradefed/build/FileDownloadCacheWrapper.java b/src/com/android/tradefed/build/FileDownloadCacheWrapper.java
index ecf3f44..0860bfa 100644
--- a/src/com/android/tradefed/build/FileDownloadCacheWrapper.java
+++ b/src/com/android/tradefed/build/FileDownloadCacheWrapper.java
@@ -59,13 +59,13 @@
 
     /** {@inheritDoc} */
     @Override
-    public void downloadPartialFiles(
+    public void downloadZippedFiles(
             File destDir,
             String remoteFilePath,
             List<String> includeFilters,
             List<String> excludeFilters)
             throws BuildRetrievalError, IOException {
-        mDelegateDownloader.downloadPartialFiles(
+        mDelegateDownloader.downloadZippedFiles(
                 destDir, remoteFilePath, includeFilters, excludeFilters);
     }
 }
diff --git a/src/com/android/tradefed/build/IFileDownloader.java b/src/com/android/tradefed/build/IFileDownloader.java
index b3196dd..0ae0168 100644
--- a/src/com/android/tradefed/build/IFileDownloader.java
+++ b/src/com/android/tradefed/build/IFileDownloader.java
@@ -46,6 +46,24 @@
     public void downloadFile(String relativeRemotePath, File destFile) throws BuildRetrievalError;
 
     /**
+     * Alternate form of {@link #downloadFile(String, File)}, that allows caller to download a
+     * section of the file and save to a specific destination file.
+     *
+     * @param relativeRemotePath the remote path to the file to download, relative to an
+     *     implementation-specific root.
+     * @param destFile the file to place the downloaded contents into. Should not exist.
+     * @param startOffset the start offset in the remote file.
+     * @param size the number of bytes to download from the remote file. Set it to a negative value
+     *     to download the whole file.
+     * @throws BuildRetrievalError if file could not be downloaded
+     */
+    public default void downloadFile(
+            String remoteFilePath, File destFile, long startOffset, long size)
+            throws BuildRetrievalError {
+        throw new UnsupportedOperationException("Partial downloading is not implemented.");
+    }
+
+    /**
      * Check local file's freshness. If local file is the same as remote file, then it's fresh. If
      * not, local file is stale. This is mainly used for cache. The default implementation will
      * always return true, so if the file is immutable it will never need to check freshness.
@@ -73,7 +91,7 @@
      * @param excludeFilters a list of filters to skip downloading matching files.
      * @throws BuildRetrievalError if files could not be downloaded.
      */
-    public default void downloadPartialFiles(
+    public default void downloadZippedFiles(
             File destDir,
             String remoteFilePath,
             List<String> includeFilters,
diff --git a/src/com/android/tradefed/build/OtaZipfileBuildProvider.java b/src/com/android/tradefed/build/OtaZipfileBuildProvider.java
index 14617c7..8ac1fa9 100644
--- a/src/com/android/tradefed/build/OtaZipfileBuildProvider.java
+++ b/src/com/android/tradefed/build/OtaZipfileBuildProvider.java
@@ -18,6 +18,7 @@
 
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.device.DeviceProperties;
 import com.android.tradefed.util.ZipUtil;
 
 import java.io.BufferedReader;
@@ -49,7 +50,7 @@
             throw new BuildRetrievalError(
                     "Error processing build.prop contents from file: " + getOtaPath(), e);
         }
-        String bid = buildProp.getProperty("ro.build.version.incremental");
+        String bid = buildProp.getProperty(DeviceProperties.BUILD_ID);
         IDeviceBuildInfo buildInfo = new DeviceBuildInfo(bid, bid);
         buildInfo.setOtaPackageFile(new File(getOtaPath()), bid);
         return buildInfo;
diff --git a/src/com/android/tradefed/command/CommandOptions.java b/src/com/android/tradefed/command/CommandOptions.java
index 64ef014..94bc35f 100644
--- a/src/com/android/tradefed/command/CommandOptions.java
+++ b/src/com/android/tradefed/command/CommandOptions.java
@@ -22,7 +22,7 @@
 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.retry.RetryStrategy;
 import com.android.tradefed.util.UniqueMultiMap;
 
 import java.util.LinkedHashSet;
@@ -201,6 +201,7 @@
     private String mHostLogSuffix = null;
 
     // [Options related to auto-retry]
+    @Deprecated
     @Option(
         name = "max-testcase-run-count",
         description =
@@ -209,6 +210,7 @@
     )
     private int mMaxRunLimit = 1;
 
+    @Deprecated
     @Option(
         name = "retry-strategy",
         description =
@@ -217,6 +219,7 @@
     )
     private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
 
+    @Deprecated
     @Option(
         name = "auto-retry",
         description =
@@ -558,28 +561,4 @@
     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/ICommandOptions.java b/src/com/android/tradefed/command/ICommandOptions.java
index 9e5061d..a6682ae 100644
--- a/src/com/android/tradefed/command/ICommandOptions.java
+++ b/src/com/android/tradefed/command/ICommandOptions.java
@@ -17,7 +17,6 @@
 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;
@@ -200,16 +199,4 @@
 
     /** 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/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index 242c566..a339f25 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -40,12 +40,16 @@
 import com.android.tradefed.targetprep.multi.IMultiTargetPreparer;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.StubTest;
+import com.android.tradefed.testtype.retry.BaseRetryDecision;
+import com.android.tradefed.testtype.retry.IRetryDecision;
 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.SystemUtil;
 import com.android.tradefed.util.keystore.IKeyStoreClient;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
 
 import org.json.JSONArray;
@@ -98,6 +102,7 @@
     public static final String METRIC_POST_PROCESSOR_TYPE_NAME = "metric_post_processor";
     public static final String SANDBOX_TYPE_NAME = "sandbox";
     public static final String SANBOX_OPTIONS_TYPE_NAME = "sandbox_options";
+    public static final String RETRY_DECISION_TYPE_NAME = "retry_decision";
 
     private static Map<String, ObjTypeInfo> sObjTypeMap = null;
     private static Set<String> sMultiDeviceSupportedTag = null;
@@ -185,6 +190,7 @@
                     METRIC_POST_PROCESSOR_TYPE_NAME,
                     new ObjTypeInfo(BasePostProcessor.class, true));
             sObjTypeMap.put(SANBOX_OPTIONS_TYPE_NAME, new ObjTypeInfo(SandboxOptions.class, false));
+            sObjTypeMap.put(RETRY_DECISION_TYPE_NAME, new ObjTypeInfo(IRetryDecision.class, false));
         }
         return sObjTypeMap;
     }
@@ -240,6 +246,7 @@
         setDeviceMetricCollectors(new ArrayList<>());
         setPostProcessors(new ArrayList<>());
         setConfigurationObjectNoThrow(SANBOX_OPTIONS_TYPE_NAME, new SandboxOptions());
+        setConfigurationObjectNoThrow(RETRY_DECISION_TYPE_NAME, new BaseRetryDecision());
     }
 
     /**
@@ -349,6 +356,12 @@
         return (ILogSaver) getConfigurationObject(LOG_SAVER_TYPE_NAME);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public IRetryDecision getRetryDecision() {
+        return (IRetryDecision) getConfigurationObject(RETRY_DECISION_TYPE_NAME);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -1299,10 +1312,14 @@
     /** {@inheritDoc} */
     @Override
     public void resolveDynamicOptions() throws ConfigurationException {
-        ICommandOptions options = getCommandOptions();
-        if (options.getShardCount() != null && options.getShardIndex() == null) {
-            CLog.w("Skipping download due to local sharding detected.");
-            return;
+        // Resolve regardless of sharding if we are in remote environment because we know that's
+        // where the execution will occur.
+        if (!isRemoteEnvironment()) {
+            ICommandOptions options = getCommandOptions();
+            if (options.getShardCount() != null && options.getShardIndex() == null) {
+                CLog.w("Skipping download due to local sharding detected.");
+                return;
+            }
         }
 
         ArgsOptionParser argsParser = new ArgsOptionParser(getAllConfigurationObjects());
@@ -1311,6 +1328,12 @@
         mRemoteFiles.addAll(argsParser.validateRemoteFilePath());
     }
 
+    /** Returns whether or not the environment of TF is a remote invocation. */
+    @VisibleForTesting
+    protected boolean isRemoteEnvironment() {
+        return SystemUtil.isRemoteEnvironment();
+    }
+
     /** {@inheritDoc} */
     @Override
     public void cleanDynamicOptionFiles() {
@@ -1528,6 +1551,13 @@
                 excludeFilters,
                 printDeprecatedOptions,
                 printUnchangedOptions);
+        ConfigurationUtil.dumpClassToXml(
+                serializer,
+                RETRY_DECISION_TYPE_NAME,
+                getRetryDecision(),
+                excludeFilters,
+                printDeprecatedOptions,
+                printUnchangedOptions);
 
         serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
         serializer.endDocument();
diff --git a/src/com/android/tradefed/config/IConfiguration.java b/src/com/android/tradefed/config/IConfiguration.java
index 53751cc..1da3daf 100644
--- a/src/com/android/tradefed/config/IConfiguration.java
+++ b/src/com/android/tradefed/config/IConfiguration.java
@@ -31,6 +31,7 @@
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.targetprep.multi.IMultiTargetPreparer;
 import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.retry.IRetryDecision;
 import com.android.tradefed.util.keystore.IKeyStoreClient;
 
 import org.json.JSONArray;
@@ -111,6 +112,9 @@
      */
     public ILogSaver getLogSaver();
 
+    /** Returns the {@link IRetryDecision} used for the invocation. */
+    public IRetryDecision getRetryDecision();
+
     /**
      * Gets the {@link IMultiTargetPreparer}s from the configuration.
      *
diff --git a/src/com/android/tradefed/config/OptionCopier.java b/src/com/android/tradefed/config/OptionCopier.java
index 3968371..5f339df 100644
--- a/src/com/android/tradefed/config/OptionCopier.java
+++ b/src/com/android/tradefed/config/OptionCopier.java
@@ -64,6 +64,43 @@
     }
 
     /**
+     * Copy the given option from {@link Option} fields in <var>origObject</var> to
+     * <var>destObject</var>
+     *
+     * @param origObject the {@link Object} to copy from
+     * @param destObject the {@link Object} tp copy to
+     * @param optionName the name of the option to copy.
+     * @throws ConfigurationException if options failed to copy
+     */
+    public static void copyOptions(Object origObject, Object destObject, String optionName)
+            throws ConfigurationException {
+        Collection<Field> origFields = OptionSetter.getOptionFieldsForClass(origObject.getClass());
+        Map<String, Field> destFieldMap = getFieldOptionMap(destObject);
+        for (Field origField : origFields) {
+            final Option option = origField.getAnnotation(Option.class);
+            if (option.name().equals(optionName)) {
+                Field destField = destFieldMap.remove(option.name());
+                if (destField != null) {
+                    Object origValue = OptionSetter.getFieldValue(origField, origObject);
+                    OptionSetter.setFieldValue(option.name(), destObject, destField, origValue);
+                }
+            }
+        }
+    }
+
+    /**
+     * Identical to {@link #copyOptions(Object, Object, String)} but will log instead of throw if
+     * exception occurs.
+     */
+    public static void copyOptionsNoThrow(Object source, Object dest, String optionName) {
+        try {
+            copyOptions(source, dest, optionName);
+        } catch (ConfigurationException e) {
+            CLog.e(e);
+        }
+    }
+
+    /**
      * Build a map of {@link Option#name()} to {@link Field} for given {@link Object}.
      *
      * @param destObject
diff --git a/src/com/android/tradefed/device/DeviceProperties.java b/src/com/android/tradefed/device/DeviceProperties.java
index b4fc512..a79c7aa 100644
--- a/src/com/android/tradefed/device/DeviceProperties.java
+++ b/src/com/android/tradefed/device/DeviceProperties.java
@@ -40,4 +40,18 @@
     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";
+    /** property name for the type of build */
+    public static final String BUILD_TYPE = "ro.build.type";
+    /** property name for the alias of the build name */
+    public static final String BUILD_ALIAS = "ro.build.id";
+    /** property name for the flavor of the device build */
+    public static final String BUILD_FLAVOR = "ro.build.flavor";
+    /** property name for whether or not the device is headless */
+    public static final String BUILD_HEADLESS = "ro.build.headless";
+    /** property name for the build id of the device */
+    public static final String BUILD_ID = "ro.build.version.incremental";
+    /** property name for the build codename of the device. Example: Q */
+    public static final String BUILD_CODENAME = "ro.build.version.codename";
+    /** property name for the build tags of the device */
+    public static final String BUILD_TAGS = "ro.build.tags";
 }
diff --git a/src/com/android/tradefed/device/ManagedTestDeviceFactory.java b/src/com/android/tradefed/device/ManagedTestDeviceFactory.java
index 284c53e..faaf079 100644
--- a/src/com/android/tradefed/device/ManagedTestDeviceFactory.java
+++ b/src/com/android/tradefed/device/ManagedTestDeviceFactory.java
@@ -27,10 +27,10 @@
 import com.android.tradefed.device.cloud.NestedRemoteDevice;
 import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
 import com.android.tradefed.device.cloud.VmRemoteDevice;
-import com.android.tradefed.invoker.RemoteInvocationExecution;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.SystemUtil;
 
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
@@ -190,10 +190,7 @@
      */
     @VisibleForTesting
     protected boolean isRemoteEnvironment() {
-        if ("1".equals(System.getenv(RemoteInvocationExecution.REMOTE_VM_VARIABLE))) {
-            return true;
-        }
-        return false;
+        return SystemUtil.isRemoteEnvironment();
     }
 
     /** Create a {@link CollectingOutputReceiver}. */
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 2ae8e43..ed01aec 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -148,6 +148,9 @@
     /** Encrypting with wipe can take up to 20 minutes. */
     private static final long ENCRYPTION_WIPE_TIMEOUT_MIN = 20;
 
+    /** The maximum system_server start delay in seconds after device boot up */
+    private static final int MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP_SEC = 10;
+
     /** The time in ms to wait before starting logcat for a device */
     private int mLogStartDelay = 5*1000;
 
@@ -156,16 +159,6 @@
     /** The time in ms to wait for a recovery that we skip because of the NONE mode */
     static final int NONE_RECOVERY_MODE_DELAY = 1000;
 
-    static final String BUILD_ID_PROP = "ro.build.version.incremental";
-    private static final String PRODUCT_NAME_PROP = "ro.product.name";
-    private static final String BUILD_TYPE_PROP = "ro.build.type";
-    private static final String BUILD_ALIAS_PROP = "ro.build.id";
-    private static final String BUILD_FLAVOR = "ro.build.flavor";
-    private static final String HEADLESS_PROP = "ro.build.headless";
-    static final String BUILD_CODENAME_PROP = "ro.build.version.codename";
-    static final String BUILD_TAGS = "ro.build.tags";
-    private static final String PS_COMMAND = "ps -A || ps";
-
     private static final String SIM_STATE_PROP = "gsm.sim.state";
     private static final String SIM_OPERATOR_PROP = "gsm.operator.alpha";
 
@@ -619,7 +612,7 @@
      */
     @Override
     public String getBuildAlias() throws DeviceNotAvailableException {
-        String alias = getProperty(BUILD_ALIAS_PROP);
+        String alias = getProperty(DeviceProperties.BUILD_ALIAS);
         if (alias == null || alias.isEmpty()) {
             return getBuildId();
         }
@@ -631,7 +624,7 @@
      */
     @Override
     public String getBuildId() throws DeviceNotAvailableException {
-        String bid = getProperty(BUILD_ID_PROP);
+        String bid = getProperty(DeviceProperties.BUILD_ID);
         if (bid == null) {
             CLog.w("Could not get device %s build id.", getSerialNumber());
             return IBuildInfo.UNKNOWN_BUILD_ID;
@@ -644,12 +637,12 @@
      */
     @Override
     public String getBuildFlavor() throws DeviceNotAvailableException {
-        String buildFlavor = getProperty(BUILD_FLAVOR);
+        String buildFlavor = getProperty(DeviceProperties.BUILD_FLAVOR);
         if (buildFlavor != null && !buildFlavor.isEmpty()) {
             return buildFlavor;
         }
-        String productName = getProperty(PRODUCT_NAME_PROP);
-        String buildType = getProperty(BUILD_TYPE_PROP);
+        String productName = getProperty(DeviceProperties.PRODUCT);
+        String buildType = getProperty(DeviceProperties.BUILD_TYPE);
         if (productName == null || buildType == null) {
             CLog.w("Could not get device %s build flavor.", getSerialNumber());
             return null;
@@ -3703,7 +3696,7 @@
     public int getApiLevel() throws DeviceNotAvailableException {
         int apiLevel = UNKNOWN_API_LEVEL;
         try {
-            String prop = getProperty("ro.build.version.sdk");
+            String prop = getProperty(DeviceProperties.SDK_VERSION);
             apiLevel = Integer.parseInt(prop);
         } catch (NumberFormatException nfe) {
             // ignore, return unknown instead
@@ -3715,7 +3708,7 @@
     @Override
     public boolean checkApiLevelAgainstNextRelease(int strictMinLevel)
             throws DeviceNotAvailableException {
-        String codeName = getProperty(BUILD_CODENAME_PROP).trim();
+        String codeName = getProperty(DeviceProperties.BUILD_CODENAME).trim();
         int apiLevel = getApiLevel() + ("REL".equals(codeName) ? 0 : 1);
         if (strictMinLevel > apiLevel) {
             return false;
@@ -3828,9 +3821,7 @@
         executeShellCommand("TZ=UTC date -u " + dateString);
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
     public long getDeviceDate() throws DeviceNotAvailableException {
         String deviceTimeString = executeShellCommand("date +%s");
@@ -4084,7 +4075,7 @@
      */
     @Override
     public String getBuildSigningKeys() throws DeviceNotAvailableException {
-        String buildTags = getProperty(BUILD_TAGS);
+        String buildTags = getProperty(DeviceProperties.BUILD_TAGS);
         if (buildTags != null) {
             String[] tags = buildTags.split(",");
             for (String tag : tags) {
@@ -4203,7 +4194,7 @@
      */
     @Override
     public boolean isHeadless() throws DeviceNotAvailableException {
-        if (getProperty(HEADLESS_PROP) != null) {
+        if (getProperty(DeviceProperties.BUILD_HEADLESS) != null) {
             return true;
         }
         return false;
@@ -4239,8 +4230,8 @@
                     getAllocationState(),
                     getDisplayString(selector.getDeviceProductType(idevice)),
                     getDisplayString(selector.getDeviceProductVariant(idevice)),
-                    getDisplayString(idevice.getProperty("ro.build.version.sdk")),
-                    getDisplayString(idevice.getProperty("ro.build.id")),
+                    getDisplayString(idevice.getProperty(DeviceProperties.SDK_VERSION)),
+                    getDisplayString(idevice.getProperty(DeviceProperties.BUILD_ALIAS)),
                     getDisplayString(getBattery()),
                     getDeviceClass(),
                     getDisplayString(getMacAddress()),
@@ -4269,11 +4260,15 @@
         if (pidString == null) {
             return null;
         }
+        long startTime = getProcessStartTimeByPid(pidString);
+        if (startTime == -1L) {
+            return null;
+        }
         return new ProcessInfo(
                 getProcessUserByPid(pidString),
                 Integer.parseInt(pidString),
                 processName,
-                getProcessStartTimeByPid(pidString));
+                startTime);
     }
 
     /** Return the process start time since epoch for the given pid string */
@@ -4305,7 +4300,9 @@
     /** {@inheritDoc} */
     @Override
     public Map<Long, String> getBootHistory() throws DeviceNotAvailableException {
-        String output = getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+        // getProperty(DeviceProperties.BOOT_REASON_HISTORY) will not be able to handle boot history
+        // output format properly (tracked by b/139192891).
+        String output = executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
         /* Sample output:
         kernel_panic,1556587278
         reboot,,1556238008
@@ -4331,17 +4328,131 @@
 
     /** {@inheritDoc} */
     @Override
-    public Map<Long, String> getBootHistorySince(long utcEpochTime)
+    public Map<Long, String> getBootHistorySince(long utcEpochTime, TimeUnit timeUnit)
             throws DeviceNotAvailableException {
+        long utcEpochTimeSec = TimeUnit.SECONDS.convert(utcEpochTime, timeUnit);
         Map<Long, String> bootHistory = new LinkedHashMap<Long, String>();
         for (Map.Entry<Long, String> entry : getBootHistory().entrySet()) {
-            if (entry.getKey() > utcEpochTime) {
+            if (entry.getKey() > utcEpochTimeSec) {
                 bootHistory.put(entry.getKey(), entry.getValue());
             }
         }
         return bootHistory;
     }
 
+    private boolean hasNormalRebootSince(long utcEpochTime, TimeUnit timeUnit)
+            throws DeviceNotAvailableException {
+        Map<Long, String> bootHistory = getBootHistorySince(utcEpochTime, timeUnit);
+        if (bootHistory.isEmpty()) {
+            CLog.w("There is no reboot history since %s", utcEpochTime);
+            return false;
+        }
+
+        CLog.i(
+                "There are new boot history since %d. NewBootHistory = %s",
+                utcEpochTime, bootHistory);
+        // Check if there is reboot reason other than "reboot".
+        // Raise RuntimeException if there is abnormal reboot.
+        for (Map.Entry<Long, String> entry : bootHistory.entrySet()) {
+            if (!"reboot".equals(entry.getValue())) {
+                throw new RuntimeException(
+                        String.format(
+                                "Device %s has abnormal reboot reason %s at %d",
+                                getSerialNumber(), entry.getValue(), entry.getKey()));
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Check current system process is restarted after last reboot
+     *
+     * @param the system_server {@link ProcessInfo}
+     * @return true if system_server process restarted after last reboot; false if not
+     * @throws DeviceNotAvailableException
+     */
+    private boolean checkSystemProcessRestartedAfterLastReboot(ProcessInfo systemServerProcess)
+            throws DeviceNotAvailableException {
+        // If time gap from last reboot to current system_server process start time is more than
+        // MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP seconds, we conclude the system_server restarted
+        // after boot up.
+        if (!hasNormalRebootSince(
+                systemServerProcess.getStartTime() - MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP_SEC,
+                TimeUnit.SECONDS)) {
+            CLog.i(
+                    "Device last reboot is more than %s seconds away from current system_server "
+                            + "process start time. The system_server process restarted after "
+                            + "last boot up",
+                    MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP_SEC);
+            return true;
+        } else {
+            // Current system_server start within MAX_SYSTEM_SERVER_DELAY_AFTER_BOOT_UP
+            // seconds after device last boot up
+            return false;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean deviceSoftRestartedSince(long utcEpochTime, TimeUnit timeUnit)
+            throws DeviceNotAvailableException {
+        ProcessInfo currSystemServerProcess = getProcessByName("system_server");
+        if (currSystemServerProcess == null) {
+            CLog.i("The system_server process is not available on the device.");
+            return true;
+        }
+
+        // The system_server process started at or before utcEpochTime, there is no soft-restart
+        if (currSystemServerProcess.getStartTime()
+                <= TimeUnit.SECONDS.convert(utcEpochTime, timeUnit)) {
+            return false;
+        }
+
+        // The system_server process restarted after device utcEpochTime in second.
+        // Check if there is new reboot history, if no new reboot, device soft-restarted.
+        // If there is no normal reboot, soft-restart is detected.
+        if (!hasNormalRebootSince(utcEpochTime, timeUnit)) {
+            return true;
+        }
+
+        // There is new reboot since utcEpochTime. Check if system_server restarted after boot up.
+        return checkSystemProcessRestartedAfterLastReboot(currSystemServerProcess);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean deviceSoftRestarted(ProcessInfo prevSystemServerProcess)
+            throws DeviceNotAvailableException {
+        if (prevSystemServerProcess == null) {
+            CLog.i("The given system_server process is null. Abort deviceSoftRestarted check.");
+            return false;
+        }
+        ProcessInfo currSystemServerProcess = getProcessByName("system_server");
+        if (currSystemServerProcess == null) {
+            CLog.i("The system_server process is not available on the device.");
+            return true;
+        }
+
+
+        if (currSystemServerProcess.getPid() == prevSystemServerProcess.getPid()
+                && currSystemServerProcess.getStartTime()
+                        == prevSystemServerProcess.getStartTime()) {
+            return false;
+        }
+
+        // The system_server process restarted.
+        // Check boot history with previous system_server start time.
+        // If there is no normal reboot, soft-restart is detected
+        if (!hasNormalRebootSince(prevSystemServerProcess.getStartTime(), TimeUnit.SECONDS)) {
+            return true;
+        }
+
+        // There is reboot since prevSystemServerProcess.getStartTime().
+        // Check if system_server restarted after boot up.
+        return checkSystemProcessRestartedAfterLastReboot(currSystemServerProcess);
+
+    }
+
     /**
      * Validates that the given input is a valid MAC address
      *
diff --git a/src/com/android/tradefed/device/cloud/CommonLogRemoteFileUtil.java b/src/com/android/tradefed/device/cloud/CommonLogRemoteFileUtil.java
index 4b9d492..83b1c1f 100644
--- a/src/com/android/tradefed/device/cloud/CommonLogRemoteFileUtil.java
+++ b/src/com/android/tradefed/device/cloud/CommonLogRemoteFileUtil.java
@@ -19,10 +19,17 @@
 import com.android.tradefed.device.TestDeviceOptions.InstanceType;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.ZipUtil;
 
+import java.io.File;
+import java.io.IOException;
 import java.util.List;
 
 /**
@@ -35,6 +42,7 @@
     public static final String NESTED_REMOTE_LOG_DIR = "/home/%s/cuttlefish_runtime/";
     /** The directory where to find debug logs for an emulator instance. */
     public static final String EMULATOR_REMOTE_LOG_DIR = "/home/%s/log/";
+    public static final String TOMBSTONES_ZIP_NAME = "tombstones-zip";
 
     public static final MultiMap<InstanceType, KnownLogFileEntry> KNOWN_FILES_TO_FETCH =
             new MultiMap<>();
@@ -53,6 +61,12 @@
                 InstanceType.CUTTLEFISH,
                 new KnownLogFileEntry(
                         NESTED_REMOTE_LOG_DIR + "cuttlefish_config.json", null, LogDataType.TEXT));
+        KNOWN_FILES_TO_FETCH.put(
+                InstanceType.CUTTLEFISH,
+                new KnownLogFileEntry(
+                        NESTED_REMOTE_LOG_DIR + "launcher.log",
+                        "cuttlefish_launcher.log",
+                        LogDataType.TEXT));
         // Emulator known files to collect
         KNOWN_FILES_TO_FETCH.put(
                 InstanceType.EMULATOR,
@@ -128,6 +142,64 @@
     }
 
     /**
+     * Fetch and log the tombstones from the remote instance.
+     *
+     * @param testLogger The {@link ITestLogger} where to log the files.
+     * @param gceAvd The descriptor of the remote instance.
+     * @param options The {@link TestDeviceOptions} describing the device options
+     * @param runUtil A {@link IRunUtil} to execute commands.
+     */
+    public static void fetchTombstones(
+            ITestLogger testLogger,
+            GceAvdInfo gceAvd,
+            TestDeviceOptions options,
+            IRunUtil runUtil) {
+        if (gceAvd == null) {
+            CLog.e("GceAvdInfo was null, cannot collect remote files.");
+            return;
+        }
+        InstanceType type = options.getInstanceType();
+        if (!InstanceType.CUTTLEFISH.equals(type) && !InstanceType.REMOTE_AVD.equals(type)) {
+            return;
+        }
+        String pattern =
+                String.format(
+                        "/home/%s/cuttlefish_runtime/tombstones/*", options.getInstanceUser());
+        CommandResult resultList =
+                GceManager.remoteSshCommandExecution(
+                        gceAvd, options, runUtil, 60000, "ls", "-A1", pattern);
+        if (!CommandStatus.SUCCESS.equals(resultList.getStatus())) {
+            CLog.e("Failed to list the tombstones: %s", resultList.getStderr());
+            return;
+        }
+        if (resultList.getStdout().split("\n").length <= 0) {
+            return;
+        }
+        File tombstonesDir =
+                RemoteFileUtil.fetchRemoteDir(
+                        gceAvd,
+                        options,
+                        runUtil,
+                        120000,
+                        String.format(
+                                "/home/%s/cuttlefish_runtime/tombstones",
+                                options.getInstanceUser()));
+        if (tombstonesDir == null) {
+            CLog.w("No tombstones directory was pulled.");
+            return;
+        }
+        try {
+            File zipTombstones = ZipUtil.createZip(tombstonesDir);
+            try (InputStreamSource source = new FileInputStreamSource(zipTombstones, true)) {
+                testLogger.testLog(TOMBSTONES_ZIP_NAME, LogDataType.ZIP, source);
+            }
+        } catch (IOException e) {
+            CLog.e("Failed to zip the tombstones:");
+            CLog.e(e);
+        }
+    }
+
+    /**
      * Captures a log from the remote destination.
      *
      * @param testLogger The {@link ITestLogger} where to log the files.
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index 508170f..b45b9fc 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -210,8 +210,18 @@
 
     /** Build and return the command to launch GCE. Exposed for testing. */
     protected List<String> buildGceCmd(File reportFile, IBuildInfo b) {
-        List<String> gceArgs =
-                ArrayUtil.list(getTestDeviceOptions().getAvdDriverBinary().getAbsolutePath());
+        File avdDriverFile = getTestDeviceOptions().getAvdDriverBinary();
+        if (!avdDriverFile.exists()) {
+            throw new RuntimeException(
+                    String.format(
+                            "Could not find the Acloud driver at %s",
+                            avdDriverFile.getAbsolutePath()));
+        }
+        if (!avdDriverFile.canExecute()) {
+            // Set the executable bit if needed
+            FileUtil.chmodGroupRWX(avdDriverFile);
+        }
+        List<String> gceArgs = ArrayUtil.list(avdDriverFile.getAbsolutePath());
         gceArgs.add(
                 TestDeviceOptions.getCreateCommandByInstanceType(
                         getTestDeviceOptions().getInstanceType()));
@@ -299,11 +309,16 @@
         gceArgs.add("delete");
         // Add extra args.
         File f = null;
+        File config = null;
         try {
+            config = FileUtil.createTempFile(getAvdConfigFile().getName(), "config");
             gceArgs.add("--instance_names");
             gceArgs.add(mGceAvdInfo.instanceName());
             gceArgs.add("--config_file");
-            gceArgs.add(getAvdConfigFile().getAbsolutePath());
+            // Copy the config in case it comes from a dynamic file. In order to ensure Acloud has
+            // the file until it's done with it.
+            FileUtil.copyFile(getAvdConfigFile(), config);
+            gceArgs.add(config.getAbsolutePath());
             if (getTestDeviceOptions().getSerivceAccountJsonKeyFile() != null) {
                 gceArgs.add("--service_account_json_private_key_path");
                 gceArgs.add(
@@ -324,8 +339,11 @@
                             "Failed to tear down GCE %s with the following arg, %s",
                             mGceAvdInfo.instanceName(), gceArgs);
                 }
+                FileUtil.deleteFile(config);
             } else {
-                getRunUtil().runCmdInBackground(gceArgs.toArray(new String[gceArgs.size()]));
+                Process p = getRunUtil().runCmdInBackground(gceArgs);
+                AcloudDeleteCleaner cleaner = new AcloudDeleteCleaner(p, config);
+                cleaner.start();
             }
         } catch (IOException e) {
             CLog.e("failed to create log file for GCE Teardown");
@@ -498,12 +516,15 @@
             GceAvdInfo gceAvd, TestDeviceOptions options, IRunUtil runUtil, String... command) {
         CommandResult res =
                 remoteSshCommandExecution(gceAvd, options, runUtil, BUGREPORT_TIMEOUT, command);
-        if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
-            CLog.e("issue when attempting to execute '%s':", Arrays.asList(command));
-            CLog.e("%s", res.getStderr());
-        }
         // We attempt to get a clean output from our command
         String output = res.getStdout().trim();
+        if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
+            CLog.e("issue when attempting to execute '%s':", Arrays.asList(command));
+            CLog.e("Stderr: %s", res.getStderr());
+        } else if (output.isEmpty()) {
+            CLog.e("Stdout from '%s' was empty", Arrays.asList(command));
+            CLog.e("Stderr: %s", res.getStderr());
+        }
         return output;
     }
 
@@ -636,4 +657,30 @@
         }
         return getTestDeviceOptions().getAvdConfigFile();
     }
+
+    /**
+     * Thread that helps cleaning the copied config when the process is done. This ensures acloud is
+     * not missing its config until its done.
+     */
+    private class AcloudDeleteCleaner extends Thread {
+        private Process mProcess;
+        private File mConfigFile;
+
+        public AcloudDeleteCleaner(Process p, File config) {
+            setDaemon(true);
+            setName("acloud-delete-cleaner");
+            mProcess = p;
+            mConfigFile = config;
+        }
+
+        @Override
+        public void run() {
+            try {
+                mProcess.waitFor();
+            } catch (InterruptedException e) {
+                CLog.e(e);
+            }
+            FileUtil.deleteFile(mConfigFile);
+        }
+    }
 }
diff --git a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
index 8e2b8f0..288f567 100644
--- a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
@@ -131,8 +131,11 @@
                 getGceHandler().cleanUp();
             }
         } finally {
+            // Reset the internal variable
+            mCopiedOptions = null;
             if (mValidationConfig != null) {
                 mValidationConfig.cleanDynamicOptionFiles();
+                mValidationConfig = null;
             }
             // Ensure parent postInvocationTearDown is always called.
             super.postInvocationTearDown(exception);
diff --git a/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java b/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
index 70d319e..2b6f6ab 100644
--- a/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
@@ -25,18 +25,17 @@
 import com.android.tradefed.invoker.RemoteInvocationExecution;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 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.google.common.base.Joiner;
 
 import java.io.File;
-import java.io.IOException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.HashMap;
@@ -182,15 +181,28 @@
             CLog.e("%s doesn't exists, skip logging it.", launcherLog.getAbsolutePath());
             return;
         }
-        try {
-            // Attempt to print the log
-            CLog.e("Launcher.log content: %s", FileUtil.readStringFromFile(launcherLog));
-        } catch (IOException e) {
-            // Ignore
-            CLog.e(e);
+        // TF runs as the primary user and may get a permission denied to read the launcher.log of
+        // other users. So use the shell to cat the file content.
+        CommandResult readLauncherLogRes =
+                getRunUtil()
+                        .runTimedCmd(
+                                60000L,
+                                "sudo",
+                                "runuser",
+                                "-l",
+                                username,
+                                "-c",
+                                String.format("'cat %s'", launcherLog.getAbsolutePath()));
+        if (!CommandStatus.SUCCESS.equals(readLauncherLogRes.getStatus())) {
+            CLog.e(
+                    "Failed to read Launcher.log content due to: %s",
+                    readLauncherLogRes.getStderr());
+            return;
         }
-        try (InputStreamSource source = new FileInputStreamSource(launcherLog)) {
+        String content = readLauncherLogRes.getStdout();
+        try (InputStreamSource source = new ByteArrayInputStreamSource(content.getBytes())) {
             logger.testLog(logName, LogDataType.TEXT, source);
         }
+        CLog.d("See %s for the launcher.log that failed to start.", logName);
     }
 }
diff --git a/src/com/android/tradefed/device/cloud/RemoteFileUtil.java b/src/com/android/tradefed/device/cloud/RemoteFileUtil.java
index 0443c43..20f9970 100644
--- a/src/com/android/tradefed/device/cloud/RemoteFileUtil.java
+++ b/src/com/android/tradefed/device/cloud/RemoteFileUtil.java
@@ -53,7 +53,7 @@
         try {
             localFile =
                     FileUtil.createTempFile(
-                            FileUtil.getBaseName(fileName), FileUtil.getExtension(fileName));
+                            FileUtil.getBaseName(fileName) + "_", FileUtil.getExtension(fileName));
             if (fetchRemoteFile(
                     remoteInstance, options, runUtil, timeout, remoteFilePath, localFile)) {
                 return localFile;
diff --git a/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollector.java b/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollector.java
index 2b64f2f..da1e38a 100644
--- a/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollector.java
+++ b/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollector.java
@@ -32,23 +32,41 @@
 
     private static final String NAME_FORMAT = "%s-debug-hostlog-on-failure";
 
-    public Long offset = null;
+    private Long offset = null;
 
     @Override
     public void onTestRunStart(DeviceMetricData runData) {
+        offset = null;
         // TODO: Improve the offset from the start of the method instead.
-        offset = getLogger().getLog().size();
+        try (InputStreamSource source = getLogger().getLog()) {
+            if (source == null) {
+                CLog.e(
+                        "Could not obtain the host logs for debugging. It won't be available "
+                                + "in the event of test cases failures.");
+                return;
+            }
+            offset = source.size();
+        }
     }
 
     @Override
     public void onTestFail(DeviceMetricData testData, TestDescription test) {
-        try (InputStreamSource source = getLogger().getLog();
-                InputStream stream = source.createInputStream()) {
-            stream.skip(offset);
-            try (InputStreamSource logSource =
-                    new SnapshotInputStreamSource("host-log-failure", stream)) {
-                super.testLog(
-                        String.format(NAME_FORMAT, test.toString()), LogDataType.TEXT, logSource);
+        if (offset == null) {
+            return;
+        }
+        try (InputStreamSource source = getLogger().getLog()) {
+            if (source == null) {
+                return;
+            }
+            try (InputStream stream = source.createInputStream()) {
+                stream.skip(offset);
+                try (InputStreamSource logSource =
+                        new SnapshotInputStreamSource("host-log-failure", stream)) {
+                    super.testLog(
+                            String.format(NAME_FORMAT, test.toString()),
+                            LogDataType.TEXT,
+                            logSource);
+                }
             }
         } catch (IOException e) {
             CLog.e(e);
diff --git a/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java b/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java
index 066b425..4f01ffa 100644
--- a/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java
+++ b/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java
@@ -16,10 +16,14 @@
 package com.android.tradefed.device.metric;
 
 import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.device.CollectingByteOutputReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ILogcatReceiver;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.LogcatReceiver;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.TestDescription;
@@ -34,16 +38,24 @@
 
     private static final int MAX_LOGAT_SIZE_BYTES = 4 * 1024 * 1024;
     /** Always include a bit of prior data to capture what happened before */
-    private static final int OFFSET_CORRECTION = 20000;
+    private static final int OFFSET_CORRECTION = 10000;
 
     private static final String NAME_FORMAT = "%s-%s-logcat-on-failure";
 
+    private static final String LOGCAT_COLLECT_CMD = "logcat -T 150";
+    // -t implies -d (dump) so it's a one time collection
+    private static final String LOGCAT_COLLECT_CMD_LEGACY = "logcat -t 5000";
+    private static final int API_LIMIT = 20;
+
     private Map<ITestDevice, ILogcatReceiver> mLogcatReceivers = new HashMap<>();
     private Map<ITestDevice, Integer> mOffset = new HashMap<>();
 
     @Override
     public void onTestRunStart(DeviceMetricData runData) {
         for (ITestDevice device : getRealDevices()) {
+            if (getApiLevelNoThrow(device) < API_LIMIT) {
+                continue;
+            }
             // 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,17 +74,9 @@
 
     @Override
     public void onTestFail(DeviceMetricData testData, TestDescription test) {
-        for (ITestDevice device : getRealDevices()) {
-            // Delay slightly for the error to get in the logcat
-            getRunUtil().sleep(100);
-            try (InputStreamSource logcatSource =
-                    mLogcatReceivers
-                            .get(device)
-                            .getLogcatData(MAX_LOGAT_SIZE_BYTES, mOffset.get(device))) {
-                String name = String.format(NAME_FORMAT, test.toString(), device.getSerialNumber());
-                super.testLog(name, LogDataType.LOGCAT, logcatSource);
-            }
-        }
+        // Delay slightly for the error to get in the logcat
+        getRunUtil().sleep(100);
+        collectAndLog(test);
     }
 
     @Override
@@ -84,7 +88,7 @@
     ILogcatReceiver createLogcatReceiver(ITestDevice device) {
         // Use logcat -T 'count' to only print a few line before we start and not the full buffer
         return new LogcatReceiver(
-                device, "logcat -T 150", device.getOptions().getMaxLogcatDataSize(), 0);
+                device, LOGCAT_COLLECT_CMD, device.getOptions().getMaxLogcatDataSize(), 0);
     }
 
     @VisibleForTesting
@@ -92,6 +96,31 @@
         return RunUtil.getDefault();
     }
 
+    private void collectAndLog(TestDescription test) {
+        for (ITestDevice device : getRealDevices()) {
+            ILogcatReceiver receiver = mLogcatReceivers.get(device);
+            // Receiver is only initialized above API 19, if not supported, we use a legacy command
+            if (receiver == null) {
+                CollectingByteOutputReceiver outputReceiver = new CollectingByteOutputReceiver();
+                try {
+                    device.executeShellCommand(LOGCAT_COLLECT_CMD_LEGACY, outputReceiver);
+                    saveLogcatSource(
+                            test,
+                            new ByteArrayInputStreamSource(outputReceiver.getOutput()),
+                            device.getSerialNumber());
+                } catch (DeviceNotAvailableException e) {
+                    CLog.e(e);
+                }
+                continue;
+            }
+            // If supported get the logcat buffer
+            saveLogcatSource(
+                    test,
+                    receiver.getLogcatData(MAX_LOGAT_SIZE_BYTES, mOffset.get(device)),
+                    device.getSerialNumber());
+        }
+    }
+
     private void initReceiver(ITestDevice device) {
         if (mLogcatReceivers.get(device) == null) {
             ILogcatReceiver receiver = createLogcatReceiver(device);
@@ -108,4 +137,19 @@
         mLogcatReceivers.clear();
         mOffset.clear();
     }
+
+    private int getApiLevelNoThrow(ITestDevice device) {
+        try {
+            return device.getApiLevel();
+        } catch (DeviceNotAvailableException e) {
+            return 1;
+        }
+    }
+
+    private void saveLogcatSource(TestDescription test, InputStreamSource source, String serial) {
+        try (InputStreamSource logcatSource = source) {
+            String name = String.format(NAME_FORMAT, test.toString(), serial);
+            super.testLog(name, LogDataType.LOGCAT, logcatSource);
+        }
+    }
 }
diff --git a/src/com/android/tradefed/device/metric/README.md b/src/com/android/tradefed/device/metric/README.md
new file mode 100644
index 0000000..a812b9d
--- /dev/null
+++ b/src/com/android/tradefed/device/metric/README.md
@@ -0,0 +1,9 @@
+# Core Metric Collectors
+
+This folder contains the core implementation and interfaces of TradeFed metric Collectors.
+The most generic implementation, used by the test harness itself (sometimes in
+automatic fashion) are located here.
+
+Most specialized implementation that test writers can use are located at:
+platform/tools/tradefederation/core/test_framework/
+
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index f52555b..9663c34 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -37,9 +37,12 @@
 import com.android.tradefed.device.metric.IMetricCollector;
 import com.android.tradefed.device.metric.IMetricCollectorReceiver;
 import com.android.tradefed.invoker.TestInvocation.Stage;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.invoker.shard.IShardHelper;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.ITestLoggerReceiver;
 import com.android.tradefed.result.InputStreamSource;
@@ -56,7 +59,10 @@
 import com.android.tradefed.testtype.IInvocationContextReceiver;
 import com.android.tradefed.testtype.IMultiDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
+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.SystemUtil;
 import com.android.tradefed.util.SystemUtil.EnvVariable;
 import com.android.tradefed.util.TimeUtil;
@@ -227,6 +233,7 @@
             // Setup timing metric. It does not include flashing time on boot tests.
             long setupDuration = System.currentTimeMillis() - start;
             context.addInvocationTimingMetric(IInvocationContext.TimingEvent.SETUP, setupDuration);
+            InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.SETUP, setupDuration);
             CLog.d("Setup duration: %s'", TimeUtil.formatElapsedTime(setupDuration));
             // Upload the setup logcat after setup is complete.
             for (String deviceName : context.getDeviceConfigNames()) {
@@ -404,6 +411,9 @@
             deferredThrowable = preTargetTearDownException;
         }
 
+        // Collect adb logs.
+        logHostAdb(logger);
+
         if (deferredThrowable != null) {
             throw deferredThrowable;
         }
@@ -740,6 +750,39 @@
         }
     }
 
+    /** Collect the logs from $TMPDIR/adb.$UID.log. */
+    @VisibleForTesting
+    void logHostAdb(ITestLogger logger) {
+        String tmpDir = "/tmp";
+        if (System.getenv("TMPDIR") != null) {
+            tmpDir = System.getenv("TMPDIR");
+        }
+        CommandResult uidRes =
+                RunUtil.getDefault()
+                        .runTimedCmd(60000, "id", "-u", System.getProperty("user.name"));
+        if (!CommandStatus.SUCCESS.equals(uidRes.getStatus())) {
+            CLog.e("Failed to collect UID for adb logs: %s", uidRes.getStderr());
+            return;
+        }
+        String uid = uidRes.getStdout().trim();
+        File adbLog = new File(tmpDir, String.format("adb.%s.log", uid));
+        if (!adbLog.exists()) {
+            CLog.i("Did not find adb log file: %s, upload skipped.", adbLog);
+            return;
+        }
+        CommandResult truncAdb =
+                RunUtil.getDefault()
+                        .runTimedCmd(60000, "tail", "--bytes=10MB", adbLog.getAbsolutePath());
+        if (!CommandStatus.SUCCESS.equals(truncAdb.getStatus())) {
+            CLog.e("Fail to truncate the adb log: %s\n%s", adbLog, truncAdb.getStderr());
+            return;
+        }
+        try (InputStreamSource source =
+                new ByteArrayInputStreamSource(truncAdb.getStdout().getBytes())) {
+            logger.testLog("host_adb_log", LogDataType.TEXT, source);
+        }
+    }
+
     /** Returns the external directory coming from the environment. */
     @VisibleForTesting
     File getExternalTestCasesDirs(EnvVariable envVar) {
diff --git a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
index f82442c..ecbdae5 100644
--- a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
@@ -22,10 +22,13 @@
 import com.android.tradefed.clearcut.ClearcutClient;
 import com.android.tradefed.command.CommandOptions;
 import com.android.tradefed.command.CommandRunner;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IDeviceConfiguration;
 import com.android.tradefed.config.OptionCopier;
+import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceSelectionOptions;
 import com.android.tradefed.device.TestDeviceOptions;
@@ -35,6 +38,8 @@
 import com.android.tradefed.device.cloud.ManagedRemoteDevice;
 import com.android.tradefed.device.cloud.MultiUserSetupUtil;
 import com.android.tradefed.device.cloud.RemoteFileUtil;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.FileInputStreamSource;
@@ -50,6 +55,7 @@
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
 import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.SystemUtil;
 import com.android.tradefed.util.TimeUtil;
 import com.android.tradefed.util.proto.TestRecordProtoUtil;
 
@@ -72,9 +78,9 @@
     public static final long PUSH_TF_TIMEOUT = 150000L;
     public static final long PULL_RESULT_TIMEOUT = 180000L;
     public static final long REMOTE_PROCESS_RUNNING_WAIT = 15000L;
-    public static final long LAUNCH_EXTRA_DEVICE = 10 * 60 * 1000L;
+    public static final long LAUNCH_EXTRA_DEVICE = 15 * 60 * 1000L;
+    public static final long SETUP_REMOTE_DIR_TIMEOUT = 10 * 60 * 1000L;
     public static final long NEW_USER_TIMEOUT = 5 * 60 * 1000L;
-    public static final String REMOTE_VM_VARIABLE = "REMOTE_VM_ENV";
 
     public static final String REMOTE_USER_DIR = "/home/{$USER}/";
     public static final String PROTO_RESULT_NAME = "output.pb";
@@ -164,23 +170,18 @@
 
                 // Log the overhead to start the device
                 long elapsedTime = System.currentTimeMillis() - startTime;
-                context.getBuildInfos()
-                        .get(0)
-                        .addBuildAttribute(SHARDING_DEVICE_SETUP_TIME, Long.toString(elapsedTime));
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.SHARDING_DEVICE_SETUP_TIME, elapsedTime);
             }
         }
 
         mRemoteAdbPath = String.format("/home/%s/bin/adb", options.getInstanceUser());
-
-        String tfPath = System.getProperty("TF_JAR_DIR");
-        if (tfPath == null) {
-            listener.invocationFailed(new RuntimeException("Failed to find $TF_JAR_DIR."));
+        // Select the TF version that should be pushed to the remote VM
+        File tfToPush = getLocalTradefedPath(listener, options.getRemoteTf());
+        if (tfToPush == null) {
             return;
         }
-        File currentTf = new File(tfPath).getAbsoluteFile();
-        if (tfPath.equals(".")) {
-            currentTf = new File("").getAbsoluteFile();
-        }
+
         mRemoteTradefedDir = mainRemoteDir + "tradefed/";
         CommandResult createRemoteDir =
                 GceManager.remoteSshCommandExecution(
@@ -202,7 +203,7 @@
                             runUtil,
                             PUSH_TF_TIMEOUT,
                             mRemoteTradefedDir,
-                            currentTf);
+                            tfToPush);
             attempt++;
         }
         if (!result) {
@@ -211,7 +212,7 @@
             return;
         }
 
-        mRemoteTradefedDir = mRemoteTradefedDir + currentTf.getName() + "/";
+        mRemoteTradefedDir = mRemoteTradefedDir + tfToPush.getName() + "/";
         CommandResult listRemoteDir =
                 GceManager.remoteSshCommandExecution(
                         info, options, runUtil, 120000L, "ls", "-l", mRemoteTradefedDir);
@@ -242,6 +243,7 @@
                     new String[] {
                         GlobalConfiguration.SCHEDULER_TYPE_NAME,
                         GlobalConfiguration.HOST_OPTIONS_TYPE_NAME,
+                        DynamicRemoteFileResolver.DYNAMIC_RESOLVER,
                         "android-build"
                     };
             try {
@@ -329,7 +331,7 @@
         StringBuilder tfCmdBuilder =
                 new StringBuilder("TF_GLOBAL_CONFIG=" + globalConfig.getName());
         // Set an env variable to notify that this a remote environment.
-        tfCmdBuilder.append(" " + REMOTE_VM_VARIABLE + "=1");
+        tfCmdBuilder.append(" " + SystemUtil.REMOTE_VM_VARIABLE + "=1");
         // Disable clearcut in the remote
         tfCmdBuilder.append(" " + ClearcutClient.DISABLE_CLEARCUT_KEY + "=1");
         tfCmdBuilder.append(" ENTRY_CLASS=" + CommandRunner.class.getCanonicalName());
@@ -501,6 +503,13 @@
                     parser.processFileProto(resultFile);
                 }
             } while (resultFile != null);
+
+            if (!parser.invocationEndedReached()) {
+                currentInvocationListener.invocationFailed(
+                        new RuntimeException(
+                                "Parsing of results protos might be incomplete: invocation ended "
+                                        + "of remote execution was not found."));
+            }
         }
         return stillRunning;
     }
@@ -575,14 +584,18 @@
      */
     @VisibleForTesting
     File createRemoteConfig(IConfiguration config, ITestLogger logger, String resultDirPath)
-            throws IOException {
+            throws IOException, ConfigurationException {
         // Setup the remote reporting to a proto file
         List<ITestInvocationListener> reporters = new ArrayList<>();
         FileProtoResultReporter protoReporter = new FileProtoResultReporter();
+        OptionSetter protoResSetter = new OptionSetter(protoReporter);
         if (config.getCommandOptions().shouldReportModuleProgression()) {
-            protoReporter.setPeriodicWriting(true);
+            protoResSetter.setOptionValue(
+                    FileProtoResultReporter.PERIODIC_PROTO_WRITING_OPTION, "true");
         }
-        protoReporter.setFileOutput(new File(resultDirPath + PROTO_RESULT_NAME));
+        protoResSetter.setOptionValue(
+                FileProtoResultReporter.PROTO_OUTPUT_FILE,
+                new File(resultDirPath + PROTO_RESULT_NAME).getPath());
         reporters.add(protoReporter);
 
         config.setTestInvocationListeners(reporters);
@@ -595,6 +608,11 @@
             }
         }
 
+        // Unset remote-tf-version to avoid re-downloading from remote VM.
+        OptionSetter deviceOptions =
+                new OptionSetter(config.getDeviceConfig().get(0).getDeviceOptions());
+        deviceOptions.setOptionValue(TestDeviceOptions.REMOTE_TF_VERSION_OPTION, "");
+
         // Dump and log the configuration
         File configFile = FileUtil.createTempFile(config.getName(), ".xml");
         config.dumpXml(
@@ -608,6 +626,24 @@
         return configFile;
     }
 
+    /** Returns the Tradefed version that should be pushed to the remote to drive the invocation. */
+    private File getLocalTradefedPath(ITestInvocationListener listener, File remoteTf) {
+        if (remoteTf != null && remoteTf.exists()) {
+            return remoteTf;
+        }
+
+        String tfPath = System.getProperty("TF_JAR_DIR");
+        if (tfPath == null) {
+            listener.invocationFailed(new RuntimeException("Failed to find $TF_JAR_DIR."));
+            return null;
+        }
+        File currentTf = new File(tfPath).getAbsoluteFile();
+        if (tfPath.equals(".")) {
+            currentTf = new File("").getAbsoluteFile();
+        }
+        return currentTf;
+    }
+
     private void fetchAndProcessResults(
             boolean wasStillRunning,
             ITestInvocationListener invocationListener,
@@ -685,7 +721,7 @@
                         info,
                         options,
                         runUtil,
-                        NEW_USER_TIMEOUT);
+                        SETUP_REMOTE_DIR_TIMEOUT);
         if (homeDirSetup != null) {
             String errorMsg =
                     String.format("Failed to setup home dir: %s", homeDirSetup.getStderr());
diff --git a/src/com/android/tradefed/invoker/ShardListener.java b/src/com/android/tradefed/invoker/ShardListener.java
index 5805a3b..7cfc0d1 100644
--- a/src/com/android/tradefed/invoker/ShardListener.java
+++ b/src/com/android/tradefed/invoker/ShardListener.java
@@ -28,10 +28,13 @@
 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 com.android.tradefed.util.TimeUtil;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
@@ -40,10 +43,12 @@
  * invocation split to run on multiple resources in parallel), and forwards them to another
  * listener.
  */
-public class ShardListener extends CollectingTestListener {
+public class ShardListener extends CollectingTestListener implements ISupportGranularResults {
 
     private ITestInvocationListener mMasterListener;
     private IInvocationContext mModuleContext = null;
+    private int mAttemptInProgress = 0;
+    private boolean mEnableGranularResults = false;
 
     /**
      * Create a {@link ShardListener}.
@@ -57,6 +62,16 @@
         mMasterListener = master;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public boolean supportGranularResults() {
+        return mEnableGranularResults;
+    }
+
+    public void setSupportGranularResults(boolean enableGranularResults) {
+        mEnableGranularResults = enableGranularResults;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -118,6 +133,13 @@
         mModuleContext = moduleContext;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void testRunStarted(String name, int numTests, int attemptNumber, long startTime) {
+        super.testRunStarted(name, numTests, attemptNumber, startTime);
+        mAttemptInProgress = attemptNumber;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -138,9 +160,11 @@
             // 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());
+                forwardRunResults(getCurrentRunResults(), mAttemptInProgress);
             }
+            mAttemptInProgress = 0;
         }
+
     }
 
     /** {@inheritDoc} */
@@ -149,34 +173,29 @@
         super.testModuleEnded();
 
         synchronized (mMasterListener) {
-            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;
+            mMasterListener.testModuleStarted(mModuleContext);
+            List<String> resultNames = new ArrayList<String>();
+            if (mEnableGranularResults) {
+                for (int i = 0; i < mAttemptInProgress + 1; i++) {
+                    List<TestRunResult> runResults = getTestRunForAttempts(i);
+                    for (TestRunResult runResult : runResults) {
+                        forwardRunResults(runResult, i);
+                        resultNames.add(runResult.getName());
+                    }
                 }
+            } else {
+                for (TestRunResult runResult : getMergedTestRunResults()) {
+                    // Forward the run level results
+                    forwardRunResults(runResult, 0);
+                    resultNames.add(runResult.getName());
+                }
+            }
 
-                // Stop or start the module
-                if (moduleContext != null
-                        && !getModuleContextForRunResult(runResult.getName())
-                                .equals(moduleContext)) {
-                    mMasterListener.testModuleEnded();
-                    moduleContext = null;
-                }
-                if (moduleContext == null
-                        && getModuleContextForRunResult(runResult.getName()) != null) {
-                    moduleContext = getModuleContextForRunResult(runResult.getName());
-                    mMasterListener.testModuleStarted(moduleContext);
-                }
-                // Forward the run level results
-                forwardRunResults(runResult);
+            // Ensure we don't carry results from one module to another.
+            for (String name : resultNames) {
+                clearResultsForName(name);
             }
-            // Close the last module
-            if (moduleContext != null) {
-                mMasterListener.testModuleEnded();
-                moduleContext = null;
-            }
+            mMasterListener.testModuleEnded();
         }
         mModuleContext = null;
     }
@@ -193,9 +212,12 @@
         }
     }
 
-    private void forwardRunResults(TestRunResult runResult) {
-        // TODO: Support attempts and retries
-        mMasterListener.testRunStarted(runResult.getName(), runResult.getExpectedTestCount());
+    private void forwardRunResults(TestRunResult runResult, int attempt) {
+        mMasterListener.testRunStarted(
+                runResult.getName(),
+                runResult.getExpectedTestCount(),
+                attempt,
+                runResult.getStartTime());
         forwardTestResults(runResult.getTestResults());
         if (runResult.isRunFailure()) {
             mMasterListener.testRunFailed(runResult.getRunFailureMessage());
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index 8f18c26..e64c8fb 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -19,6 +19,7 @@
 import com.android.tradefed.build.BuildRetrievalError;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.command.CommandRunner.ExitCode;
+import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -33,6 +34,7 @@
 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.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecution;
 import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
 import com.android.tradefed.invoker.shard.ShardBuildCloner;
@@ -56,6 +58,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.IRetryDecision;
 import com.android.tradefed.testtype.retry.ResultAggregator;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
@@ -484,10 +487,14 @@
             ITestInvocationListener listener, IConfiguration config, String name) {
         ILeveledLogOutput logger = config.getLogOutput();
         try (InputStreamSource globalLogSource = logger.getLog()) {
-            if (config.getCommandOptions().getHostLogSuffix() != null) {
-                name += config.getCommandOptions().getHostLogSuffix();
+            if (globalLogSource != null) {
+                if (config.getCommandOptions().getHostLogSuffix() != null) {
+                    name += config.getCommandOptions().getHostLogSuffix();
+                }
+                listener.testLog(name, LogDataType.TEXT, globalLogSource);
+            } else {
+                CLog.i("Skip logging %s to a file with logger '%s'", name, logger);
             }
-            listener.testLog(name, LogDataType.TEXT, globalLogSource);
         }
         // once tradefed log is reported, all further log calls for this invocation can get lost
         // unregister logger so future log calls get directed to the tradefed global log
@@ -620,6 +627,45 @@
         return false;
     }
 
+    /**
+     * Invoke {@link IConfiguration#resolveDynamicOptions()} to resolve the dynamic files.
+     *
+     * @param context the {@link IInvocationContext} of the invocation.
+     * @param config the {@link IConfiguration} of this test run.
+     * @param rescheduler the {@link IRescheduler}, for rescheduling portions of the invocation for
+     *     execution on another resource(s)
+     * @param listener the {@link ITestInvocation} to report build download failures.
+     * @param invocationPath the {@link IInvocationExecution} driving the invocation.
+     * @param mode The current {@link RunMode} of the invocation.
+     * @return True if we successfully downloaded the build, false otherwise.
+     */
+    private boolean invokeRemoteDynamic(
+            IInvocationContext context,
+            IConfiguration config,
+            IRescheduler rescheduler,
+            ITestInvocationListener listener,
+            IInvocationExecution invocationPath,
+            RunMode mode) {
+        try {
+            // Don't resolve for remote invocation, wait until we are inside the remote.
+            if (!RunMode.REMOTE_INVOCATION.equals(mode)) {
+                config.resolveDynamicOptions();
+            }
+            return true;
+        } catch (RuntimeException | ConfigurationException e) {
+            // Report an empty invocation, so this error is sent to listeners
+            startInvocation(config, context, listener);
+            // Don't want to use #reportFailure, since that will call buildNotTested
+            listener.invocationFailed(e);
+            for (ITestDevice device : context.getDevices()) {
+                invocationPath.reportLogs(device, listener, Stage.ERROR);
+            }
+            reportHostLog(listener, config);
+            listener.invocationEnded(0L);
+            return false;
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public void invoke(
@@ -635,12 +681,12 @@
         ITestInvocationListener listener = null;
 
         // Auto retry feature
-        if (config.getCommandOptions().isAutoRetryEnabled()
-                && config.getCommandOptions().getMaxRetryCount() > 1) {
+        IRetryDecision decision = config.getRetryDecision();
+        ResultAggregator aggregator = null;
+        decision.setInvocationContext(context);
+        if (decision.isAutoRetryEnabled() && decision.getMaxRetryCount() > 1) {
             CLog.d("Auto-retry enabled, using the ResultAggregator to handle multiple retries.");
-            ResultAggregator aggregator =
-                    new ResultAggregator(
-                            allListeners, config.getCommandOptions().getRetryStrategy());
+            aggregator = new ResultAggregator(allListeners, decision.getRetryStrategy());
             allListeners = Arrays.asList(aggregator);
         }
 
@@ -685,7 +731,12 @@
             }
             getLogRegistry().registerLogger(leveledLogOutput);
             mStatus = "resolving dynamic options";
-            config.resolveDynamicOptions();
+            boolean resolverSuccess =
+                    invokeRemoteDynamic(
+                            context, config, rescheduler, listener, invocationPath, mode);
+            if (!resolverSuccess) {
+                return;
+            }
 
             mStatus = "fetching build";
             for (String deviceName : context.getDeviceConfigNames()) {
@@ -712,6 +763,8 @@
             long fetchBuildDuration = System.currentTimeMillis() - start;
             context.addInvocationTimingMetric(IInvocationContext.TimingEvent.FETCH_BUILD,
                     fetchBuildDuration);
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.FETCH_BUILD, fetchBuildDuration);
             CLog.d("Fetch build duration: %s", TimeUtil.formatElapsedTime(fetchBuildDuration));
             if (!providerSuccess) {
                 return;
@@ -762,6 +815,11 @@
                     // Log the chunk of parent host_log before sharding
                     reportHostLog(listener, config, TRADEFED_LOG_NAME + BEFORE_SHARDING_SUFFIX);
                     config.getLogSaver().invocationEnded(0L);
+                    if (aggregator != null) {
+                        // The host_log is not available yet to reporters that don't support
+                        // granular results, so forward it.
+                        aggregator.forwardAggregatedInvocationLogs();
+                    }
                     return;
                 }
             }
diff --git a/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
index bf2eb87..d79a552 100644
--- a/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
+++ b/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -15,6 +15,8 @@
  */
 package com.android.tradefed.invoker.logger;
 
+import com.android.tradefed.log.LogUtil.CLog;
+
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -24,19 +26,32 @@
 
     /** 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");
+        WIFI_AP_NAME("wifi_ap_name", false),
+        CLEARED_RUN_ERROR("cleared_run_error", false),
+        FETCH_BUILD("fetch_build_time_ms", true),
+        SETUP("setup_time_ms", true),
+        SHARDING_DEVICE_SETUP_TIME("remote_device_sharding_setup_ms", true),
+        AUTO_RETRY_TIME("auto_retry_time_ms", true),
+        STAGE_TESTS_TIME("stage_tests_time_ms", true),
+        STAGE_TESTS_BYTES("stage_tests_bytes", true);
 
         private final String mKeyName;
+        // Whether or not to add the value when the key is added again.
+        private final boolean mAdditive;
 
-        private InvocationMetricKey(String key) {
+        private InvocationMetricKey(String key, boolean additive) {
             mKeyName = key;
+            mAdditive = additive;
         }
 
         @Override
         public String toString() {
             return mKeyName;
         }
+
+        public boolean shouldAdd() {
+            return mAdditive;
+        }
     }
 
     private InvocationMetricLogger() {}
@@ -54,6 +69,30 @@
      * @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, long value) {
+        if (key.shouldAdd()) {
+            String existingVal = getInvocationMetrics().get(key.toString());
+            long existingLong = 0L;
+            if (existingVal != null) {
+                try {
+                    existingLong = Long.parseLong(existingVal);
+                } catch (NumberFormatException e) {
+                    CLog.e(
+                            "%s is expected to contain a number, instead found: %s",
+                            key.toString(), existingVal);
+                }
+            }
+            value += existingLong;
+        }
+        addInvocationMetrics(key.toString(), Long.toString(value));
+    }
+
+    /**
+     * 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);
     }
diff --git a/src/com/android/tradefed/invoker/shard/ShardHelper.java b/src/com/android/tradefed/invoker/shard/ShardHelper.java
index b0935ee..ab734ec 100644
--- a/src/com/android/tradefed/invoker/shard/ShardHelper.java
+++ b/src/com/android/tradefed/invoker/shard/ShardHelper.java
@@ -40,6 +40,7 @@
 import com.android.tradefed.testtype.IMultiDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.IShardableTest;
+import com.android.tradefed.testtype.retry.IRetryDecision;
 import com.android.tradefed.util.QuotationAwareTokenizer;
 import com.android.tradefed.util.keystore.IKeyStoreClient;
 import com.android.tradefed.util.keystore.KeyStoreException;
@@ -69,6 +70,8 @@
         CONFIG_OBJ_TO_CLONE.add(Configuration.LOGGER_TYPE_NAME);
         // Deep clone of log_saver to ensure each shard manages its own logs
         CONFIG_OBJ_TO_CLONE.add(Configuration.LOG_SAVER_TYPE_NAME);
+        // Deep clone RetryDecision to ensure each shard retry independently
+        CONFIG_OBJ_TO_CLONE.add(Configuration.RETRY_DECISION_TYPE_NAME);
     }
 
     /**
@@ -176,7 +179,7 @@
         ShardBuildCloner.cloneBuildInfos(config, shardConfig, context);
 
         shardConfig.setTestInvocationListeners(
-                buildShardListeners(resultCollector, config.getTestInvocationListeners()));
+                buildShardListeners(resultCollector, config, config.getTestInvocationListeners()));
 
         // Set the host_log suffix to avoid similar names
         String suffix = String.format("_shard_index_%s", index);
@@ -317,7 +320,9 @@
      * shard collector.
      */
     private static List<ITestInvocationListener> buildShardListeners(
-            ITestInvocationListener resultCollector, List<ITestInvocationListener> origListeners) {
+            ITestInvocationListener resultCollector,
+            IConfiguration config,
+            List<ITestInvocationListener> origListeners) {
         List<ITestInvocationListener> shardListeners = new ArrayList<ITestInvocationListener>();
         for (ITestInvocationListener l : origListeners) {
             if (l instanceof IShardableListener) {
@@ -325,10 +330,19 @@
             }
         }
         ShardListener origConfigListener = new ShardListener(resultCollector);
+        origConfigListener.setSupportGranularResults(isAutoRetryEnabled(config));
         shardListeners.add(origConfigListener);
         return shardListeners;
     }
 
+    private static boolean isAutoRetryEnabled(IConfiguration config) {
+        IRetryDecision decision = config.getRetryDecision();
+        if (decision.isAutoRetryEnabled() && decision.getMaxRetryCount() > 0) {
+            return true;
+        }
+        return false;
+    }
+
     private Collection<ITokenRequest> extractTokenTests(Collection<IRemoteTest> shardableTests) {
         List<ITokenRequest> tokenPool = new ArrayList<>();
         Iterator<IRemoteTest> itr = new ArrayList<>(shardableTests).iterator();
diff --git a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
index 85c0c28..b49b906 100644
--- a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
+++ b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
@@ -193,9 +193,6 @@
                 if (test instanceof IBuildReceiver) {
                     ((IBuildReceiver) test).setBuild(mBuildInfo);
                 }
-                if (test instanceof IConfigurationReceiver) {
-                    ((IConfigurationReceiver) test).setConfiguration(mConfig);
-                }
                 if (test instanceof IDeviceTest) {
                     ((IDeviceTest) test).setDevice(mDevice);
                 }
@@ -216,6 +213,11 @@
                     validationConfig.setTest(test);
                     validationConfig.validateOptions();
                     validationConfig.resolveDynamicOptions();
+                    // Set the configuration after the validation, otherwise we override the config
+                    // available to the test.
+                    if (test instanceof IConfigurationReceiver) {
+                        ((IConfigurationReceiver) test).setConfiguration(mConfig);
+                    }
                     // 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/ILeveledLogOutput.java b/src/com/android/tradefed/log/ILeveledLogOutput.java
index bed4c4d..31ee5a7 100644
--- a/src/com/android/tradefed/log/ILeveledLogOutput.java
+++ b/src/com/android/tradefed/log/ILeveledLogOutput.java
@@ -49,13 +49,13 @@
 
     /**
      * Grabs a snapshot stream of the log data.
-     * <p/>
-     * Must not be called after {@link ILeveledLogOutput#closeLog()}.
-     * <p/>
-     * The returned stream is not guaranteed to have optimal performance. Callers may wish to
+     *
+     * <p>Must not be called after {@link ILeveledLogOutput#closeLog()}.
+     *
+     * <p>The returned stream is not guaranteed to have optimal performance. Callers may wish to
      * wrap result in a {@link BufferedInputStream}.
      *
-     * @return a {@link InputStreamSource} of the log data
+     * @return a {@link InputStreamSource} of the log data. May return null if not supported.
      * @throws IllegalStateException if called when log has been closed.
      */
     public InputStreamSource getLog();
diff --git a/src/com/android/tradefed/log/LogRegistry.java b/src/com/android/tradefed/log/LogRegistry.java
index 2acb8eb..c96d25d 100644
--- a/src/com/android/tradefed/log/LogRegistry.java
+++ b/src/com/android/tradefed/log/LogRegistry.java
@@ -135,10 +135,13 @@
      */
     @Override
     public void dumpToGlobalLog(ILeveledLogOutput log) {
-        try (InputStreamSource source = log.getLog();
-                InputStream stream = source.createInputStream()) {
-            mGlobalLogger.dumpToLog(stream);
-        } catch (IOException e) {
+        try (InputStreamSource source = log.getLog()) {
+            if (source != null) {
+                try (InputStream stream = source.createInputStream()) {
+                    mGlobalLogger.dumpToLog(stream);
+                }
+            }
+        } catch (IOException | RuntimeException e) {
             System.err.println("Failed to dump log");
             e.printStackTrace();
         }
diff --git a/src/com/android/tradefed/log/StdoutLogger.java b/src/com/android/tradefed/log/StdoutLogger.java
index 50a68a1..d19dd44 100644
--- a/src/com/android/tradefed/log/StdoutLogger.java
+++ b/src/com/android/tradefed/log/StdoutLogger.java
@@ -19,7 +19,6 @@
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
 import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
 
 import java.io.IOException;
@@ -80,8 +79,8 @@
      */
     @Override
     public InputStreamSource getLog() {
-        // not supported - return empty stream
-        return new ByteArrayInputStreamSource(new byte[0]);
+        // Not supported - return null
+        return null;
     }
 
     @Override
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index aa8a4c2..5aaba96 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -21,7 +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.android.tradefed.retry.MergeStrategy;
 
 import com.google.common.annotations.VisibleForTesting;
 
diff --git a/src/com/android/tradefed/result/ConsoleResultReporter.java b/src/com/android/tradefed/result/ConsoleResultReporter.java
index 48fd727..082bbfd 100644
--- a/src/com/android/tradefed/result/ConsoleResultReporter.java
+++ b/src/com/android/tradefed/result/ConsoleResultReporter.java
@@ -23,9 +23,10 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.LinkedList;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Result reporter to print the test results to the console.
@@ -35,20 +36,26 @@
  */
 @OptionClass(alias = "console-result-reporter")
 public class ConsoleResultReporter extends CollectingTestListener implements ILogSaverListener {
-    private static final String LOG_TAG = ConsoleResultReporter.class.getSimpleName();
 
     @Option(name = "suppress-passed-tests", description = "For functional tests, ommit summary for "
             + "passing tests, only print failed and ignored ones")
     private boolean mSuppressPassedTest = false;
 
-    private List<LogFile> mLogFiles = new LinkedList<>();
+    private Set<LogFile> mLogFiles = new LinkedHashSet<>();
 
     /**
      * {@inheritDoc}
      */
     @Override
     public void invocationEnded(long elapsedTime) {
-        Log.logAndDisplay(LogLevel.INFO, LOG_TAG, getInvocationSummary());
+        Log.logAndDisplay(LogLevel.INFO, this.getClass().getSimpleName(), getInvocationSummary());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void logAssociation(String dataName, LogFile logFile) {
+        super.logAssociation(dataName, logFile);
+        mLogFiles.add(logFile);
     }
 
     /**
diff --git a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
index 41776e9..acddd3b 100644
--- a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
+++ b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
@@ -59,6 +59,7 @@
         Test test = new TestIdentifierResult(testId);
         // TODO: is it accurate to represent the trace as AssertionFailedError?
         mJUnitListener.addFailure(test, new AssertionFailedError(trace));
+        Log.i(LOG_TAG, String.format("Test %s failed with:\n %s", testId.toString(), trace));
     }
 
     @Override
@@ -82,7 +83,7 @@
     @Override
     public void testRunFailed(String errorMessage) {
         // TODO: no run failed method on TestListener - would be good to propagate this up
-        Log.e(LOG_TAG, String.format("run failed: %s", errorMessage));
+        Log.e(LOG_TAG, String.format("Run failed: %s", errorMessage));
     }
 
     /**
@@ -107,7 +108,7 @@
     /** {@inheritDoc} */
     @Override
     public void testStarted(TestDescription test) {
-        Log.d(LOG_TAG, test.toString());
+        Log.d(LOG_TAG, String.format("Starting test: %s", test.toString()));
         mJUnitListener.startTest(new TestIdentifierResult(test));
     }
 
diff --git a/src/com/android/tradefed/result/LogSaverResultForwarder.java b/src/com/android/tradefed/result/LogSaverResultForwarder.java
index fc937e7..2488ee3 100644
--- a/src/com/android/tradefed/result/LogSaverResultForwarder.java
+++ b/src/com/android/tradefed/result/LogSaverResultForwarder.java
@@ -20,6 +20,8 @@
 import com.android.tradefed.invoker.TestInvocation;
 import com.android.tradefed.log.LogRegistry;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.SystemUtil;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -69,9 +71,22 @@
 
     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);
+        try (InputStreamSource source = registry.getLogger().getLog()) {
+            if (source == null) {
+                CLog.e("%s stream was null, skip saving it.", TestInvocation.TRADEFED_END_HOST_LOG);
+                return;
+            }
+            try (InputStream stream = source.createInputStream()) {
+                saver.saveLogData(TestInvocation.TRADEFED_END_HOST_LOG, LogDataType.TEXT, stream);
+                if (SystemUtil.isRemoteEnvironment()) {
+                    // In remote environment, dump to the stdout so we can get the logs in the
+                    // console.
+                    System.out.println(
+                            String.format(
+                                    "===== Result Reporters =====\n%s",
+                                    StreamUtil.getStringFromStream(stream)));
+                }
+            }
         } catch (IOException e) {
             CLog.e(e);
         }
@@ -88,6 +103,10 @@
     public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
         testLogForward(dataName, dataType, dataStream);
         try {
+            if (dataStream == null) {
+                CLog.w("Skip forwarding of '%s', data stream is null.", dataName);
+                return;
+            }
             LogFile logFile = mLogSaver.saveLogData(dataName, dataType,
                     dataStream.createInputStream());
             for (ITestInvocationListener listener : getListeners()) {
diff --git a/src/com/android/tradefed/result/SubprocessResultsReporter.java b/src/com/android/tradefed/result/SubprocessResultsReporter.java
index 9f9075d..0a2c47a 100644
--- a/src/com/android/tradefed/result/SubprocessResultsReporter.java
+++ b/src/com/android/tradefed/result/SubprocessResultsReporter.java
@@ -18,6 +18,7 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.util.FileUtil;
@@ -47,6 +48,7 @@
 import java.net.Socket;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Implements {@link ITestInvocationListener} to be specified as a result_reporter and forward from
@@ -225,8 +227,10 @@
         if (mPrimaryBuildInfo == null) {
             return;
         }
-        InvocationEndedEventInfo eventEnd =
-                new InvocationEndedEventInfo(mPrimaryBuildInfo.getBuildAttributes());
+        Map<String, String> metrics = mPrimaryBuildInfo.getBuildAttributes();
+        // All the invocation level metrics collected
+        metrics.putAll(InvocationMetricLogger.getInvocationMetrics());
+        InvocationEndedEventInfo eventEnd = new InvocationEndedEventInfo(metrics);
         printEvent(SubprocessTestResultsParser.StatusKeys.INVOCATION_ENDED, eventEnd);
         // Upon invocation ended, trigger the end of the socket when the process finishes
         SocketFinisher thread = new SocketFinisher();
diff --git a/src/com/android/tradefed/result/proto/FileProtoResultReporter.java b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
index 0d2fe41..f75215b 100644
--- a/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
@@ -27,14 +27,18 @@
 /** Proto reporter that dumps the {@link TestRecord} into a file. */
 public class FileProtoResultReporter extends ProtoResultReporter {
 
+    public static final String PROTO_OUTPUT_FILE = "proto-output-file";
+
     @Option(
-        name = "proto-output-file",
+        name = PROTO_OUTPUT_FILE,
         description = "File where the proto output will be saved. If unset, reporter will be inop."
     )
     private File mOutputFile = null;
 
+    public static final String PERIODIC_PROTO_WRITING_OPTION = "periodic-proto-writing";
+
     @Option(
-        name = "periodic-proto-writing",
+        name = PERIODIC_PROTO_WRITING_OPTION,
         description =
                 "Whether or not to output intermediate proto per module following a numbered "
                         + "sequence."
diff --git a/src/com/android/tradefed/result/proto/ProtoResultParser.java b/src/com/android/tradefed/result/proto/ProtoResultParser.java
index c715a99..18f9bca 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultParser.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultParser.java
@@ -18,6 +18,8 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.invoker.proto.InvocationContext.Context;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
@@ -32,6 +34,7 @@
 import com.android.tradefed.result.proto.TestRecordProto.ChildReference;
 import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
 import com.android.tradefed.testtype.suite.ModuleDefinition;
+import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.proto.TestRecordProtoUtil;
 
 import com.google.common.base.Strings;
@@ -65,6 +68,7 @@
     private boolean mQuietParsing = true;
 
     private boolean mInvocationStarted = false;
+    private boolean mInvocationEnded = false;
 
     /** Ctor. */
     public ProtoResultParser(
@@ -163,6 +167,11 @@
         }
     }
 
+    /** Returns whether or not the parsing reached an invocation ended. */
+    public boolean invocationEndedReached() {
+        return mInvocationEnded;
+    }
+
     private void evalChildrenProto(List<ChildReference> children, boolean isInRun) {
         for (ChildReference child : children) {
             TestRecord childProto = child.getInlineTestRecord();
@@ -255,6 +264,7 @@
         }
 
         log("Invocation ended proto");
+        mInvocationEnded = true;
         if (!mReportInvocation) {
             CLog.d("Skipping invocation ended reporting.");
             return;
@@ -470,7 +480,28 @@
             return;
         }
         // Copy invocation attributes
-        receiverContext.addInvocationAttributes(endInvocationContext.getAttributes());
+        MultiMap<String, String> attributes = endInvocationContext.getAttributes();
+        for (InvocationMetricKey key : InvocationMetricKey.values()) {
+            if (!attributes.containsKey(key.toString())) {
+                continue;
+            }
+            List<String> values = attributes.get(key.toString());
+            attributes.remove(key.toString());
+
+            for (String val : values) {
+                if (key.shouldAdd()) {
+                    try {
+                        InvocationMetricLogger.addInvocationMetrics(key, Long.parseLong(val));
+                    } catch (NumberFormatException e) {
+                        CLog.e("Key %s should have a number value, instead was: %s", key, val);
+                        CLog.e(e);
+                    }
+                } else {
+                    InvocationMetricLogger.addInvocationMetrics(key, val);
+                }
+            }
+        }
+        receiverContext.addInvocationAttributes(attributes);
     }
 
     private void log(String format, Object... obj) {
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
index f415b4c..2cd7a42 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.result.proto;
 
+import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -28,6 +29,7 @@
 import com.android.tradefed.result.proto.TestRecordProto.DebugInfo;
 import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
 import com.android.tradefed.result.proto.TestRecordProto.TestStatus;
+import com.android.tradefed.result.retry.ISupportGranularResults;
 import com.android.tradefed.testtype.suite.ModuleDefinition;
 import com.android.tradefed.util.StreamUtil;
 
@@ -44,7 +46,15 @@
  * extended to handle what to do with the final proto in {@link #processFinalProto(TestRecord)}.
  */
 @OptionClass(alias = "proto-reporter")
-public abstract class ProtoResultReporter implements ITestInvocationListener, ILogSaverListener {
+public abstract class ProtoResultReporter
+        implements ITestInvocationListener, ILogSaverListener, ISupportGranularResults {
+
+    @Option(
+        name = "enable-granular-attempts",
+        description =
+                "Whether or not to allow this reporter receiving granular attempts. Feature flag."
+    )
+    private boolean mReportGranularResults = true;
 
     private Stack<TestRecord.Builder> mLatestChild;
     private TestRecord.Builder mInvocationRecordBuilder;
@@ -55,6 +65,11 @@
     /** Whether or not a testModuleStart had currently been called. */
     private boolean mModuleInProgress = false;
 
+    @Override
+    public boolean supportGranularResults() {
+        return mReportGranularResults;
+    }
+
     /**
      * Handling of the partial invocation test record proto after {@link
      * #invocationStarted(IInvocationContext)} occurred.
diff --git a/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java b/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java
index 16216dd..bedd4a4 100644
--- a/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java
+++ b/src/com/android/tradefed/result/suite/FormattedGeneratorReporter.java
@@ -47,7 +47,9 @@
     public void invocationFailed(Throwable cause) {
         // Some exception indicate a harness level issue, the tests result cannot be trusted at
         // that point so we should skip the reporting.
-        if (cause instanceof TargetSetupError || cause instanceof RuntimeException) {
+        if (cause instanceof TargetSetupError
+                || cause instanceof RuntimeException
+                || cause instanceof OutOfMemoryError) {
             mTestHarnessError = cause;
         }
         super.invocationFailed(cause);
diff --git a/src/com/android/tradefed/result/suite/SuiteResultReporter.java b/src/com/android/tradefed/result/suite/SuiteResultReporter.java
index 6fac2cc..5d5f74f 100644
--- a/src/com/android/tradefed/result/suite/SuiteResultReporter.java
+++ b/src/com/android/tradefed/result/suite/SuiteResultReporter.java
@@ -326,16 +326,17 @@
     }
 
     private void printModuleRetriesInformation() {
-        if (mModuleRetrySuccess.isEmpty() || mTotalRetrySuccess == 0L) {
+        if (mModuleRetrySuccess.isEmpty() || mTotalRetryTime == 0L) {
             return;
         }
         mSummary.append("============== Modules Retries Information ==============\n");
         for (String t : mModuleRetrySuccess.keySet()) {
             mSummary.append(
                     String.format(
-                            "    %s: Retry Success (Failed test became Pass) = %s\n"
-                                    + "        Retry Failure (Fail test stayed Fail)   = %s\n"
-                                    + "        Retry Time                              = %s\n",
+                            "    %s:\n"
+                                    + "        Retry Success (Failed test became Pass)   = %s\n"
+                                    + "        Retry Failure (Failed test stayed Failed) = %s\n"
+                                    + "        Retry Time                                = %s\n",
                             t,
                             mModuleRetrySuccess.get(t),
                             mModuleRetryFail.get(t),
@@ -345,8 +346,8 @@
         mSummary.append(
                 String.format(
                         "Total Retry Success (Failed test became Pass) = %s\n"
-                                + "Total Retry Failure (Fail test stayed Fail)   = %s\n"
-                                + "Total Retry Time                              = %s\n",
+                                + "Total Retry Failure (Failed test stayed Failed) = %s\n"
+                                + "Total Retry Time                                = %s\n",
                         mTotalRetrySuccess,
                         mTotalRetryFail,
                         TimeUtil.formatElapsedTime(mTotalRetryTime)));
diff --git a/src/com/android/tradefed/sandbox/TradefedSandbox.java b/src/com/android/tradefed/sandbox/TradefedSandbox.java
index eafa82c..a2dbc0b 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandbox.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandbox.java
@@ -323,6 +323,8 @@
                                 createClasspath(mRootFolder), mRunUtil, args, mode, mGlobalConfig);
             } catch (SandboxConfigurationException e) {
                 // TODO: Improve our detection of that scenario
+                CLog.e(e);
+                CLog.e("%s", args[0]);
                 if (e.getMessage().contains(String.format("Can not find local config %s", args[0]))
                         || e.getMessage()
                                 .contains(
@@ -431,11 +433,12 @@
 
     private File handleChildMissingConfig(String[] args) {
         IConfiguration parentConfig = null;
+        File tmpParentConfig = null;
+        PrintWriter pw = null;
         try {
+            tmpParentConfig = FileUtil.createTempFile("parent-config", ".xml", mSandboxTmpFolder);
+            pw = new PrintWriter(tmpParentConfig);
             parentConfig = ConfigurationFactory.getInstance().createConfigurationFromArgs(args);
-            File tmpParentConfig =
-                    FileUtil.createTempFile("parent-config", ".xml", mSandboxTmpFolder);
-            PrintWriter pw = new PrintWriter(tmpParentConfig);
             // Do not print deprecated options to avoid compatibility issues, and do not print
             // unchanged options.
             parentConfig.dumpXml(pw, new ArrayList<>(), false, false);
@@ -443,7 +446,10 @@
         } catch (ConfigurationException | IOException e) {
             CLog.e("Parent doesn't understand the command either:");
             CLog.e(e);
+            FileUtil.deleteFile(tmpParentConfig);
             return null;
+        } finally {
+            StreamUtil.close(pw);
         }
     }
 }
diff --git a/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorChecker.java b/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorChecker.java
index 839be17..c058d48 100644
--- a/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorChecker.java
+++ b/src/com/android/tradefed/suite/checker/SystemServerFileDescriptorChecker.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.suite.checker;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceProperties;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
@@ -25,7 +26,6 @@
 
     /** Process will fail to allocate beyond 1024, so heuristic considers 900 a bad state */
     private static final int MAX_EXPECTED_FDS = 900;
-    private static final String BUILD_TYPE_PROP = "ro.build.type";
     private static final String USER_BUILD = "user";
 
     private String mBuildType = null;
@@ -35,7 +35,7 @@
             throws DeviceNotAvailableException {
         if (mBuildType == null) {
             // build type not initialized yet, check on device
-            mBuildType = device.getProperty(BUILD_TYPE_PROP);
+            mBuildType = device.getProperty(DeviceProperties.BUILD_TYPE);
         }
         return new StatusCheckerResult(CheckStatus.SUCCESS);
     }
diff --git a/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java b/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java
index 87366e9..537626b 100644
--- a/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java
+++ b/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java
@@ -16,13 +16,14 @@
 package com.android.tradefed.suite.checker;
 
 import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
 import com.android.tradefed.util.ProcessInfo;
+import com.android.tradefed.util.StreamUtil;
 
-import java.util.Map;
 
 /**
  * Check if the pid of system_server has changed from before and after a module run. A new pid would
@@ -30,8 +31,14 @@
  */
 public class SystemServerStatusChecker implements ISystemStatusChecker {
 
+    @Option(
+        name = "disable-recovery-reboot",
+        description =
+                "If status checker is detected down (no process), attempt to reboot the device."
+    )
+    private boolean mShouldRecover = true;
+
     private ProcessInfo mSystemServerProcess;
-    private Long mModuleStartTime = null;
 
     /** {@inheritDoc} */
     @Override
@@ -40,15 +47,16 @@
         mSystemServerProcess = device.getProcessByName("system_server");
         StatusCheckerResult result = new StatusCheckerResult(CheckStatus.SUCCESS);
         if (mSystemServerProcess == null) {
+            if (mShouldRecover) {
+                device.reboot();
+            }
             String message = "No valid system_server process is found.";
             CLog.w(message);
             result.setStatus(CheckStatus.FAILED);
             result.setBugreportNeeded(true);
             result.setErrorMessage(message);
-            mModuleStartTime = null;
             return result;
         }
-        mModuleStartTime = getCurrentTime();
         return result;
     }
 
@@ -62,48 +70,21 @@
                             + "skipping system_server postExecutionCheck.");
             return new StatusCheckerResult(CheckStatus.SUCCESS);
         }
-        String message = null;
-        ProcessInfo currSystemServerProcess = device.getProcessByName("system_server");
-        if (currSystemServerProcess == null) {
-            message = "system_server is down";
-            CLog.w(message);
+        try {
+            if (!device.deviceSoftRestarted(mSystemServerProcess)) {
+                return new StatusCheckerResult(CheckStatus.SUCCESS);
+            }
+        } catch (RuntimeException e) {
+            CLog.w(StreamUtil.getStackTrace(e));
             StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
             result.setBugreportNeeded(true);
-            result.setErrorMessage(message);
+            result.setErrorMessage(StreamUtil.getStackTrace(e));
             return result;
         }
 
-        if (currSystemServerProcess.getPid() == mSystemServerProcess.getPid()
-                && currSystemServerProcess.getStartTime() == mSystemServerProcess.getStartTime()) {
-            return new StatusCheckerResult(CheckStatus.SUCCESS);
-        }
-        //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);
         result.setBugreportNeeded(true);
-        result.setErrorMessage(message);
+        result.setErrorMessage("The system-server crashed during test execution");
         return result;
     }
 
diff --git a/src/com/android/tradefed/targetprep/AbstractTargetCleaner.java b/src/com/android/tradefed/targetprep/AbstractTargetCleaner.java
deleted file mode 100644
index af1894c..0000000
--- a/src/com/android/tradefed/targetprep/AbstractTargetCleaner.java
+++ /dev/null
@@ -1,35 +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.device.ITestDevice;
-
-/**
- * An {@link ITargetCleaner} class with a stub {@link #setUp} method
- */
-public abstract class AbstractTargetCleaner implements ITargetCleaner {
-
-    /**
-     * Implementation is a no-op
-     *
-     * {@inheritDoc}
-     */
-    @Override
-    public void setUp(ITestDevice device, IBuildInfo buildInfo) {
-        return;
-    }
-}
diff --git a/src/com/android/tradefed/targetprep/BaseTargetPreparer.java b/src/com/android/tradefed/targetprep/BaseTargetPreparer.java
index b0d0ee4..ccb3bb7 100644
--- a/src/com/android/tradefed/targetprep/BaseTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/BaseTargetPreparer.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.targetprep;
 
 import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionCopier;
 
 /**
  * Base implementation class for {@link ITargetPreparer} that allows to control whether the object
@@ -23,11 +24,15 @@
  */
 public abstract class BaseTargetPreparer implements ITargetPreparer {
 
-    @Option(name = "disable", description = "disables the target preparer")
+    private static final String DISABLE_OPTION_NAME = "disable";
+
+    @Option(name = DISABLE_OPTION_NAME, description = "disables the target preparer")
     private boolean mDisable = false;
 
+    private static final String DISABLE_TEARDOWN_OPTION_NAME = "disable-tear-down";
+
     @Option(
-        name = "disable-tear-down",
+        name = DISABLE_TEARDOWN_OPTION_NAME,
         description = "disables the clean up step of a target cleaner"
     )
     private boolean mDisableTearDown = false;
@@ -48,11 +53,15 @@
     @Override
     public final void setDisable(boolean isDisabled) {
         mDisable = isDisabled;
+        // Update the option this way to mark it as modified.
+        OptionCopier.copyOptionsNoThrow(this, this, DISABLE_OPTION_NAME);
     }
 
     /** {@inheritDoc} */
     @Override
     public final void setDisableTearDown(boolean isDisabled) {
         mDisableTearDown = isDisabled;
+        // Update the option this way to mark it as modified.
+        OptionCopier.copyOptionsNoThrow(this, this, DISABLE_TEARDOWN_OPTION_NAME);
     }
 }
diff --git a/src/com/android/tradefed/targetprep/BuildInfoAttributePreparer.java b/src/com/android/tradefed/targetprep/BuildInfoAttributePreparer.java
deleted file mode 100644
index 307affc..0000000
--- a/src/com/android/tradefed/targetprep/BuildInfoAttributePreparer.java
+++ /dev/null
@@ -1,46 +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.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 java.util.HashMap;
-import java.util.Map;
-
-/** A {@link ITargetPreparer} that adds arbitrary attributes to the {@link IBuildInfo}. */
-@OptionClass(alias = "buildinfo-preparer")
-public class BuildInfoAttributePreparer extends BaseTargetPreparer {
-
-    @Option(name = "build-attribute", description = "build attributes to add")
-    private Map<String, String> mBuildAttributes = new HashMap<String, String>();
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
-            BuildError, DeviceNotAvailableException {
-        for (Map.Entry<String, String> attr : mBuildAttributes.entrySet()) {
-            String key = attr.getKey();
-            String value = attr.getValue();
-            buildInfo.addBuildAttribute(key, value);
-        }
-    }
-}
diff --git a/src/com/android/tradefed/targetprep/ConnectionChecker.java b/src/com/android/tradefed/targetprep/ConnectionChecker.java
deleted file mode 100644
index 7e50fc1..0000000
--- a/src/com/android/tradefed/targetprep/ConnectionChecker.java
+++ /dev/null
@@ -1,48 +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.targetprep;
-
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.util.RunUtil;
-
-/** Target preparer that waits until an ip address is asigned to any of the specified interfaces. */
-@OptionClass(alias = "connection-checker")
-public class ConnectionChecker extends BaseTargetPreparer {
-
-    @Option(name="max-wait", description="How long to wait for the device to connect, in seconds")
-    private long mTimeout = 600;
-
-    @Option(name="poll-interval", description="How often to poll the device, in seconds")
-    private long mInterval = 30;
-
-    @Override
-    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
-            BuildError, DeviceNotAvailableException {
-        long startTime = System.currentTimeMillis();
-        long timeout = mTimeout * 1000;
-        while (!device.checkConnectivity()) {
-            if (System.currentTimeMillis() - startTime > timeout) {
-                throw new TargetSetupError("Device did not connect to the network",
-                        device.getDeviceDescriptor());
-            }
-            RunUtil.getDefault().sleep(mInterval * 1000);
-        }
-    }
-}
diff --git a/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java b/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java
index e10ee76..fa667ef 100644
--- a/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java
+++ b/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.targetprep;
 
+import com.android.tradefed.build.BootstrapBuildProvider;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -27,7 +28,8 @@
  * <p>This is useful for testing devices with builds generated from an external source (e.g.
  * external partner devices)
  *
- * @see {@link DeviceBuildInfoInjector}, {@link BootstrapBuildProvider}
+ * @see DeviceBuildInfoInjector
+ * @see BootstrapBuildProvider
  */
 public class DeviceBuildInfoBootStrapper extends BaseTargetPreparer {
 
diff --git a/src/com/android/tradefed/targetprep/DeviceBuildInfoInjector.java b/src/com/android/tradefed/targetprep/DeviceBuildInfoInjector.java
index 6e570e9..99a5f12 100644
--- a/src/com/android/tradefed/targetprep/DeviceBuildInfoInjector.java
+++ b/src/com/android/tradefed/targetprep/DeviceBuildInfoInjector.java
@@ -20,6 +20,7 @@
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceProperties;
 import com.android.tradefed.device.ITestDevice;
 
 /**
@@ -67,8 +68,11 @@
             buildInfo.addBuildAttribute(DeviceBuildDescriptor.DEVICE_BUILD_FLAVOR,
                     mOverrideDeviceBuildFlavor);
         } else {
-            String buildFlavor = String.format("%s-%s", device.getProperty("ro.product.name"),
-                    device.getProperty("ro.build.type"));
+            String buildFlavor =
+                    String.format(
+                            "%s-%s",
+                            device.getProperty(DeviceProperties.PRODUCT),
+                            device.getProperty(DeviceProperties.BUILD_TYPE));
             buildInfo.addBuildAttribute(DeviceBuildDescriptor.DEVICE_BUILD_FLAVOR, buildFlavor);
         }
         buildInfo.addBuildAttribute(DeviceBuildDescriptor.DEVICE_DESC,
diff --git a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
index 24d72d1..b77668a 100644
--- a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
@@ -470,4 +470,12 @@
     void setShouldFlashRamdisk(boolean shouldFlashRamdisk) {
         mShouldFlashRamdisk = shouldFlashRamdisk;
     }
+
+    protected void setSkipPostFlashFlavorCheck(boolean skipPostFlashFlavorCheck) {
+        mSkipPostFlashFlavorCheck = skipPostFlashFlavorCheck;
+    }
+
+    protected void setSkipPostFlashBuildIdCheck(boolean skipPostFlashBuildIdCheck) {
+        mSkipPostFlashBuildIdCheck = skipPostFlashBuildIdCheck;
+    }
 }
diff --git a/src/com/android/tradefed/targetprep/DeviceSetup.java b/src/com/android/tradefed/targetprep/DeviceSetup.java
index 9ac68ef..cdfe09a 100644
--- a/src/com/android/tradefed/targetprep/DeviceSetup.java
+++ b/src/com/android/tradefed/targetprep/DeviceSetup.java
@@ -23,6 +23,8 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.StubDevice;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.util.BinaryState;
 import com.android.tradefed.util.MultiMap;
@@ -889,14 +891,19 @@
         }
 
         if (mWifiSsid != null && device.connectToWifiNetwork(mWifiSsid, mWifiPsk)) {
+            InvocationMetricLogger.addInvocationMetrics(
+                    InvocationMetricKey.WIFI_AP_NAME, mWifiSsid);
             return;
         }
         for (Map.Entry<String, String> ssidToPsk : mWifiSsidToPsk.entrySet()) {
             String psk = "".equals(ssidToPsk.getValue()) ? null : ssidToPsk.getValue();
             if (device.connectToWifiNetwork(ssidToPsk.getKey(), psk)) {
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.WIFI_AP_NAME, ssidToPsk.getKey());
                 return;
             }
         }
+
         // Error message does not acknowledge mWifiSsidToPsk for parity with existing monitoring.
         if (mWifiSsid != null || !mWifiSsidToPsk.isEmpty()) {
             throw new TargetSetupError(
diff --git a/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java b/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java
index c1a6f8f..185741c 100644
--- a/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java
+++ b/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java
@@ -416,11 +416,11 @@
 
     /**
      * Get the boot partition name for this device flasher.
-     * <p/>
-     * Defaults to 'hboot'. Subclasses should override if necessary.
+     *
+     * <p>Defaults to 'bootloader'. Subclasses should override if necessary.
      */
     protected String getBootPartitionName() {
-        return "hboot";
+        return "bootloader";
     }
 
     /**
diff --git a/src/com/android/tradefed/targetprep/FastbootUpdateBootstrapPreparer.java b/src/com/android/tradefed/targetprep/FastbootUpdateBootstrapPreparer.java
new file mode 100644
index 0000000..f6990d9
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/FastbootUpdateBootstrapPreparer.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.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.targetprep.IDeviceFlasher.UserDataFlashOption;
+import com.android.tradefed.util.BuildInfoUtil;
+
+import java.io.File;
+
+/**
+ * An {@link ITargetPreparer} that stages specified files (bootloader, radio, device image zip) into
+ * {@link IDeviceBuildInfo} to get devices flashed with {@link FastbootDeviceFlasher}, then injects
+ * post-boot device attributes into the build info for result reporting purposes.
+ *
+ * <p>This is useful for using <code>fastboot update</code> as device image update mechanism from
+ * externally sourced devices and builds, to fit into existing automation infrastructure.
+ */
+public class FastbootUpdateBootstrapPreparer extends DeviceFlashPreparer {
+
+    @Option(name = "bootloader-image", description = "bootloader image file to be used for update")
+    private File mBootloaderImage = null;
+
+    @Option(name = "baseband-image", description = "radio image file to be used for update")
+    private File mBasebandImage = null;
+
+    @Option(name = "device-image", description = "device image file to be used for update")
+    private File mDeviceImage = null;
+
+    @Option(
+        name = "bootstrap-build-info",
+        description =
+                "whether build info should be"
+                        + "bootstrapped based on device attributes after flashing"
+    )
+    private boolean mBootStrapBuildInfo = true;
+
+    // parameters below are the same as DeviceBuildInfoBootStrapper
+    @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 {
+        if (!(buildInfo instanceof IDeviceBuildInfo)) {
+            throw new IllegalArgumentException("Provided build info must be a IDeviceBuildInfo");
+        }
+        // forcing the wipe mechanism to WIPE because the FLASH* based options are not feasible here
+        setUserDataFlashOption(UserDataFlashOption.WIPE);
+        IDeviceBuildInfo deviceBuildInfo = (IDeviceBuildInfo) buildInfo;
+        deviceBuildInfo.setBootloaderImageFile(mBootloaderImage, "0");
+        deviceBuildInfo.setBasebandImage(mBasebandImage, "0");
+        deviceBuildInfo.setDeviceImageFile(mDeviceImage, "0");
+        setSkipPostFlashBuildIdCheck(true);
+        setSkipPostFlashFlavorCheck(true);
+        // performs the actual flashing
+        super.setUp(device, buildInfo);
+
+        if (mBootStrapBuildInfo) {
+            BuildInfoUtil.bootstrapDeviceBuildAttributes(
+                    buildInfo,
+                    device,
+                    mOverrideDeviceBuildId,
+                    mOverrideDeviceBuildFlavor,
+                    mOverrideDeviceBuildBranch,
+                    mOverrideDeviceBuildAlias);
+        }
+    }
+
+    @Override
+    protected IDeviceFlasher createFlasher(ITestDevice device) throws DeviceNotAvailableException {
+        return new FastbootDeviceFlasher();
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/FileCleaner.java b/src/com/android/tradefed/targetprep/FileCleaner.java
deleted file mode 100644
index 72eccd5..0000000
--- a/src/com/android/tradefed/targetprep/FileCleaner.java
+++ /dev/null
@@ -1,54 +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.targetprep;
-
-import com.android.tradefed.build.IBuildInfo;
-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.util.FileUtil;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collection;
-
-/** A {@link ITargetCleaner} that removes filesystem files on teardown */
-@OptionClass(alias = "file-cleaner")
-public class FileCleaner extends BaseTargetPreparer implements ITargetCleaner {
-
-    @Option(name = "apk-path", description =
-        "the filesystem path of the apk to cleanup. Can be repeated.",
-        importance = Importance.IF_UNSET)
-    private Collection<File> mApkPaths = new ArrayList<File>();
-
-    @Override
-    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
-            BuildError, DeviceNotAvailableException {
-        // ignore
-
-    }
-
-    @Override
-    public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
-            throws DeviceNotAvailableException {
-        for (File file : mApkPaths) {
-            FileUtil.deleteFile(file);
-        }
-    }
-}
diff --git a/src/com/android/tradefed/targetprep/PreloadedClassesPreparer.java b/src/com/android/tradefed/targetprep/PreloadedClassesPreparer.java
index 6f4f7ad..b14e00c 100644
--- a/src/com/android/tradefed/targetprep/PreloadedClassesPreparer.java
+++ b/src/com/android/tradefed/targetprep/PreloadedClassesPreparer.java
@@ -1,47 +1,17 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
 package com.android.tradefed.targetprep;
 
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
-import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
-import com.android.tradefed.util.IRunUtil;
-import com.android.tradefed.util.RunUtil;
-
-import com.google.common.annotations.VisibleForTesting;
 
 import java.io.File;
 
-/**
- * A {@link ITargetPreparer} that replaces the preloaded classes file on a device.
- *
- * <p>Note that this preparer requires a rooted, debug build to work.
- */
-@OptionClass(alias = "preloaded-classes-preparer")
+/** @deprecated Delete after July 29th week deployment */
+@Deprecated
 public class PreloadedClassesPreparer extends BaseTargetPreparer {
-    // Preload tool commands
-    private static final String TOOL_CMD = "java -cp %s com.android.preload.Main --seq %s %s";
-    private static final String WRITE_CMD = "write %s";
-    // Defaults
-    public static final long DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
+
+    private static final long DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
 
     @Option(
         name = "preload-file",
@@ -62,75 +32,9 @@
     )
     private long mWriteTimeout = DEFAULT_TIMEOUT_MS;
 
-    /** {@inheritDoc} */
     @Override
     public void setUp(ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, BuildError, DeviceNotAvailableException {
-        if (mSkip) {
-            return;
-        } else if (getPreloadedClassesPath().isEmpty()) {
-            CLog.w("No preloaded classes file specified. Skipping preparer.");
-            return;
-        }
-
-        // Download preload tool, if not supplied
-        if (getPreloadToolPath().isEmpty()) {
-            File preload = buildInfo.getFile("preload2.jar");
-            if (preload != null && preload.exists()) {
-                setPreloadToolPath(preload.getAbsolutePath());
-            } else {
-                throw new TargetSetupError(
-                        "Unable to find the preload tool.", device.getDeviceDescriptor());
-            }
-        }
-
-        // Root, disable verity, and remount
-        device.enableAdbRoot();
-        device.remountSystemWritable();
-        // Root again after rebooting
-        device.enableAdbRoot();
-        // Construct the command
-        String exportCmd = String.format(WRITE_CMD, getPreloadedClassesPath());
-        String[] fullCmd =
-                String.format(TOOL_CMD, getPreloadToolPath(), device.getSerialNumber(), exportCmd)
-                        .split(" ");
-        CommandResult result = getRunUtil().runTimedCmd(mWriteTimeout, fullCmd);
-        if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
-            throw new TargetSetupError(
-                    String.format("Error writing: %s", result.getStderr()),
-                    device.getDeviceDescriptor());
-        }
-        // Wait for the device to be reconnected
-        device.waitForDeviceAvailable();
-    }
-
-    /**
-     * Get the {@link IRunUtil} instance to use.
-     *
-     * <p>Exposed so unit tests can mock.
-     */
-    @VisibleForTesting
-    protected IRunUtil getRunUtil() {
-        return RunUtil.getDefault();
-    }
-
-    /**
-     * Get the preloaded classes file.
-     *
-     * <p>Exposed so unit tests can mock.
-     */
-    @VisibleForTesting
-    protected String getPreloadedClassesPath() {
-        return (mNewClassesFile != null) ? mNewClassesFile.getAbsolutePath() : "";
-    }
-
-    /** Get the preload tool path. */
-    protected String getPreloadToolPath() {
-        return mPreloadToolJarPath;
-    }
-
-    /** Set the preload tool path. */
-    protected void setPreloadToolPath(String path) {
-        mPreloadToolJarPath = path;
+        // Inop
     }
 }
diff --git a/src/com/android/tradefed/targetprep/README.md b/src/com/android/tradefed/targetprep/README.md
new file mode 100644
index 0000000..5543432
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/README.md
@@ -0,0 +1,9 @@
+# Core Trade Federation Target Preparers interface
+
+This folder contains the core interfaces that describes a target (device)
+preparer in Tradefed.
+It also contains some base implementation that are used for documentation
+and self validation use cases.
+
+More specialized implementation can be located at:
+platform/tools/tradefederation/core/test_framework/
diff --git a/src/com/android/tradefed/targetprep/TimeSetterTargetPreparer.java b/src/com/android/tradefed/targetprep/TimeSetterTargetPreparer.java
deleted file mode 100644
index bfccf8a..0000000
--- a/src/com/android/tradefed/targetprep/TimeSetterTargetPreparer.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.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 java.util.Date;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Target preparer to restore the correct time to the device on cleanup. This allows tests to modify
- * the time however they like since the original time will be restored, making sure the state of the
- * device isn't changed at the end.
- *
- * <p>Can also optionally set the time on setup. The time restored on cleanup will be the time set
- * before this target preparer ran.
- */
-@OptionClass(alias = "time-setter")
-public class TimeSetterTargetPreparer extends BaseTargetPreparer implements ITargetCleaner {
-    @Option(
-        name = "time",
-        description = "Time to set (epoch time in milliseconds).",
-        mandatory = true
-    )
-    private Long mTimeToSet;
-
-    private long mStartNanoTime, mDeviceStartTimeMillis;
-
-    // Exposed for testing.
-    long getNanoTime() {
-        return System.nanoTime();
-    }
-
-    private void setDeviceTime(ITestDevice device, long time) throws DeviceNotAvailableException {
-        device.setDate(new Date(time));
-    }
-
-    @Override
-    public void setUp(ITestDevice device, IBuildInfo buildInfo)
-            throws TargetSetupError, BuildError, DeviceNotAvailableException {
-        if (mTimeToSet == null) {
-            return;
-        }
-        mStartNanoTime = getNanoTime();
-        mDeviceStartTimeMillis = device.getDeviceDate();
-        setDeviceTime(device, mTimeToSet);
-    }
-
-    @Override
-    public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
-            throws DeviceNotAvailableException {
-        if (mTimeToSet == null) {
-            return;
-        }
-        long elapsedNanos = getNanoTime() - mStartNanoTime;
-        long newTime = mDeviceStartTimeMillis + TimeUnit.NANOSECONDS.toMillis(elapsedNanos);
-        setDeviceTime(device, newTime);
-    }
-}
diff --git a/src/com/android/tradefed/targetprep/multi/README.md b/src/com/android/tradefed/targetprep/multi/README.md
new file mode 100644
index 0000000..e1a46dc
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/multi/README.md
@@ -0,0 +1,9 @@
+# Core Trade Federation Multi Target Preparers interface
+
+This folder contains the core interfaces that describes a multi-target
+preparers in Tradefed.
+It also contains some base implementation that are used for documentation
+and self validation use cases.
+
+More specialized implementation can be located at:
+platform/tools/tradefederation/core/test_framework/
diff --git a/src/com/android/tradefed/testtype/README.md b/src/com/android/tradefed/testtype/README.md
new file mode 100644
index 0000000..741e21e
--- /dev/null
+++ b/src/com/android/tradefed/testtype/README.md
@@ -0,0 +1,8 @@
+# Core Trade Federation Tests Interfaces
+
+This folder contains the core interfaces that describes a test in TradeFed.
+It also contains some base implementation that are used automatically and
+for self validation tests.
+
+More specialized implementation can be located at:
+platform/tools/tradefederation/core/test_framework/
diff --git a/src/com/android/tradefed/testtype/StubTest.java b/src/com/android/tradefed/testtype/StubTest.java
index 05cfd78..9daf578 100644
--- a/src/com/android/tradefed/testtype/StubTest.java
+++ b/src/com/android/tradefed/testtype/StubTest.java
@@ -17,6 +17,8 @@
 package com.android.tradefed.testtype;
 
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
@@ -30,10 +32,8 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 
-/**
- * No-op empty test implementation.
- */
-public class StubTest implements IShardableTest {
+/** No-op empty test implementation. */
+public class StubTest implements IShardableTest, IConfigurationReceiver {
 
     public static final String DNAE_MESSAGE = "StubTest DeviceNotAvailableException";
 
@@ -73,6 +73,8 @@
     )
     private boolean mRunTest = false;
 
+    private IConfiguration mConfig;
+
     /**
      * {@inheritDoc}
      */
@@ -111,4 +113,13 @@
         }
         return null;
     }
+
+    @Override
+    public void setConfiguration(IConfiguration configuration) {
+        mConfig = configuration;
+    }
+
+    public IConfiguration getConfiguration() {
+        return mConfig;
+    }
 }
diff --git a/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java b/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java
index 862dd36..37607b6 100644
--- a/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java
+++ b/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java
@@ -15,15 +15,23 @@
  */
 package com.android.tradefed.testtype.retry;
 
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+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.result.TestDescription;
 import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.retry.RetryStrategy;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Base implementation of {@link IRetryDecision}. Base implementation only take local signals into
@@ -31,17 +39,67 @@
  */
 public class BaseRetryDecision implements IRetryDecision {
 
-    private RetryStrategy mRetryStrategy;
+    @Option(
+        name = "reboot-at-last-retry",
+        description = "Reboot the device at the last retry attempt."
+    )
+    private boolean mRebootAtLastRetry = false;
+
+    @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 mMaxRetryAttempts = 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;
+
+    private IInvocationContext mContext;
+
     private IRemoteTest mCurrentlyConsideredTest;
     private RetryStatsHelper mStatistics;
 
-    /** Constructor for the retry decision, always based on the {@link RetryStrategy}. */
-    public BaseRetryDecision(RetryStrategy strategy) {
-        mRetryStrategy = strategy;
+    /** Constructor for the retry decision */
+    public BaseRetryDecision() {}
+
+    @Override
+    public boolean isAutoRetryEnabled() {
+        return mEnableAutoRetry;
     }
 
     @Override
-    public boolean shouldRetry(IRemoteTest test, List<TestRunResult> previousResults) {
+    public RetryStrategy getRetryStrategy() {
+        return mRetryStrategy;
+    }
+
+    @Override
+    public int getMaxRetryCount() {
+        return mMaxRetryAttempts;
+    }
+
+    @Override
+    public void setInvocationContext(IInvocationContext context) {
+        mContext = context;
+    }
+
+    @Override
+    public boolean shouldRetry(
+            IRemoteTest test, int attemptJustExecuted, List<TestRunResult> previousResults)
+            throws DeviceNotAvailableException {
         // Keep track of some results for the test in progress for statistics purpose.
         if (test != mCurrentlyConsideredTest) {
             mCurrentlyConsideredTest = test;
@@ -74,7 +132,12 @@
         // 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);
+        boolean shouldRetry = handleRetryFailures(filterableTest, previousResults);
+        if (shouldRetry) {
+            // In case of retry, go through the recovery routine
+            recoverStateOfDevices(getDevices(), attemptJustExecuted);
+        }
+        return shouldRetry;
     }
 
     @Override
@@ -83,7 +146,10 @@
     }
 
     @Override
-    public RetryStatistics getRetryStats() {
+    public RetryStatistics getRetryStatistics() {
+        if (mStatistics == null) {
+            return new RetryStatsHelper().calculateStatistics();
+        }
         return mStatistics.calculateStatistics();
     }
 
@@ -146,4 +212,25 @@
             test.addIncludeFilter(filter);
         }
     }
+
+    /** Returns all the non-stub device associated with the {@link IRemoteTest}. */
+    private List<ITestDevice> getDevices() {
+        List<ITestDevice> listDevices = new ArrayList<>(mContext.getDevices());
+        // Return all the non-stub device (the one we can actually do some recovery against)
+        return listDevices
+                .stream()
+                .filter(d -> (!(d.getIDevice() instanceof StubDevice)))
+                .collect(Collectors.toList());
+    }
+
+    /** Recovery attempt on the device to get it a better state before next retry. */
+    private void recoverStateOfDevices(List<ITestDevice> devices, int lastAttempt)
+            throws DeviceNotAvailableException {
+        for (ITestDevice device : devices) {
+            if (mRebootAtLastRetry && (lastAttempt == (mMaxRetryAttempts - 2))) {
+                device.reboot();
+                continue;
+            }
+        }
+    }
 }
diff --git a/src/com/android/tradefed/testtype/retry/IRetryDecision.java b/src/com/android/tradefed/testtype/retry/IRetryDecision.java
index 808520f..a8cc116 100644
--- a/src/com/android/tradefed/testtype/retry/IRetryDecision.java
+++ b/src/com/android/tradefed/testtype/retry/IRetryDecision.java
@@ -15,7 +15,10 @@
  */
 package com.android.tradefed.testtype.retry;
 
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.retry.RetryStrategy;
 import com.android.tradefed.testtype.IRemoteTest;
 
 import java.util.List;
@@ -26,18 +29,34 @@
  */
 public interface IRetryDecision {
 
+    /** Whether or not to enable auto-retry. */
+    public boolean isAutoRetryEnabled();
+
+    /** The {@link RetryStrategy} used during auto-retry. */
+    public RetryStrategy getRetryStrategy();
+
+    /** The maximum number of attempts during auto-retry. */
+    public int getMaxRetryCount();
+
+    /** Set the current invocation context. */
+    public void setInvocationContext(IInvocationContext context);
+
     /**
      * 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 attemptJustExecuted The number of the attempt that we just ran.
      * @param previousResults The list of {@link TestRunResult} of the test that just ran.
      * @return True if we should retry, False otherwise.
+     * @throws DeviceNotAvailableException Can be thrown during device recovery
      */
-    public boolean shouldRetry(IRemoteTest test, List<TestRunResult> previousResults);
+    public boolean shouldRetry(
+            IRemoteTest test, int attemptJustExecuted, List<TestRunResult> previousResults)
+            throws DeviceNotAvailableException;
 
     /**
-     * {@link #shouldRetry(IRemoteTest, List)} will most likely be called before the last retry
+     * {@link #shouldRetry(IRemoteTest, int, List)} will most likely be called before the last retry
      * attempt, so we might be missing the very last attempt results for statistics purpose. This
      * method allows those results to be provided for proper statistics calculations.
      *
@@ -46,5 +65,5 @@
     public void addLastAttempt(List<TestRunResult> lastResults);
 
     /** Returns the {@link RetryStatistics} representing the retry. */
-    public RetryStatistics getRetryStats();
+    public RetryStatistics getRetryStatistics();
 }
diff --git a/src/com/android/tradefed/testtype/retry/ResultAggregator.java b/src/com/android/tradefed/testtype/retry/ResultAggregator.java
index b8ed0bc..5ba348b 100644
--- a/src/com/android/tradefed/testtype/retry/ResultAggregator.java
+++ b/src/com/android/tradefed/testtype/retry/ResultAggregator.java
@@ -16,6 +16,8 @@
 package com.android.tradefed.testtype.retry;
 
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.CollectingTestListener;
 import com.android.tradefed.result.ILogSaver;
@@ -29,6 +31,10 @@
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
 import com.android.tradefed.result.retry.ISupportGranularResults;
+import com.android.tradefed.retry.MergeStrategy;
+import com.android.tradefed.retry.RetryStrategy;
+
+import com.google.api.client.repackaged.com.google.common.base.Joiner;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -56,6 +62,10 @@
     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<>();
+    //
+    private TestRunResult mDetailedRunResults = null;
+    private boolean mShouldReportFailure = true;
+    private List<String> mAllDetailedFailures = new ArrayList<>();
 
     public ResultAggregator(List<ITestInvocationListener> listeners, RetryStrategy strategy) {
         mAllForwarder = new ResultAndLogForwarder(listeners);
@@ -108,10 +118,23 @@
             forwardTestRunResults(mPureRunResults, mAggregatedForwarder);
             mPureRunResults.clear();
         }
+        forwardDetailedFailure();
         super.invocationEnded(elapsedTime);
+        // Make sure to forward the logs for the invocation.
+        forwardAggregatedInvocationLogs();
         mAllForwarder.invocationEnded(elapsedTime);
     }
 
+    /**
+     * Forward all the invocation level logs to the result reporters that don't support the granular
+     * results.
+     */
+    public final void forwardAggregatedInvocationLogs() {
+        for (Entry<String, LogFile> invocLog : getNonAssociatedLogFiles().entrySet()) {
+            mAggregatedForwarder.logAssociation(invocLog.getKey(), invocLog.getValue());
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public void testModuleStarted(IInvocationContext moduleContext) {
@@ -120,6 +143,11 @@
             mPureRunResults.clear();
         }
 
+        if (mDetailedRunResults != null) {
+            mShouldReportFailure = true;
+            forwardDetailedFailure();
+        }
+
         mModuleInProgress = true;
         super.testModuleStarted(moduleContext);
         mAllForwarder.testModuleStarted(moduleContext);
@@ -140,6 +168,23 @@
             forwardTestRunResults(mPureRunResults, mAggregatedForwarder);
             mPureRunResults.clear();
         }
+
+        if (mDetailedRunResults != null) {
+            if (mDetailedRunResults.getName().equals(name)) {
+                if (!mDetailedRunResults.isRunFailure()) {
+                    if (RetryStrategy.RETRY_ANY_FAILURE.equals(mRetryStrategy)) {
+                        mShouldReportFailure = false;
+                    }
+                }
+                mDetailedForwarder.testRunEnded(
+                        mDetailedRunResults.getElapsedTime(),
+                        mDetailedRunResults.getRunProtoMetrics());
+                mDetailedRunResults = null;
+            } else {
+                mShouldReportFailure = true;
+                forwardDetailedFailure();
+            }
+        }
         super.testRunStarted(name, testCount, attemptNumber, startTime);
         mDetailedForwarder.testRunStarted(name, testCount, attemptNumber, startTime);
     }
@@ -147,7 +192,7 @@
     @Override
     public void testRunFailed(String errorMessage) {
         super.testRunFailed(errorMessage);
-        mDetailedForwarder.testRunFailed(errorMessage);
+        // Don't forward here to the detailed forwarder in case we need to clear it.
     }
 
     @Override
@@ -198,7 +243,10 @@
     @Override
     public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
         super.testRunEnded(elapsedTime, runMetrics);
-        mDetailedForwarder.testRunEnded(elapsedTime, runMetrics);
+        mDetailedRunResults = getCurrentRunResults();
+        if (mDetailedRunResults.isRunFailure()) {
+            mAllDetailedFailures.add(mDetailedRunResults.getRunFailureMessage());
+        }
 
         // If we are not a module and we reach here. This allows to support non-suite scenarios
         if (!mModuleInProgress) {
@@ -209,6 +257,8 @@
 
     @Override
     public void testModuleEnded() {
+        forwardDetailedFailure();
+
         mModuleInProgress = false;
         super.testModuleEnded();
         // We still forward the testModuleEnd to the detailed reporters
@@ -307,4 +357,27 @@
         // Ensure we don't keep track of the results we just forwarded
         clearResultsForName(result.getName());
     }
+
+    private void forwardDetailedFailure() {
+        if (mDetailedRunResults != null) {
+            if (mDetailedRunResults.isRunFailure() && mShouldReportFailure) {
+                mDetailedForwarder.testRunFailed(Joiner.on("\n\n").join(mAllDetailedFailures));
+            } else {
+                // Log the run failure that was cleared
+                String value =
+                        InvocationMetricLogger.getInvocationMetrics()
+                                .get(InvocationMetricKey.CLEARED_RUN_ERROR.toString());
+                if (value != null) {
+                    mAllDetailedFailures.add(0, value);
+                }
+                InvocationMetricLogger.addInvocationMetrics(
+                        InvocationMetricKey.CLEARED_RUN_ERROR,
+                        Joiner.on("\n\n").join(mAllDetailedFailures));
+            }
+            mAllDetailedFailures.clear();
+            mDetailedForwarder.testRunEnded(
+                    mDetailedRunResults.getElapsedTime(), mDetailedRunResults.getRunProtoMetrics());
+            mDetailedRunResults = null;
+        }
+    }
 }
diff --git a/src/com/android/tradefed/testtype/retry/RetryStatistics.java b/src/com/android/tradefed/testtype/retry/RetryStatistics.java
index 4417928..f4f5e99 100644
--- a/src/com/android/tradefed/testtype/retry/RetryStatistics.java
+++ b/src/com/android/tradefed/testtype/retry/RetryStatistics.java
@@ -15,6 +15,7 @@
  */
 package com.android.tradefed.testtype.retry;
 
+import com.android.tradefed.retry.RetryStrategy;
 import com.android.tradefed.testtype.IRemoteTest;
 
 import java.util.List;
diff --git a/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java b/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java
index c9748ce..ddb55dc 100644
--- a/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java
+++ b/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java
@@ -25,7 +25,7 @@
 import java.util.Set;
 
 /** Calculate the retry statistics and metrics based on attempts comparison. */
-public class RetryStatsHelper {
+final class RetryStatsHelper {
 
     private List<List<TestRunResult>> mResults = new ArrayList<>();
     private RetryStatistics mStats = new RetryStatistics();
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index 9c01b43..b283546 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -27,7 +27,10 @@
 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.IModuleParameter;
 import com.android.tradefed.testtype.suite.params.ModuleParameters;
+import com.android.tradefed.testtype.suite.params.ModuleParametersHelper;
+import com.android.tradefed.testtype.suite.params.NegativeHandler;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.FileUtil;
 
@@ -51,6 +54,7 @@
     public static final String INCLUDE_FILTER_OPTION = "include-filter";
     public static final String EXCLUDE_FILTER_OPTION = "exclude-filter";
     public static final String MODULE_OPTION = "module";
+    public static final char MODULE_OPTION_SHORT_NAME = 'm';
     public static final String TEST_ARG_OPTION = "test-arg";
     public static final String TEST_OPTION = "test";
     public static final char TEST_OPTION_SHORT_NAME = 't';
@@ -74,7 +78,7 @@
 
     @Option(
         name = MODULE_OPTION,
-        shortName = 'm',
+        shortName = MODULE_OPTION_SHORT_NAME,
         description = "the test module to run. Only works for configuration in the tests dir.",
         importance = Importance.IF_UNSET
     )
@@ -247,6 +251,16 @@
             mModuleRepo =
                     createModuleLoader(
                             mIncludeFiltersParsed, mExcludeFiltersParsed, mTestArgs, mModuleArgs);
+            if (mForceParameter != null && !mEnableParameter) {
+                throw new IllegalArgumentException(
+                        "'module-parameter' option was specified without "
+                                + "'enable-optional-parameterization'");
+            }
+            if (mEnableOptionalParameter && !mEnableParameter) {
+                throw new IllegalArgumentException(
+                        "'enable-optional-parameterization' option was specified without "
+                                + "'enable-parameterized-modules'");
+            }
             mModuleRepo.setParameterizedModules(mEnableParameter);
             mModuleRepo.setOptionalParameterizedModules(mEnableOptionalParameter);
             mModuleRepo.setModuleParameter(mForceParameter);
@@ -376,43 +390,65 @@
      * @throws FileNotFoundException if any file is not found.
      */
     protected void setupFilters(File testsDir) throws FileNotFoundException {
-        if (mModuleName != null) {
-            // If this option (-m / --module) is set only the matching unique module should run.
-            Set<File> modules =
-                    SuiteModuleLoader.getModuleNamesMatching(
-                            testsDir, mSuitePrefix, String.format(".*%s.*.config", mModuleName));
-            // If multiple modules match, do exact match.
-            if (modules.size() > 1) {
-                Set<File> newModules = new HashSet<>();
-                String exactModuleName = String.format("%s.config", mModuleName);
-                for (File module : modules) {
-                    if (module.getName().equals(exactModuleName)) {
-                        newModules.add(module);
-                        modules = newModules;
-                        break;
-                    }
+        if (mModuleName == null) {
+            if (mTestName != null) {
+                throw new IllegalArgumentException(
+                        "Test name given without module name. Add --module <module-name>");
+            }
+            return;
+        }
+        // If this option (-m / --module) is set only the matching unique module should run.
+        Set<File> modules =
+                SuiteModuleLoader.getModuleNamesMatching(
+                        testsDir, mSuitePrefix, String.format(".*%s.*.config", mModuleName));
+        // If multiple modules match, do exact match.
+        if (modules.size() > 1) {
+            Set<File> newModules = new HashSet<>();
+            String exactModuleName = String.format("%s.config", mModuleName);
+            for (File module : modules) {
+                if (module.getName().equals(exactModuleName)) {
+                    newModules.add(module);
+                    modules = newModules;
+                    break;
                 }
             }
-            if (modules.size() == 0) {
-                throw new IllegalArgumentException(
-                        String.format("No modules found matching %s", mModuleName));
-            } else if (modules.size() > 1) {
-                throw new IllegalArgumentException(
-                        String.format(
-                                "Multiple modules found matching %s:\n%s\nWhich one did you "
-                                        + "mean?\n",
-                                mModuleName, ArrayUtil.join("\n", modules)));
-            } else {
-                File mod = modules.iterator().next();
-                String moduleName = mod.getName().replace(".config", "");
-                checkFilters(mIncludeFilters, moduleName);
-                checkFilters(mExcludeFilters, moduleName);
-                mIncludeFilters.add(
-                        new SuiteTestFilter(getRequestedAbi(), moduleName, mTestName).toString());
-            }
-        } else if (mTestName != null) {
+        }
+        if (modules.size() == 0) {
             throw new IllegalArgumentException(
-                    "Test name given without module name. Add --module <module-name>");
+                    String.format("No modules found matching %s", mModuleName));
+        } else if (modules.size() > 1) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "Multiple modules found matching %s:\n%s\nWhich one did you "
+                                    + "mean?\n",
+                            mModuleName, ArrayUtil.join("\n", modules)));
+        } else {
+            File mod = modules.iterator().next();
+            String moduleName = mod.getName().replace(".config", "");
+            checkFilters(mIncludeFilters, moduleName);
+            checkFilters(mExcludeFilters, moduleName);
+            mIncludeFilters.add(
+                    new SuiteTestFilter(getRequestedAbi(), moduleName, mTestName).toString());
+            // Create the matching filters for the parameterized version of it if needed.
+            if (mEnableParameter) {
+                for (ModuleParameters param : ModuleParameters.values()) {
+                    IModuleParameter moduleParam =
+                            ModuleParametersHelper.getParameterHandler(
+                                    param, mEnableOptionalParameter);
+                    if (moduleParam == null) {
+                        continue;
+                    }
+                    if (moduleParam instanceof NegativeHandler) {
+                        continue;
+                    }
+                    String paramModuleName =
+                            String.format(
+                                    "%s[%s]", moduleName, moduleParam.getParameterIdentifier());
+                    mIncludeFilters.add(
+                            new SuiteTestFilter(getRequestedAbi(), paramModuleName, mTestName)
+                                    .toString());
+                }
+            }
         }
     }
 
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index 172ac97..777192b 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -19,25 +19,23 @@
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.device.StubDevice;
 import com.android.tradefed.device.metric.CollectorHelper;
 import com.android.tradefed.device.metric.IMetricCollector;
 import com.android.tradefed.device.metric.IMetricCollectorReceiver;
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ILogSaver;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogSaverResultForwarder;
 import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.retry.MergeStrategy;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestCollector;
 import com.android.tradefed.testtype.ITestFilterReceiver;
-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;
@@ -70,6 +68,7 @@
  */
 public class GranularRetriableTestWrapper implements IRemoteTest, ITestCollector {
 
+    private IRetryDecision mRetryDecision;
     private IRemoteTest mTest;
     private List<IMetricCollector> mRunMetricCollectors;
     private TestFailureListener mFailureListener;
@@ -87,9 +86,6 @@
     // Tracking of the metrics
     private RetryStatistics mRetryStats = null;
 
-    private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
-    private boolean mRebootAtLastRetry = false;
-
     public GranularRetriableTestWrapper(
             IRemoteTest test,
             ITestInvocationListener mainListener,
@@ -103,6 +99,11 @@
         mMaxRunLimit = maxRunLimit;
     }
 
+    /** Sets the {@link IRetryDecision} to be used. */
+    public void setRetryDecision(IRetryDecision decision) {
+        mRetryDecision = decision;
+    }
+
     /**
      * Set the {@link ModuleDefinition} name as a {@link GranularRetriableTestWrapper} attribute.
      *
@@ -161,16 +162,6 @@
         mLogSaver = logSaver;
     }
 
-    /** Sets the {@link RetryStrategy} to be used when retrying. */
-    public final void setRetryStrategy(RetryStrategy retryStrategy) {
-        mRetryStrategy = retryStrategy;
-    }
-
-    /** Sets the flag to reboot devices at the last intra-module retry. */
-    public final void setRebootAtLastRetry(boolean rebootAtLastRetry) {
-        mRebootAtLastRetry = rebootAtLastRetry;
-    }
-
     /**
      * Initialize a new {@link ModuleListener} for each test run.
      *
@@ -225,9 +216,14 @@
             return;
         }
 
+        if (mRetryDecision == null) {
+            CLog.e("RetryDecision is null. Something is misconfigured this shouldn't happen");
+            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))) {
+        if (!mRetryDecision.shouldRetry(
+                mTest, 0, mMainGranularRunListener.getTestRunForAttempts(0))) {
             return;
         }
 
@@ -237,35 +233,40 @@
             CLog.d("Starting intra-module retry.");
             for (int attemptNumber = 1; attemptNumber < mMaxRunLimit; attemptNumber++) {
                 boolean retry =
-                        retryDecision.shouldRetry(
+                        mRetryDecision.shouldRetry(
                                 mTest,
+                                attemptNumber - 1,
                                 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))) {
-                    for (ITestDevice device : mModuleInvocationContext.getDevices()) {
-                        if (!(device.getIDevice() instanceof StubDevice)) {
-                            CLog.i("Rebooting device: %s at the last intra-module retry.",
-                                    device.getSerialNumber());
-                            device.reboot();
-                        }
-                    }
-                }
+                CLog.d("Intra-module retry attempt number %s", attemptNumber);
                 // Run the tests again
                 intraModuleRun(allListeners);
             }
             // Feed the last attempt if we reached here.
-            retryDecision.addLastAttempt(
+            mRetryDecision.addLastAttempt(
                     mMainGranularRunListener.getTestRunForAttempts(mMaxRunLimit - 1));
         } finally {
-            mRetryStats = retryDecision.getRetryStats();
+            mRetryStats = mRetryDecision.getRetryStatistics();
             // Track how long we spend in retry
             mRetryStats.mRetryTime = System.currentTimeMillis() - startTime;
+            addRetryTime(mRetryStats.mRetryTime);
         }
     }
 
+    private void addRetryTime(long retryTimeMs) {
+        long totalRetryMs = retryTimeMs;
+        String retryTime =
+                InvocationMetricLogger.getInvocationMetrics()
+                        .get(InvocationMetricKey.AUTO_RETRY_TIME.toString());
+        if (retryTime != null) {
+            totalRetryMs += Long.parseLong(retryTime) + retryTimeMs;
+        }
+        InvocationMetricLogger.addInvocationMetrics(
+                InvocationMetricKey.AUTO_RETRY_TIME, Long.toString(totalRetryMs));
+    }
+
     /** The workflow for each individual {@link IRemoteTest} run. */
     private final void intraModuleRun(ITestInvocationListener runListener)
             throws DeviceNotAvailableException {
@@ -315,7 +316,7 @@
 
     /** Get the merged TestRunResults from each {@link IRemoteTest} run. */
     public final List<TestRunResult> getFinalTestRunResults() {
-        MergeStrategy strategy = MergeStrategy.getMergeStrategy(mRetryStrategy);
+        MergeStrategy strategy = MergeStrategy.getMergeStrategy(mRetryDecision.getRetryStrategy());
         mMainGranularRunListener.setMergeStrategy(strategy);
         return mMainGranularRunListener.getMergedTestRunResults();
     }
@@ -342,14 +343,6 @@
         return mMainGranularRunListener.getExpectedTests();
     }
 
-    /**
-     * 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. */
     public ModuleListener getResultListener() {
         return mMainGranularRunListener;
diff --git a/src/com/android/tradefed/testtype/suite/ITestSuite.java b/src/com/android/tradefed/testtype/suite/ITestSuite.java
index 370d6e5..fc71352 100644
--- a/src/com/android/tradefed/testtype/suite/ITestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/ITestSuite.java
@@ -28,6 +28,7 @@
 import com.android.tradefed.config.Option.Importance;
 import com.android.tradefed.config.OptionCopier;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceProperties;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.NullDevice;
 import com.android.tradefed.device.StubDevice;
@@ -36,6 +37,8 @@
 import com.android.tradefed.device.metric.IMetricCollector;
 import com.android.tradefed.device.metric.IMetricCollectorReceiver;
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.invoker.shard.token.ITokenRequest;
 import com.android.tradefed.invoker.shard.token.TokenProperty;
 import com.android.tradefed.log.ITestLogger;
@@ -44,6 +47,7 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.ITestLoggerReceiver;
 import com.android.tradefed.result.ResultForwarder;
+import com.android.tradefed.retry.RetryStrategy;
 import com.android.tradefed.suite.checker.ISystemStatusChecker;
 import com.android.tradefed.suite.checker.ISystemStatusCheckerReceiver;
 import com.android.tradefed.suite.checker.StatusCheckerResult;
@@ -60,7 +64,6 @@
 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;
@@ -164,11 +167,6 @@
     @Option(name = "reboot-per-module", description = "Reboot the device before every module run.")
     private boolean mRebootPerModule = false;
 
-    @Deprecated
-    @Option(name = "reboot-at-last-retry",
-        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."
@@ -461,6 +459,8 @@
             }
         }
         long elapsedTime = System.currentTimeMillis() - startTime;
+        InvocationMetricLogger.addInvocationMetrics(
+                InvocationMetricKey.STAGE_TESTS_TIME, elapsedTime);
         CLog.i(
                 String.format(
                         "Staging test artifacts for %d modules finished in %s.",
@@ -714,7 +714,7 @@
             TestFailureListener failureListener)
             throws DeviceNotAvailableException {
         if (mRebootPerModule) {
-            if ("user".equals(mDevice.getProperty("ro.build.type"))) {
+            if ("user".equals(mDevice.getProperty(DeviceProperties.BUILD_TYPE))) {
                 CLog.e(
                         "reboot-per-module should only be used during development, "
                                 + "this is a\" user\" build device");
@@ -735,17 +735,16 @@
         // Pass the main invocation logSaver
         module.setLogSaver(mMainConfiguration.getLogSaver());
         // Pass the retry strategy to the module
-        module.setRetryStrategy(
-                getConfiguration().getCommandOptions().getRetryStrategy(), mMergeAttempts);
-        // Pass the reboot strategy at the last intra-module retry to the module
-        module.setRebootAtLastRetry(mRebootAtLastRetry);
+        module.setMergeAttemps(mMergeAttempts);
+        // Pass the retry decision to be used.
+        module.setRetryDecision(mMainConfiguration.getRetryDecision());
 
         // Actually run the module
         module.run(
                 listener,
                 moduleListeners,
                 failureListener,
-                getConfiguration().getCommandOptions().getMaxRetryCount());
+                getConfiguration().getRetryDecision().getMaxRetryCount());
 
         if (!mSkipAllSystemStatusCheck) {
             runPostModuleCheck(module.getId(), mSystemStatusCheckers, mDevice, listener);
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index 665ec6f..7f3b5dc 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -56,8 +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.retry.IRetryDecision;
 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;
@@ -136,9 +136,8 @@
     // Tracking of retry performance
     private List<RetryStatistics> mRetryStats = new ArrayList<>();
 
-    private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
     private boolean mMergeAttempts = true;
-    private boolean mRebootAtLastRetry = false;
+    private IRetryDecision mRetryDecision;
 
     // Token during sharding
     private Set<TokenProperty> mRequiredTokens = new HashSet<>();
@@ -486,14 +485,16 @@
 
                     mExpectedTests += retriableTest.getExpectedTestsCount();
                     // Get information about retry
-                    RetryStatistics res = retriableTest.getRetryStatistics();
-                    if (res != null) {
-                        mRetryStats.add(res);
+                    if (mRetryDecision != null) {
+                        RetryStatistics res = mRetryDecision.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.getResultListener().hasFailed()) {
+                if (retriableTest.getResultListener().hasLastAttemptFailed()) {
                     captureBugreport(listener, getId());
                 }
             }
@@ -584,8 +585,7 @@
         retriableTest.setModuleConfig(mModuleConfiguration);
         retriableTest.setInvocationContext(mModuleInvocationContext);
         retriableTest.setLogSaver(mLogSaver);
-        retriableTest.setRetryStrategy(mRetryStrategy);
-        retriableTest.setRebootAtLastRetry(mRebootAtLastRetry);
+        retriableTest.setRetryDecision(mRetryDecision);
         return retriableTest;
     }
 
@@ -866,15 +866,14 @@
         mCollectTestsOnly = collectTestsOnly;
     }
 
-    /** Sets the {@link RetryStrategy} to be used when retrying. */
-    public final void setRetryStrategy(RetryStrategy retryStrategy, boolean mergeAttempts) {
-        mRetryStrategy = retryStrategy;
+    /** Sets whether or not we should merge results. */
+    public final void setMergeAttemps(boolean mergeAttempts) {
         mMergeAttempts = mergeAttempts;
     }
 
-    /** Sets the flag to reboot devices at the last intra-module retry. */
-    public final void setRebootAtLastRetry(boolean rebootAtLastRetry) {
-        mRebootAtLastRetry = rebootAtLastRetry;
+    /** Sets the {@link IRetryDecision} to be used for intra-module retry. */
+    public final void setRetryDecision(IRetryDecision decision) {
+        mRetryDecision = decision;
     }
 
     /** Returns a list of tests that ran in this module. */
diff --git a/src/com/android/tradefed/testtype/suite/ModuleListener.java b/src/com/android/tradefed/testtype/suite/ModuleListener.java
index 86057d2..3dad444 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleListener.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleListener.java
@@ -40,7 +40,6 @@
     private boolean mTestFailed = false;
     private int mTestsRan = 1;
     private ITestInvocationListener mMainListener;
-    private boolean mHasFailed = false;
 
     private boolean mCollectTestsOnly = false;
     /** Track runs in progress for logging purpose */
@@ -82,7 +81,6 @@
     /** {@inheritDoc} */
     @Override
     public void testRunFailed(String errorMessage) {
-        mHasFailed = true;
         CLog.d("ModuleListener.testRunFailed(%s)", errorMessage);
         super.testRunFailed(errorMessage);
     }
@@ -94,9 +92,9 @@
         mRunInProgress = false;
     }
 
-    /** Returns whether or not the listener session has failed. */
-    public boolean hasFailed() {
-        return mHasFailed;
+    /** Returns whether or not the listener last retry session has failed. */
+    public boolean hasLastAttemptFailed() {
+        return getCurrentRunResults().isRunFailure();
     }
 
     /** {@inheritDoc} */
diff --git a/src/com/android/tradefed/testtype/suite/TestFailureListener.java b/src/com/android/tradefed/testtype/suite/TestFailureListener.java
index df099d3..e38b2f6 100644
--- a/src/com/android/tradefed/testtype/suite/TestFailureListener.java
+++ b/src/com/android/tradefed/testtype/suite/TestFailureListener.java
@@ -16,6 +16,7 @@
 package com.android.tradefed.testtype.suite;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceProperties;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -74,7 +75,7 @@
             try {
                 // Rebooting on all failures can hide legitimate issues and platform instabilities,
                 // therefore only allowed on "user-debug" and "eng" builds.
-                if ("user".equals(device.getProperty("ro.build.type"))) {
+                if ("user".equals(device.getProperty(DeviceProperties.BUILD_TYPE))) {
                     CLog.e("Reboot-on-failure should only be used during development," +
                             " this is a\" user\" build device");
                 } else {
diff --git a/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java b/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
index cdaaca4..4b01ecc 100644
--- a/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
+++ b/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
@@ -15,13 +15,19 @@
  */
 package com.android.tradefed.testtype.suite.params;
 
+import android.platform.test.annotations.SystemUserOnly;
+
 import com.android.tradefed.config.IConfiguration;
 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 com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.ITestAnnotationFilterReceiver;
 
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /** Handler for {@link ModuleParameters#SECONDARY_USER}. */
 public class SecondaryUserHandler implements IModuleParameter {
@@ -40,6 +46,21 @@
             // Add a preparer to setup the location settings on the new user
             preparers.add(1, createLocationPreparer());
         }
+
+        // Add filter to exclude @SystemUserOnly
+        for (IRemoteTest test : moduleConfiguration.getTests()) {
+            if (test instanceof ITestAnnotationFilterReceiver) {
+                ITestAnnotationFilterReceiver filterTest = (ITestAnnotationFilterReceiver) test;
+                // Retrieve the current set of excludeAnnotations to maintain for after the
+                // clearing/reset of the annotations.
+                Set<String> excludeAnnotations = new HashSet<>(filterTest.getExcludeAnnotations());
+                // Prevent system user only tests from running
+                excludeAnnotations.add(SystemUserOnly.class.getCanonicalName());
+                // Reset the annotations of the tests
+                filterTest.clearExcludeAnnotations();
+                filterTest.addAllExcludeAnnotation(excludeAnnotations);
+            }
+        }
     }
 
     private RunCommandTargetPreparer createLocationPreparer() {
diff --git a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
index 666bd44..edf5522 100644
--- a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
+++ b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
@@ -76,6 +76,13 @@
                             + "and \"not_executed\".")
     private RetryType mRetryType = null;
 
+    @Option(
+        name = BaseTestSuite.MODULE_OPTION,
+        shortName = BaseTestSuite.MODULE_OPTION_SHORT_NAME,
+        description = "the test module to run. Only works for configuration in the tests dir."
+    )
+    private String mModuleName = null;
+
     /**
      * It's possible to add extra exclusion from the rerun. But these tests will not change their
      * state.
@@ -236,6 +243,22 @@
             types.add(mRetryType);
         }
 
+        // Expand the --module option in case no abi is specified.
+        Set<String> expandedModuleOption = new HashSet<>();
+        if (mModuleName != null) {
+            SuiteTestFilter moduleFilter = SuiteTestFilter.createFrom(mModuleName);
+            expandedModuleOption.add(mModuleName);
+            if (moduleFilter.getAbi() == null) {
+                Set<String> abis = AbiUtils.getAbisSupportedByCompatibility();
+                for (String abi : abis) {
+                    SuiteTestFilter namingFilter =
+                            new SuiteTestFilter(
+                                    abi, moduleFilter.getName(), moduleFilter.getTest());
+                    expandedModuleOption.add(namingFilter.toString());
+                }
+            }
+        }
+
         // Expand the exclude-filter in case no abi is specified.
         Set<String> extendedExcludeRetryFilters = new HashSet<>();
         for (String excludeFilter : mExcludeFilters) {
@@ -257,6 +280,8 @@
         for (TestRunResult moduleResult : results.getMergedTestRunResults()) {
             // If the module is explicitly excluded from retries, preserve the original results.
             if (!extendedExcludeRetryFilters.contains(moduleResult.getName())
+                    && (expandedModuleOption.isEmpty()
+                            || expandedModuleOption.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/util/BuildTestsZipUtils.java b/src/com/android/tradefed/util/BuildTestsZipUtils.java
index cb32862..ae33b8a 100644
--- a/src/com/android/tradefed/util/BuildTestsZipUtils.java
+++ b/src/com/android/tradefed/util/BuildTestsZipUtils.java
@@ -67,8 +67,9 @@
         Collections.reverse(dirs);
 
         List<File> expandedTestDirs = new ArrayList<>();
+        File testsDir = null;
         if (buildInfo != null && buildInfo instanceof IDeviceBuildInfo) {
-            File testsDir = ((IDeviceBuildInfo) buildInfo).getTestsDir();
+            testsDir = ((IDeviceBuildInfo) buildInfo).getTestsDir();
             if (testsDir != null && testsDir.exists()) {
                 expandedTestDirs.add(FileUtil.getFileForPath(testsDir, "DATA", "app"));
                 expandedTestDirs.add(FileUtil.getFileForPath(testsDir, "DATA", "app", apkBase));
@@ -81,6 +82,13 @@
                 File testcasesSubDir = FileUtil.findFile(testsDir, apkBase);
                 if (testcasesSubDir != null) {
                     expandedTestDirs.add(testcasesSubDir);
+                } else {
+                    // If there doesn't exist a directory named after apkBase, it's possible that
+                    // the apk is built output to a different module directory. Therefore, try to
+                    // search entire testsDir to locate the apk.
+                    // TODO(dshi): Find a better way to locate apk. Ideally we should start with the
+                    // test module's directory to avoid false-positive result.
+                    expandedTestDirs.add(testsDir);
                 }
             }
         }
@@ -133,6 +141,14 @@
             // If we couldn't find a resource, we delete the tmp file
             FileUtil.deleteFile(apkTempFile);
         }
+
+        // Try to stage the files from remote zip files.
+        if (testsDir != null) {
+            File apkFile = buildInfo.stageRemoteFile(apkFileName, testsDir);
+            if (apkFile != null) {
+                return apkFile;
+            }
+        }
         return null;
     }
 }
diff --git a/src/com/android/tradefed/util/RemoteZip.java b/src/com/android/tradefed/util/RemoteZip.java
new file mode 100644
index 0000000..8e3ef97
--- /dev/null
+++ b/src/com/android/tradefed/util/RemoteZip.java
@@ -0,0 +1,184 @@
+/*
+ * 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.build.BuildRetrievalError;
+import com.android.tradefed.build.IFileDownloader;
+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 com.android.tradefed.util.zip.MergedZipEntryCollection;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+
+/** Utilities to unzip individual files inside a remote zip file. */
+public class RemoteZip {
+
+    private String mRemoteFilePath;
+    private List<CentralDirectoryInfo> mZipEntries;
+    private long mFileSize;
+    private IFileDownloader mDownloader;
+    // Last time this object is accessed. The timestamp is used to maintain the cache of RemoteZip
+    // objects.
+    private long mLastAccess;
+
+    /**
+     * Constructor
+     *
+     * @param remoteFilePath the remote path to the file to download.
+     * @param fileSize size of the remote file.
+     * @param downloader a @{link IFileDownloader} used to download a remote file.
+     */
+    public RemoteZip(String remoteFilePath, long fileSize, IFileDownloader downloader) {
+        mRemoteFilePath = remoteFilePath;
+        mFileSize = fileSize;
+        mDownloader = downloader;
+        mZipEntries = null;
+        mLastAccess = System.currentTimeMillis();
+    }
+
+    /** Get the remote file path of the remote zip artifact. */
+    public String getRemoteFilePath() {
+        return mRemoteFilePath;
+    }
+
+    /** Get the last time this object is accessed. */
+    public long getLastAccess() {
+        return mLastAccess;
+    }
+
+    /** Update the last access timestamp of the object. */
+    public void setLastAccess(long timestamp) {
+        mLastAccess = timestamp;
+    }
+
+    /**
+     * Gets the zip file entries of a remote zip file.
+     *
+     * @throws BuildRetrievalError if file could not be downloaded.
+     */
+    public List<CentralDirectoryInfo> getZipEntries() throws BuildRetrievalError, IOException {
+        if (mZipEntries != null) {
+            return mZipEntries;
+        }
+
+        File partialZipFile = FileUtil.createTempFileForRemote(mRemoteFilePath, null);
+        // Delete it so name is available
+        partialZipFile.delete();
+        try {
+            // Get the end central directory of the zip file requested.
+            // Download last 64kb (or entire file if the size is less than 64kb)
+            long size = EndCentralDirectoryInfo.MAX_LOOKBACK;
+            long startOffset = mFileSize - size;
+            if (startOffset < 0) {
+                // The default lookback size is greater than the size of the file, so read the whole
+                // file by setting size to -1.
+                startOffset = 0;
+                size = -1;
+            }
+
+            mDownloader.downloadFile(mRemoteFilePath, partialZipFile, startOffset, size);
+            EndCentralDirectoryInfo endCentralDirInfo = new EndCentralDirectoryInfo(partialZipFile);
+            partialZipFile.delete();
+
+            // Read central directory infos
+            mDownloader.downloadFile(
+                    mRemoteFilePath,
+                    partialZipFile,
+                    endCentralDirInfo.getCentralDirOffset(),
+                    endCentralDirInfo.getCentralDirSize());
+
+            mZipEntries = ZipUtil.getZipCentralDirectoryInfos(partialZipFile, endCentralDirInfo);
+            return mZipEntries;
+        } catch (IOException e) {
+            throw new BuildRetrievalError(
+                    String.format("Failed to get zip entries of remote file %s", mRemoteFilePath),
+                    e);
+        } finally {
+            FileUtil.deleteFile(partialZipFile);
+        }
+    }
+
+    /**
+     * Download the specified files in the remote zip file.
+     *
+     * @param destDir the directory to place the downloaded files to.
+     * @param files a list of entries to download from the remote zip file.
+     * @throws BuildRetrievalError
+     * @throws IOException
+     */
+    public void downloadFiles(File destDir, List<CentralDirectoryInfo> files)
+            throws BuildRetrievalError, IOException {
+        long totalDownloadedSize = 0;
+        long startTime = System.currentTimeMillis();
+
+        // Merge the entries into sections to minimize the download attempts.
+        List<MergedZipEntryCollection> collections =
+                MergedZipEntryCollection.createCollections(files);
+        CLog.d(
+                "Downloading %d files from remote zip file %s in %d sections.",
+                files.size(), mRemoteFilePath, collections.size());
+        for (MergedZipEntryCollection collection : collections) {
+            File partialZipFile = null;
+            try {
+                partialZipFile = FileUtil.createTempFileForRemote(mRemoteFilePath, null);
+                // Delete it so name is available
+                partialZipFile.delete();
+                // End offset is based on the maximum guess of local file header size (2KB). So it
+                // can exceed the file size.
+                long downloadedSize = collection.getEndOffset() - collection.getStartOffset();
+                if (collection.getStartOffset() + downloadedSize > mFileSize) {
+                    downloadedSize = mFileSize - collection.getStartOffset();
+                }
+                mDownloader.downloadFile(
+                        mRemoteFilePath,
+                        partialZipFile,
+                        collection.getStartOffset(),
+                        downloadedSize);
+                totalDownloadedSize += downloadedSize;
+
+                // Extract each file from the partial download.
+                for (CentralDirectoryInfo entry : collection.getZipEntries()) {
+                    File targetFile =
+                            new File(Paths.get(destDir.toString(), entry.getFileName()).toString());
+                    LocalFileHeader localFileHeader =
+                            new LocalFileHeader(
+                                    partialZipFile,
+                                    (int)
+                                            (entry.getLocalHeaderOffset()
+                                                    - collection.getStartOffset()));
+                    ZipUtil.unzipPartialZipFile(
+                            partialZipFile,
+                            targetFile,
+                            entry,
+                            localFileHeader,
+                            entry.getLocalHeaderOffset() - collection.getStartOffset());
+                }
+            } finally {
+                FileUtil.deleteFile(partialZipFile);
+            }
+        }
+        CLog.d(
+                "%d files downloaded from remote zip file in %s. Total download size: %,d bytes.",
+                files.size(),
+                TimeUtil.formatElapsedTime(System.currentTimeMillis() - startTime),
+                totalDownloadedSize);
+    }
+}
diff --git a/src/com/android/tradefed/util/SubprocessTestResultsParser.java b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
index 9bd7cbc..2da9f31 100644
--- a/src/com/android/tradefed/util/SubprocessTestResultsParser.java
+++ b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
@@ -17,6 +17,8 @@
 
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ILogSaverListener;
@@ -55,7 +57,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -101,21 +103,29 @@
      */
     private class EventReceiverThread extends Thread {
         private ServerSocket mSocket;
-        private CountDownLatch mCountDown;
+        // initial state: 1 permit available, joins that don't wait for connection will succeed
+        private Semaphore mSemaphore = new Semaphore(1);
         private boolean mShouldParse = true;
 
         public EventReceiverThread() throws IOException {
             super("EventReceiverThread");
             mSocket = new ServerSocket(0);
-            mCountDown = new CountDownLatch(1);
         }
 
         protected int getLocalPort() {
             return mSocket.getLocalPort();
         }
 
-        protected CountDownLatch getCountDown() {
-            return mCountDown;
+        /** @return True if parsing completes before timeout (optionally waiting for connection). */
+        boolean await(long millis, boolean waitForConnection) throws InterruptedException {
+            // As only 1 permit is available prior to connecting, changing the number of permits
+            // requested controls whether the receiver will wait for a connection.
+            int permits = waitForConnection ? 2 : 1;
+            if (mSemaphore.tryAcquire(permits, millis, TimeUnit.MILLISECONDS)) {
+                mSemaphore.release(permits);
+                return true;
+            }
+            return false;
         }
 
         public void cancel() throws IOException {
@@ -138,6 +148,7 @@
             BufferedReader in = null;
             try {
                 client = mSocket.accept();
+                mSemaphore.acquire(); // connected: 0 permits available, all joins will wait
                 in = new BufferedReader(new InputStreamReader(client.getInputStream()));
                 String event = null;
                 while ((event = in.readLine()) != null) {
@@ -152,27 +163,39 @@
                         CLog.e(e);
                     }
                 }
-            } catch (IOException e) {
+            } catch (IOException | InterruptedException e) {
                 CLog.e(e);
             } finally {
                 StreamUtil.close(in);
-                mCountDown.countDown();
+                mSemaphore.release(2); // finished: 2 permits available, all joins succeed
             }
             CLog.d("EventReceiverThread done.");
         }
     }
 
     /**
-     * If the event receiver is being used, ensure that we wait for it to terminate.
+     * Wait for the event receiver to finish processing events. Will wait even if a connection
+     * wasn't established, i.e. processing hasn't begun yet.
      *
      * @param millis timeout in milliseconds.
      * @return True if receiver thread terminate before timeout, False otherwise.
      */
     public boolean joinReceiver(long millis) {
+        return joinReceiver(millis, true);
+    }
+
+    /**
+     * Wait for the event receiver to finish processing events.
+     *
+     * @param millis timeout in milliseconds.
+     * @param waitForConnection False to skip waiting if a connection was never established.
+     * @return True if receiver thread terminate before timeout, False otherwise.
+     */
+    public boolean joinReceiver(long millis, boolean waitForConnection) {
         if (mEventReceiver != null) {
             try {
                 CLog.i("Waiting for events to finish being processed.");
-                if (!mEventReceiver.getCountDown().await(millis, TimeUnit.MILLISECONDS)) {
+                if (!mEventReceiver.await(millis, waitForConnection)) {
                     mEventReceiver.stopParsing();
                     CLog.e("Event receiver thread did not complete. Some events may be missing.");
                     return false;
@@ -505,7 +528,24 @@
             // provider of the running configuration).
             List<IBuildInfo> infos = mContext.getBuildInfos();
             if (!infos.isEmpty()) {
-                infos.get(0).addBuildAttributes(eventEnd.mBuildAttributes);
+                Map<String, String> attributes = eventEnd.mBuildAttributes;
+                for (InvocationMetricKey key : InvocationMetricKey.values()) {
+                    if (!attributes.containsKey(key.toString())) {
+                        continue;
+                    }
+                    String val = attributes.remove(key.toString());
+                    if (key.shouldAdd()) {
+                        try {
+                            InvocationMetricLogger.addInvocationMetrics(key, Long.parseLong(val));
+                        } catch (NumberFormatException e) {
+                            CLog.e("Key %s should have a number value, instead was: %s", key, val);
+                            CLog.e(e);
+                        }
+                    } else {
+                        InvocationMetricLogger.addInvocationMetrics(key, val);
+                    }
+                }
+                infos.get(0).addBuildAttributes(attributes);
             }
         }
     }
diff --git a/src/com/android/tradefed/util/SystemUtil.java b/src/com/android/tradefed/util/SystemUtil.java
index 6a66c89..a38bc0b 100644
--- a/src/com/android/tradefed/util/SystemUtil.java
+++ b/src/com/android/tradefed/util/SystemUtil.java
@@ -42,6 +42,8 @@
         ANDROID_HOST_OUT_TESTCASES,
     }
 
+    public static final String REMOTE_VM_VARIABLE = "REMOTE_VM_ENV";
+
     private static final String HOST_TESTCASES = "host/testcases";
     private static final String TARGET_TESTCASES = "target/testcases";
 
@@ -163,4 +165,12 @@
             return new File(path);
         }
     }
+
+    /** Return true if we are currently running in a remote environment. */
+    public static boolean isRemoteEnvironment() {
+        if ("1".equals(System.getenv(REMOTE_VM_VARIABLE))) {
+            return true;
+        }
+        return false;
+    }
 }
diff --git a/test_framework/Android.bp b/test_framework/Android.bp
index 561d9a8..ae775da 100644
--- a/test_framework/Android.bp
+++ b/test_framework/Android.bp
@@ -18,8 +18,14 @@
     srcs: [
         "com/**/*.java",
     ],
+    static_libs: [
+        "longevity-host-lib",
+        "perfetto_config-full",
+        "test-composers",
+    ],
     libs: [
         "tradefed-lib-core",
+        "loganalysis",
     ],
 }
 
diff --git a/test_framework/OWNERS b/test_framework/OWNERS
new file mode 100644
index 0000000..1a475dc
--- /dev/null
+++ b/test_framework/OWNERS
@@ -0,0 +1,5 @@
+# Base Owners + extra folks that can review the test_framework layer
+allenhair@google.com
+gelanchezhian@google.com
+mrosenfeld@google.com
+
diff --git a/test_framework/README.md b/test_framework/README.md
new file mode 100644
index 0000000..1a33d67
--- /dev/null
+++ b/test_framework/README.md
@@ -0,0 +1,12 @@
+# Trade Federation Test Framework
+
+This is the top-layer provided to write and run tests against.
+
+The goal of this layer is to provide a simple framework to
+write and execute tests.
+
+This directory should contain classes that are:
+* Related to tests (IRemoteTest types)
+* Related to tests setup (ITargetPreparer types)
+* Related to metrics collection during tests (IMetricCollector types)
+* Utilities specific to the tests, preparers or collectors
diff --git a/src/com/android/tradefed/device/metric/AtraceCollector.java b/test_framework/com/android/tradefed/device/metric/AtraceCollector.java
similarity index 98%
rename from src/com/android/tradefed/device/metric/AtraceCollector.java
rename to test_framework/com/android/tradefed/device/metric/AtraceCollector.java
index 628410b..c6ea63b 100644
--- a/src/com/android/tradefed/device/metric/AtraceCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/AtraceCollector.java
@@ -189,7 +189,7 @@
         }
     }
 
-    private void postProcess(File trace, String id) {
+    private void postProcess(File trace) {
         if (mLogProcessingBinary == null
                 || !mLogProcessingBinary.exists()
                 || !mLogProcessingBinary.canExecute()) {
@@ -265,7 +265,7 @@
                                 streamSource);
                     }
 
-                    postProcess(trace, device.getSerialNumber());
+                    postProcess(trace);
                     trace.delete();
                 } else {
                     throw new DeviceRuntimeException("failed to pull log: " + fullLogPath());
diff --git a/src/com/android/tradefed/device/metric/AtraceRunMetricCollector.java b/test_framework/com/android/tradefed/device/metric/AtraceRunMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/AtraceRunMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/AtraceRunMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/BuddyInfoMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/BugreportzMetricCollector.java b/test_framework/com/android/tradefed/device/metric/BugreportzMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/BugreportzMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/BugreportzMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/DumpHeapCollector.java b/test_framework/com/android/tradefed/device/metric/DumpHeapCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/DumpHeapCollector.java
rename to test_framework/com/android/tradefed/device/metric/DumpHeapCollector.java
diff --git a/src/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java b/test_framework/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/GraphicsStatsMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/HostStatsdMetricCollector.java b/test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/HostStatsdMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/HostStatsdMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/IonHeapInfoMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/MemInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/MemInfoMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/MemInfoMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/MemInfoMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java b/test_framework/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/PagetypeInfoMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java b/test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java b/test_framework/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java
rename to test_framework/com/android/tradefed/device/metric/ProcessMaxMemoryCollector.java
diff --git a/src/com/android/tradefed/device/metric/RebootReasonCollector.java b/test_framework/com/android/tradefed/device/metric/RebootReasonCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/RebootReasonCollector.java
rename to test_framework/com/android/tradefed/device/metric/RebootReasonCollector.java
diff --git a/src/com/android/tradefed/device/metric/RuntimeRestartCollector.java b/test_framework/com/android/tradefed/device/metric/RuntimeRestartCollector.java
similarity index 97%
rename from src/com/android/tradefed/device/metric/RuntimeRestartCollector.java
rename to test_framework/com/android/tradefed/device/metric/RuntimeRestartCollector.java
index 16e28c1..1293f71 100644
--- a/src/com/android/tradefed/device/metric/RuntimeRestartCollector.java
+++ b/test_framework/com/android/tradefed/device/metric/RuntimeRestartCollector.java
@@ -216,6 +216,13 @@
     /** Helper method to add metrics from StatsdStatsReport according to timestamps. */
     private void addStatsdStatsBasedMetrics(
             final Map<String, Metric> metrics, List<Integer> timestampsSecs, String serial) {
+        // Always add a count of system server crashes, regardless of whether there are any.
+        // The statsd metadata-based count is used as the atom-based data can miss runtime restart
+        // instances.
+        // TODO(b/135770315): Re-assess this after the root cause for missing instances in the atom
+        // -based results is found.
+        String countMetricKey = createMetricKey(METRIC_SUFFIX_COUNT, serial);
+        metrics.put(countMetricKey, stringToMetric(String.valueOf(timestampsSecs.size())));
         // If there are runtime restarts, add a comma-separated list of timestamps.
         if (!timestampsSecs.isEmpty()) {
             // Store both the raw timestamp and the formatted, more readable version.
@@ -245,10 +252,6 @@
     /** Helper method to add metrics from the AppCrashOccurred atoms according to timestamps. */
     private void addAtomBasedMetrics(
             final Map<String, Metric> metrics, List<Long> timestampsNanos, String serial) {
-        // Always add a count of system server crashes, regardless of whether there are any.
-        // The atom-based count is used as the statsd-metadata-based one tops out at 20.
-        String countMetricKey = createMetricKey(METRIC_SUFFIX_COUNT, serial);
-        metrics.put(countMetricKey, stringToMetric(String.valueOf(timestampsNanos.size())));
         // If there are runtime restarts, add a comma-separated list of device uptime timestamps.
         if (!timestampsNanos.isEmpty()) {
             // Store both the raw timestamp and the formatted, more readable version.
diff --git a/src/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java b/test_framework/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/ScheduleMultipleDeviceMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java b/test_framework/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/ScheduledDeviceMetricCollector.java
diff --git a/src/com/android/tradefed/device/metric/TemperatureCollector.java b/test_framework/com/android/tradefed/device/metric/TemperatureCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/TemperatureCollector.java
rename to test_framework/com/android/tradefed/device/metric/TemperatureCollector.java
diff --git a/src/com/android/tradefed/device/metric/TraceCmdCollector.java b/test_framework/com/android/tradefed/device/metric/TraceCmdCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/TraceCmdCollector.java
rename to test_framework/com/android/tradefed/device/metric/TraceCmdCollector.java
diff --git a/src/com/android/tradefed/device/metric/TraceMetricCollector.java b/test_framework/com/android/tradefed/device/metric/TraceMetricCollector.java
similarity index 100%
rename from src/com/android/tradefed/device/metric/TraceMetricCollector.java
rename to test_framework/com/android/tradefed/device/metric/TraceMetricCollector.java
diff --git a/src/com/android/tradefed/result/JUnit4ResultForwarder.java b/test_framework/com/android/tradefed/result/JUnit4ResultForwarder.java
similarity index 100%
rename from src/com/android/tradefed/result/JUnit4ResultForwarder.java
rename to test_framework/com/android/tradefed/result/JUnit4ResultForwarder.java
diff --git a/src/com/android/tradefed/targetprep/AdditionalFilesInstaller.java b/test_framework/com/android/tradefed/targetprep/AdditionalFilesInstaller.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/AdditionalFilesInstaller.java
rename to test_framework/com/android/tradefed/targetprep/AdditionalFilesInstaller.java
diff --git a/src/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java b/test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
rename to test_framework/com/android/tradefed/targetprep/AllTestAppsInstallSetup.java
diff --git a/src/com/android/tradefed/targetprep/AoaTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/AoaTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/AoaTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/AoaTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/AppSetup.java b/test_framework/com/android/tradefed/targetprep/AppSetup.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/AppSetup.java
rename to test_framework/com/android/tradefed/targetprep/AppSetup.java
diff --git a/src/com/android/tradefed/targetprep/CdmaDeviceFlasher.java b/test_framework/com/android/tradefed/targetprep/CdmaDeviceFlasher.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/CdmaDeviceFlasher.java
rename to test_framework/com/android/tradefed/targetprep/CdmaDeviceFlasher.java
diff --git a/src/com/android/tradefed/targetprep/CpuThrottlingWaiter.java b/test_framework/com/android/tradefed/targetprep/CpuThrottlingWaiter.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/CpuThrottlingWaiter.java
rename to test_framework/com/android/tradefed/targetprep/CpuThrottlingWaiter.java
diff --git a/src/com/android/tradefed/targetprep/CrashCollector.java b/test_framework/com/android/tradefed/targetprep/CrashCollector.java
similarity index 97%
rename from src/com/android/tradefed/targetprep/CrashCollector.java
rename to test_framework/com/android/tradefed/targetprep/CrashCollector.java
index 63f80e8..b9e55a1 100644
--- a/src/com/android/tradefed/targetprep/CrashCollector.java
+++ b/test_framework/com/android/tradefed/targetprep/CrashCollector.java
@@ -21,6 +21,7 @@
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.BackgroundDeviceAction;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceProperties;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.LargeOutputReceiver;
 import com.android.tradefed.log.ITestLogger;
@@ -62,7 +63,7 @@
             return true;
         }
         // first get pseudo API level to check for platform support
-        String codeName = device.getProperty("ro.build.version.codename").trim();
+        String codeName = device.getProperty(DeviceProperties.BUILD_CODENAME).trim();
         int apiLevel = device.getApiLevel();
         if (!"REL".equals(codeName)) {
             apiLevel++;
diff --git a/src/com/android/tradefed/targetprep/DeviceStorageFiller.java b/test_framework/com/android/tradefed/targetprep/DeviceStorageFiller.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/DeviceStorageFiller.java
rename to test_framework/com/android/tradefed/targetprep/DeviceStorageFiller.java
diff --git a/src/com/android/tradefed/targetprep/DeviceStringPusher.java b/test_framework/com/android/tradefed/targetprep/DeviceStringPusher.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/DeviceStringPusher.java
rename to test_framework/com/android/tradefed/targetprep/DeviceStringPusher.java
diff --git a/src/com/android/tradefed/targetprep/DeviceWiper.java b/test_framework/com/android/tradefed/targetprep/DeviceWiper.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/DeviceWiper.java
rename to test_framework/com/android/tradefed/targetprep/DeviceWiper.java
diff --git a/src/com/android/tradefed/targetprep/DisableSELinuxTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/DisableSELinuxTargetPreparer.java
similarity index 95%
rename from src/com/android/tradefed/targetprep/DisableSELinuxTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/DisableSELinuxTargetPreparer.java
index 4344fcf..7993334 100644
--- a/src/com/android/tradefed/targetprep/DisableSELinuxTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/DisableSELinuxTargetPreparer.java
@@ -56,7 +56,8 @@
         result = device.executeShellV2Command("setenforce 0");
         if (result.getStatus() != CommandStatus.SUCCESS) {
             throw new TargetSetupError(
-                    "Disabling SELinux failed with status: " + result.getStatus());
+                    "Disabling SELinux failed with status: " + result.getStatus(),
+                    device.getDeviceDescriptor());
         }
         if (!mWasRoot) {
             device.disableAdbRoot();
@@ -73,7 +74,7 @@
             device.enableAdbRoot();
         }
         CLog.d("Enabling SELinux.");
-        CommandResult result = device.executeShellV2Command("setenforce 1");
+        device.executeShellV2Command("setenforce 1");
         if (!mWasRoot) {
             device.disableAdbRoot();
         }
diff --git a/src/com/android/tradefed/targetprep/EraseUserDataPreparer.java b/test_framework/com/android/tradefed/targetprep/EraseUserDataPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/EraseUserDataPreparer.java
rename to test_framework/com/android/tradefed/targetprep/EraseUserDataPreparer.java
diff --git a/src/com/android/tradefed/targetprep/FastbootCommandPreparer.java b/test_framework/com/android/tradefed/targetprep/FastbootCommandPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/FastbootCommandPreparer.java
rename to test_framework/com/android/tradefed/targetprep/FastbootCommandPreparer.java
diff --git a/src/com/android/tradefed/targetprep/FolderSaver.java b/test_framework/com/android/tradefed/targetprep/FolderSaver.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/FolderSaver.java
rename to test_framework/com/android/tradefed/targetprep/FolderSaver.java
diff --git a/src/com/android/tradefed/targetprep/InstallAllTestZipAppsSetup.java b/test_framework/com/android/tradefed/targetprep/InstallAllTestZipAppsSetup.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/InstallAllTestZipAppsSetup.java
rename to test_framework/com/android/tradefed/targetprep/InstallAllTestZipAppsSetup.java
diff --git a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
similarity index 95%
rename from src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
index 9be70dd..8e66961 100644
--- a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
@@ -171,7 +171,7 @@
             installTrain(device, buildInfo, testAppFileNames, new String[] {"--staged"});
             return;
         }
-        installTrain(device, buildInfo, testAppFileNames, null);
+        installTrain(device, buildInfo, testAppFileNames, new String[] {});
     }
 
     /**
@@ -185,9 +185,21 @@
     protected void installTrain(
             ITestDevice device,
             IBuildInfo buildInfo,
-            Collection<String> moduleFilenames,
+            List<String> moduleFilenames,
             final String[] extraArgs)
             throws TargetSetupError, DeviceNotAvailableException {
+        // TODO(b/137883918):remove after new adb is released, which supports installing
+        // single apk/apex using 'install-multi-package'
+        if (moduleFilenames.size() == 1) {
+            String moduleFileName = moduleFilenames.get(0);
+            File module = getLocalPathForFilename(buildInfo, moduleFileName, device);
+            device.installPackage(module, true, extraArgs);
+            if (moduleFileName.endsWith(APK_SUFFIX)) {
+                String packageName = parsePackageName(module, device.getDeviceDescriptor());
+                mApkInstalled.add(packageName);
+            }
+            return;
+        }
 
         List<String> apkPackageNames = new ArrayList<>();
         List<String> trainInstallCmd = new ArrayList<>();
@@ -498,8 +510,7 @@
             List<String> testAppFileNames, ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, DeviceNotAvailableException {
         for (String moduleFileName : testAppFileNames) {
-            if (moduleFileName.endsWith(APK_SUFFIX) &&
-                isPersistentApk(moduleFileName, device, buildInfo)) {
+            if (isPersistentApk(moduleFileName, device, buildInfo)) {
                 return true;
             }
         }
@@ -516,6 +527,9 @@
      */
     protected boolean isPersistentApk(String filename, ITestDevice device, IBuildInfo buildInfo)
             throws TargetSetupError, DeviceNotAvailableException {
+        if (!filename.endsWith(APK_SUFFIX)) {
+            return false;
+        }
         File moduleFile = getLocalPathForFilename(buildInfo, filename, device);
         PackageInfo pkgInfo =
             device.getAppPackageInfo(parsePackageName(moduleFile, device.getDeviceDescriptor()));
diff --git a/src/com/android/tradefed/targetprep/InstallApkSetup.java b/test_framework/com/android/tradefed/targetprep/InstallApkSetup.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/InstallApkSetup.java
rename to test_framework/com/android/tradefed/targetprep/InstallApkSetup.java
diff --git a/src/com/android/tradefed/targetprep/InstallBuildEnvApkSetup.java b/test_framework/com/android/tradefed/targetprep/InstallBuildEnvApkSetup.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/InstallBuildEnvApkSetup.java
rename to test_framework/com/android/tradefed/targetprep/InstallBuildEnvApkSetup.java
diff --git a/src/com/android/tradefed/targetprep/InstrumentationPreparer.java b/test_framework/com/android/tradefed/targetprep/InstrumentationPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/InstrumentationPreparer.java
rename to test_framework/com/android/tradefed/targetprep/InstrumentationPreparer.java
diff --git a/src/com/android/tradefed/targetprep/NativeLeakCollector.java b/test_framework/com/android/tradefed/targetprep/NativeLeakCollector.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/NativeLeakCollector.java
rename to test_framework/com/android/tradefed/targetprep/NativeLeakCollector.java
diff --git a/src/com/android/tradefed/targetprep/PerfettoPreparer.java b/test_framework/com/android/tradefed/targetprep/PerfettoPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/PerfettoPreparer.java
rename to test_framework/com/android/tradefed/targetprep/PerfettoPreparer.java
diff --git a/src/com/android/tradefed/targetprep/PushFileInvoker.java b/test_framework/com/android/tradefed/targetprep/PushFileInvoker.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/PushFileInvoker.java
rename to test_framework/com/android/tradefed/targetprep/PushFileInvoker.java
diff --git a/src/com/android/tradefed/targetprep/PushFilePreparer.java b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
similarity index 96%
rename from src/com/android/tradefed/targetprep/PushFilePreparer.java
rename to test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
index e7ed4da..02f209d 100644
--- a/src/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -197,6 +197,7 @@
                         } else {
                             CLog.e("Did not find any module directory for '%s'", mModuleName);
                         }
+
                     } catch (IOException e) {
                         CLog.w(
                                 "Something went wrong while searching for the module '%s' "
@@ -234,6 +235,14 @@
                 CLog.w("Failed to find test files from directory.");
                 src = null;
             }
+
+            if (src == null && testDir != null) {
+                // TODO(b/138416078): Once build dependency can be fixed and test required
+                // APKs are all under the test module directory, we can remove this fallback
+                // approach to do individual download from remote artifact.
+                // Try to stage the files from remote zip files.
+                src = buildInfo.stageRemoteFile(fileName, testDir);
+            }
         }
         return src;
     }
diff --git a/src/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java b/test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
rename to test_framework/com/android/tradefed/targetprep/PythonVirtualenvPreparer.java
diff --git a/src/com/android/tradefed/targetprep/RebootTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RebootTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/RebootTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/RebootTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java b/test_framework/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java
rename to test_framework/com/android/tradefed/targetprep/RemoveSystemAppPreparer.java
diff --git a/src/com/android/tradefed/targetprep/RestartSystemServerTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RestartSystemServerTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/RestartSystemServerTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/RestartSystemServerTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/RootTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/RootTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/RootTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/RunHostCommandTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/SemaphoreTokenTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/SemaphoreTokenTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/SemaphoreTokenTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/SemaphoreTokenTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/StopServicesSetup.java b/test_framework/com/android/tradefed/targetprep/StopServicesSetup.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/StopServicesSetup.java
rename to test_framework/com/android/tradefed/targetprep/StopServicesSetup.java
diff --git a/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/SystemUpdaterDeviceFlasher.java b/test_framework/com/android/tradefed/targetprep/SystemUpdaterDeviceFlasher.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/SystemUpdaterDeviceFlasher.java
rename to test_framework/com/android/tradefed/targetprep/SystemUpdaterDeviceFlasher.java
diff --git a/src/com/android/tradefed/targetprep/TearDownPassThroughPreparer.java b/test_framework/com/android/tradefed/targetprep/TearDownPassThroughPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/TearDownPassThroughPreparer.java
rename to test_framework/com/android/tradefed/targetprep/TearDownPassThroughPreparer.java
diff --git a/src/com/android/tradefed/targetprep/TemperatureThrottlingWaiter.java b/test_framework/com/android/tradefed/targetprep/TemperatureThrottlingWaiter.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/TemperatureThrottlingWaiter.java
rename to test_framework/com/android/tradefed/targetprep/TemperatureThrottlingWaiter.java
diff --git a/src/com/android/tradefed/targetprep/TestFilePushSetup.java b/test_framework/com/android/tradefed/targetprep/TestFilePushSetup.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/TestFilePushSetup.java
rename to test_framework/com/android/tradefed/targetprep/TestFilePushSetup.java
diff --git a/src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java b/test_framework/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java
rename to test_framework/com/android/tradefed/targetprep/TestSystemAppInstallSetup.java
diff --git a/src/com/android/tradefed/targetprep/TimeWaster.java b/test_framework/com/android/tradefed/targetprep/TimeWaster.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/TimeWaster.java
rename to test_framework/com/android/tradefed/targetprep/TimeWaster.java
diff --git a/src/com/android/tradefed/targetprep/UserCleaner.java b/test_framework/com/android/tradefed/targetprep/UserCleaner.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/UserCleaner.java
rename to test_framework/com/android/tradefed/targetprep/UserCleaner.java
diff --git a/src/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java b/test_framework/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
rename to test_framework/com/android/tradefed/targetprep/WaitForDeviceDatetimePreparer.java
diff --git a/src/com/android/tradefed/targetprep/WifiPreparer.java b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
similarity index 93%
rename from src/com/android/tradefed/targetprep/WifiPreparer.java
rename to test_framework/com/android/tradefed/targetprep/WifiPreparer.java
index ad5dee0..6d053ab 100644
--- a/src/com/android/tradefed/targetprep/WifiPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/WifiPreparer.java
@@ -20,6 +20,8 @@
 import com.android.tradefed.config.OptionClass;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
 import com.android.tradefed.log.LogUtil.CLog;
 
 /**
@@ -74,6 +76,7 @@
             throw new TargetSetupError("wifi-network not specified", device.getDeviceDescriptor());
         }
 
+        InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.WIFI_AP_NAME, mWifiNetwork);
         if (!device.connectToWifiNetworkIfNeeded(mWifiNetwork, mWifiPsk)) {
             throw new TargetSetupError(String.format("Failed to connect to wifi network %s on %s",
                     mWifiNetwork, device.getSerialNumber()), device.getDeviceDescriptor());
diff --git a/src/com/android/tradefed/targetprep/adb/AdbStopServerPreparer.java b/test_framework/com/android/tradefed/targetprep/adb/AdbStopServerPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/adb/AdbStopServerPreparer.java
rename to test_framework/com/android/tradefed/targetprep/adb/AdbStopServerPreparer.java
diff --git a/src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java b/test_framework/com/android/tradefed/targetprep/app/NoApkTestSkipper.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java
rename to test_framework/com/android/tradefed/targetprep/app/NoApkTestSkipper.java
diff --git a/src/com/android/tradefed/targetprep/companion/CheckPairingPreparer.java b/test_framework/com/android/tradefed/targetprep/companion/CheckPairingPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/companion/CheckPairingPreparer.java
rename to test_framework/com/android/tradefed/targetprep/companion/CheckPairingPreparer.java
diff --git a/src/com/android/tradefed/targetprep/companion/CompanionAllocator.java b/test_framework/com/android/tradefed/targetprep/companion/CompanionAllocator.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/companion/CompanionAllocator.java
rename to test_framework/com/android/tradefed/targetprep/companion/CompanionAllocator.java
diff --git a/src/com/android/tradefed/targetprep/companion/CompanionAwarePreparer.java b/test_framework/com/android/tradefed/targetprep/companion/CompanionAwarePreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/companion/CompanionAwarePreparer.java
rename to test_framework/com/android/tradefed/targetprep/companion/CompanionAwarePreparer.java
diff --git a/src/com/android/tradefed/targetprep/companion/CompanionDeviceTracker.java b/test_framework/com/android/tradefed/targetprep/companion/CompanionDeviceTracker.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/companion/CompanionDeviceTracker.java
rename to test_framework/com/android/tradefed/targetprep/companion/CompanionDeviceTracker.java
diff --git a/src/com/android/tradefed/targetprep/companion/CompanionRunCommandTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/companion/CompanionRunCommandTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/companion/CompanionRunCommandTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/companion/CompanionRunCommandTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/companion/CompanionTestAppInstallSetup.java b/test_framework/com/android/tradefed/targetprep/companion/CompanionTestAppInstallSetup.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/companion/CompanionTestAppInstallSetup.java
rename to test_framework/com/android/tradefed/targetprep/companion/CompanionTestAppInstallSetup.java
diff --git a/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java b/test_framework/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java
similarity index 97%
rename from src/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java
rename to test_framework/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java
index 174bb9c..2c3ca05 100644
--- a/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java
+++ b/test_framework/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java
@@ -110,14 +110,15 @@
             // 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");
+                throw new TargetSetupError(
+                        "Timeout to boot into DSU", device.getDeviceDescriptor());
             }
             CommandResult result = device.executeShellV2Command("gsi_tool enable");
             if (CommandStatus.SUCCESS.equals(result.getStatus())) {
                 // success
                 return;
             } else {
-                throw new TargetSetupError("fail on gsi_tool enable");
+                throw new TargetSetupError("fail on gsi_tool enable", device.getDeviceDescriptor());
             }
         } catch (IOException e) {
             CLog.e(e);
diff --git a/src/com/android/tradefed/targetprep/multi/MergeMultiBuildTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/multi/MergeMultiBuildTargetPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/multi/MergeMultiBuildTargetPreparer.java
rename to test_framework/com/android/tradefed/targetprep/multi/MergeMultiBuildTargetPreparer.java
diff --git a/src/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java b/test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
rename to test_framework/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
diff --git a/src/com/android/tradefed/targetprep/suite/SuiteApkInstaller.java b/test_framework/com/android/tradefed/targetprep/suite/SuiteApkInstaller.java
similarity index 89%
rename from src/com/android/tradefed/targetprep/suite/SuiteApkInstaller.java
rename to test_framework/com/android/tradefed/targetprep/suite/SuiteApkInstaller.java
index bb063c9..cdf9554 100644
--- a/src/com/android/tradefed/targetprep/suite/SuiteApkInstaller.java
+++ b/test_framework/com/android/tradefed/targetprep/suite/SuiteApkInstaller.java
@@ -78,7 +78,15 @@
             IDeviceBuildInfo deviceBuildInfo = (IDeviceBuildInfo) buildInfo;
             File testDir = deviceBuildInfo.getTestsDir();
             if (testDir != null && testDir.isDirectory()) {
-                return FileUtil.findFile(testDir, apkFileName);
+                File apkFile = FileUtil.findFile(testDir, apkFileName);
+                if (apkFile == null) {
+                    // TODO(b/138416078): Once build dependency can be fixed and test required
+                    // APKs are all under the test module directory, we can remove this fallback
+                    // approach to do individual download from remote artifact.
+                    // Try to stage the files from remote zip files.
+                    apkFile = buildInfo.stageRemoteFile(apkFileName, testDir);
+                }
+                return apkFile;
             }
         }
         return null;
diff --git a/src/com/android/tradefed/testtype/AndroidJUnitTest.java b/test_framework/com/android/tradefed/testtype/AndroidJUnitTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/AndroidJUnitTest.java
rename to test_framework/com/android/tradefed/testtype/AndroidJUnitTest.java
diff --git a/src/com/android/tradefed/testtype/CodeCoverageReportFormat.java b/test_framework/com/android/tradefed/testtype/CodeCoverageReportFormat.java
similarity index 100%
rename from src/com/android/tradefed/testtype/CodeCoverageReportFormat.java
rename to test_framework/com/android/tradefed/testtype/CodeCoverageReportFormat.java
diff --git a/src/com/android/tradefed/testtype/CodeCoverageTest.java b/test_framework/com/android/tradefed/testtype/CodeCoverageTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/CodeCoverageTest.java
rename to test_framework/com/android/tradefed/testtype/CodeCoverageTest.java
diff --git a/src/com/android/tradefed/testtype/CompanionAwareTest.java b/test_framework/com/android/tradefed/testtype/CompanionAwareTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/CompanionAwareTest.java
rename to test_framework/com/android/tradefed/testtype/CompanionAwareTest.java
diff --git a/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java b/test_framework/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java
similarity index 100%
rename from src/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java
rename to test_framework/com/android/tradefed/testtype/DeviceJUnit4ClassRunner.java
diff --git a/src/com/android/tradefed/testtype/DeviceSuite.java b/test_framework/com/android/tradefed/testtype/DeviceSuite.java
similarity index 100%
rename from src/com/android/tradefed/testtype/DeviceSuite.java
rename to test_framework/com/android/tradefed/testtype/DeviceSuite.java
diff --git a/src/com/android/tradefed/testtype/DeviceTestCase.java b/test_framework/com/android/tradefed/testtype/DeviceTestCase.java
similarity index 100%
rename from src/com/android/tradefed/testtype/DeviceTestCase.java
rename to test_framework/com/android/tradefed/testtype/DeviceTestCase.java
diff --git a/src/com/android/tradefed/testtype/DeviceTestResult.java b/test_framework/com/android/tradefed/testtype/DeviceTestResult.java
similarity index 100%
rename from src/com/android/tradefed/testtype/DeviceTestResult.java
rename to test_framework/com/android/tradefed/testtype/DeviceTestResult.java
diff --git a/src/com/android/tradefed/testtype/DeviceTestSuite.java b/test_framework/com/android/tradefed/testtype/DeviceTestSuite.java
similarity index 100%
rename from src/com/android/tradefed/testtype/DeviceTestSuite.java
rename to test_framework/com/android/tradefed/testtype/DeviceTestSuite.java
diff --git a/src/com/android/tradefed/testtype/GTest.java b/test_framework/com/android/tradefed/testtype/GTest.java
similarity index 98%
rename from src/com/android/tradefed/testtype/GTest.java
rename to test_framework/com/android/tradefed/testtype/GTest.java
index 2898afa..052fe23 100644
--- a/src/com/android/tradefed/testtype/GTest.java
+++ b/test_framework/com/android/tradefed/testtype/GTest.java
@@ -389,7 +389,9 @@
             mDevice.executeShellCommand("stop");
         }
         // Insert the coverage listener if code coverage collection is enabled.
-        listener = addNativeCoverageListenerIfEnabled(mDevice, listener);
+        listener =
+                addNativeCoverageListenerIfEnabled(
+                        mDevice, mCoverageFlush, mCoverageProcesses, listener);
         NativeCodeCoverageFlusher flusher = new NativeCodeCoverageFlusher(mDevice);
 
         Throwable throwable = null;
@@ -406,10 +408,6 @@
             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/test_framework/com/android/tradefed/testtype/GTestBase.java
similarity index 97%
rename from src/com/android/tradefed/testtype/GTestBase.java
rename to test_framework/com/android/tradefed/testtype/GTestBase.java
index eee17c5..defe996 100644
--- a/src/com/android/tradefed/testtype/GTestBase.java
+++ b/test_framework/com/android/tradefed/testtype/GTestBase.java
@@ -523,6 +523,10 @@
             gTestCmdLine.append(String.format("LD_LIBRARY_PATH=%s ", mLdLibraryPath));
         }
 
+        if (isCoverageEnabled()) {
+            gTestCmdLine.append("GCOV_PREFIX=/data/misc/trace/testcoverage ");
+        }
+
         // su to requested user
         if (mRunTestAs != null) {
             gTestCmdLine.append(String.format("su %s ", mRunTestAs));
@@ -559,13 +563,19 @@
      * Adds a {@link NativeCodeCoverageListener} to the chain if code coverage is enabled.
      *
      * @param device the device to pull the coverage results from
+     * @param coverageFlush whether to flush coverage before pulling the measurements
+     * @param coverageProcesses the processes to flush coverage from
      * @param listener the original listener
      * @return a chained listener if code coverage is enabled, otherwise the original listener
      */
     protected ITestInvocationListener addNativeCoverageListenerIfEnabled(
-            ITestDevice device, ITestInvocationListener listener) {
+            ITestDevice device,
+            boolean coverageFlush,
+            List<String> coverageProcesses,
+            ITestInvocationListener listener) {
         if (mCoverage) {
-            return new NativeCodeCoverageListener(device, listener);
+            return new NativeCodeCoverageListener(
+                    device, coverageFlush, coverageProcesses, listener);
         }
         return listener;
     }
diff --git a/src/com/android/tradefed/testtype/GTestListTestParser.java b/test_framework/com/android/tradefed/testtype/GTestListTestParser.java
similarity index 100%
rename from src/com/android/tradefed/testtype/GTestListTestParser.java
rename to test_framework/com/android/tradefed/testtype/GTestListTestParser.java
diff --git a/src/com/android/tradefed/testtype/GTestResultParser.java b/test_framework/com/android/tradefed/testtype/GTestResultParser.java
similarity index 100%
rename from src/com/android/tradefed/testtype/GTestResultParser.java
rename to test_framework/com/android/tradefed/testtype/GTestResultParser.java
diff --git a/src/com/android/tradefed/testtype/GTestXmlResultParser.java b/test_framework/com/android/tradefed/testtype/GTestXmlResultParser.java
similarity index 100%
rename from src/com/android/tradefed/testtype/GTestXmlResultParser.java
rename to test_framework/com/android/tradefed/testtype/GTestXmlResultParser.java
diff --git a/src/com/android/tradefed/testtype/GoogleBenchmarkResultParser.java b/test_framework/com/android/tradefed/testtype/GoogleBenchmarkResultParser.java
similarity index 100%
rename from src/com/android/tradefed/testtype/GoogleBenchmarkResultParser.java
rename to test_framework/com/android/tradefed/testtype/GoogleBenchmarkResultParser.java
diff --git a/src/com/android/tradefed/testtype/GoogleBenchmarkTest.java b/test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/GoogleBenchmarkTest.java
rename to test_framework/com/android/tradefed/testtype/GoogleBenchmarkTest.java
diff --git a/src/com/android/tradefed/testtype/HostGTest.java b/test_framework/com/android/tradefed/testtype/HostGTest.java
similarity index 99%
rename from src/com/android/tradefed/testtype/HostGTest.java
rename to test_framework/com/android/tradefed/testtype/HostGTest.java
index bfb4f54..8619236 100644
--- a/src/com/android/tradefed/testtype/HostGTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostGTest.java
@@ -180,6 +180,8 @@
                             String.format("Command run timed out after %d ms", maxTestTimeMs));
                 case EXCEPTION:
                     throw new RuntimeException("Command run failed with exception");
+                default:
+                    break;
             }
         } finally {
             resultParser.flush();
diff --git a/src/com/android/tradefed/testtype/HostTest.java b/test_framework/com/android/tradefed/testtype/HostTest.java
similarity index 98%
rename from src/com/android/tradefed/testtype/HostTest.java
rename to test_framework/com/android/tradefed/testtype/HostTest.java
index af30957..f5d7731 100644
--- a/src/com/android/tradefed/testtype/HostTest.java
+++ b/test_framework/com/android/tradefed/testtype/HostTest.java
@@ -871,6 +871,11 @@
                             classes.add(cls);
                             classNames.add(className);
                         }
+                    } catch (UnsupportedClassVersionError ucve) {
+                        throw new IllegalArgumentException(
+                                String.format(
+                                        "Could not load class %s from jar %s. Reason:\n%s",
+                                        className, jarName, StreamUtil.getStackTrace(ucve)));
                     } catch (ClassNotFoundException cnfe) {
                         throw new IllegalArgumentException(
                                 String.format("Cannot find test class %s", className));
diff --git a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java b/test_framework/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
rename to test_framework/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
diff --git a/src/com/android/tradefed/testtype/InstrumentationFileTest.java b/test_framework/com/android/tradefed/testtype/InstrumentationFileTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/InstrumentationFileTest.java
rename to test_framework/com/android/tradefed/testtype/InstrumentationFileTest.java
diff --git a/src/com/android/tradefed/testtype/InstrumentationSerialTest.java b/test_framework/com/android/tradefed/testtype/InstrumentationSerialTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/InstrumentationSerialTest.java
rename to test_framework/com/android/tradefed/testtype/InstrumentationSerialTest.java
diff --git a/src/com/android/tradefed/testtype/InstrumentationTest.java b/test_framework/com/android/tradefed/testtype/InstrumentationTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/InstrumentationTest.java
rename to test_framework/com/android/tradefed/testtype/InstrumentationTest.java
diff --git a/src/com/android/tradefed/testtype/JUnitRunUtil.java b/test_framework/com/android/tradefed/testtype/JUnitRunUtil.java
similarity index 100%
rename from src/com/android/tradefed/testtype/JUnitRunUtil.java
rename to test_framework/com/android/tradefed/testtype/JUnitRunUtil.java
diff --git a/src/com/android/tradefed/testtype/JavaCodeCoverageListener.java b/test_framework/com/android/tradefed/testtype/JavaCodeCoverageListener.java
similarity index 100%
rename from src/com/android/tradefed/testtype/JavaCodeCoverageListener.java
rename to test_framework/com/android/tradefed/testtype/JavaCodeCoverageListener.java
diff --git a/src/com/android/tradefed/testtype/MetricTestCase.java b/test_framework/com/android/tradefed/testtype/MetricTestCase.java
similarity index 100%
rename from src/com/android/tradefed/testtype/MetricTestCase.java
rename to test_framework/com/android/tradefed/testtype/MetricTestCase.java
diff --git a/src/com/android/tradefed/testtype/NativeBenchmarkTest.java b/test_framework/com/android/tradefed/testtype/NativeBenchmarkTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/NativeBenchmarkTest.java
rename to test_framework/com/android/tradefed/testtype/NativeBenchmarkTest.java
diff --git a/src/com/android/tradefed/testtype/NativeBenchmarkTestParser.java b/test_framework/com/android/tradefed/testtype/NativeBenchmarkTestParser.java
similarity index 100%
rename from src/com/android/tradefed/testtype/NativeBenchmarkTestParser.java
rename to test_framework/com/android/tradefed/testtype/NativeBenchmarkTestParser.java
diff --git a/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java b/test_framework/com/android/tradefed/testtype/NativeCodeCoverageListener.java
similarity index 79%
rename from src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
rename to test_framework/com/android/tradefed/testtype/NativeCodeCoverageListener.java
index dca83db..713c1fc 100644
--- a/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
+++ b/test_framework/com/android/tradefed/testtype/NativeCodeCoverageListener.java
@@ -26,9 +26,11 @@
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.ResultForwarder;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.NativeCodeCoverageFlusher;
 import com.android.tradefed.util.ZipUtil;
 
 import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
 
 import java.io.File;
 import java.io.IOException;
@@ -37,6 +39,7 @@
 import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * A {@link ResultForwarder} that will pull native coverage measurements off of the device and log
@@ -44,17 +47,35 @@
  */
 public final class NativeCodeCoverageListener extends ResultForwarder {
 
-    private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace/proc/self/cwd/out";
+    private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace";
     private static final String COVERAGE_FILE_LIST_COMMAND =
             String.format("find %s -name '*.gcda'", NATIVE_COVERAGE_DEVICE_PATH);
 
+    private final boolean mFlushCoverage;
+    private final List<String> mCoverageProcesses;
     private final ITestDevice mDevice;
+    private final NativeCodeCoverageFlusher mFlusher;
 
     private String mCurrentRunName;
 
     public NativeCodeCoverageListener(ITestDevice device, ITestInvocationListener... listeners) {
         super(listeners);
         mDevice = device;
+        mFlushCoverage = false;
+        mCoverageProcesses = ImmutableList.of();
+        mFlusher = new NativeCodeCoverageFlusher(mDevice);
+    }
+
+    public NativeCodeCoverageListener(
+            ITestDevice device,
+            boolean flushCoverage,
+            List<String> coverageProcesses,
+            ITestInvocationListener... listeners) {
+        super(listeners);
+        mDevice = device;
+        mFlushCoverage = flushCoverage;
+        mCoverageProcesses = ImmutableList.copyOf(coverageProcesses);
+        mFlusher = new NativeCodeCoverageFlusher(mDevice);
     }
 
     @Override
@@ -72,8 +93,15 @@
         try {
             localDir = FileUtil.createTempDir("native_coverage");
 
-            // Enable abd root on the device, otherwise the list command will fail.
+            // Enable abd root on the device, otherwise the following commands will fail.
             verify(mDevice.enableAdbRoot(), "Failed to enable adb root.");
+
+            // Flush cross-process coverage.
+            if (mFlushCoverage) {
+                mFlusher.forceCoverageFlush(mCoverageProcesses);
+            }
+
+            // List native coverage files on the device and pull them.
             String findResult = mDevice.executeShellCommand(COVERAGE_FILE_LIST_COMMAND);
 
             Path devicePathRoot = Paths.get(NATIVE_COVERAGE_DEVICE_PATH);
diff --git a/src/com/android/tradefed/testtype/NativeStressTest.java b/test_framework/com/android/tradefed/testtype/NativeStressTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/NativeStressTest.java
rename to test_framework/com/android/tradefed/testtype/NativeStressTest.java
diff --git a/src/com/android/tradefed/testtype/NativeStressTestParser.java b/test_framework/com/android/tradefed/testtype/NativeStressTestParser.java
similarity index 100%
rename from src/com/android/tradefed/testtype/NativeStressTestParser.java
rename to test_framework/com/android/tradefed/testtype/NativeStressTestParser.java
diff --git a/src/com/android/tradefed/testtype/UiAutomatorRunner.java b/test_framework/com/android/tradefed/testtype/UiAutomatorRunner.java
similarity index 100%
rename from src/com/android/tradefed/testtype/UiAutomatorRunner.java
rename to test_framework/com/android/tradefed/testtype/UiAutomatorRunner.java
diff --git a/src/com/android/tradefed/testtype/UiAutomatorTest.java b/test_framework/com/android/tradefed/testtype/UiAutomatorTest.java
similarity index 100%
rename from src/com/android/tradefed/testtype/UiAutomatorTest.java
rename to test_framework/com/android/tradefed/testtype/UiAutomatorTest.java
diff --git a/src/com/android/tradefed/testtype/host/PrettyTestEventLogger.java b/test_framework/com/android/tradefed/testtype/host/PrettyTestEventLogger.java
similarity index 100%
rename from src/com/android/tradefed/testtype/host/PrettyTestEventLogger.java
rename to test_framework/com/android/tradefed/testtype/host/PrettyTestEventLogger.java
diff --git a/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java b/test_framework/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java
similarity index 100%
rename from src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java
rename to test_framework/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java
diff --git a/src/com/android/tradefed/testtype/junit4/CarryDnaeError.java b/test_framework/com/android/tradefed/testtype/junit4/CarryDnaeError.java
similarity index 100%
rename from src/com/android/tradefed/testtype/junit4/CarryDnaeError.java
rename to test_framework/com/android/tradefed/testtype/junit4/CarryDnaeError.java
diff --git a/src/com/android/tradefed/testtype/junit4/DeviceParameterizedRunner.java b/test_framework/com/android/tradefed/testtype/junit4/DeviceParameterizedRunner.java
similarity index 100%
rename from src/com/android/tradefed/testtype/junit4/DeviceParameterizedRunner.java
rename to test_framework/com/android/tradefed/testtype/junit4/DeviceParameterizedRunner.java
diff --git a/src/com/android/tradefed/testtype/junit4/DeviceTestRunOptions.java b/test_framework/com/android/tradefed/testtype/junit4/DeviceTestRunOptions.java
similarity index 100%
rename from src/com/android/tradefed/testtype/junit4/DeviceTestRunOptions.java
rename to test_framework/com/android/tradefed/testtype/junit4/DeviceTestRunOptions.java
diff --git a/src/com/android/tradefed/testtype/junit4/LongevityHostRunner.java b/test_framework/com/android/tradefed/testtype/junit4/LongevityHostRunner.java
similarity index 100%
rename from src/com/android/tradefed/testtype/junit4/LongevityHostRunner.java
rename to test_framework/com/android/tradefed/testtype/junit4/LongevityHostRunner.java
diff --git a/src/com/android/tradefed/testtype/junit4/RunNotifierWrapper.java b/test_framework/com/android/tradefed/testtype/junit4/RunNotifierWrapper.java
similarity index 100%
rename from src/com/android/tradefed/testtype/junit4/RunNotifierWrapper.java
rename to test_framework/com/android/tradefed/testtype/junit4/RunNotifierWrapper.java
diff --git a/src/com/android/tradefed/testtype/junit4/builder/DeviceJUnit4ClassRunnerBuilder.java b/test_framework/com/android/tradefed/testtype/junit4/builder/DeviceJUnit4ClassRunnerBuilder.java
similarity index 100%
rename from src/com/android/tradefed/testtype/junit4/builder/DeviceJUnit4ClassRunnerBuilder.java
rename to test_framework/com/android/tradefed/testtype/junit4/builder/DeviceJUnit4ClassRunnerBuilder.java
diff --git a/src/com/android/tradefed/testtype/suite/AtestRunner.java b/test_framework/com/android/tradefed/testtype/suite/AtestRunner.java
similarity index 100%
rename from src/com/android/tradefed/testtype/suite/AtestRunner.java
rename to test_framework/com/android/tradefed/testtype/suite/AtestRunner.java
diff --git a/src/com/android/tradefed/util/clockwork/ClockworkUtils.java b/test_framework/com/android/tradefed/util/clockwork/ClockworkUtils.java
similarity index 100%
rename from src/com/android/tradefed/util/clockwork/ClockworkUtils.java
rename to test_framework/com/android/tradefed/util/clockwork/ClockworkUtils.java
diff --git a/src/com/android/tradefed/util/statsd/ConfigUtil.java b/test_framework/com/android/tradefed/util/statsd/ConfigUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/statsd/ConfigUtil.java
rename to test_framework/com/android/tradefed/util/statsd/ConfigUtil.java
diff --git a/src/com/android/tradefed/util/statsd/MetricUtil.java b/test_framework/com/android/tradefed/util/statsd/MetricUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/statsd/MetricUtil.java
rename to test_framework/com/android/tradefed/util/statsd/MetricUtil.java
diff --git a/test_result_interfaces/README.md b/test_result_interfaces/README.md
new file mode 100644
index 0000000..42fbdf1
--- /dev/null
+++ b/test_result_interfaces/README.md
@@ -0,0 +1,4 @@
+# Trade Federation Test Result Component
+
+A Tradefed component that describes how test cases results
+and logs are represented.
diff --git a/tests/.classpath b/tests/.classpath
index 79969f2..24d5cfa 100644
--- a/tests/.classpath
+++ b/tests/.classpath
@@ -23,5 +23,6 @@
 	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/prebuilts/tools/common/m2/truth-prebuilt-jar/linux_glibc_common/combined/truth-prebuilt-jar.jar"/>
 	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/external/jacoco/jacoco-cli/linux_glibc_common/combined/jacoco-cli.jar"/>
 	<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/tools/tradefederation/core/tradefed-protos/linux_glibc_common/combined/tradefed-protos.jar"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/platform-annotations"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/tests/res/config/tf/unit-runner-tests.xml b/tests/res/config/tf/unit-runner-tests.xml
new file mode 100644
index 0000000..75c86cf
--- /dev/null
+++ b/tests/res/config/tf/unit-runner-tests.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Executes the TF unit tests">
+    <!-- Use HostTest runner to have JUnit3 and JUnit4 Suite support -->
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="com.android.loganalysis.UnitTests" />
+        <option name="class" value="com.android.tradefed.UnitTests" />
+        <option name="class" value="com.android.tradefed.prodtests.UnitTests" />
+    </test>
+</configuration>
diff --git a/tests/res/config/tf/unit-runner.xml b/tests/res/config/tf/unit-runner.xml
index 8c11a96..ddc527c 100644
--- a/tests/res/config/tf/unit-runner.xml
+++ b/tests/res/config/tf/unit-runner.xml
@@ -17,12 +17,9 @@
     <option name="loop" value="false" />
     <option name="null-device" value="true" />
     <build_provider class="com.android.tradefed.build.StubBuildProvider" />
-    <!-- Use HostTest runner to have JUnit3 and JUnit4 Suite support -->
-    <test class="com.android.tradefed.testtype.HostTest" >
-        <option name="class" value="com.android.loganalysis.UnitTests" />
-        <option name="class" value="com.android.tradefed.UnitTests" />
-        <option name="class" value="com.android.tradefed.prodtests.UnitTests" />
-    </test>
+
+    <include name="tf/unit-runner-tests" />
+
     <logger class="com.android.tradefed.log.FileLogger" />
     <result_reporter class="com.android.tradefed.result.SubprocessResultsReporter">
         <option name="output-test-log" value="true" />
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 1f5647d..650ef8c 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -184,7 +184,7 @@
 import com.android.tradefed.targetprep.AllTestAppsInstallSetupTest;
 import com.android.tradefed.targetprep.AoaTargetPreparerTest;
 import com.android.tradefed.targetprep.AppSetupTest;
-import com.android.tradefed.targetprep.BuildInfoAttributePreparerTest;
+import com.android.tradefed.targetprep.BaseTargetPreparerTest;
 import com.android.tradefed.targetprep.CreateUserPreparerTest;
 import com.android.tradefed.targetprep.DefaultTestsZipInstallerTest;
 import com.android.tradefed.targetprep.DeviceFlashPreparerTest;
@@ -198,7 +198,6 @@
 import com.android.tradefed.targetprep.InstallApexModuleTargetPreparerTest;
 import com.android.tradefed.targetprep.InstallApkSetupTest;
 import com.android.tradefed.targetprep.InstrumentationPreparerTest;
-import com.android.tradefed.targetprep.PreloadedClassesPreparerTest;
 import com.android.tradefed.targetprep.PushFilePreparerTest;
 import com.android.tradefed.targetprep.PythonVirtualenvPreparerTest;
 import com.android.tradefed.targetprep.RebootTargetPreparerTest;
@@ -211,7 +210,6 @@
 import com.android.tradefed.targetprep.SystemUpdaterDeviceFlasherTest;
 import com.android.tradefed.targetprep.TestAppInstallSetupTest;
 import com.android.tradefed.targetprep.TestFilePushSetupTest;
-import com.android.tradefed.targetprep.TimeSetterTargetPreparerTest;
 import com.android.tradefed.targetprep.UserCleanerTest;
 import com.android.tradefed.targetprep.adb.AdbStopServerPreparerTest;
 import com.android.tradefed.targetprep.app.NoApkTestSkipperTest;
@@ -278,6 +276,7 @@
 import com.android.tradefed.testtype.suite.module.NativeBridgeModuleControllerTest;
 import com.android.tradefed.testtype.suite.params.InstantAppHandlerTest;
 import com.android.tradefed.testtype.suite.params.ModuleParametersHelperTest;
+import com.android.tradefed.testtype.suite.params.SecondaryUserHandlerTest;
 import com.android.tradefed.testtype.suite.retry.ResultsPlayerTest;
 import com.android.tradefed.testtype.suite.retry.RetryReschedulerTest;
 import com.android.tradefed.util.AaptParserTest;
@@ -344,6 +343,7 @@
 import com.android.tradefed.util.net.XmlRpcHelperTest;
 import com.android.tradefed.util.proto.TestRecordProtoUtilTest;
 import com.android.tradefed.util.proto.TfMetricProtoUtilTest;
+import com.android.tradefed.util.RemoteZipTest;
 import com.android.tradefed.util.sl4a.Sl4aClientTest;
 import com.android.tradefed.util.sl4a.Sl4aEventDispatcherTest;
 import com.android.tradefed.util.statsd.ConfigUtilTest;
@@ -578,7 +578,7 @@
     AllTestAppsInstallSetupTest.class,
     AoaTargetPreparerTest.class,
     AppSetupTest.class,
-    BuildInfoAttributePreparerTest.class,
+    BaseTargetPreparerTest.class,
     CreateUserPreparerTest.class,
     DefaultTestsZipInstallerTest.class,
     DeviceFlashPreparerTest.class,
@@ -593,7 +593,6 @@
     InstallApexModuleTargetPreparerTest.class,
     InstallApkSetupTest.class,
     InstrumentationPreparerTest.class,
-    PreloadedClassesPreparerTest.class,
     PushFilePreparerTest.class,
     PythonVirtualenvPreparerTest.class,
     RebootTargetPreparerTest.class,
@@ -605,7 +604,6 @@
     SystemUpdaterDeviceFlasherTest.class,
     TestAppInstallSetupTest.class,
     TestFilePushSetupTest.class,
-    TimeSetterTargetPreparerTest.class,
     SwitchUserTargetPreparerTest.class,
     UserCleanerTest.class,
 
@@ -715,6 +713,7 @@
     // testtype/suite/params
     InstantAppHandlerTest.class,
     ModuleParametersHelperTest.class,
+    SecondaryUserHandlerTest.class,
 
     // testtype/suite/retry
     ResultsPlayerTest.class,
@@ -756,6 +755,7 @@
     PsParserTest.class,
     QuotationAwareTokenizerTest.class,
     RegexTrieTest.class,
+    RemoteZipTest.class,
     RunUtilTest.class,
     SerializationUtilTest.class,
     ShellOutputReceiverStreamTest.class,
diff --git a/tests/src/com/android/tradefed/config/ConfigurationTest.java b/tests/src/com/android/tradefed/config/ConfigurationTest.java
index 5649fb0..39a57c9 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationTest.java
@@ -684,6 +684,13 @@
      * shards are kicked-off in new invocations.
      */
     public void testValidateOptions_localSharding_skipDownload() throws ConfigurationException {
+        mConfig =
+                new Configuration(CONFIG_NAME, CONFIG_DESCRIPTION) {
+                    @Override
+                    protected boolean isRemoteEnvironment() {
+                        return false;
+                    }
+                };
         CommandOptions options = new CommandOptions();
         options.setShardCount(5);
         options.setShardIndex(null);
diff --git a/tests/src/com/android/tradefed/device/NativeDeviceTest.java b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
index 9afd711..3c6bf79 100644
--- a/tests/src/com/android/tradefed/device/NativeDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
@@ -50,6 +50,7 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.ProcessInfo;
 import com.android.tradefed.util.StreamUtil;
 
 import org.easymock.EasyMock;
@@ -2211,7 +2212,7 @@
 
     /** Test get ProcessInfo by process name */
     @Test
-    public void testGetProcessWithStartTimeByName() throws Exception {
+    public void testGetProcessByName() throws Exception {
         final String fakePid = "914";
         final String fakeCreationTime = "1559091922";
         TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
@@ -2228,7 +2229,7 @@
 
     /** Test get ProcessInfo by process name return null for invalid process */
     @Test
-    public void testGetProcessWithStartTimeByNameInvalidProcess() throws Exception {
+    public void testGetProcessByNameInvalidProcess() throws Exception {
         TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
         doReturn("").when(spy).executeShellCommand("pidof system_server");
         EasyMock.replay(mMockIDevice);
@@ -2236,6 +2237,19 @@
         EasyMock.verify(mMockIDevice);
     }
 
+    /** Test get ProcessInfo by process name return null for invalid process */
+    @Test
+    public void testGetProcessByNameInvalidStartTime() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("120").when(spy).executeShellCommand("pidof system_server");
+        doReturn("stat: '/proc/120': No such file or directory")
+                .when(spy)
+                .executeShellCommand("stat -c%Z /proc/120");
+        EasyMock.replay(mMockIDevice);
+        assertNull(spy.getProcessByName("system_server"));
+        EasyMock.verify(mMockIDevice);
+    }
+
     /** Test get boot history */
     @Test
     public void testGetBootHistory() throws Exception {
@@ -2246,7 +2260,7 @@
                                 + "        reboot,,1556237796\n"
                                 + "        reboot,,1556237725\n")
                 .when(spy)
-                .getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
         Map<Long, String> history = new LinkedHashMap<Long, String>();
         history.put(1556587278L, "kernel_panic");
         history.put(1556238008L, "reboot");
@@ -2261,7 +2275,9 @@
     @Test
     public void testGetBootHistoryEmpty() throws Exception {
         TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
-        doReturn("").when(spy).getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+        doReturn("")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
         EasyMock.replay(mMockIDevice);
         assertTrue(spy.getBootHistory().isEmpty());
         EasyMock.verify(mMockIDevice);
@@ -2271,7 +2287,9 @@
     @Test
     public void testGetBootHistoryInvalid() throws Exception {
         TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
-        doReturn("invalid output").when(spy).getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+        doReturn("invalid output")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
         EasyMock.replay(mMockIDevice);
         assertTrue(spy.getBootHistory().isEmpty());
         EasyMock.verify(mMockIDevice);
@@ -2287,11 +2305,221 @@
                                 + "        reboot,,1556237796\n"
                                 + "        reboot,,1556237725\n")
                 .when(spy)
-                .getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+                .executeShellCommand("getprop " + 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));
+        assertEquals(history, spy.getBootHistorySince(1556238008L, TimeUnit.SECONDS));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test get boot history since */
+    @Test
+    public void testGetBootHistorySinceInMillisecond() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn(
+                        "kernel_panic,1556587278\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
+        Map<Long, String> history = new LinkedHashMap<Long, String>();
+        history.put(1556587278L, "kernel_panic");
+        EasyMock.replay(mMockIDevice);
+        assertEquals(history, spy.getBootHistorySince(1556238008000L, TimeUnit.MILLISECONDS));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test deviceSoftRestartedSince */
+    @Test
+    public void testDeviceSoftRestartedSince() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("914").when(spy).executeShellCommand("pidof system_server");
+        doReturn("1559091922").when(spy).executeShellCommand("stat -c%Z /proc/914");
+        doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/914");
+        doReturn(
+                        "kernel_panic,1556587278\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        assertFalse(spy.deviceSoftRestartedSince(1559091923L, TimeUnit.SECONDS));
+        assertFalse(spy.deviceSoftRestartedSince(1559091923000L, TimeUnit.MILLISECONDS));
+        assertFalse(spy.deviceSoftRestartedSince(1559091922L, TimeUnit.SECONDS));
+        assertFalse(spy.deviceSoftRestartedSince(1559091922000L, TimeUnit.MILLISECONDS));
+        assertTrue(spy.deviceSoftRestartedSince(1559091921L, TimeUnit.SECONDS));
+        assertTrue(spy.deviceSoftRestartedSince(1559091921000L, TimeUnit.MILLISECONDS));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test deviceSoftRestartedSince return true with system_server stopped */
+    @Test
+    public void testDeviceSoftRestartedSinceWithSystemServerStopped() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("").when(spy).executeShellCommand("pidof system_server");
+        assertTrue(spy.deviceSoftRestartedSince(1559091922L, TimeUnit.SECONDS));
+    }
+
+    /** Test deviceSoftRestartedSince throw RuntimeException with abnormal reboot */
+    @Test
+    public void testDeviceSoftRestartedSinceWithAbnormalReboot() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("914").when(spy).executeShellCommand("pidof system_server");
+        doReturn("1559091999").when(spy).executeShellCommand("stat -c%Z /proc/914");
+        doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/914");
+        doReturn(
+                        "kernel_panic,1559091933\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        try {
+            spy.deviceSoftRestartedSince(1559091922L, TimeUnit.SECONDS);
+        } catch (RuntimeException e) {
+            //expected
+            return;
+        }
+        fail("RuntimeException is expected");
+    }
+
+    /** Test deviceSoftRestartedSince return false with normal reboot */
+    @Test
+    public void testDeviceSoftRestartedSinceNotAfterNormalReboot() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("914").when(spy).executeShellCommand("pidof system_server");
+        doReturn("1559091939").when(spy).executeShellCommand("stat -c%Z /proc/914");
+        doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/914");
+        doReturn(
+                        "reboot,1559091933\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        assertFalse(spy.deviceSoftRestartedSince(1559091921L, TimeUnit.SECONDS));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test deviceSoftRestartedSince return false with normal reboot */
+    @Test
+    public void testDeviceSoftRestartedSinceAfterNormalReboot() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("914").when(spy).executeShellCommand("pidof system_server");
+        doReturn("1559091992").when(spy).executeShellCommand("stat -c%Z /proc/914");
+        doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/914");
+        doReturn(
+                        "reboot,1559091933\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        assertTrue(spy.deviceSoftRestartedSince(1559091921L, TimeUnit.SECONDS));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test deviceSoftRestarted given the previous system_server {@link ProcessInfo} */
+    @Test
+    public void testDeviceSoftRestarted() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        ProcessInfo prev1 = new ProcessInfo("system", 123, "system_server", 1559000000L);
+        ProcessInfo prev2 = new ProcessInfo("system", 914, "system_server", 1559091922L);
+        doReturn("914").when(spy).executeShellCommand("pidof system_server");
+        doReturn("1559091922").when(spy).executeShellCommand("stat -c%Z /proc/914");
+        doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/914");
+        doReturn(
+                        "kernel_panic,1556587278\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        assertTrue(spy.deviceSoftRestarted(prev1));
+        assertFalse(spy.deviceSoftRestarted(prev2));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test deviceSoftRestarted return true with system_server stopped */
+    @Test
+    public void testDeviceSoftRestartedWithSystemServerStopped() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("").when(spy).executeShellCommand("pidof system_server");
+        assertTrue(
+                spy.deviceSoftRestarted(
+                        new ProcessInfo("system", 123, "system_server", 1559000000L)));
+    }
+
+    /** Test deviceSoftRestarted throw RuntimeException with abnormal reboot */
+    @Test
+    public void testDeviceSoftRestartedWithAbnormalReboot() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("914").when(spy).executeShellCommand("pidof system_server");
+        doReturn("1559091999").when(spy).executeShellCommand("stat -c%Z /proc/914");
+        doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/914");
+        doReturn(
+                        "kernel_panic,1559091933\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        try {
+            spy.deviceSoftRestarted(new ProcessInfo("system", 123, "system_server", 1559000000L));
+        } catch (RuntimeException e) {
+            //expected
+            return;
+        }
+        fail("Abnormal reboot is detected, RuntimeException is expected");
+    }
+
+    /** Test ddeviceSoftRestarted return false with normal reboot */
+    @Test
+    public void testDeviceSoftRestartedNotAfterNormalReboot() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("914").doReturn("914").when(spy).executeShellCommand("pidof system_server");
+        doReturn("1559091935").when(spy).executeShellCommand("stat -c%Z /proc/914");
+        doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/914");
+        doReturn(
+                        "reboot,,1559091933\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        assertFalse(
+                spy.deviceSoftRestarted(
+                        new ProcessInfo("system", 123, "system_server", 1559000000L)));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test deviceSoftRestarted return true if system_server restarted after normal reboot */
+    @Test
+    public void testDeviceSoftRestartedAfterNormalReboot() throws Exception {
+        TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+        doReturn("914").doReturn("914").when(spy).executeShellCommand("pidof system_server");
+        doReturn("1559091995").when(spy).executeShellCommand("stat -c%Z /proc/914");
+        doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/914");
+        doReturn(
+                        "reboot,,1559091933\n"
+                                + "        reboot,,1556238008\n"
+                                + "        reboot,,1556237796\n"
+                                + "        reboot,,1556237725\n")
+                .when(spy)
+                .executeShellCommand("getprop " + DeviceProperties.BOOT_REASON_HISTORY);
+        EasyMock.replay(mMockIDevice);
+        assertTrue(
+                spy.deviceSoftRestarted(
+                        new ProcessInfo("system", 123, "system_server", 1559000000L)));
         EasyMock.verify(mMockIDevice);
     }
 
diff --git a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
index 1f0de39..0cf8ae7 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
@@ -36,6 +36,7 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.KeyguardControllerState;
+import com.android.tradefed.util.ProcessInfo;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
 
@@ -52,6 +53,7 @@
 import java.io.IOException;
 import java.net.URLConnection;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import javax.imageio.ImageIO;
 
@@ -607,6 +609,60 @@
         }
     }
 
+    private ProcessInfo waitForSystemServerProcess() throws DeviceNotAvailableException {
+        ProcessInfo systemServer = null;
+        for (int i = 0; i < 5; i++) {
+            systemServer = mTestDevice.getProcessByName("system_server");
+            if (systemServer != null) {
+                return systemServer;
+            }
+            RunUtil.getDefault().sleep(1000);
+        }
+        Log.i(LOG_TAG, "The system_server process fails to come up");
+        return null;
+    }
+
+    /** Test device soft-restart detection API. */
+    @Test
+    public void testDeviceSoftRestart() throws DeviceNotAvailableException {
+        Log.i(LOG_TAG, "testDeviceSoftRestartSince");
+
+        // Get system_server process info
+        ProcessInfo prev = mTestDevice.getProcessByName("system_server");
+        long deviceTimeMs = mTestDevice.getDeviceDate();
+        if (prev == null) {
+            Log.i(LOG_TAG, "System_server process does not exist. Abort testDeviceSoftRestart.");
+            return;
+        }
+        assertFalse(mTestDevice.deviceSoftRestartedSince(prev.getStartTime(), TimeUnit.SECONDS));
+        assertFalse(mTestDevice.deviceSoftRestarted(prev));
+        if (!mTestDevice.isAdbRoot()) {
+            mTestDevice.enableAdbRoot();
+        }
+        mTestDevice.executeShellCommand(String.format("kill %s", prev.getPid()));
+        RunUtil.getDefault().sleep(1000);
+        assertTrue(mTestDevice.deviceSoftRestartedSince(deviceTimeMs, TimeUnit.MILLISECONDS));
+        assertTrue(mTestDevice.deviceSoftRestarted(prev));
+        prev = waitForSystemServerProcess();
+        deviceTimeMs = mTestDevice.getDeviceDate();
+        mTestDevice.reboot();
+        if (!mTestDevice.isAdbRoot()) {
+            mTestDevice.enableAdbRoot();
+        }
+        assertFalse(mTestDevice.deviceSoftRestartedSince(deviceTimeMs, TimeUnit.MILLISECONDS));
+        assertFalse(mTestDevice.deviceSoftRestarted(prev));
+        // Restart system_server 10 seconds after reboot
+        RunUtil.getDefault().sleep(10000);
+        mTestDevice.executeShellCommand(
+                String.format("kill %s", mTestDevice.getProcessByName("system_server").getPid()));
+        RunUtil.getDefault().sleep(1000);
+        assertTrue(mTestDevice.deviceSoftRestartedSince(deviceTimeMs, TimeUnit.MILLISECONDS));
+        assertTrue(mTestDevice.deviceSoftRestarted(prev));
+        waitForSystemServerProcess();
+        assertTrue(mTestDevice.deviceSoftRestartedSince(deviceTimeMs, TimeUnit.MILLISECONDS));
+        assertTrue(mTestDevice.deviceSoftRestarted(prev));
+    }
+
     /**
      * Verify that {@link TestDevice#clearErrorDialogs()} can successfully clear an error dialog
      * from screen.
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index c3c1b00..d870399 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -1308,8 +1308,8 @@
      */
     public void testRuntimePermissionSupportedLmpRelease() throws Exception {
         injectSystemProperty("ro.build.version.sdk", "21");
-        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "REL");
-        injectSystemProperty(TestDevice.BUILD_ID_PROP, "1642709");
+        injectSystemProperty(DeviceProperties.BUILD_CODENAME, "REL");
+        injectSystemProperty(DeviceProperties.BUILD_ID, "1642709");
         replayMocks();
         assertFalse(mTestDevice.isRuntimePermissionSupported());
     }
@@ -1321,8 +1321,8 @@
      */
     public void testRuntimePermissionSupportedLmpMr1Dev() throws Exception {
         injectSystemProperty("ro.build.version.sdk", "22");
-        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "REL");
-        injectSystemProperty(TestDevice.BUILD_ID_PROP, "1844090");
+        injectSystemProperty(DeviceProperties.BUILD_CODENAME, "REL");
+        injectSystemProperty(DeviceProperties.BUILD_ID, "1844090");
         replayMocks();
         assertFalse(mTestDevice.isRuntimePermissionSupported());
     }
@@ -1334,8 +1334,8 @@
      */
     public void testRuntimePermissionSupportedNonMncLocal() throws Exception {
         injectSystemProperty("ro.build.version.sdk", "21");
-        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "LMP");
-        injectSystemProperty(TestDevice.BUILD_ID_PROP, "eng.foo.20150414.190304");
+        injectSystemProperty(DeviceProperties.BUILD_CODENAME, "LMP");
+        injectSystemProperty(DeviceProperties.BUILD_ID, "eng.foo.20150414.190304");
         replayMocks();
         assertFalse(mTestDevice.isRuntimePermissionSupported());
     }
@@ -2550,7 +2550,7 @@
      */
     public void testMaxNumberOfRunningUsersSupported() throws Exception {
         injectSystemProperty("ro.build.version.sdk", "28");
-        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "REL");
+        injectSystemProperty(DeviceProperties.BUILD_CODENAME, "REL");
         final String getMaxRunningUsersCommand = "pm get-max-running-users";
         injectShellResponse(getMaxRunningUsersCommand, "Maximum supported running users: 4");
         replayMocks();
@@ -2560,7 +2560,7 @@
     /** Test that invalid output is handled by {@link TestDevice#getMaxNumberOfUsersSupported()}. */
     public void testMaxNumberOfRunningUsersSupported_invalid() throws Exception {
         injectSystemProperty("ro.build.version.sdk", "28");
-        injectSystemProperty(TestDevice.BUILD_CODENAME_PROP, "REL");
+        injectSystemProperty(DeviceProperties.BUILD_CODENAME, "REL");
         final String getMaxRunningUsersCommand = "pm get-max-running-users";
         injectShellResponse(getMaxRunningUsersCommand, "not the output we expect");
         replayMocks();
@@ -2847,7 +2847,7 @@
      * @throws Exception
      */
     public void testGetBuildSigningKeys_test_keys() throws Exception {
-        injectSystemProperty(TestDevice.BUILD_TAGS, "test-keys");
+        injectSystemProperty(DeviceProperties.BUILD_TAGS, "test-keys");
         replayMocks();
         assertEquals("test-keys", mTestDevice.getBuildSigningKeys());
     }
@@ -2858,7 +2858,7 @@
      * @throws Exception
      */
     public void testGetBuildSigningKeys_test_keys_commas() throws Exception {
-        injectSystemProperty(TestDevice.BUILD_TAGS, "test-keys,foo,bar,yadda");
+        injectSystemProperty(DeviceProperties.BUILD_TAGS, "test-keys,foo,bar,yadda");
         replayMocks();
         assertEquals("test-keys", mTestDevice.getBuildSigningKeys());
     }
@@ -2868,7 +2868,7 @@
      * @throws Exception
      */
     public void testGetBuildSigningKeys_not_matched() throws Exception {
-        injectSystemProperty(TestDevice.BUILD_TAGS, "huh,foo,bar,yadda");
+        injectSystemProperty(DeviceProperties.BUILD_TAGS, "huh,foo,bar,yadda");
         replayMocks();
         assertNull(mTestDevice.getBuildSigningKeys());
     }
diff --git a/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java b/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java
index 269e4ce..a38ced6 100644
--- a/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/GceManagerTest.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doReturn;
 
@@ -36,6 +37,7 @@
 
 import com.google.common.net.HostAndPort;
 
+import org.easymock.Capture;
 import org.easymock.EasyMock;
 import org.junit.After;
 import org.junit.Assert;
@@ -570,7 +572,7 @@
                                 EasyMock.eq("--instance_names"),
                                 EasyMock.eq("instance1"),
                                 EasyMock.eq("--config_file"),
-                                EasyMock.eq(mGceManager.getAvdConfigFile().getAbsolutePath()),
+                                EasyMock.contains(mGceManager.getAvdConfigFile().getAbsolutePath()),
                                 EasyMock.eq("--report_file"),
                                 EasyMock.anyObject()))
                 .andReturn(cmd);
@@ -580,6 +582,34 @@
         EasyMock.verify(mMockRunUtil);
     }
 
+    @Test
+    public void testShutdownGce_noWait() throws Exception {
+        OptionSetter setter = new OptionSetter(mOptions);
+        setter.setOptionValue("wait-gce-teardown", "false");
+        mGceManager =
+                new GceManager(
+                        mMockDeviceDesc, mOptions, mMockBuildInfo, null, "instance1", "host1") {
+                    @Override
+                    IRunUtil getRunUtil() {
+                        return mMockRunUtil;
+                    }
+                };
+        mGceManager.startGce();
+        CommandResult cmd = new CommandResult();
+        cmd.setStatus(CommandStatus.SUCCESS);
+        cmd.setStdout("output");
+        Capture<List<String>> capture = new Capture<>();
+        EasyMock.expect(mMockRunUtil.runCmdInBackground(EasyMock.<List<String>>capture(capture)))
+                .andReturn(Mockito.mock(Process.class));
+
+        EasyMock.replay(mMockRunUtil);
+        mGceManager.shutdownGce();
+        EasyMock.verify(mMockRunUtil);
+
+        List<String> args = capture.getValue();
+        assertTrue(args.get(5).contains(mAvdBinary.getName()));
+    }
+
     /**
      * Test for {@link GceManager#shutdownGce() }.
      *
@@ -609,7 +639,7 @@
                                 EasyMock.eq("--instance_names"),
                                 EasyMock.eq("instance1"),
                                 EasyMock.eq("--config_file"),
-                                EasyMock.eq(mGceManager.getAvdConfigFile().getAbsolutePath()),
+                                EasyMock.contains(mGceManager.getAvdConfigFile().getAbsolutePath()),
                                 EasyMock.eq("--service_account_json_private_key_path"),
                                 EasyMock.eq("/path/to/key.json"),
                                 EasyMock.eq("--report_file"),
diff --git a/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java
index 7bf87e6..38d7254 100644
--- a/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java
@@ -15,14 +15,16 @@
  */
 package com.android.tradefed.device.cloud;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotEquals;
 
 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 com.android.tradefed.log.ITestLogger;
 
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -38,6 +40,7 @@
     private IDevice mIDevice;
     private IDeviceStateMonitor mStateMonitor;
     private IDeviceMonitor mDeviceMonitor;
+    private ITestLogger mMockLogger;
 
     @BeforeClass
     public static void setUpClass() throws Exception {
@@ -53,7 +56,9 @@
         mIDevice = Mockito.mock(IDevice.class);
         mStateMonitor = Mockito.mock(IDeviceStateMonitor.class);
         mDeviceMonitor = Mockito.mock(IDeviceMonitor.class);
+        mMockLogger = Mockito.mock(ITestLogger.class);
         mDevice = new ManagedRemoteDevice(mIDevice, mStateMonitor, mDeviceMonitor);
+        mDevice.setTestLogger(mMockLogger);
     }
 
     @Test
@@ -63,6 +68,11 @@
         TestDeviceOptions get = mDevice.getOptions();
         assertFalse(get.equals(originalOptions));
         TestDeviceOptions get2 = mDevice.getOptions();
-        assertTrue(get2.equals(get));
+        // Same during the same session
+        assertEquals(get2, get);
+
+        mDevice.postInvocationTearDown(null);
+        TestDeviceOptions get3 = mDevice.getOptions();
+        assertNotEquals(get2, get3);
     }
 }
diff --git a/tests/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollectorTest.java
index 7933634..a19c8c9 100644
--- a/tests/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/DebugHostLogOnFailureCollectorTest.java
@@ -17,6 +17,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
 
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
@@ -126,4 +127,38 @@
                         Mockito.<HashMap<String, Metric>>any());
         Mockito.verify(mMockListener).testRunEnded(0L, new HashMap<String, Metric>());
     }
+
+    /** Test when we fail to obtain the host_log from the start of the collector. */
+    @Test
+    public void testCollect_null() throws Exception {
+        TestDescription test = new TestDescription("class", "test");
+        // Buffer at testRunStarted, then the one we want to log
+        Mockito.when(mMockLogger.getLog()).thenReturn(null);
+        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>());
+
+        // Ensure the callback went through
+        assertTrue(mCollector.mOnTestStartCalled);
+        assertTrue(mCollector.mOnTestFailCalled);
+
+        Mockito.verify(mMockListener).testRunStarted("runName", 1);
+        Mockito.verify(mMockListener).testStarted(Mockito.eq(test), Mockito.anyLong());
+        Mockito.verify(mMockListener).testFailed(Mockito.eq(test), Mockito.any());
+        // No file is logged
+        Mockito.verify(mMockListener, never())
+                .testLog(
+                        Mockito.eq("class#test-debug-hostlog-on-failure"),
+                        Mockito.eq(LogDataType.TEXT),
+                        Mockito.any());
+        Mockito.verify(mMockListener)
+                .testEnded(
+                        Mockito.eq(test),
+                        Mockito.anyLong(),
+                        Mockito.<HashMap<String, Metric>>any());
+        Mockito.verify(mMockListener).testRunEnded(0L, new HashMap<String, Metric>());
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
index d213104..61945dc 100644
--- a/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
@@ -82,7 +82,7 @@
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         mMockDevice = EasyMock.createMock(ITestDevice.class);
         mNullMockDevice = EasyMock.createMock(ITestDevice.class);
         mMockListener = EasyMock.createMock(ITestInvocationListener.class);
@@ -101,6 +101,7 @@
 
     @Test
     public void testCollect() throws Exception {
+        EasyMock.expect(mMockDevice.getApiLevel()).andReturn(20);
         mMockReceiver.start();
         mMockReceiver.clear();
         mMockReceiver.stop();
@@ -137,6 +138,41 @@
         assertTrue(mCollector.mOnTestFailCalled);
     }
 
+    /**
+     * If the API level support of the device is lower than a threshold we fall back to a different
+     * collection for the logcat.
+     */
+    @Test
+    public void testCollect_legacy() throws Exception {
+        EasyMock.expect(mMockDevice.getApiLevel()).andReturn(18);
+        mMockListener.testRunStarted("runName", 1);
+        TestDescription test = new TestDescription("class", "test");
+        mMockListener.testStarted(EasyMock.eq(test), EasyMock.anyLong());
+        mMockListener.testFailed(EasyMock.eq(test), EasyMock.anyObject());
+        mMockListener.testEnded(
+                EasyMock.eq(test),
+                EasyMock.anyLong(),
+                EasyMock.<HashMap<String, Metric>>anyObject());
+        mMockListener.testRunEnded(0L, new HashMap<String, Metric>());
+        mMockDevice.executeShellCommand(EasyMock.eq("logcat -t 5000"), EasyMock.anyObject());
+        mMockListener.testLog(
+                EasyMock.eq("class#test-serial-logcat-on-failure"),
+                EasyMock.eq(LogDataType.LOGCAT),
+                EasyMock.anyObject());
+
+        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, mNullMockDevice);
+        // Ensure the callback went through
+        assertTrue(mCollector.mOnTestStartCalled);
+        assertTrue(mCollector.mOnTestFailCalled);
+    }
+
     @Test
     public void testCollect_noRuns() throws Exception {
         // If there was no runs, nothing should be done.
@@ -149,6 +185,7 @@
 
     @Test
     public void testCollect_multiRun() throws Exception {
+        EasyMock.expect(mMockDevice.getApiLevel()).andStubReturn(20);
         mMockReceiver.start();
         EasyMock.expectLastCall().times(2);
         mMockReceiver.clear();
diff --git a/tests/src/com/android/tradefed/device/metric/RuntimeRestartCollectorTest.java b/tests/src/com/android/tradefed/device/metric/RuntimeRestartCollectorTest.java
index c626eca..0d48c7c 100644
--- a/tests/src/com/android/tradefed/device/metric/RuntimeRestartCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/RuntimeRestartCollectorTest.java
@@ -438,11 +438,12 @@
     }
 
     /**
-     * Test that the collector reports counts based on the {@link AppCrashOccurred} results when it
-     * disagrees with info from statsd metadata.
+     * Test that the collector reports counts based on the {@link StatsdStatsReport} results when it
+     * disagrees with info from the {@link AppCrashOccurred} atom.
      */
     @Test
-    public void testAddingMetrics_withRuntimeRestart_useAtomResultsForCount() throws Exception {
+    public void testAddingMetrics_withRuntimeRestart_useStatsdMetadataResultsForCount()
+            throws Exception {
         ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
         doReturn(Arrays.asList(testDevice)).when(mContext).getDevices();
         // Two data points from the AppCrashOccurred data.
@@ -471,7 +472,7 @@
         // Count should be two as in the stubbed EventMetricDataResults, even though statsd metadata
         // only reported one timestamp.
         int count = getCount(runMetrics);
-        Assert.assertEquals(2, count);
+        Assert.assertEquals(1, count);
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java
index 8b8c6ac..1dfe325 100644
--- a/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java
@@ -75,6 +75,7 @@
     private IConfiguration mConfig;
     private ITestInvocationListener mMockListener;
     private ITestDevice mMockDevice;
+    private ITestLogger mMockLogger;
 
     @Before
     public void setUp() {
@@ -82,6 +83,7 @@
         mContext = new InvocationContext();
         mConfig = new Configuration("test", "test");
         mMockListener = mock(ITestInvocationListener.class);
+        mMockLogger = mock(ITestLogger.class);
         mMockDevice = EasyMock.createMock(ITestDevice.class);
         // Reset the counters
         TestBaseMetricCollector.sTotalInit = 0;
@@ -253,7 +255,7 @@
         mContext.addAllocatedDevice("default", mock(ITestDevice.class));
 
         mExec.doSetup(mContext, mConfig, mMockListener);
-        mExec.doTeardown(mContext, mConfig, null, null);
+        mExec.doTeardown(mContext, mConfig, mMockLogger, null);
 
         // Pre multi preparers are always called before.
         InOrder inOrder = Mockito.inOrder(stub1, stub2, stub3, stub4, cleaner);
@@ -300,7 +302,7 @@
         mContext.addAllocatedDevice("default", mock(ITestDevice.class));
         // Ensure that the original error is the one passed around.
         Throwable exception = new Throwable("Original error");
-        mExec.doTeardown(mContext, mConfig, null, exception);
+        mExec.doTeardown(mContext, mConfig, mMockLogger, exception);
 
         InOrder inOrder = Mockito.inOrder(stub1, stub2, stub3, stub4, cleaner);
 
diff --git a/tests/src/com/android/tradefed/invoker/RemoteInvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/RemoteInvocationExecutionTest.java
index bb688b0..25de280 100644
--- a/tests/src/com/android/tradefed/invoker/RemoteInvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/RemoteInvocationExecutionTest.java
@@ -104,6 +104,9 @@
                     ((DeviceSelectionOptions)
                                     reparse.getDeviceConfig().get(0).getDeviceRequirements())
                             .getDeviceTypeRequested());
+            assertEquals(
+                    "",
+                    reparse.getDeviceConfig().get(0).getDeviceOptions().getRemoteTf().getPath());
         } finally {
             FileUtil.deleteFile(res);
         }
diff --git a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
index 8623025..3f5536e 100644
--- a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
@@ -372,8 +372,6 @@
         // No tests to run but we still call start/end
         Mockito.verify(mMockListener).invocationStarted(mContext);
         Mockito.verify(mMockListener).invocationFailed(exception);
-        Mockito.verify(mMockListener)
-                .testLog(eq(TestInvocation.TRADEFED_LOG_NAME), eq(LogDataType.TEXT), any());
         Mockito.verify(mMockListener).invocationEnded(0L);
         // Invocation did not start for real so context is not locked.
         mContext.addInvocationAttribute("test", "test");
diff --git a/tests/src/com/android/tradefed/invoker/ShardListenerTest.java b/tests/src/com/android/tradefed/invoker/ShardListenerTest.java
index a8127b1..c7c26d9 100644
--- a/tests/src/com/android/tradefed/invoker/ShardListenerTest.java
+++ b/tests/src/com/android/tradefed/invoker/ShardListenerTest.java
@@ -61,7 +61,8 @@
     @Test
     public void testBufferAndReplay() {
         mMockListener.invocationStarted(mContext);
-        mMockListener.testRunStarted("run1", 1);
+        mMockListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         TestDescription tid = new TestDescription("class1", "name1");
         mMockListener.testStarted(tid, 0l);
         mMockListener.testEnded(tid, 0l, new HashMap<String, Metric>());
@@ -82,7 +83,8 @@
     @Test
     public void testPlayRuns() {
         mMockListener.invocationStarted(mContext);
-        mMockListener.testRunStarted("run1", 1);
+        mMockListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         TestDescription tid = new TestDescription("class1", "name1");
         mMockListener.testStarted(tid, 0l);
         mMockListener.testEnded(tid, 0l, new HashMap<String, Metric>());
@@ -121,19 +123,22 @@
         IInvocationContext module2 = new InvocationContext();
         mMockListener.invocationStarted(mContext);
         mMockListener.testModuleStarted(module1);
-        mMockListener.testRunStarted("run1", 1);
+        mMockListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         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.testRunStarted("run2", 1);
+        mMockListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         mMockListener.testStarted(tid, 0l);
         mMockListener.testEnded(tid, 0l, new HashMap<String, Metric>());
         mMockListener.testRunEnded(0l, new HashMap<String, Metric>());
         mMockListener.testModuleEnded();
         // expectation on second module
         mMockListener.testModuleStarted(module2);
-        mMockListener.testRunStarted("run3", 1);
+        mMockListener.testRunStarted(
+                EasyMock.eq("run3"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         mMockListener.testStarted(tid, 0l);
         mMockListener.testEnded(tid, 0l, new HashMap<String, Metric>());
         mMockListener.testRunEnded(0l, new HashMap<String, Metric>());
@@ -165,6 +170,60 @@
         EasyMock.verify(mMockListener, mMockDevice);
     }
 
+    @Test
+    public void testBufferAndReplay_withModule_attempts() {
+        IInvocationContext module1 = new InvocationContext();
+        IInvocationContext module2 = new InvocationContext();
+        mMockListener.invocationStarted(mContext);
+        mMockListener.testModuleStarted(module1);
+        mMockListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+        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.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(1), EasyMock.eq(1), EasyMock.anyLong());
+        mMockListener.testStarted(tid, 0l);
+        mMockListener.testEnded(tid, 0l, new HashMap<String, Metric>());
+        mMockListener.testRunEnded(0l, new HashMap<String, Metric>());
+        mMockListener.testModuleEnded();
+        // expectation on second module
+        mMockListener.testModuleStarted(module2);
+        mMockListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+        mMockListener.testStarted(tid, 0l);
+        mMockListener.testEnded(tid, 0l, new HashMap<String, Metric>());
+        mMockListener.testRunEnded(0l, new HashMap<String, Metric>());
+        mMockListener.testModuleEnded();
+        mMockListener.invocationEnded(0l);
+
+        EasyMock.replay(mMockListener, mMockDevice);
+        mShardListener.setSupportGranularResults(true);
+        mShardListener.invocationStarted(mContext);
+        // 1st module
+        mShardListener.testModuleStarted(module1);
+        mShardListener.testRunStarted("run1", 1, 0);
+        mShardListener.testStarted(tid, 0l);
+        mShardListener.testEnded(tid, 0l, new HashMap<String, Metric>());
+        mShardListener.testRunEnded(0l, new HashMap<String, Metric>());
+        mShardListener.testRunStarted("run1", 1, 1);
+        mShardListener.testStarted(tid, 0l);
+        mShardListener.testEnded(tid, 0l, new HashMap<String, Metric>());
+        mShardListener.testRunEnded(0l, new HashMap<String, Metric>());
+        mShardListener.testModuleEnded();
+        // 2nd module
+        mShardListener.testModuleStarted(module2);
+        mShardListener.testRunStarted("run2", 1, 0);
+        mShardListener.testStarted(tid, 0l);
+        mShardListener.testEnded(tid, 0l, new HashMap<String, Metric>());
+        mShardListener.testRunEnded(0l, new HashMap<String, Metric>());
+        mShardListener.testModuleEnded();
+
+        mShardListener.invocationEnded(0l);
+        EasyMock.verify(mMockListener, mMockDevice);
+    }
+
     /** Test the full ordering structure during a sharded pattern. */
     @Test
     public void testLogOrderingForSharding() throws Exception {
@@ -207,7 +266,8 @@
                 EasyMock.anyObject(),
                 EasyMock.eq(testFile));
 
-        mockListener.testRunStarted("run1", 1);
+        mockListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
         TestDescription tid = new TestDescription("class1", "name1");
         mockListener.testStarted(tid, 0l);
         // Log association played in order for the test.
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index c9a22e5..9d59eb8 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.command.CommandOptions;
 import com.android.tradefed.command.CommandRunner.ExitCode;
 import com.android.tradefed.config.ConfigurationDescriptor;
+import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.DeviceConfigurationHolder;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.device.ITestDevice;
@@ -39,6 +40,7 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.result.LogFile;
+import com.android.tradefed.testtype.retry.BaseRetryDecision;
 
 import org.easymock.Capture;
 import org.easymock.EasyMock;
@@ -77,6 +79,7 @@
 
         mMockConfig = EasyMock.createMock(IConfiguration.class);
         EasyMock.expect(mMockConfig.getPostProcessors()).andReturn(mPostProcessors);
+        EasyMock.expect(mMockConfig.getRetryDecision()).andReturn(new BaseRetryDecision());
         mMockRescheduler = EasyMock.createMock(IRescheduler.class);
         mMockTestListener = EasyMock.createMock(ITestInvocationListener.class);
         mMockLogSaver = EasyMock.createMock(ILogSaver.class);
@@ -231,6 +234,89 @@
         stubBuild.cleanUp();
     }
 
+    /**
+     * Test when the {@link IConfiguration#resolveDynamicOptions()} fails, ensure we report all the
+     * logs and error.
+     */
+    @Test
+    public void testResolveDynamicFails() throws Throwable {
+        mDevice1 = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(mDevice1.getIDevice()).andStubReturn(new StubDevice("serial1"));
+        mDevice2 = EasyMock.createMock(ITestDevice.class);
+        EasyMock.expect(mDevice2.getIDevice()).andStubReturn(new StubDevice("serial1"));
+        mContext.addAllocatedDevice("device1", mDevice1);
+        mContext.addAllocatedDevice("device2", mDevice2);
+
+        List<ITestInvocationListener> configListener = new ArrayList<>();
+        configListener.add(mMockTestListener);
+        EasyMock.expect(mMockConfig.getTestInvocationListeners())
+                .andReturn(configListener)
+                .times(2);
+        EasyMock.expect(mMockConfig.getLogSaver()).andReturn(mMockLogSaver);
+        EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogger);
+        EasyMock.expect(mMockConfig.getConfigurationDescription()).andReturn(mConfigDesc);
+        mMockLogger.init();
+        EasyMock.expect(mMockLogger.getLog())
+                .andReturn(new ByteArrayInputStreamSource("fake".getBytes()));
+        mMockLogger.closeLog();
+        EasyMock.expectLastCall().times(2);
+
+        mMockLogRegistry.registerLogger(mMockLogger);
+        mMockLogRegistry.dumpToGlobalLog(mMockLogger);
+        mMockLogRegistry.unregisterLogger();
+        EasyMock.expectLastCall().times(2);
+
+        EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
+        EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
+        EasyMock.expect(mMockConfig.getTests()).andStubReturn(new ArrayList<>());
+
+        ConfigurationException configException = new ConfigurationException("failed to resolve");
+        mMockConfig.resolveDynamicOptions();
+        EasyMock.expectLastCall().andThrow(configException);
+
+        mMockConfig.cleanDynamicOptionFiles();
+
+        mMockTestListener.invocationStarted(mContext);
+        EasyMock.expect(mMockTestListener.getSummary()).andReturn(null);
+        mMockLogSaver.invocationStarted(mContext);
+        mMockTestListener.invocationFailed(EasyMock.eq(configException));
+        mMockTestListener.testLog(EasyMock.anyObject(), EasyMock.anyObject(), EasyMock.anyObject());
+        EasyMock.expect(
+                        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());
+
+        EasyMock.replay(
+                mMockConfig,
+                mMockRescheduler,
+                mMockTestListener,
+                mMockLogSaver,
+                mMockLogger,
+                mMockLogRegistry,
+                mDevice1,
+                mDevice2);
+        mInvocation.invoke(
+                mContext, mMockConfig, mMockRescheduler, new ITestInvocationListener[] {});
+        EasyMock.verify(
+                mMockConfig,
+                mMockRescheduler,
+                mMockTestListener,
+                mMockLogSaver,
+                mMockLogger,
+                mMockLogRegistry,
+                mDevice1,
+                mDevice2);
+    }
+
     @Test
     public void testRunBuildProvider_oneThrow() throws Throwable {
         makeTwoDeviceContext();
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index 38bca14..9aad2e1 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -59,6 +59,7 @@
 import com.android.tradefed.invoker.shard.ShardHelper;
 import com.android.tradefed.log.ILeveledLogOutput;
 import com.android.tradefed.log.ILogRegistry;
+import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric.Builder;
@@ -85,6 +86,7 @@
 import com.android.tradefed.testtype.IRetriableTest;
 import com.android.tradefed.testtype.IShardableTest;
 import com.android.tradefed.testtype.StubTest;
+import com.android.tradefed.testtype.retry.IRetryDecision;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.SystemUtil.EnvVariable;
 import com.android.tradefed.util.keystore.StubKeyStoreFactory;
@@ -165,7 +167,6 @@
 
     @Before
     public void setUp() throws Exception {
-
         mStubConfiguration = new Configuration("foo", "bar");
         mStubMultiConfiguration = new Configuration("foo", "bar");
 
@@ -283,6 +284,11 @@
                             protected String getAdbVersion() {
                                 return null;
                             }
+
+                            @Override
+                            void logHostAdb(ITestLogger logger) {
+                                // inop for the common test case.
+                            }
                         };
                     }
 
@@ -429,6 +435,45 @@
         stubBuild.cleanUp();
     }
 
+    /**
+     * Test when the reporting of host_log is returning null, in this case we don't log anything.
+     */
+    @Test
+    public void testInvoke_noBuild_noHostLog() throws Throwable {
+        EasyMock.expect(mMockBuildProvider.getBuild()).andReturn(null);
+        setupInvoke();
+        setupMockFailureListeners(
+                new BuildRetrievalError("No build found to test."),
+                true, /* don't expect host log */
+                false);
+
+        EasyMock.reset(mMockLogger, mMockLogRegistry);
+        mMockLogRegistry.registerLogger(mMockLogger);
+        mMockLogger.init();
+        mMockLogger.closeLog();
+        EasyMock.expectLastCall().times(2);
+
+        IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
+        mStubConfiguration.setTest(test);
+        // Host log fails to report
+        EasyMock.expect(mMockLogger.getLog()).andReturn(null);
+        EasyMock.expect(mMockDevice.getLogcat()).andReturn(EMPTY_STREAM_SOURCE).times(2);
+        mMockDevice.clearLogcat();
+        EasyMock.expectLastCall().times(2);
+        Capture<IBuildInfo> captured = new Capture<>();
+        mMockBuildProvider.cleanUp(EasyMock.capture(captured));
+        mMockLogRegistry.unregisterLogger();
+        EasyMock.expectLastCall().times(2);
+        mMockLogRegistry.dumpToGlobalLog(mMockLogger);
+        replayMocks(test, mockRescheduler);
+        mTestInvocation.invoke(mStubInvocationMetadata, mStubConfiguration, mockRescheduler);
+        verifyMocks(test);
+
+        IBuildInfo stubBuild = captured.getValue();
+        assertEquals(BuildInfo.UNKNOWN_BUILD_ID, stubBuild.getBuildId());
+        stubBuild.cleanUp();
+    }
+
     /** Test the invoke scenario where there is no build to test for a {@link IRetriableTest}. */
     @Test
     public void testInvoke_noBuildRetry() throws Throwable {
@@ -1161,7 +1206,11 @@
      * calls.
      */
     private void setupMockListeners(
-            InvocationStatus status, Throwable throwable, boolean stubFailures) throws IOException {
+            InvocationStatus status,
+            Throwable throwable,
+            boolean stubFailures,
+            boolean reportHostLog)
+            throws IOException {
         // invocationStarted
         mMockLogSaver.invocationStarted(mStubInvocationMetadata);
         mMockTestListener.invocationStarted(mStubInvocationMetadata);
@@ -1265,20 +1314,26 @@
 
         EasyMock.expect(
                         mMockLogSaver.saveLogData(
-                                EasyMock.eq(TestInvocation.TRADEFED_LOG_NAME),
-                                EasyMock.eq(LogDataType.TEXT),
-                                (InputStream) EasyMock.anyObject()))
-                .andReturn(new LogFile(PATH, URL, LogDataType.TEXT));
-        mMockTestListener.testLog(EasyMock.eq(TestInvocation.TRADEFED_LOG_NAME),
-                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));
+        if (reportHostLog) {
+            EasyMock.expect(
+                            mMockLogSaver.saveLogData(
+                                    EasyMock.eq(TestInvocation.TRADEFED_LOG_NAME),
+                                    EasyMock.eq(LogDataType.TEXT),
+                                    (InputStream) EasyMock.anyObject()))
+                    .andReturn(new LogFile(PATH, URL, LogDataType.TEXT));
+            mMockTestListener.testLog(
+                    EasyMock.eq(TestInvocation.TRADEFED_LOG_NAME),
+                    EasyMock.eq(LogDataType.TEXT),
+                    (InputStreamSource) EasyMock.anyObject());
+            mMockSummaryListener.testLog(
+                    EasyMock.eq(TestInvocation.TRADEFED_LOG_NAME),
+                    EasyMock.eq(LogDataType.TEXT),
+                    (InputStreamSource) EasyMock.anyObject());
+        }
 
         // invocationEnded, getSummary (mMockTestListener)
         mMockTestListener.invocationEnded(EasyMock.anyLong());
@@ -1346,6 +1401,72 @@
         verifyMocks(test, mockRescheduler, shard1, shard2, mGlobalConfiguration);
     }
 
+    /** Test that the before sharding log is properly carried even with auto-retry. */
+    @Test
+    public void testInvoke_shardableTest_autoRetry() throws Throwable {
+        List<ITestInvocationListener> listenerList =
+                mStubConfiguration.getTestInvocationListeners();
+        ILogSaverListener logSaverListener = EasyMock.createMock(ILogSaverListener.class);
+        listenerList.add(logSaverListener);
+        mStubConfiguration.setTestInvocationListeners(listenerList);
+
+        logSaverListener.setLogSaver(mMockLogSaver);
+        logSaverListener.invocationStarted(mStubInvocationMetadata);
+
+        String command = "empty --test-tag t";
+        String[] commandLine = {"empty", "--test-tag", "t"};
+        int shardCount = 2;
+        IShardableTest test = EasyMock.createMock(IShardableTest.class);
+        List<IRemoteTest> shards = new ArrayList<>();
+        IRemoteTest shard1 = EasyMock.createMock(IRemoteTest.class);
+        IRemoteTest shard2 = EasyMock.createMock(IRemoteTest.class);
+        shards.add(shard1);
+        shards.add(shard2);
+        EasyMock.expect(test.split()).andReturn(shards);
+        mStubConfiguration.setTest(test);
+        mStubConfiguration.setCommandLine(commandLine);
+
+        IRetryDecision decision = mStubConfiguration.getRetryDecision();
+        OptionSetter decisionSetter = new OptionSetter(decision);
+        decisionSetter.setOptionValue("auto-retry", "true");
+        decisionSetter.setOptionValue("max-testcase-run-count", "2");
+
+        mMockBuildProvider.cleanUp(mMockBuildInfo);
+        // The keystore is cloned for each shard.
+        EasyMock.expect(mGlobalConfiguration.getKeyStoreFactory())
+                .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);
+        // Ensure that the host_log gets logged after sharding.
+        EasyMock.expect(mMockLogger.getLog()).andReturn(EMPTY_STREAM_SOURCE);
+        String logName = "host_log_before_sharding";
+        LogFile loggedFile = new LogFile(PATH, URL, LogDataType.TEXT);
+        EasyMock.expect(
+                        mMockLogSaver.saveLogData(
+                                EasyMock.eq(logName),
+                                EasyMock.eq(LogDataType.TEXT),
+                                EasyMock.anyObject()))
+                .andReturn(loggedFile);
+        logSaverListener.logAssociation(logName, loggedFile);
+        mMockLogRegistry.unregisterLogger();
+        EasyMock.expectLastCall();
+        mMockLogger.closeLog();
+        EasyMock.expectLastCall();
+
+        mMockLogRegistry.dumpToGlobalLog(mMockLogger);
+        replayMocks(test, mockRescheduler, shard1, shard2, mGlobalConfiguration, logSaverListener);
+        mTestInvocation.invoke(mStubInvocationMetadata, mStubConfiguration, mockRescheduler);
+        verifyMocks(test, mockRescheduler, shard1, shard2, mGlobalConfiguration, logSaverListener);
+    }
+
     /**
      * Test that {@link TestInvocation#logDeviceBatteryLevel(IInvocationContext, String)} is not
      * adding battery information for placeholder device.
@@ -1495,16 +1616,21 @@
     }
 
     private void setupMockSuccessListeners() throws IOException {
-        setupMockListeners(InvocationStatus.SUCCESS, null, false);
+        setupMockListeners(InvocationStatus.SUCCESS, null, false, true);
     }
 
     private void setupMockFailureListeners(Throwable throwable) throws IOException {
-        setupMockListeners(InvocationStatus.FAILED, throwable, false);
+        setupMockListeners(InvocationStatus.FAILED, throwable, false, true);
     }
 
     private void setupMockFailureListenersAny(Throwable throwable, boolean stubFailures)
             throws IOException {
-        setupMockListeners(InvocationStatus.FAILED, throwable, stubFailures);
+        setupMockListeners(InvocationStatus.FAILED, throwable, stubFailures, true);
+    }
+
+    private void setupMockFailureListeners(
+            Throwable throwable, boolean stubFailures, boolean reportHostLog) throws IOException {
+        setupMockListeners(InvocationStatus.FAILED, throwable, stubFailures, reportHostLog);
     }
 
     private void verifySummaryListener() {
@@ -1578,6 +1704,11 @@
                             protected String getAdbVersion() {
                                 return null;
                             }
+
+                            @Override
+                            void logHostAdb(ITestLogger logger) {
+                                // inop for the common test case.
+                            }
                         };
                     }
 
@@ -1658,6 +1789,11 @@
                                 protected String getAdbVersion() {
                                     return null;
                                 }
+
+                                @Override
+                                void logHostAdb(ITestLogger logger) {
+                                    // inop for the common test case.
+                                }
                             };
                         }
 
@@ -1750,6 +1886,11 @@
                                 protected String getAdbVersion() {
                                     return null;
                                 }
+
+                                @Override
+                                void logHostAdb(ITestLogger logger) {
+                                    // inop for the common test case.
+                                }
                             };
                         }
 
diff --git a/tests/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecutionTest.java
index adc27ad..6ec710d 100644
--- a/tests/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/sandbox/ParentSandboxInvocationExecutionTest.java
@@ -38,6 +38,7 @@
 import com.android.tradefed.invoker.IInvocationContext;
 import com.android.tradefed.invoker.InvocationContext;
 import com.android.tradefed.invoker.TestInvocation.Stage;
+import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.sandbox.SandboxOptions;
 import com.android.tradefed.targetprep.ITargetCleaner;
 import com.android.tradefed.targetprep.TargetSetupError;
@@ -59,12 +60,14 @@
     private SandboxOptions mOptions;
     private ITargetCleaner mMockPreparer;
     private ITestDevice mMockDevice;
+    private ITestLogger mMockLogger;
 
     @Before
     public void setUp() {
         mMockFactory = Mockito.mock(IConfigurationFactory.class);
         mMockPreparer = Mockito.mock(ITargetCleaner.class);
         mMockDevice = Mockito.mock(ITestDevice.class);
+        mMockLogger = Mockito.mock(ITestLogger.class);
 
         mParentSandbox =
                 new ParentSandboxInvocationExecution() {
@@ -108,7 +111,7 @@
                 .createConfigurationFromArgs(new String[] {"parent-config"});
 
         mParentSandbox.doSetup(mContext, mConfig, null);
-        mParentSandbox.doTeardown(mContext, mConfig, null, null);
+        mParentSandbox.doTeardown(mContext, mConfig, mMockLogger, null);
         mParentSandbox.doCleanUp(mContext, mConfig, null);
 
         verify(mMockFactory, times(1)).createConfigurationFromArgs(Mockito.any());
@@ -132,7 +135,7 @@
         doReturn(new StubDevice("stub")).when(mMockDevice).getIDevice();
 
         mParentSandbox.doSetup(mContext, mConfig, null);
-        mParentSandbox.doTeardown(mContext, mConfig, null, null);
+        mParentSandbox.doTeardown(mContext, mConfig, mMockLogger, null);
         mParentSandbox.doCleanUp(mContext, mConfig, null);
         mParentSandbox.reportLogs(
                 mMockDevice, configParent.getTestInvocationListeners().get(0), Stage.ERROR);
diff --git a/tests/src/com/android/tradefed/invoker/shard/TestsPoolPollerTest.java b/tests/src/com/android/tradefed/invoker/shard/TestsPoolPollerTest.java
index ca2cfdf..907b33a 100644
--- a/tests/src/com/android/tradefed/invoker/shard/TestsPoolPollerTest.java
+++ b/tests/src/com/android/tradefed/invoker/shard/TestsPoolPollerTest.java
@@ -22,6 +22,8 @@
 import static org.junit.Assert.fail;
 
 import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
@@ -59,6 +61,7 @@
     private ITestDevice mDevice;
     private List<IMetricCollector> mMetricCollectors;
     private ILogRegistry mMockRegistry;
+    private IConfiguration mConfiguration;
 
     @Before
     public void setUp() {
@@ -66,6 +69,7 @@
         mDevice = Mockito.mock(ITestDevice.class);
         mMockRegistry = Mockito.mock(ILogRegistry.class);
         Mockito.doReturn("serial").when(mDevice).getSerialNumber();
+        mConfiguration = new Configuration("test", "test");
         mMetricCollectors = new ArrayList<>();
     }
 
@@ -104,9 +108,13 @@
      */
     @Test
     public void testPollingRun() throws Exception {
+        StubTest first = new StubTest();
+        OptionSetter setterFirst = new OptionSetter(first);
+        setterFirst.setOptionValue("run-a-test", "true");
         int numTests = 5;
         List<IRemoteTest> testsList = new ArrayList<>();
-        for (int i = 0; i < numTests; i++) {
+        testsList.add(first);
+        for (int i = 0; i < numTests - 1; i++) {
             IRemoteTest test = new StubTest();
             OptionSetter setter = new OptionSetter(test);
             setter.setOptionValue("run-a-test", "true");
@@ -114,6 +122,7 @@
         }
         CountDownLatch tracker = new CountDownLatch(1);
         TestsPoolPoller poller = new TestsPoolPoller(testsList, tracker);
+        poller.setConfiguration(mConfiguration);
         poller.setMetricCollectors(mMetricCollectors);
         poller.run(mListener);
         Mockito.verify(mListener, Mockito.times(numTests))
@@ -121,6 +130,9 @@
         Mockito.verify(mListener, Mockito.times(numTests))
                 .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
         assertEquals(0, tracker.getCount());
+
+        // Ensure that the configuration set is the one that we passed.
+        assertEquals(mConfiguration, first.getConfiguration());
     }
 
     /**
diff --git a/tests/src/com/android/tradefed/result/TestResultTest.java b/tests/src/com/android/tradefed/result/TestResultTest.java
index cb971e2..92b46ef 100644
--- a/tests/src/com/android/tradefed/result/TestResultTest.java
+++ b/tests/src/com/android/tradefed/result/TestResultTest.java
@@ -15,10 +15,10 @@
  */
 package com.android.tradefed.result;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
-import com.android.tradefed.testtype.retry.MergeStrategy;
+import com.android.tradefed.retry.MergeStrategy;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,6 +52,7 @@
                 TestResult.merge(testResults, MergeStrategy.ONE_TESTCASE_PASS_IS_PASS);
         // Merge Strategy leave it a PASSED
         assertEquals(TestStatus.PASSED, finalRes.getStatus());
+        assertTrue(finalRes.getProtoMetrics().containsKey(TestResult.IS_FLAKY));
         assertEquals(2, finalRes.getStartTime());
         assertEquals(7, finalRes.getEndTime());
         assertEquals("failed", finalRes.getStackTrace());
diff --git a/tests/src/com/android/tradefed/sandbox/TradefedSandboxTest.java b/tests/src/com/android/tradefed/sandbox/TradefedSandboxTest.java
index 1ea83af..e1eff2c 100644
--- a/tests/src/com/android/tradefed/sandbox/TradefedSandboxTest.java
+++ b/tests/src/com/android/tradefed/sandbox/TradefedSandboxTest.java
@@ -185,6 +185,48 @@
         assertEquals("Error when dumping the config. stderr: Ouch I failed.", res.getMessage());
     }
 
+    /** Test that the fallback dump config also attempt to parse the config. */
+    @Test
+    public void testPrepareEnvironment_dumpConfigFail_fallback_fail() throws Exception {
+        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
+        EasyMock.expectLastCall().times(2);
+        mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
+        EasyMock.expectLastCall().times(2);
+        mMockRunUtil.setEnvVariable(
+                EasyMock.eq(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE), EasyMock.anyObject());
+        mMockRunUtil.setEnvVariablePriority(EnvPriority.SET);
+        mMockListener.testLog(
+                EasyMock.eq("sandbox-global-config"),
+                EasyMock.eq(LogDataType.XML),
+                EasyMock.anyObject());
+        CommandResult result = new CommandResult();
+        result.setStatus(CommandStatus.FAILED);
+        result.setStderr("Could not find configuration 'empty'");
+        EasyMock.expect(
+                        mMockRunUtil.runTimedCmd(
+                                EasyMock.anyLong(),
+                                EasyMock.eq("java"),
+                                EasyMock.eq("-cp"),
+                                EasyMock.anyObject(),
+                                EasyMock.eq(SandboxConfigDump.class.getCanonicalName()),
+                                EasyMock.eq("RUN_CONFIG"),
+                                EasyMock.anyObject(),
+                                EasyMock.eq("empty"),
+                                EasyMock.eq("--arg"),
+                                EasyMock.eq("1"),
+                                EasyMock.eq("--use-proto-reporter")))
+                .andReturn(result);
+        setPrepareConfigurationExpectations();
+        EasyMock.replay(mMockConfig, mMockListener, mMockRunUtil);
+        Exception res = mSandbox.prepareEnvironment(mMockContext, mMockConfig, mMockListener);
+        EasyMock.verify(mMockConfig, mMockListener, mMockRunUtil);
+        assertNotNull(res);
+        assertTrue(res instanceof ConfigurationException);
+        assertEquals(
+                "Error when dumping the config. stderr: Could not find configuration 'empty'",
+                res.getMessage());
+    }
+
     /**
      * Test a case where the {@link
      * com.android.tradefed.sandbox.TradefedSandbox#prepareEnvironment(IInvocationContext,
diff --git a/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java b/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java
index dee9e5d..b8eb13e 100644
--- a/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java
+++ b/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java
@@ -28,8 +28,6 @@
 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)
@@ -55,37 +53,20 @@
     @Test
     public void testSystemServerProcessNotRestarted() throws Exception {
         EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
-                .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L))
-                .times(2);
+                .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L));
+        EasyMock.expect(mMockDevice.deviceSoftRestarted(EasyMock.anyObject())).andReturn(false);
         EasyMock.replay(mMockDevice);
         assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
         assertEquals(CheckStatus.SUCCESS, mChecker.postExecutionCheck(mMockDevice).getStatus());
         EasyMock.verify(mMockDevice);
     }
 
-    /** Test that system checker fail if system_server crashed and didn't come back. */
-    @Test
-    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.expect(mMockDevice.deviceSoftRestarted(EasyMock.anyObject())).andReturn(true);
         EasyMock.replay(mMockDevice);
         assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
         StatusCheckerResult result = mChecker.postExecutionCheck(mMockDevice);
@@ -96,16 +77,11 @@
 
     /** 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");
+    public void testSystemServerProcessRestartedWithAbnormalDeviceReboot() 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(history);
-        EasyMock.expect(mMockDevice.getLastExpectedRebootTimeMillis()).andReturn(200L);
+        EasyMock.expect(mMockDevice.deviceSoftRestarted(EasyMock.anyObject()))
+                .andThrow(new RuntimeException("abnormal reboot"));
         EasyMock.replay(mMockDevice);
         assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
         StatusCheckerResult result = mChecker.postExecutionCheck(mMockDevice);
@@ -115,34 +91,13 @@
     }
 
     /**
-     * Test that if the pid changed but there was a Tradefed reboot, we still not fail the checker.
-     */
-    @Test
-    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.SUCCESS, result.getStatus());
-        EasyMock.verify(mMockDevice);
-    }
-
-    /**
      * Test that if fail to get system_server process at preExecutionCheck, we skip the
      * system_server check in postExecution.
      */
     @Test
     public void testFailToGetSystemServerProcess() throws Exception {
         EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server"))).andReturn(null);
+        mMockDevice.reboot();
         EasyMock.replay(mMockDevice);
         assertEquals(CheckStatus.FAILED, mChecker.preExecutionCheck(mMockDevice).getStatus());
         assertEquals(CheckStatus.SUCCESS, mChecker.postExecutionCheck(mMockDevice).getStatus());
diff --git a/tests/src/com/android/tradefed/targetprep/BaseTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/BaseTargetPreparerTest.java
new file mode 100644
index 0000000..d64e547
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/BaseTargetPreparerTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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 static org.junit.Assert.*;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/** Unit tests for {@link BaseTargetPreparer}. */
+@RunWith(JUnit4.class)
+public class BaseTargetPreparerTest {
+
+    private static class DisabledBaseTargetPreparer extends BaseTargetPreparer {
+
+        DisabledBaseTargetPreparer() {
+            setDisable(true);
+        }
+
+        @Override
+        public void setUp(ITestDevice device, IBuildInfo buildInfo)
+                throws TargetSetupError, BuildError, DeviceNotAvailableException {
+            // Ignore
+        }
+    }
+
+    @Test
+    public void testDisabledByDefault() throws Exception {
+        DisabledBaseTargetPreparer preparer = new DisabledBaseTargetPreparer();
+        assertTrue(preparer.isDisabled());
+        IConfiguration config = new Configuration("test", "test");
+        config.setTargetPreparer(preparer);
+        File configFile = FileUtil.createTempFile("base-target-prep-config", ".xml");
+        try (PrintWriter pw = new PrintWriter(configFile)) {
+            config.dumpXml(pw, new ArrayList<>(), false, false);
+            String value = FileUtil.readStringFromFile(configFile);
+            assertTrue(value.contains("<option name=\"disable\" value=\"true\" />"));
+            // Disable-tear-down was not modified so it should not appear.
+            assertFalse(value.contains("disable-tear-down"));
+        } finally {
+            FileUtil.deleteFile(configFile);
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/BuildInfoAttributePreparerTest.java b/tests/src/com/android/tradefed/targetprep/BuildInfoAttributePreparerTest.java
deleted file mode 100644
index efccdcb..0000000
--- a/tests/src/com/android/tradefed/targetprep/BuildInfoAttributePreparerTest.java
+++ /dev/null
@@ -1,52 +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.targetprep;
-
-import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.OptionSetter;
-
-import junit.framework.TestCase;
-
-import java.util.Map;
-
-/**
- * Unit tests for {@link BuildInfoAttributePreparer}
- */
-public class BuildInfoAttributePreparerTest extends TestCase {
-    private BuildInfoAttributePreparer mPrep = null;
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        mPrep = new BuildInfoAttributePreparer();
-    }
-
-    public void testSimple() throws Exception {
-        final IBuildInfo build = new BuildInfo();
-
-        OptionSetter opt = new OptionSetter(mPrep);
-        opt.setOptionValue("build-attribute", "key", "value");
-        mPrep.setUp(null, build);
-
-        Map<String, String> map = build.getBuildAttributes();
-        assertTrue(map.containsKey("key"));
-        assertEquals("value", map.get("key"));
-    }
-}
diff --git a/tests/src/com/android/tradefed/targetprep/DisableSELinuxTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/DisableSELinuxTargetPreparerTest.java
index 42f5c4a..2d6210e 100644
--- a/tests/src/com/android/tradefed/targetprep/DisableSELinuxTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DisableSELinuxTargetPreparerTest.java
@@ -122,6 +122,7 @@
         CommandResult result = new CommandResult();
         result.setStdout(ENFORCED);
         result.setStatus(CommandStatus.FAILED);
+        EasyMock.expect(mMockDevice.getDeviceDescriptor()).andReturn(null);
         EasyMock.expect(mMockDevice.executeShellV2Command(GETENFORCE)).andReturn(result).once();
         EasyMock.expect(mMockDevice.isAdbRoot()).andReturn(false).once();
         EasyMock.expect(mMockDevice.enableAdbRoot()).andReturn(true).once();
diff --git a/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java b/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java
index 215f623..ce7936e 100644
--- a/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java
+++ b/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java
@@ -529,8 +529,11 @@
             EasyMock.expect(mockBuild.getBootloaderImageFile()).andReturn(bootloaderFake);
             CommandResult res = new CommandResult(CommandStatus.SUCCESS);
             res.setStderr("flashing");
-            EasyMock.expect(mMockDevice.executeFastbootCommand(EasyMock.eq("flash"),
-                    EasyMock.eq("hboot"), EasyMock.eq(bootloaderFake.getAbsolutePath())))
+            EasyMock.expect(
+                            mMockDevice.executeFastbootCommand(
+                                    EasyMock.eq("flash"),
+                                    EasyMock.eq("bootloader"),
+                                    EasyMock.eq(bootloaderFake.getAbsolutePath())))
                     .andReturn(res);
             mMockDevice.rebootIntoBootloader();
             EasyMock.expectLastCall();
diff --git a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
index 4cde810..59c8926 100644
--- a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
@@ -353,11 +353,15 @@
         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(mFakeApk.getAbsolutePath());
-        EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
-                .andReturn("Success")
+        //TODO:add back once new adb is deployed to the lab
+        // 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.installPackage((File) EasyMock.anyObject(), EasyMock.eq(true)))
+                .andReturn(null)
                 .once();
         EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
 
@@ -723,11 +727,15 @@
     }
 
     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")
+        //TODO:add back once new adb is deployed to the lab
+        // 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();
+        EasyMock.expect(mMockDevice.installPackage((File) EasyMock.anyObject(), EasyMock.eq(true)))
+                .andReturn(null)
                 .once();
         mMockDevice.reboot();
         EasyMock.expectLastCall().once();
diff --git a/tests/src/com/android/tradefed/targetprep/PreloadedClassesPreparerTest.java b/tests/src/com/android/tradefed/targetprep/PreloadedClassesPreparerTest.java
deleted file mode 100644
index e0e1830..0000000
--- a/tests/src/com/android/tradefed/targetprep/PreloadedClassesPreparerTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.targetprep;
-
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.when;
-
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
-import com.android.tradefed.util.IRunUtil;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
-
-import java.io.File;
-
-/** Unit tests for {@link PushFilePreparer} */
-@RunWith(JUnit4.class)
-public class PreloadedClassesPreparerTest {
-    private static final String FAKE_FILE_PATH = "/file/path";
-    private static final String FAKE_TOOL_PATH = "/tool/path";
-    private static final String PRELOAD_TOOL_NAME = "preload2.jar";
-    private static final String WRITE_COMMAND =
-            "java -cp %s com.android.preload.Main --seq SERIAL write %s";
-
-    private IBuildInfo mMockBuildInfo;
-    private ITestDevice mMockDevice;
-    private IRunUtil mMockRunUtil;
-
-    private PreloadedClassesPreparer mRealPreparer;
-    private PreloadedClassesPreparer mSpyPreparer;
-
-    @Before
-    public void setUp() throws Exception {
-        // Setup mocks and spies
-        mMockDevice = Mockito.mock(ITestDevice.class);
-        mMockBuildInfo = Mockito.mock(IBuildInfo.class);
-        mMockRunUtil = Mockito.mock(IRunUtil.class);
-        mRealPreparer = new PreloadedClassesPreparer();
-        mSpyPreparer = Mockito.spy(mRealPreparer);
-        // Setup mock returns
-        when(mMockDevice.getDeviceDescriptor()).thenReturn(null);
-        when(mMockDevice.getSerialNumber()).thenReturn("SERIAL");
-        when(mSpyPreparer.getRunUtil()).thenReturn(mMockRunUtil);
-    }
-
-    // Using the build info to get the preload tool is specific to remote runs.
-    @Test
-    public void testSetUp_RemoteSuccess() throws Exception {
-        // Create a fully mocked success case
-        File tool = Mockito.mock(File.class);
-        when(tool.exists()).thenReturn(true);
-        when(tool.getAbsolutePath()).thenReturn(FAKE_TOOL_PATH);
-        when(mMockBuildInfo.getFile(PRELOAD_TOOL_NAME)).thenReturn(tool);
-        when(mSpyPreparer.getPreloadedClassesPath()).thenReturn(FAKE_FILE_PATH);
-        CommandResult result = new CommandResult();
-        result.setStatus(CommandStatus.SUCCESS);
-        // Expected output command based on the above.
-        String[] command = String.format(WRITE_COMMAND, FAKE_TOOL_PATH, FAKE_FILE_PATH).split(" ");
-        when(mMockRunUtil.runTimedCmd(PreloadedClassesPreparer.DEFAULT_TIMEOUT_MS, command))
-                .thenReturn(result);
-        // Run and don't encounter issues
-        mSpyPreparer.setUp(mMockDevice, mMockBuildInfo);
-    }
-
-    // Using the build info to get the preload tool is specific to remote runs.
-    @Test
-    public void testSetUp_RemoteNoTool() throws Exception {
-        // Set mocks to fail returning the tool
-        when(mSpyPreparer.getPreloadedClassesPath()).thenReturn(FAKE_FILE_PATH);
-        when(mMockBuildInfo.getFile(PRELOAD_TOOL_NAME)).thenReturn(null);
-        try {
-            mSpyPreparer.setUp(mMockDevice, mMockBuildInfo);
-            fail("Did not fail when there was no tool available.");
-        } catch (TargetSetupError e) {
-            // Good, this should throw
-        }
-    }
-
-    @Test
-    public void testSetUp_LocalSuccess() throws Exception {
-        when(mSpyPreparer.getPreloadToolPath()).thenReturn(FAKE_TOOL_PATH);
-        when(mSpyPreparer.getPreloadedClassesPath()).thenReturn(FAKE_FILE_PATH);
-        CommandResult result = new CommandResult();
-        result.setStatus(CommandStatus.SUCCESS);
-        // Expected output command based on the above.
-        String[] command = String.format(WRITE_COMMAND, FAKE_TOOL_PATH, FAKE_FILE_PATH).split(" ");
-        when(mMockRunUtil.runTimedCmd(PreloadedClassesPreparer.DEFAULT_TIMEOUT_MS, command))
-                .thenReturn(result);
-        // Run and don't encounter issues
-        mSpyPreparer.setUp(mMockDevice, mMockBuildInfo);
-    }
-
-    @Test
-    public void testSetUp_NoFile() throws Exception {
-        // If not skipped, expect this to error out.
-        mSpyPreparer.setUp(mMockDevice, mMockBuildInfo);
-    }
-
-    @Test
-    public void testSetUp_WriteFailure() throws Exception {
-        when(mSpyPreparer.getPreloadToolPath()).thenReturn(FAKE_TOOL_PATH);
-        when(mSpyPreparer.getPreloadedClassesPath()).thenReturn(FAKE_FILE_PATH);
-        CommandResult result = new CommandResult();
-        result.setStatus(CommandStatus.FAILED);
-        // Expected output command based on the above.
-        String[] command = String.format(WRITE_COMMAND, FAKE_TOOL_PATH, FAKE_FILE_PATH).split(" ");
-        when(mMockRunUtil.runTimedCmd(PreloadedClassesPreparer.DEFAULT_TIMEOUT_MS, command))
-                .thenReturn(result);
-        // Run and encounter a write issue
-        try {
-            mSpyPreparer.setUp(mMockDevice, mMockBuildInfo);
-            fail("Did not fail when writing with the tool failed.");
-        } catch (TargetSetupError e) {
-            // Good, this should throw.
-        }
-    }
-}
diff --git a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
index 3817c98..c8ca2e6 100644
--- a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
@@ -314,6 +314,40 @@
     }
 
     /**
+     * Test {@link PushFilePreparer#resolveRelativeFilePath(IBuildInfo, String)} can locate a source
+     * file existed in a remote zip of a device build.
+     */
+    @Test
+    public void testResolveRelativeFilePath_withDeviceBuildInfo_remoteZip() throws Exception {
+        IDeviceBuildInfo buildInfo = EasyMock.createStrictMock(IDeviceBuildInfo.class);
+        String fileName = "source_file";
+
+        File testsDir = null;
+        try {
+            testsDir = FileUtil.createTempDir("tests_dir");
+            File hostTestCasesDir = FileUtil.getFileForPath(testsDir, HOST_TESTCASES);
+            FileUtil.mkdirsRWX(hostTestCasesDir);
+            File sourceFile = FileUtil.createTempFile(fileName, null, hostTestCasesDir);
+
+            // Change the file name so direct file search will return null.
+            fileName = sourceFile.getName() + "-2";
+            EasyMock.expect(buildInfo.getFile(fileName)).andReturn(null);
+            EasyMock.expect(buildInfo.getTestsDir()).andReturn(testsDir);
+            EasyMock.expect(buildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR)).andReturn(null);
+            EasyMock.expect(buildInfo.stageRemoteFile(EasyMock.eq(fileName), EasyMock.eq(testsDir)))
+                    .andReturn(sourceFile);
+            EasyMock.replay(buildInfo);
+
+            assertEquals(
+                    sourceFile.getAbsolutePath(),
+                    mPreparer.resolveRelativeFilePath(buildInfo, fileName).getAbsolutePath());
+            EasyMock.verify(buildInfo);
+        } finally {
+            FileUtil.recursiveDelete(testsDir);
+        }
+    }
+
+    /**
      * If a folder is found match it first and push it while filtering the abi that are not
      * considered.
      */
diff --git a/tests/src/com/android/tradefed/targetprep/TimeSetterTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/TimeSetterTargetPreparerTest.java
deleted file mode 100644
index 6a043c8..0000000
--- a/tests/src/com/android/tradefed/targetprep/TimeSetterTargetPreparerTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.targetprep;
-
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-
-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.Date;
-import java.util.concurrent.TimeUnit;
-
-/** Unit Tests for {@link TimeSetterTargetPreparer}. */
-@RunWith(JUnit4.class)
-public class TimeSetterTargetPreparerTest {
-    private TimeSetterTargetPreparer mTimeSetterTargetPreparer;
-    private ITestDevice mMockDevice;
-    private IBuildInfo mMockBuildInfo;
-    private long mMockNanoTime;
-
-    @Before
-    public void setUp() {
-        mMockDevice = EasyMock.createMock(ITestDevice.class);
-        mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
-        mTimeSetterTargetPreparer =
-                new TimeSetterTargetPreparer() {
-                    @Override
-                    long getNanoTime() {
-                        return mMockNanoTime;
-                    }
-                };
-    }
-
-    @Test
-    public void testSaveTime() throws Exception {
-        OptionSetter optionSetter = new OptionSetter(mTimeSetterTargetPreparer);
-        optionSetter.setOptionValue("time", "555");
-
-        EasyMock.expect(mMockDevice.getDeviceDate()).andReturn(123L).once();
-        mMockDevice.setDate(new Date(555L));
-        EasyMock.expectLastCall().once();
-        mMockDevice.setDate(new Date(128L));
-        EasyMock.expectLastCall().once();
-
-        EasyMock.replay(mMockDevice, mMockBuildInfo);
-
-        mMockNanoTime = TimeUnit.MILLISECONDS.toNanos(2);
-        mTimeSetterTargetPreparer.setUp(mMockDevice, mMockBuildInfo);
-        mMockNanoTime = TimeUnit.MILLISECONDS.toNanos(7);
-        mTimeSetterTargetPreparer.tearDown(mMockDevice, mMockBuildInfo, null);
-        EasyMock.verify(mMockBuildInfo, mMockDevice);
-    }
-}
diff --git a/tests/src/com/android/tradefed/targetprep/suite/SuiteApkInstallerTest.java b/tests/src/com/android/tradefed/targetprep/suite/SuiteApkInstallerTest.java
index 5ba4a4d..83ca46e 100644
--- a/tests/src/com/android/tradefed/targetprep/suite/SuiteApkInstallerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/suite/SuiteApkInstallerTest.java
@@ -260,6 +260,42 @@
         }
     }
 
+    /**
+     * Test that {@link SuiteApkInstaller#getLocalPathForFilename(IBuildInfo, String, ITestDevice)}
+     * returns the apk file retrieved from remote artifacts.
+     */
+    @Test
+    public void testGetLocalPathForFileName_remoteZip() throws Exception {
+        mPreparer =
+                new SuiteApkInstaller() {
+                    @Override
+                    protected File getRootDir(IBuildInfo buildInfo) throws FileNotFoundException {
+                        return null;
+                    }
+                };
+        IDeviceBuildInfo deviceBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+        File tmpDir = null;
+        try {
+            tmpDir = FileUtil.createTempDir("test");
+            Mockito.doReturn(null).when(mMockBuildInfo).getFile("foo.apk");
+            EasyMock.expect(deviceBuildInfo.getTestsDir()).andReturn(tmpDir);
+            // Change the name so direct file search will return null.
+            File tmpApk = FileUtil.createTempFile("suite-apk-installer-2", ".apk", tmpDir);
+            EasyMock.expect(
+                            deviceBuildInfo.stageRemoteFile(
+                                    EasyMock.eq("suite-apk-installer.apk"), EasyMock.eq(tmpDir)))
+                    .andReturn(tmpApk);
+            EasyMock.replay(deviceBuildInfo);
+            File apk =
+                    mPreparer.getLocalPathForFilename(
+                            deviceBuildInfo, "suite-apk-installer.apk", mMockDevice);
+            assertEquals(tmpApk.getAbsolutePath(), apk.getAbsolutePath());
+            EasyMock.verify(deviceBuildInfo);
+        } finally {
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
+
     /** If the file is found in the build shared resources directory, use it. */
     @Test
     public void testGetLocalPathForFileName_inSharedDir() throws Exception {
diff --git a/tests/src/com/android/tradefed/testtype/GTestBaseTest.java b/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
index 27a816f..82cce6f 100644
--- a/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
@@ -27,6 +27,8 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.result.ITestInvocationListener;
 
+import com.google.common.collect.ImmutableList;
+
 import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
@@ -198,7 +200,8 @@
         mSetter.setOptionValue("coverage", "true");
 
         ITestInvocationListener listener =
-                gTestBase.addNativeCoverageListenerIfEnabled(mMockTestDevice, mMockListener);
+                gTestBase.addNativeCoverageListenerIfEnabled(
+                        mMockTestDevice, false, ImmutableList.of(), mMockListener);
 
         assertThat(listener).isInstanceOf(NativeCodeCoverageListener.class);
     }
@@ -211,7 +214,8 @@
         mSetter.setOptionValue("coverage", "false");
 
         ITestInvocationListener listener =
-                gTestBase.addNativeCoverageListenerIfEnabled(mMockTestDevice, mMockListener);
+                gTestBase.addNativeCoverageListenerIfEnabled(
+                        mMockTestDevice, false, ImmutableList.of(), mMockListener);
 
         assertThat(listener).isSameAs(mMockListener);
     }
diff --git a/tests/src/com/android/tradefed/testtype/GTestTest.java b/tests/src/com/android/tradefed/testtype/GTestTest.java
index 56a2b6f..97e71d6 100644
--- a/tests/src/com/android/tradefed/testtype/GTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestTest.java
@@ -512,9 +512,6 @@
                 (TimeUnit) EasyMock.anyObject(),
                 EasyMock.anyInt());
 
-        EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
-        EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
-
         replayMocks();
 
         mGTest.run(mMockInvocationListener);
@@ -574,11 +571,6 @@
                 (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);
diff --git a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java b/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
index 98d88eb..78718d9 100644
--- a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
+++ b/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
 
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -32,6 +33,7 @@
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
 import com.google.common.base.VerifyException;
+import com.google.common.collect.ImmutableList;
 import com.google.protobuf.ByteString;
 
 import org.junit.Before;
@@ -84,19 +86,18 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-
-        mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
     }
 
     @Test
     public void test_logsCoverageZip() throws DeviceNotAvailableException, IOException {
+        mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
+
         // Setup mocks to write the coverage measurement to the file.
         doReturn(true).when(mMockDevice).enableAdbRoot();
         doReturn(
                         new StringJoiner("\n")
-                                .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")
+                                .add("/data/misc/trace/path/to/coverage.gcda")
+                                .add("/data/misc/trace/path/to/.hidden/coverage2.gcda")
                                 .toString())
                 .when(mMockDevice)
                 .executeShellCommand(anyString());
@@ -139,6 +140,8 @@
 
     @Test
     public void testNoCoverageFiles_logsEmptyZip() throws DeviceNotAvailableException, IOException {
+        mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
+
         doReturn(true).when(mMockDevice).enableAdbRoot();
         doReturn("").when(mMockDevice).executeShellCommand(anyString());
 
@@ -160,10 +163,54 @@
     }
 
     @Test
+    public void testCoverageFlushAllProcesses_flushAllCommandCalled()
+            throws DeviceNotAvailableException, IOException {
+        mCodeCoverageListener =
+                new NativeCodeCoverageListener(
+                        mMockDevice, true, ImmutableList.of(), mFakeListener);
+
+        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
+        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 the flush-all-coverage command was called.
+        verify(mMockDevice).executeShellCommand("kill -37 -1");
+    }
+
+    @Test
+    public void testCoverageFlushSpecificProcesses_flushCommandCalled()
+            throws DeviceNotAvailableException, IOException {
+        mCodeCoverageListener =
+                new NativeCodeCoverageListener(
+                        mMockDevice, true, ImmutableList.of("mediaserver", "adbd"), mFakeListener);
+
+        doReturn(true).when(mMockDevice).enableAdbRoot();
+        doReturn(true).when(mMockDevice).isAdbRoot();
+        doReturn("123").when(mMockDevice).getProcessPid("mediaserver");
+        doReturn("56789").when(mMockDevice).getProcessPid("adbd");
+        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 the flush-coverage command was called with the specific pids.
+        verify(mMockDevice).executeShellCommand("kill -37 123 56789");
+    }
+
+    @Test
     public void testFailure_unableToPullFile() throws DeviceNotAvailableException {
+        mCodeCoverageListener = new NativeCodeCoverageListener(mMockDevice, mFakeListener);
+
         // Setup mocks.
         doReturn(true).when(mMockDevice).enableAdbRoot();
-        doReturn("/data/misc/trace/proc/self/cwd/out/some/path/to/coverage.gcda\n")
+        doReturn("/data/misc/trace/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/retry/ResultAggregatorTest.java b/tests/src/com/android/tradefed/testtype/retry/ResultAggregatorTest.java
index f71c4e4..115770b 100644
--- a/tests/src/com/android/tradefed/testtype/retry/ResultAggregatorTest.java
+++ b/tests/src/com/android/tradefed/testtype/retry/ResultAggregatorTest.java
@@ -23,8 +23,11 @@
 import com.android.tradefed.result.ILogSaver;
 import com.android.tradefed.result.ILogSaverListener;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.LogFile;
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.retry.ISupportGranularResults;
+import com.android.tradefed.retry.RetryStrategy;
 
 import org.easymock.EasyMock;
 import org.junit.Before;
@@ -60,6 +63,195 @@
 
     @Test
     public void testForwarding() {
+        LogFile beforeEnd = new LogFile("path", "url", LogDataType.TEXT);
+        TestDescription test1 = new TestDescription("classname", "test1");
+        TestDescription test2 = new TestDescription("classname", "test2");
+        ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+        EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+        // Invocation level
+        mAggListener.setLogSaver(logger);
+        mAggListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+        mDetailedListener.setLogSaver(logger);
+        mDetailedListener.invocationStarted(mInvocationContext);
+        EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+        mAggListener.testModuleStarted(mModuleContext);
+        mDetailedListener.testModuleStarted(mModuleContext);
+
+        // Detailed receives the breakdown
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+        mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+        mDetailedListener.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();
+        mAggListener.logAssociation("before-end", beforeEnd);
+        mAggListener.invocationEnded(500L);
+        mDetailedListener.logAssociation("before-end", beforeEnd);
+        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.logAssociation("before-end", beforeEnd);
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+    }
+
+    @Test
+    public void testForwarding_runFailure() {
+        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\n\nrun fail 2");
+        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.testRunFailed(EasyMock.eq("run fail\n\nrun fail 2"));
+        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.testRunFailed("run fail 2");
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.testModuleEnded();
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+    }
+
+    @Test
+    public void testForwarding_runFailure_noRerun() {
         TestDescription test1 = new TestDescription("classname", "test1");
         TestDescription test2 = new TestDescription("classname", "test2");
         ILogSaver logger = EasyMock.createMock(ILogSaver.class);
@@ -93,14 +285,6 @@
                 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(
@@ -111,10 +295,12 @@
                 EasyMock.anyLong(),
                 EasyMock.<HashMap<String, Metric>>anyObject());
         mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+        mAggListener.testFailed(test2, "I failed. retry me.");
         mAggListener.testEnded(
                 EasyMock.eq(test2),
                 EasyMock.anyLong(),
                 EasyMock.<HashMap<String, Metric>>anyObject());
+        mAggListener.testRunFailed(EasyMock.eq("run fail"));
         mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
 
         mAggListener.testModuleEnded();
@@ -139,12 +325,6 @@
         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);
@@ -222,6 +402,7 @@
         mAggregator.testStarted(test2);
         mAggregator.testFailed(test2, "I failed. retry me.");
         mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunFailed("I failed");
         mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
         // Attempt 2
         mAggregator.testRunStarted("run1", 2, 1);
@@ -233,6 +414,201 @@
         EasyMock.verify(mAggListener, mDetailedListener);
     }
 
+    @Test
+    public void testForwarding_singleRun_noModules_runFailures() {
+        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.testRunFailed(EasyMock.eq("I failed\n\nI failed 2"));
+        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.testRunFailed(EasyMock.eq("I failed\n\nI failed 2"));
+        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.testRunFailed("I failed");
+        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.testRunFailed("I failed 2");
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.invocationEnded(500L);
+        EasyMock.verify(mAggListener, mDetailedListener);
+    }
+
+    @Test
+    public void testForwarding_noModules_runFailures() {
+        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.testRunFailed(EasyMock.eq("I failed\n\nI failed 2"));
+        mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run2"), EasyMock.eq(1), 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.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.testRunFailed(EasyMock.eq("I failed\n\nI failed 2"));
+        mAggListener.testRunEnded(900L, new HashMap<String, Metric>());
+        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.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.testRunFailed("I failed");
+        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.testRunFailed("I failed 2");
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        // run 2
+        mAggregator.testRunStarted("run2", 1, 0);
+        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 module first then from a simple test run. */
     @Test
     public void testForwarding_module_noModule() {
@@ -342,6 +718,7 @@
         mAggregator.testStarted(test2);
         mAggregator.testFailed(test2, "I failed. retry me.");
         mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunFailed("I failed");
         mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
         // Attempt 2
         mAggregator.testRunStarted("run1", 2, 1);
@@ -500,6 +877,143 @@
         EasyMock.verify(mAggListener, mDetailedListener);
     }
 
+    @Test
+    public void testForwarding_noModule_module_runFailure() {
+        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.testRunFailed(EasyMock.eq("I failed\n\nI failed 2"));
+        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.testRunFailed(EasyMock.eq("I failed\n\nI failed 2"));
+        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.testRunFailed("I failed");
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+        mAggregator.testRunStarted("run2", 1, 1);
+        mAggregator.testStarted(test1);
+        mAggregator.testEnded(test1, new HashMap<String, Metric>());
+        mAggregator.testRunFailed("I failed 2");
+        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() {
@@ -642,4 +1156,112 @@
         mAggregator.invocationEnded(500L);
         EasyMock.verify(mAggListener, mDetailedListener);
     }
+
+    @Test
+    public void testForwarding_module_pass_fail_fail() {
+        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>());
+        mDetailedListener.testRunStarted(
+                EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(2), 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);
+
+        // 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.testRunFailed("failed2");
+        mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+        // Attempt 3
+        mAggregator.testRunStarted("run1", 2, 2);
+        mAggregator.testStarted(test2);
+        mAggregator.testEnded(test2, new HashMap<String, Metric>());
+        mAggregator.testRunFailed("failed3");
+        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 659391e..43d36b8 100644
--- a/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
@@ -134,6 +134,41 @@
         }
     }
 
+    /**
+     * Test that we create a module and the parameterized version of it for the include filter if
+     * not explicitly excluded.
+     */
+    @Test
+    public void testSetupFilters_parameterized_filter() throws Exception {
+        File tmpDir = FileUtil.createTempDir(TEST_MODULE);
+        File moduleConfig = new File(tmpDir, "CtsGestureTestCases.config");
+        moduleConfig.createNewFile();
+        try {
+            OptionSetter setter = new OptionSetter(mRunner);
+            setter.setOptionValue("enable-parameterized-modules", "true");
+            // The Gesture module has a parameter "instant".
+            setter.setOptionValue("module", "Gesture");
+            mRunner.setupFilters(tmpDir);
+            assertEquals(2, mRunner.getIncludeFilter().size());
+            assertThat(
+                    mRunner.getIncludeFilter(),
+                    hasItem(
+                            new SuiteTestFilter(
+                                            mRunner.getRequestedAbi(), "CtsGestureTestCases", null)
+                                    .toString()));
+            assertThat(
+                    mRunner.getIncludeFilter(),
+                    hasItem(
+                            new SuiteTestFilter(
+                                            mRunner.getRequestedAbi(),
+                                            "CtsGestureTestCases[instant]",
+                                            null)
+                                    .toString()));
+        } finally {
+            FileUtil.recursiveDelete(tmpDir);
+        }
+    }
+
     @Test
     public void testSetupFilters_match() throws Exception {
         File tmpDir = FileUtil.createTempDir(TEST_MODULE);
diff --git a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
index b64498b..15e0560 100644
--- a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
@@ -15,12 +15,17 @@
  */
 package com.android.tradefed.testtype.suite;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.testrunner.TestResult.TestStatus;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.DeviceUnresponsiveException;
 import com.android.tradefed.device.ITestDevice;
@@ -36,10 +41,12 @@
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.result.TestResult;
 import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
+import com.android.tradefed.testtype.retry.BaseRetryDecision;
+import com.android.tradefed.testtype.retry.IRetryDecision;
 import com.android.tradefed.testtype.retry.RetryStatistics;
-import com.android.tradefed.testtype.retry.RetryStrategy;
 
 import org.easymock.EasyMock;
 import org.junit.Before;
@@ -63,6 +70,7 @@
     private static final String RUN_NAME = "test run";
     private static final String RUN_NAME_2 = "test run 2";
     private InvocationContext mModuleInvocationContext;
+    private IRetryDecision mDecision;
 
     private class BasicFakeTest implements IRemoteTest {
 
@@ -139,7 +147,9 @@
         }
     }
 
-    private class FakeTest extends BasicFakeTest implements ITestFilterReceiver {
+    private class FakeTest extends BasicFakeTest implements ITestFilterReceiver, IDeviceTest {
+
+        private ITestDevice mDevice;
 
         public FakeTest(ArrayList<TestDescription> testCases) {
             super(testCases);
@@ -180,6 +190,16 @@
 
         @Override
         public void clearExcludeFilters() {}
+
+        @Override
+        public void setDevice(ITestDevice device) {
+            mDevice = device;
+        }
+
+        @Override
+        public ITestDevice getDevice() {
+            return mDevice;
+        }
     }
 
     private class MultiTestOneRunFakeTest extends FakeTest {
@@ -238,12 +258,12 @@
     }
 
     private GranularRetriableTestWrapper createGranularTestWrapper(
-            IRemoteTest test, int maxRunCount) {
+            IRemoteTest test, int maxRunCount) throws Exception {
         return createGranularTestWrapper(test, maxRunCount, new ArrayList<>());
     }
 
     private GranularRetriableTestWrapper createGranularTestWrapper(
-            IRemoteTest test, int maxRunCount, List<IMetricCollector> collectors) {
+            IRemoteTest test, int maxRunCount, List<IMetricCollector> collectors) throws Exception {
         GranularRetriableTestWrapper granularTestWrapper =
                 new GranularRetriableTestWrapper(test, null, null, null, maxRunCount);
         granularTestWrapper.setModuleId("test module");
@@ -255,6 +275,12 @@
         granularTestWrapper.setLogSaver(new FileSystemLogSaver());
         IConfiguration mockModuleConfiguration = Mockito.mock(IConfiguration.class);
         granularTestWrapper.setModuleConfig(mockModuleConfiguration);
+        mDecision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(mDecision);
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(maxRunCount));
+        mDecision.setInvocationContext(mModuleInvocationContext);
+        granularTestWrapper.setRetryDecision(mDecision);
         return granularTestWrapper;
     }
 
@@ -275,7 +301,6 @@
                 .run(Mockito.any(ITestInvocationListener.class));
 
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(mockTest, 1);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         try {
             granularTestWrapper.run(new CollectingTestListener());
             fail("Should have thrown an exception.");
@@ -327,7 +352,6 @@
         int maxRunCount = 5;
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        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());
@@ -363,7 +387,7 @@
         }
 
         // Since tests stay failed, we have two failure in our monitoring.
-        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        RetryStatistics stats = mDecision.getRetryStatistics();
         assertEquals(0, stats.mRetrySuccess);
         assertEquals(2, stats.mRetryFailure);
     }
@@ -387,7 +411,6 @@
         int maxRunCount = 5;
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        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());
@@ -423,7 +446,7 @@
         }
 
         // One success since one test recover, one test never recover so one failure
-        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        RetryStatistics stats = mDecision.getRetryStatistics();
         assertEquals(1, stats.mRetrySuccess);
         assertEquals(1, stats.mRetryFailure);
     }
@@ -448,7 +471,6 @@
         int maxRunCount = 5;
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        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());
@@ -512,7 +534,7 @@
                         .containsKey(fakeTestCase2));
 
         // One success since one test recover, one test never recover so one failure\
-        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        RetryStatistics stats = mDecision.getRetryStatistics();
         assertEquals(2, stats.mRetrySuccess);
         assertEquals(0, stats.mRetryFailure);
     }
@@ -535,7 +557,6 @@
         int maxRunCount = 3;
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
 
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -575,7 +596,6 @@
 
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, maxRunCount);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
         // Two runs.
         assertEquals(2, granularTestWrapper.getTestRunResultCollected().size());
@@ -616,7 +636,6 @@
         FakeTest test = new FakeTest(testCases);
         test.setRunFailure("I failed!");
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 3);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
 
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -632,7 +651,7 @@
         }
 
         // No Test cases tracking since it was a run retry.
-        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        RetryStatistics stats = mDecision.getRetryStatistics();
         assertEquals(0, stats.mRetrySuccess);
         assertEquals(0, stats.mRetryFailure);
     }
@@ -652,7 +671,6 @@
         test.setRunFailure("I failed!");
         test.setClearRunFailure(3);
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 7);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
 
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -672,7 +690,7 @@
         assertEquals(2, lastRes.getNumCompleteTests());
 
         // No Test cases tracking since it was a run retry.
-        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        RetryStatistics stats = mDecision.getRetryStatistics();
         assertEquals(0, stats.mRetrySuccess);
         assertEquals(0, stats.mRetryFailure);
     }
@@ -687,7 +705,12 @@
         testCases.add(fakeTestCase2);
         FakeTest test = new FakeTest(testCases);
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 3);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.ITERATIONS);
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("retry-strategy", "ITERATIONS");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
+        granularTestWrapper.setRetryDecision(decision);
         granularTestWrapper.run(new CollectingTestListener());
 
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -703,7 +726,7 @@
         }
 
         // No Test cases tracking since it was a run retry.
-        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        RetryStatistics stats = mDecision.getRetryStatistics();
         assertEquals(0, stats.mRetrySuccess);
         assertEquals(0, stats.mRetryFailure);
     }
@@ -719,7 +742,13 @@
         FakeTest test = new FakeTest(testCases);
         test.addFailedTestCase(fakeTestCase2);
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 3);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RERUN_UNTIL_FAILURE);
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("retry-strategy", "RERUN_UNTIL_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
+        granularTestWrapper.setRetryDecision(decision);
+
         granularTestWrapper.run(new CollectingTestListener());
 
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -735,8 +764,10 @@
         assertEquals(2, res.getNumCompleteTests());
 
         // No stats since no retry occurred.
-        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
-        assertNull(stats);
+        RetryStatistics stats = mDecision.getRetryStatistics();
+        assertNotNull(stats);
+        assertEquals(0, stats.mRetrySuccess);
+        assertEquals(0, stats.mRetryFailure);
     }
 
     /**
@@ -768,7 +799,6 @@
         test.addTestBecomePass(fakeTestCase1, 5);
         GranularRetriableTestWrapper granularTestWrapper =
                 createGranularTestWrapper(test, 7, collectors);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
         granularTestWrapper.run(new CollectingTestListener());
 
         assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -809,7 +839,7 @@
         assertFalse(lastRes.getRunProtoMetrics().containsKey("not-called"));
 
         // Check that failure are cleared
-        RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+        RetryStatistics stats = mDecision.getRetryStatistics();
         assertEquals(1, stats.mRetrySuccess);
         assertEquals(0, stats.mRetryFailure);
     }
@@ -819,15 +849,20 @@
      */
     @Test
     public void testIntraModuleRun_rebootAtLastIntraModuleRetry() throws Exception {
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("reboot-at-last-retry", "true");
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
         FakeTest test = new FakeTest();
         test.setRunFailure("I failed!");
         ITestDevice mMockDevice = EasyMock.createMock(ITestDevice.class);
+        test.setDevice(mMockDevice);
         mModuleInvocationContext.addAllocatedDevice("default-device1", mMockDevice);
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 3);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
-        granularTestWrapper.setRebootAtLastRetry(true);
+        granularTestWrapper.setRetryDecision(decision);
         EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(EasyMock.createMock(IDevice.class));
-        EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("SERIAL");
         mMockDevice.reboot();
         EasyMock.replay(mMockDevice);
         granularTestWrapper.run(new CollectingTestListener());
@@ -839,26 +874,28 @@
      */
     @Test
     public void testIntraModuleRun_rebootMultiDevicesAtLastIntraModuleRetry() throws Exception {
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("reboot-at-last-retry", "true");
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModuleInvocationContext);
         FakeTest test = new FakeTest();
         test.setRunFailure("I failed!");
         ITestDevice mMockDevice = EasyMock.createMock(ITestDevice.class);
         ITestDevice mMockDevice2 = EasyMock.createMock(ITestDevice.class);
+        test.setDevice(mMockDevice);
         mModuleInvocationContext.addAllocatedDevice("default-device1", mMockDevice);
         mModuleInvocationContext.addAllocatedDevice("default-device2", mMockDevice2);
         GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 3);
-        granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
-        granularTestWrapper.setRebootAtLastRetry(true);
+        granularTestWrapper.setRetryDecision(decision);
         EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(EasyMock.createMock(IDevice.class));
-        EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("SERIAL");
         EasyMock.expect(mMockDevice2.getIDevice()).andStubReturn(EasyMock.createMock(IDevice.class));
-        EasyMock.expect(mMockDevice2.getSerialNumber()).andReturn("SERIAL-2");
         mMockDevice.reboot();
         mMockDevice2.reboot();
-        EasyMock.replay(mMockDevice);
-        EasyMock.replay(mMockDevice2);
+        EasyMock.replay(mMockDevice, mMockDevice2);
         granularTestWrapper.run(new CollectingTestListener());
-        EasyMock.verify(mMockDevice);
-        EasyMock.verify(mMockDevice2);
+        EasyMock.verify(mMockDevice, mMockDevice2);
     }
 
     /** Collector that track if it was called or not */
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
index 62b3512..c5a62c4 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
@@ -66,6 +66,8 @@
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
 import com.android.tradefed.testtype.StubTest;
+import com.android.tradefed.testtype.retry.BaseRetryDecision;
+import com.android.tradefed.testtype.retry.IRetryDecision;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.MultiMap;
 
@@ -1492,9 +1494,15 @@
         mTestSuite.setDevice(mMockDevice);
         mTestSuite.setBuild(mMockBuildInfo);
         mTestSuite.setConfiguration(mStubMainConfiguration);
-        mStubMainConfiguration.getCommandOptions().setMaxRetryCount(maxRunLimit);
         OptionSetter cmdSetter = new OptionSetter(mStubMainConfiguration.getCommandOptions());
         cmdSetter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(maxRunLimit));
+        decision.setInvocationContext(mContext);
+        mStubMainConfiguration.setConfigurationObject(
+                Configuration.RETRY_DECISION_TYPE_NAME, decision);
         mContext = new InvocationContext();
         mTestSuite.setInvocationContext(mContext);
         mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
index 6bb0d10..e2c178a 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
@@ -57,7 +57,8 @@
 import com.android.tradefed.testtype.IDeviceTest;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.ITestFilterReceiver;
-import com.android.tradefed.testtype.retry.RetryStrategy;
+import com.android.tradefed.testtype.retry.BaseRetryDecision;
+import com.android.tradefed.testtype.retry.IRetryDecision;
 import com.android.tradefed.testtype.suite.module.BaseModuleController;
 import com.android.tradefed.testtype.suite.module.IModuleController;
 import com.android.tradefed.testtype.suite.module.TestFailureModuleController;
@@ -100,6 +101,8 @@
     private ILogSaver mMockLogSaver;
     private ILogSaverListener mMockLogSaverListener;
 
+    private IRetryDecision mDecision = new BaseRetryDecision();
+
     private interface ITestInterface extends IRemoteTest, IBuildReceiver, IDeviceTest {}
 
     /** Test implementation that allows us to exercise different use cases * */
@@ -289,7 +292,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
-
+        mModule.setRetryDecision(mDecision);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -398,6 +401,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
+        mModule.setRetryDecision(mDecision);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -751,6 +755,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
+        mModule.setRetryDecision(mDecision);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -799,6 +804,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
+        mModule.setRetryDecision(mDecision);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -861,6 +867,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
+        mModule.setRetryDecision(mDecision);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -1020,6 +1027,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         config);
+        mModule.setRetryDecision(mDecision);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -1031,9 +1039,9 @@
         mMockListener.testEnded(
                 EasyMock.anyObject(),
                 EasyMock.anyLong(),
-                (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.<HashMap<String, Metric>>anyObject());
         mMockListener.testRunEnded(
-                EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
         replayMocks();
         mModule.run(mMockListener, null, null);
         verifyMocks();
@@ -1081,6 +1089,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
+        mModule.setRetryDecision(mDecision);
         mModule.setLogSaver(mMockLogSaver);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
@@ -1164,11 +1173,12 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         config);
+        mModule.setRetryDecision(mDecision);
         mModule.setLogSaver(mMockLogSaver);
         mMockListener.testRunStarted(
                 EasyMock.eq("fakeName"), EasyMock.eq(0), EasyMock.eq(0), EasyMock.anyLong());
         mMockListener.testRunEnded(
-                EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
+                EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
         replayMocks();
         mModule.run(mMockListener, null, failureListener);
         verifyMocks();
@@ -1187,6 +1197,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
+        mModule.setRetryDecision(mDecision);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -1255,6 +1266,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
+        mModule.setRetryDecision(mDecision);
         mModule.setLogSaver(mMockLogSaver);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
@@ -1324,7 +1336,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
-
+        mModule.setRetryDecision(mDecision);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -1377,6 +1389,7 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
+        mModule.setRetryDecision(mDecision);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -1418,7 +1431,13 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
-        mModule.setRetryStrategy(RetryStrategy.ITERATIONS, false);
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("retry-strategy", "ITERATIONS");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModule.getModuleInvocationContext());
+        mModule.setRetryDecision(decision);
+        mModule.setMergeAttemps(false);
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
                 .addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
@@ -1503,7 +1522,13 @@
                         mMapDeviceTargetPreparer,
                         mMultiTargetPrepList,
                         new Configuration("", ""));
-        mModule.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE, false);
+        IRetryDecision decision = new BaseRetryDecision();
+        OptionSetter setter = new OptionSetter(decision);
+        setter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
+        setter.setOptionValue("max-testcase-run-count", Integer.toString(3));
+        decision.setInvocationContext(mModule.getModuleInvocationContext());
+        mModule.setRetryDecision(decision);
+        mModule.setMergeAttemps(false);
 
         mModule.getModuleInvocationContext().addAllocatedDevice(DEFAULT_DEVICE_NAME, mMockDevice);
         mModule.getModuleInvocationContext()
@@ -1518,6 +1543,9 @@
         EasyMock.expect(mMockCleaner.isTearDownDisabled()).andStubReturn(false);
         mMockCleaner.tearDown(
                 EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
+        EasyMock.expect(mMockDevice.getIDevice())
+                .andReturn(EasyMock.createMock(IDevice.class))
+                .times(3);
         // We expect a total count on the run start so 4, all aggregated under the same run
         for (int attempt = 0; attempt < 3; attempt++) {
             if (attempt == 0) {
diff --git a/tests/src/com/android/tradefed/testtype/suite/params/InstantAppHandlerTest.java b/tests/src/com/android/tradefed/testtype/suite/params/InstantAppHandlerTest.java
index 24209c5..42970ec 100644
--- a/tests/src/com/android/tradefed/testtype/suite/params/InstantAppHandlerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/params/InstantAppHandlerTest.java
@@ -48,7 +48,7 @@
         mModuleConfig = new Configuration("test", "test");
     }
 
-    private class TestFilterable implements IRemoteTest, ITestAnnotationFilterReceiver {
+    protected static class TestFilterable implements IRemoteTest, ITestAnnotationFilterReceiver {
 
         public Set<String> mReceivedFiltered = new HashSet<>();
 
diff --git a/tests/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandlerTest.java b/tests/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandlerTest.java
new file mode 100644
index 0000000..5573742
--- /dev/null
+++ b/tests/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandlerTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.suite.params;
+
+import static org.junit.Assert.assertEquals;
+
+import android.platform.test.annotations.SystemUserOnly;
+
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.testtype.suite.params.InstantAppHandlerTest.TestFilterable;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SecondaryUserHandler}. */
+@RunWith(JUnit4.class)
+public class SecondaryUserHandlerTest {
+
+    private SecondaryUserHandler mHandler;
+    private IConfiguration mModuleConfig;
+
+    @Before
+    public void setUp() {
+        mHandler = new SecondaryUserHandler();
+        mModuleConfig = new Configuration("test", "test");
+    }
+
+    /** Test that when a module configuration go through the handler it gets tuned properly. */
+    @Test
+    public void testApplySetup() {
+        TestFilterable test = new TestFilterable();
+        assertEquals(0, test.mReceivedFiltered.size());
+        mModuleConfig.setTest(test);
+        mHandler.applySetup(mModuleConfig);
+
+        // User zero is filtered
+        assertEquals(1, test.mReceivedFiltered.size());
+        assertEquals(
+                SystemUserOnly.class.getCanonicalName(), test.mReceivedFiltered.iterator().next());
+    }
+}
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 380e75c..000e565 100644
--- a/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
@@ -347,6 +347,45 @@
         verify(mSuite).setExcludeFilter(excludeRun1);
     }
 
+    /** Ensure that the --module option can be used to force a single module to run. */
+    @Test
+    public void testReschedule_module_option() throws Exception {
+        OptionSetter setter = new OptionSetter(mTest);
+        setter.setOptionValue(BaseTestSuite.MODULE_OPTION, "run0");
+        populateFakeResults(2, 2, 1, 0, 0, false);
+        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<>();
+        excludeRun0.add("run0 test.class#testPass0");
+        verify(mSuite).setExcludeFilter(excludeRun0);
+        Set<String> excludeRun1 = new HashSet<>();
+        // Only run0 was requested to run.
+        excludeRun1.add("run1");
+        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.
@@ -391,6 +430,47 @@
         verify(mSuite).setExcludeFilter(excludeRun1);
     }
 
+    /** Ensure that --module works when abi are present. */
+    @Test
+    public void testReschedule_moduleOption_abi() throws Exception {
+        OptionSetter setter = new OptionSetter(mTest);
+        // We specify to exclude "run1"
+        setter.setOptionValue(BaseTestSuite.MODULE_OPTION, "run0");
+        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 {
diff --git a/tests/src/com/android/tradefed/util/BuildTestsZipUtilsTest.java b/tests/src/com/android/tradefed/util/BuildTestsZipUtilsTest.java
index 29f2f0f..22da4fd 100644
--- a/tests/src/com/android/tradefed/util/BuildTestsZipUtilsTest.java
+++ b/tests/src/com/android/tradefed/util/BuildTestsZipUtilsTest.java
@@ -113,4 +113,35 @@
             FileUtil.recursiveDelete(testDir);
         }
     }
+
+    /**
+     * Tests that when the search finds the file in the sub testcases dir and under a different
+     * module directory we still properly find it and return it.
+     */
+    @Test
+    public void testGetApkFile_fromTestDir_differentModule_testCase() throws Exception {
+        DeviceBuildInfo buildInfo = new DeviceBuildInfo();
+        File testDir = FileUtil.createTempDir("test-dir-build-tests");
+        try {
+            buildInfo.setTestsDir(testDir, "1");
+            File testcasedir = new File(testDir, "testcases");
+            testcasedir.mkdir();
+            File apkDir = new File(testcasedir, "DifferentTestModule");
+            apkDir.mkdir();
+            File apk = new File(apkDir, "TestApk.apk");
+            apk.createNewFile();
+            File apkFile =
+                    BuildTestsZipUtils.getApkFile(
+                            buildInfo,
+                            "TestApk.apk",
+                            new ArrayList<>(),
+                            AltDirBehavior.FALLBACK,
+                            true,
+                            null);
+            assertNotNull(apkFile);
+            assertEquals(apk, apkFile);
+        } finally {
+            FileUtil.recursiveDelete(testDir);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/RemoteZipTest.java b/tests/src/com/android/tradefed/util/RemoteZipTest.java
new file mode 100644
index 0000000..ac461c8
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/RemoteZipTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.build.IFileDownloader;
+import com.android.tradefed.util.zip.CentralDirectoryInfo;
+import com.android.tradefed.util.zip.EndCentralDirectoryInfo;
+
+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.stubbing.Answer;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Unit tests for {@link RemoteZip} */
+@RunWith(JUnit4.class)
+public class RemoteZipTest {
+
+    private static final String REMOTE_FILE =
+            "aosp_master-linux-yakju-userdebug/P123/device-tests.zip";
+
+    private IFileDownloader mDownloader;
+    private List<CentralDirectoryInfo> mExpectedEntries;
+    private long mZipFileSize;
+
+    private void saveTestDataFile(File destFile, long startOffset, long size) {
+        try {
+            final InputStream inputStream =
+                    ZipUtilTest.class.getResourceAsStream("/util/partial_zip.zip");
+            FileUtil.writeToFile(inputStream, destFile, false, startOffset, size);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mDownloader = Mockito.mock(IFileDownloader.class);
+
+        Mockito.doAnswer(
+                        (Answer)
+                                invocation -> {
+                                    String remoteFilePath = (String) invocation.getArgument(0);
+                                    File destFile = (File) invocation.getArgument(1);
+                                    long startOffset = (long) invocation.getArgument(2);
+                                    long size = (long) invocation.getArgument(3);
+                                    saveTestDataFile(destFile, startOffset, size);
+                                    return null;
+                                })
+                .when(mDownloader)
+                .downloadFile(
+                        Mockito.eq(REMOTE_FILE),
+                        Mockito.anyObject(),
+                        Mockito.anyLong(),
+                        Mockito.anyLong());
+
+        File zipFile = FileUtil.createTempFileForRemote("zipfile", null);
+        // Delete it so name is available
+        zipFile.delete();
+        try {
+            saveTestDataFile(zipFile, -1, 0);
+            EndCentralDirectoryInfo endCentralDirInfo = new EndCentralDirectoryInfo(zipFile);
+            mExpectedEntries =
+                    ZipUtil.getZipCentralDirectoryInfos(
+                            zipFile, endCentralDirInfo, endCentralDirInfo.getCentralDirOffset());
+            mZipFileSize = zipFile.length();
+        } finally {
+            FileUtil.deleteFile(zipFile);
+        }
+    }
+
+    @Test
+    public void testGetZipEntries() throws Exception {
+        File destDir = null;
+        try {
+            destDir = FileUtil.createTempDir("test");
+
+            RemoteZip remoteZip = new RemoteZip(REMOTE_FILE, mZipFileSize, mDownloader);
+
+            List<CentralDirectoryInfo> entries = remoteZip.getZipEntries();
+
+            assertEquals(7, entries.size());
+            assertTrue(mExpectedEntries.containsAll(entries));
+        } finally {
+            FileUtil.recursiveDelete(destDir);
+        }
+    }
+
+    @Test
+    public void testDownloadFilesFromZip() throws Exception {
+        File destDir = null;
+        try {
+            destDir = FileUtil.createTempDir("test");
+
+            List<CentralDirectoryInfo> files = new ArrayList<>();
+            for (CentralDirectoryInfo info : mExpectedEntries) {
+                if (info.getFileName().equals("large_text/file.txt")
+                        || info.getFileName().equals("executable/executable_file")) {
+                    files.add(info);
+                }
+            }
+
+            RemoteZip remoteZip = new RemoteZip(REMOTE_FILE, mZipFileSize, mDownloader);
+            remoteZip.downloadFiles(destDir, files);
+
+            File targetFile = Paths.get(destDir.getPath(), "large_text", "file.txt").toFile();
+            assertEquals(4146093769L, FileUtil.calculateCrc32(targetFile));
+            targetFile = Paths.get(destDir.getPath(), "executable", "executable_file").toFile();
+            assertTrue(targetFile.exists());
+            // File not in the list is not unzipped.
+            targetFile = Paths.get(destDir.getPath(), "empty_file").toFile();
+            assertFalse(targetFile.exists());
+        } finally {
+            FileUtil.recursiveDelete(destDir);
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java b/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
index 9d84bab..7d95731 100644
--- a/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
+++ b/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
@@ -346,6 +346,19 @@
         }
     }
 
+    /** Tests that the parser can be joined immediately if no connection was established. */
+    @Test
+    public void testParser_noConnection() throws Exception {
+        ITestInvocationListener listener = EasyMock.createMock(ITestInvocationListener.class);
+        EasyMock.replay(listener);
+        try (SubprocessTestResultsParser parser =
+                new SubprocessTestResultsParser(listener, true, new InvocationContext())) {
+            // returns immediately as a connection was not established
+            assertTrue(parser.joinReceiver(50, false));
+            EasyMock.verify(listener);
+        }
+    }
+
     /** Tests the parser receiving event on updating test tag. */
     @Test
     public void testParse_testTag() throws Exception {
diff --git a/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java b/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java
index 5cd860d..3ce254a 100644
--- a/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java
+++ b/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java
@@ -46,7 +46,7 @@
         startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
 
         List<MergedZipEntryCollection> collections =
-                MergedZipEntryCollection.CreateCollections(entries);
+                MergedZipEntryCollection.createCollections(entries);
         assertEquals(1, collections.size());
         assertEquals(2, collections.get(0).getZipEntries().size());
         assertEquals(10, collections.get(0).getStartOffset());
@@ -69,7 +69,7 @@
         startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
 
         List<MergedZipEntryCollection> collections =
-                MergedZipEntryCollection.CreateCollections(entries);
+                MergedZipEntryCollection.createCollections(entries);
         assertEquals(2, collections.size());
         assertEquals(1, collections.get(0).getZipEntries().size());
         assertEquals(10, collections.get(0).getStartOffset());
@@ -97,7 +97,7 @@
         startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
 
         List<MergedZipEntryCollection> collections =
-                MergedZipEntryCollection.CreateCollections(entries);
+                MergedZipEntryCollection.createCollections(entries);
         assertEquals(1, collections.size());
         assertEquals(11, collections.get(0).getZipEntries().size());
         assertEquals(10, collections.get(0).getStartOffset());
@@ -123,7 +123,7 @@
         startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
 
         List<MergedZipEntryCollection> collections =
-                MergedZipEntryCollection.CreateCollections(entries);
+                MergedZipEntryCollection.createCollections(entries);
         assertEquals(2, collections.size());
         assertEquals(10, collections.get(0).getZipEntries().size());
         assertEquals(10, collections.get(0).getStartOffset());