Updates to WiFi RvR pass/fail, plotting, etc. am: c01e1efd1c
am: 5d462d263b

Change-Id: Id01922b2a063d48f01a4c9e5eb8ceabaeb866274
diff --git a/Android.mk b/Android.mk
index 1d1ffed..11b923b 100644
--- a/Android.mk
+++ b/Android.mk
@@ -20,6 +20,7 @@
 
 ifeq ($(HOST_OS),linux)
 
+# general Android Conntectivity Test Suite
 ACTS_DISTRO := $(HOST_OUT)/acts-dist/acts.zip
 
 $(ACTS_DISTRO): $(sort $(shell find $(LOCAL_PATH)/acts))
@@ -31,4 +32,30 @@
 
 $(call dist-for-goals,tests,$(ACTS_DISTRO))
 
+# Wear specific Android Connectivity Test Suite
+WTS_ACTS_DISTRO_DIR := $(HOST_OUT)/wts-acts-dist
+WTS_ACTS_DISTRO := $(WTS_ACTS_DISTRO_DIR)/wts-acts
+WTS_ACTS_DISTRO_ARCHIVE := $(WTS_ACTS_DISTRO_DIR)/wts-acts.zip
+WTS_LOCAL_ACTS_DIR := tools/test/connectivity/acts/framework/acts/
+
+$(WTS_ACTS_DISTRO): $(SOONG_ZIP)
+	@echo "Packaging WTS-ACTS into $(WTS_ACTS_DISTRO)"
+	# clean-up and mkdir for dist
+	@rm -Rf $(WTS_ACTS_DISTRO_DIR)
+	@mkdir -p $(WTS_ACTS_DISTRO_DIR)
+	# grab the files from local acts framework and zip them up
+	$(hide) find $(WTS_LOCAL_ACTS_DIR) | sort >$@.list
+	$(hide) $(SOONG_ZIP) -d -P acts -o $(WTS_ACTS_DISTRO_ARCHIVE) -C tools/test/connectivity/acts/framework/acts/ -l $@.list
+	# add in the local wts py files for use with the prebuilt
+	$(hide) zip -r $(WTS_ACTS_DISTRO_ARCHIVE) -j tools/test/connectivity/wts-acts/*.py
+	# create executable tool from the archive
+	$(hide) echo '#!/usr/bin/env python' | cat - $(WTS_ACTS_DISTRO_DIR)/wts-acts.zip > $(WTS_ACTS_DISTRO_DIR)/wts-acts
+	$(hide) chmod 755 $(WTS_ACTS_DISTRO)
+
+wts-acts: $(WTS_ACTS_DISTRO)
+
+$(call dist-for-goals,tests,$(WTS_ACTS_DISTRO))
+
+
+
 endif
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 5ad47bf..a07b050 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -6,7 +6,7 @@
 acts_base_class_test = ./acts/framework/tests/acts_base_class_test.py
 acts_logger_test = ./acts/framework/tests/acts_logger_test.py
 acts_records_test = ./acts/framework/tests/acts_records_test.py
-acts_sl4a_client_test = ./acts/framework/tests/acts_sl4a_client_test.py
+sl4a_lib_suite = ./acts/framework/tests/controllers/sl4a_lib/test_suite.py
 acts_job_test = ./acts/framework/tests/acts_job_test.py
 acts_test_runner_test = ./acts/framework/tests/acts_test_runner_test.py
 acts_unittest_suite = ./acts/framework/tests/acts_unittest_suite.py
diff --git a/acts/framework/acts/base_test.py b/acts/framework/acts/base_test.py
index 21394e5..7be85a9 100755
--- a/acts/framework/acts/base_test.py
+++ b/acts/framework/acts/base_test.py
@@ -685,7 +685,7 @@
             bugreport_path = os.path.join(ad.log_path, test_name)
             utils.create_dir(bugreport_path)
             ad.check_crash_report(test_name, begin_time, True)
-            if getattr(ad, "qxdm_always_on", False):
+            if getattr(ad, "qxdm_log", False):
                 ad.get_qxdm_logs()
         except Exception as e:
             ad.log.error("Failed to take a bug report for %s with error %s",
diff --git a/acts/framework/acts/bin/__init__.py b/acts/framework/acts/bin/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/bin/__init__.py
diff --git a/acts/framework/acts/bin/act.py b/acts/framework/acts/bin/act.py
index 6227b53..2e533cd 100755
--- a/acts/framework/acts/bin/act.py
+++ b/acts/framework/acts/bin/act.py
@@ -135,9 +135,9 @@
         try:
             ret = _run_test(c, test_identifiers, repeat)
             ok = ok and ret
-        except:
-            print("Exception occurred when executing test bed %s" %
-                  c[keys.Config.key_testbed.value])
+        except Exception as e:
+            print("Exception occurred when executing test bed %s. %s" %
+                  (c[keys.Config.key_testbed.value], e))
     return ok
 
 
diff --git a/acts/framework/acts/config_parser.py b/acts/framework/acts/config_parser.py
index faa479d..176e7c3 100755
--- a/acts/framework/acts/config_parser.py
+++ b/acts/framework/acts/config_parser.py
@@ -104,18 +104,10 @@
     Raises:
         If any part of the configuration is invalid, ActsConfigError is raised.
     """
-    seen_names = set()
     # Cross checks testbed configs for resource conflicts.
-    for config in testbed_configs:
+    for name, config in testbed_configs.items():
         _update_file_paths(config, config_path)
-        # Check for conflicts between multiple concurrent testbed configs.
-        # No need to call it if there's only one testbed config.
-        name = config[keys.Config.key_testbed_name.value]
         _validate_testbed_name(name)
-        # Test bed names should be unique.
-        if name in seen_names:
-            raise ActsConfigError("Duplicate testbed name %s found." % name)
-        seen_names.add(name)
 
 
 def _verify_test_class_name(test_cls_name):
@@ -264,17 +256,29 @@
     if override_test_case_iterations:
         configs[keys.Config.key_test_case_iterations.value] = \
             override_test_case_iterations
+
+    testbeds = configs[keys.Config.key_testbed.value]
+    if type(testbeds) is list:
+        tb_dict = dict()
+        for testbed in testbeds:
+            tb_dict[testbed[keys.Config.key_testbed_name.value]] = testbed
+        testbeds = tb_dict
+    elif type(testbeds) is dict:
+        # For compatibility, make sure the entry name is the same as
+        # the testbed's "name" entry
+        for name, testbed in testbeds.items():
+            testbed[keys.Config.key_testbed_name.value] = name
+
     if tb_filters:
-        tbs = []
-        for tb in configs[keys.Config.key_testbed.value]:
-            if tb[keys.Config.key_testbed_name.value] in tb_filters:
-                tbs.append(tb)
-        if len(tbs) != len(tb_filters):
-            raise ActsConfigError(
-                ("Expect to find %d test bed configs, found %d. Check if"
-                 " you have the correct test bed names.") % (len(tb_filters),
-                                                             len(tbs)))
-        configs[keys.Config.key_testbed.value] = tbs
+        tbs = {}
+        for name in tb_filters:
+            if name in testbeds:
+                tbs[name] = testbeds[name]
+            else:
+                raise ActsConfigError(
+                    'Expected testbed named "%s", but none was found. Check'
+                    'if you have the correct testbed names.' % name)
+        testbeds = tbs
 
     if (not keys.Config.key_log_path.value in configs
             and _ENV_ACTS_LOGPATH in os.environ):
@@ -290,18 +294,17 @@
 
     k_log_path = keys.Config.key_log_path.value
     configs[k_log_path] = utils.abs_path(configs[k_log_path])
-    config_path, _ = os.path.split(utils.abs_path(test_config_path))
-    configs[keys.Config.key_config_path] = config_path
-    _validate_test_config(configs)
-    _validate_testbed_configs(configs[keys.Config.key_testbed.value],
-                              config_path)
-    # Unpack testbeds into separate json objects.
-    beds = configs.pop(keys.Config.key_testbed.value)
-    config_jsons = []
+
     # TODO: See if there is a better way to do this: b/29836695
     config_path, _ = os.path.split(utils.abs_path(test_config_path))
     configs[keys.Config.key_config_path] = config_path
-    for original_bed_config in beds:
+    _validate_test_config(configs)
+    _validate_testbed_configs(testbeds, config_path)
+    # Unpack testbeds into separate json objects.
+    configs.pop(keys.Config.key_testbed.value)
+    config_jsons = []
+
+    for _, original_bed_config in testbeds.items():
         new_test_config = dict(configs)
         new_test_config[keys.Config.key_testbed.value] = original_bed_config
         # Keys in each test bed config will be copied to a level up to be
diff --git a/acts/framework/acts/controllers/adb.py b/acts/framework/acts/controllers/adb.py
index 0a652a3..3461071 100644
--- a/acts/framework/acts/controllers/adb.py
+++ b/acts/framework/acts/controllers/adb.py
@@ -17,14 +17,9 @@
 from builtins import str
 
 import logging
-import random
 import re
 import shellescape
-import socket
-import time
 
-from acts.controllers.utils_lib import host_utils
-from acts.controllers.utils_lib.ssh import connection
 from acts.libs.proc import job
 
 DEFAULT_ADB_TIMEOUT = 60
@@ -32,6 +27,9 @@
 # Uses a regex to be backwards compatible with previous versions of ADB
 # (N and above add the serial to the error msg).
 DEVICE_NOT_FOUND_REGEX = re.compile('^error: device (?:\'.*?\' )?not found')
+DEVICE_OFFLINE_REGEX = re.compile('^error: device offline')
+ROOT_USER_ID = '0'
+SHELL_USER_ID = '2000'
 
 
 def parsing_parcel_output(output):
@@ -80,7 +78,7 @@
         Args:
             serial: str serial number of Android device from `adb devices`
             ssh_connection: SshConnection instance if the Android device is
-                            conected to a remote host that we can reach via SSH.
+                            connected to a remote host that we can reach via SSH.
         """
         self.serial = serial
         adb_path = self._exec_cmd("which adb")
@@ -107,6 +105,47 @@
         self.adb_str = " ".join(adb_cmd)
         self._ssh_connection = ssh_connection
 
+    def get_user_id(self):
+        """Returns the adb user. Either 2000 (shell) or 0 (root)."""
+        return self.shell('id -u')
+
+    def is_root(self, user_id=None):
+        """Checks if the user is root.
+
+        Args:
+            user_id: if supplied, the id to check against.
+        Returns:
+            True if the user is root. False otherwise.
+        """
+        if not user_id:
+            user_id = self.get_user_id()
+        return user_id == ROOT_USER_ID
+
+    def ensure_root(self):
+        """Ensures the user is root after making this call.
+
+        Note that this will still fail if the device is a user build, as root
+        is not accessible from a user build.
+
+        Returns:
+            False if the device is a user build. True otherwise.
+        """
+        self.ensure_user(ROOT_USER_ID)
+        return self.is_root()
+
+    def ensure_user(self, user_id=SHELL_USER_ID):
+        """Ensures the user is set to the given user.
+
+        Args:
+            user_id: The id of the user.
+        """
+        if self.is_root(user_id):
+            self.root()
+        else:
+            self.unroot()
+        self.wait_for_device()
+        return self.get_user_id() == user_id
+
     def _exec_cmd(self, cmd, ignore_status=False, timeout=DEFAULT_ADB_TIMEOUT):
         """Executes adb commands in a new shell.
 
@@ -126,10 +165,12 @@
 
         logging.debug("cmd: %s, stdout: %s, stderr: %s, ret: %s", cmd, out,
                       err, ret)
-        if not ignore_status and ret == 1 and DEVICE_NOT_FOUND_REGEX.match(err):
-            raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret)
-        elif "Result: Parcel" in out:
+        if "Result: Parcel" in out:
             return parsing_parcel_output(out)
+        if ignore_status:
+            return out or err
+        if ret == 1 and DEVICE_NOT_FOUND_REGEX.match(err):
+            raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret)
         else:
             return out
 
@@ -156,6 +197,9 @@
         Args:
             host_port: Port number to use on localhost
             device_port: Port number to use on the android device.
+
+        Returns:
+            The command output for the forward command.
         """
         if self._ssh_connection:
             # We have to hop through a remote host first.
@@ -166,7 +210,7 @@
             self._ssh_connection.create_ssh_tunnel(
                 remote_port, local_port=host_port)
             host_port = remote_port
-        self.forward("tcp:%d tcp:%d" % (host_port, device_port))
+        return self.forward("tcp:%d tcp:%d" % (host_port, device_port))
 
     def remove_tcp_forward(self, host_port):
         """Stop tcp forwarding a port from localhost to this android device.
diff --git a/acts/framework/acts/controllers/android_device.py b/acts/framework/acts/controllers/android_device.py
index 29e2be0..f388c29 100755
--- a/acts/framework/acts/controllers/android_device.py
+++ b/acts/framework/acts/controllers/android_device.py
@@ -21,6 +21,7 @@
 import logging
 import os
 import re
+import shellescape
 import socket
 import time
 
@@ -29,10 +30,8 @@
 from acts import tracelogger
 from acts import utils
 from acts.controllers import adb
-from acts.controllers import event_dispatcher
 from acts.controllers import fastboot
-from acts.controllers import sl4a_client
-from acts.controllers.utils_lib import host_utils
+from acts.controllers.sl4a_lib import sl4a_manager
 from acts.controllers.utils_lib.ssh import connection
 from acts.controllers.utils_lib.ssh import settings
 
@@ -53,6 +52,10 @@
 PORT_RETRY_COUNT = 3
 IPERF_TIMEOUT = 60
 SL4A_APK_NAME = "com.googlecode.android_scripting"
+WAIT_FOR_DEVICE_TIMEOUT = 180
+ENCRYPTION_WINDOW = "CryptKeeper"
+DEFAULT_DEVICE_PASSWORD = "1111"
+RELEASE_ID_REGEXES = [re.compile(r'\w+\.\d+\.\d+'), re.compile(r'N\w+')]
 
 
 class AndroidDeviceError(signals.ControllerError):
@@ -127,6 +130,18 @@
     return device_info
 
 
+def get_post_job_info(ads):
+    """Returns the tracked build id to test_run_summary.json
+
+    Args:
+        ads: A list of AndroidDevice objects.
+
+    Returns:
+        A dict consisting of {'build_id': ads[0].build_info}
+    """
+    return 'Build Info', ads[0].build_info
+
+
 def _start_services_on_ads(ads):
     """Starts long running services on multiple AndroidDevice objects.
 
@@ -143,13 +158,10 @@
             ad.log.error("User window cannot come up")
             destroy(running_ads)
             raise AndroidDeviceError("User window cannot come up")
-        if not ad.is_sl4a_installed():
+        if not ad.skip_sl4a and not ad.is_sl4a_installed():
             ad.log.error("sl4a.apk is not installed")
-            if not ad.skip_sl4a:
-                ad.log.exception("The required sl4a.apk is not installed")
-                destroy(running_ads)
-                raise AndroidDeviceError(
-                    "The required sl4a.apk is not installed")
+            destroy(running_ads)
+            raise AndroidDeviceError("The required sl4a.apk is not installed")
         try:
             ad.start_services(skip_sl4a=ad.skip_sl4a)
         except:
@@ -346,11 +358,7 @@
     device.
 
     Attributes:
-        serial: A string that's the serial number of the Androi device.
-        h_port: An integer that's the port number for adb port forwarding used
-                on the computer the Android device is connected
-        d_port: An integer  that's the port number used on the Android device
-                for adb port forwarding.
+        serial: A string that's the serial number of the Android device.
         log_path: A string that is the path where all logs collected on this
                   android device should be stored.
         log: A logger adapted from root logger with added token specific to an
@@ -363,21 +371,15 @@
                   via fastboot.
     """
 
-    def __init__(self,
-                 serial="",
-                 host_port=None,
-                 device_port=sl4a_client.DEFAULT_DEVICE_SIDE_PORT,
-                 ssh_connection=None):
+    def __init__(self, serial='', ssh_connection=None):
         self.serial = serial
-        self.h_port = host_port
-        self.d_port = device_port
         # logging.log_path only exists when this is used in an ACTS test run.
-        log_path_base = getattr(logging, "log_path", "/tmp/logs")
-        self.log_path = os.path.join(log_path_base, "AndroidDevice%s" % serial)
+        log_path_base = getattr(logging, 'log_path', '/tmp/logs')
+        self.log_path = os.path.join(log_path_base, 'AndroidDevice%s' % serial)
         self.log = tracelogger.TraceLogger(
-            AndroidDeviceLoggerAdapter(logging.getLogger(),
-                                       {"serial": self.serial}))
-        self._droid_sessions = {}
+            AndroidDeviceLoggerAdapter(logging.getLogger(), {
+                'serial': self.serial
+            }))
         self._event_dispatchers = {}
         self.adb_logcat_process = None
         self.adb_logcat_file_path = None
@@ -389,22 +391,19 @@
         self._ssh_connection = ssh_connection
         self.skip_sl4a = False
         self.crash_report = None
-        self.device_password = None
+        self._sl4a_manager = sl4a_manager.Sl4aManager(self.adb)
 
     def clean_up(self):
         """Cleans up the AndroidDevice object and releases any resources it
         claimed.
         """
         self.stop_services()
-        if self.h_port:
-            self.adb.remove_tcp_forward(self.h_port)
-            self.h_port = None
         if self._ssh_connection:
             self._ssh_connection.close()
 
     # TODO(angli): This function shall be refactored to accommodate all services
     # and not have hard coded switch for SL4A when b/29157104 is done.
-    def start_services(self, skip_sl4a=False):
+    def start_services(self, skip_sl4a=False, skip_setup_wizard=True):
         """Starts long running services on the android device.
 
         1. Start adb logcat capture.
@@ -412,13 +411,15 @@
 
         Args:
             skip_sl4a: Does not attempt to start SL4A if True.
+            skip_setup_wizard: Whether or not to skip the setup wizard.
         """
         try:
             self.start_adb_logcat()
         except:
             self.log.exception("Failed to start adb logcat!")
             raise
-        self.exit_setup_wizard()
+        if skip_setup_wizard:
+            self.exit_setup_wizard()
         if not skip_sl4a:
             try:
                 droid, ed = self.get_droid()
@@ -426,13 +427,6 @@
             except:
                 self.log.exception("Failed to start sl4a!")
                 raise
-            # Enable or Disable Device Password per test bed config
-            if self.device_password:
-                self.log.info("Enable device password")
-                droid.setDevicePassword(self.device_password)
-            else:
-                self.log.debug("Disable device password")
-                droid.disableDevicePassword()
 
     def stop_services(self):
         """Stops long running services on the android device.
@@ -464,9 +458,20 @@
             self.log.error("Device is in fastboot mode, could not get build "
                            "info.")
             return
-        info = {}
-        info["build_id"] = self.adb.getprop("ro.build.id")
-        info["build_type"] = self.adb.getprop("ro.build.type")
+
+        build_id = self.adb.getprop("ro.build.id")
+        valid_build_id = False
+        for regex in RELEASE_ID_REGEXES:
+            if re.match(regex, build_id):
+                valid_build_id = True
+                break
+        if not valid_build_id:
+            build_id = self.adb.getprop("ro.build.version.incremental")
+
+        info = {
+            "build_id": build_id,
+            "build_type": self.adb.getprop("ro.build.type")
+        }
         return info
 
     @property
@@ -488,8 +493,7 @@
 
     @property
     def model(self):
-        """The Android code name for the device.
-        """
+        """The Android code name for the device."""
         # If device is in bootloader mode, get mode name from fastboot.
         if self.is_bootloader:
             out = self.fastboot.getvar("product").strip()
@@ -509,50 +513,27 @@
 
     @property
     def droid(self):
-        """The first sl4a session initiated on this device. None if there isn't
-        one.
-        """
-        try:
-            session_id = sorted(self._droid_sessions)[0]
-            return self._droid_sessions[session_id][0]
-        except IndexError:
+        """Returns the RPC Service of the first Sl4aSession created."""
+        if len(self._sl4a_manager.sessions) > 0:
+            session_id = sorted(self._sl4a_manager.sessions.keys())[0]
+            return self._sl4a_manager.sessions[session_id].rpc_client
+        else:
             return None
 
     @property
     def ed(self):
-        """The first event_dispatcher instance created on this device. None if
-        there isn't one.
-        """
-        try:
-            session_id = sorted(self._event_dispatchers)[0]
-            return self._event_dispatchers[session_id]
-        except IndexError:
+        """Returns the event dispatcher of the first Sl4aSession created."""
+        if len(self._sl4a_manager.sessions) > 0:
+            session_id = sorted(self._sl4a_manager.sessions.keys())[0]
+            return self._sl4a_manager.sessions[
+                session_id].get_event_dispatcher()
+        else:
             return None
 
     @property
-    def droids(self):
-        """A list of the active sl4a sessions on this device.
-
-        If multiple connections exist for the same session, only one connection
-        is listed.
-        """
-        keys = sorted(self._droid_sessions)
-        results = []
-        for k in keys:
-            results.append(self._droid_sessions[k][0])
-        return results
-
-    @property
-    def eds(self):
-        """A list of the event_dispatcher objects on this device.
-
-        The indexing of the list matches that of the droids property.
-        """
-        keys = sorted(self._event_dispatchers)
-        results = []
-        for k in keys:
-            results.append(self._event_dispatchers[k])
-        return results
+    def sl4a_sessions(self):
+        """Returns a dictionary of session ids to sessions."""
+        return list(self._sl4a_manager.sessions)
 
     @property
     def is_adb_logcat_on(self):
@@ -563,6 +544,9 @@
                 utils._assert_subprocess_running(self.adb_logcat_process)
                 return True
             except Exception:
+                # if skip_sl4a is true, there is no sl4a session
+                # if logcat died due to device reboot and sl4a session has
+                # not restarted there is no droid.
                 if self.droid:
                     self.droid.logI('Logcat died')
                 self.log.info("Logcat to %s died", self.adb_logcat_file_path)
@@ -581,7 +565,7 @@
         """
         for k, v in config.items():
             # skip_sl4a value can be reset from config file
-            if hasattr(self, k) and k not in ("skip_sl4a", "device_password"):
+            if hasattr(self, k) and k != "skip_sl4a":
                 raise AndroidDeviceError(
                     "Attempting to set existing attribute %s on %s" %
                     (k, self.serial))
@@ -622,41 +606,46 @@
             >>> ad = AndroidDevice()
             >>> droid, ed = ad.get_droid()
         """
-        forward_success = False
-        last_error = None
-        for _ in range(PORT_RETRY_COUNT):
-            if not self.h_port or not host_utils.is_port_available(
-                    self.h_port):
-                self.h_port = host_utils.get_available_host_port()
-            try:
-                self.adb.tcp_forward(self.h_port, self.d_port)
-                forward_success = True
-                break
-            except adb.AdbError as e:
-                last_error = e
-                pass
-        if not forward_success:
-            self.log.error(last_error)
-            raise last_error
+        session = self._sl4a_manager.create_session()
+        droid = session.rpc_client
+        if handle_event:
+            ed = session.get_event_dispatcher()
+            return droid, ed
+        return droid
 
-        for i in range(PORT_RETRY_COUNT):
+    def get_package_pid(self, package_name):
+        """Gets the pid for a given package. Returns None if not running.
+        Args:
+            package_name: The name of the package.
+        Returns:
+            The first pid found under a given package name. None if no process
+            was found running the package.
+        Raises:
+            AndroidDeviceError if the output of the phone's process list was
+            in an unexpected format.
+        """
+        for cmd in ("ps -A", "ps"):
             try:
-                if self.is_sl4a_running():
-                    self.log.info("Stop sl4a apk")
-                    self.stop_sl4a()
-                    time.sleep(15)
-                self.log.info("Start sl4a apk")
-                self.start_sl4a()
-                time.sleep(5)
-                droid = self.start_new_session()
-                if handle_event:
-                    ed = self.get_dispatcher(droid)
-                    return droid, ed
-                return droid
+                out = self.adb.shell(
+                    '%s | grep "S %s"' % (cmd, package_name),
+                    ignore_status=True)
+                if package_name not in out:
+                    continue
+                try:
+                    pid = int(out.split()[1])
+                    self.log.info('apk %s has pid %s.', package_name, pid)
+                    return pid
+                except (IndexError, ValueError) as e:
+                    # Possible ValueError from string to int cast.
+                    # Possible IndexError from split.
+                    self.log.warn('Command \"%s\" returned output line: '
+                                  '\"%s\".\nError: %s', cmd, out, e)
             except Exception as e:
-                self.log.warning("get_droid with exception: %s", e)
-                if i == PORT_RETRY_COUNT - 1:
-                    raise
+                self.log.warn(
+                    'Device fails to check if %s running with \"%s\"\n'
+                    'Exception %s', package_name, cmd, e)
+        self.log.debug("apk %s is not running", package_name)
+        return None
 
     def get_dispatcher(self, droid):
         """Return an EventDispatcher for an sl4a session
@@ -667,17 +656,7 @@
         Returns:
             ed: An EventDispatcher for specified session.
         """
-        ed_key = self.serial + str(droid.uid)
-        if ed_key in self._event_dispatchers:
-            if self._event_dispatchers[ed_key] is None:
-                raise AndroidDeviceError("EventDispatcher Key Empty")
-            self.log.debug("Returning existing key %s for event dispatcher!",
-                           ed_key)
-            return self._event_dispatchers[ed_key]
-        event_droid = self.add_new_connection_to_session(droid.uid)
-        ed = event_dispatcher.EventDispatcher(event_droid)
-        self._event_dispatchers[ed_key] = ed
-        return ed
+        return self._sl4a_manager.sessions[droid.uid].get_event_dispatcher()
 
     def _is_timestamp_in_range(self, target, begin_time, end_time):
         low = acts_logger.logline_timestamp_comparator(begin_time, target) <= 0
@@ -767,7 +746,7 @@
             extra_params = "-b all"
 
         # TODO(markdr): Pull 'adb -s %SERIAL' from the AdbProxy object.
-        cmd = "adb -s {} logcat -T 1 -v threadtime {} >> {}".format(
+        cmd = "adb -s {} logcat -T 1 -v year {} >> {}".format(
             self.serial, extra_params, logcat_file_path)
         self.adb_logcat_process = utils.start_standing_subprocess(cmd)
         self.adb_logcat_file_path = logcat_file_path
@@ -782,6 +761,23 @@
         utils.stop_standing_subprocess(self.adb_logcat_process)
         self.adb_logcat_process = None
 
+    def get_apk_uid(self, apk_name):
+        """Get the uid of the given apk.
+
+        Args:
+        apk_name: Name of the package, e.g., com.android.phone.
+
+        Returns:
+        Linux UID for the apk.
+        """
+        output = self.adb.shell(
+            "dumpsys package %s | grep userId=" % apk_name, ignore_status=True)
+        result = re.search(r"userId=(\d+)", output)
+        if result:
+            return result.group(1)
+        else:
+            None
+
     def is_apk_installed(self, package_name):
         """Check if the given apk is already installed.
 
@@ -794,8 +790,8 @@
 
         try:
             return bool(
-                self.adb.shell('pm list packages | grep -w "package:%s"' %
-                               package_name))
+                self.adb.shell(
+                    'pm list packages | grep -w "package:%s"' % package_name))
 
         except Exception as err:
             self.log.error('Could not determine if %s is installed. '
@@ -809,7 +805,7 @@
         """Check if the given apk is running.
 
         Args:
-        package_name: Name of the package, e.g., com.android.phone.
+            package_name: Name of the package, e.g., com.android.phone.
 
         Returns:
         True if package is installed. False otherwise.
@@ -851,7 +847,7 @@
         return self.force_stop_apk(SL4A_APK_NAME)
 
     def start_sl4a(self):
-        sl4a_client.start_sl4a(self.adb)
+        self._sl4a_manager.start_sl4a_service()
 
     def take_bug_report(self, test_name, begin_time):
         """Takes a bug report on the device and stores it in a file.
@@ -860,7 +856,7 @@
             test_name: Name of the test case that triggered this bug report.
             begin_time: Logline format timestamp taken when the test started.
         """
-        self.adb.wait_for_device()
+        self.adb.wait_for_device(timeout=WAIT_FOR_DEVICE_TIMEOUT)
         new_br = True
         try:
             stdout = self.adb.shell("bugreportz -v")
@@ -872,8 +868,8 @@
             new_br = False
         br_path = os.path.join(self.log_path, test_name)
         utils.create_dir(br_path)
-        out_name = "AndroidDevice%s_%s" % (self.serial, begin_time.replace(
-            " ", "_").replace(":", "-"))
+        out_name = "AndroidDevice%s_%s" % (
+            self.serial, begin_time.replace(" ", "_").replace(":", "-"))
         out_name = "%s.zip" % out_name if new_br else "%s.txt" % out_name
         full_out_path = os.path.join(br_path, out_name)
         # in case device restarted, wait for adb interface to return
@@ -891,12 +887,17 @@
                 " > {}".format(full_out_path), timeout=BUG_REPORT_TIMEOUT)
         self.log.info("Bugreport for %s taken at %s.", test_name,
                       full_out_path)
-        self.adb.wait_for_device()
+        self.adb.wait_for_device(timeout=WAIT_FOR_DEVICE_TIMEOUT)
 
     def get_file_names(self, directory):
         """Get files names with provided directory."""
-        file_names = []
-        out = self.adb.shell("ls %s" % directory, ignore_status=True)
+        # -1 (the number one) prints one file per line.
+        out = self.adb.shell(
+            "ls -1 %s" % directory, ignore_status=True)
+        if "Permission denied" in out:
+            self.root_adb()
+            out = self.adb.shell(
+                "ls -1 %s" % directory, ignore_status=True)
         if out and "No such" not in out:
             return out.split('\n')
         else:
@@ -916,6 +917,9 @@
                            log_crash_report=False):
         """check crash report on the device."""
         crash_reports = []
+        if begin_time:
+            begin_time = "%s-%s" % (datetime.now().year, begin_time)
+            begin_time = datetime.strptime(begin_time, "%Y-%m-%d %H:%M:%S.%f")
         for crash_path in CRASH_REPORT_PATHS:
             for report in self.get_file_names(crash_path):
                 if report in CRASH_REPORT_SKIPS:
@@ -923,29 +927,36 @@
                 file_path = os.path.join(crash_path, report)
                 if begin_time:
                     file_time = self.adb.shell('stat -c "%%y" %s' % file_path)
-                    if begin_time < file_time.split('-', 1)[1]:
+                    file_time = datetime.strptime(file_time[:-3],
+                                                  "%Y-%m-%d %H:%M:%S.%f")
+                    if begin_time < file_time:
                         crash_reports.append(file_path)
                 else:
                     crash_reports.append(file_path)
         if crash_reports and log_crash_report:
-            test_name = test_name or begin_time or time.strftime(
-                "%m-%d-%Y-%H-%M-%S")
-            crash_log_path = os.path.join(self.log_path, test_name, "Crashes")
+            test_name = test_name or time.strftime("%Y-%m-%d-%Y-%H-%M-%S")
+            crash_log_path = os.path.join(self.log_path, test_name,
+                                          "Crashes_%s" % self.serial)
             utils.create_dir(crash_log_path)
             self.pull_files(crash_reports, crash_log_path)
         return crash_reports
 
     def get_qxdm_logs(self):
         """Get qxdm logs."""
-        ad.log.info("Pull QXDM Logs")
+        # Sleep 10 seconds for the buffered log to be written in qxdm log file
+        time.sleep(10)
+        log_path = getattr(self, "qxdm_log_path", DEFAULT_QXDM_LOG_PATH)
         qxdm_logs = self.get_file_names(
-            "/data/vendor/radio/diag_logs/logs/*.qmdl")
+            log_path, begin_time=begin_time, match_string="*.qmdl")
         if qxdm_logs:
-            qxdm_path = os.path.join(ad.log_path, "QXDM_Logs")
+            qxdm_path = os.path.join(self.log_path, "QXDM_Logs")
             utils.create_dir(qxdm_path)
-            ad.pull_files(qxdm_logs, qxdm_path)
+            self.pull_files(qxdm_logs, qxdm_path)
+            self.adb.pull(
+                "/firmware/image/qdsp6m.qdb %s" % qxdm_path,
+                timeout=PULL_TIMEOUT, ignore_status=True)
 
-    def start_new_session(self):
+    def start_new_session(self, max_connections=None, server_port=None):
         """Start a new session in sl4a.
 
         Also caches the droid in a dict with its uid being the key.
@@ -958,73 +969,18 @@
             Sl4aException: Something is wrong with sl4a and it returned an
             existing uid to a new session.
         """
-        droid = sl4a_client.Sl4aClient(port=self.h_port)
-        droid.open()
-        if droid.uid in self._droid_sessions:
-            raise sl4a_client.Sl4aException(
-                "SL4A returned an existing uid for a new session. Abort.")
-        self.log.info("Add new sl4a session %s", droid.uid)
-        self._droid_sessions[droid.uid] = [droid]
-        return droid
+        session = self._sl4a_manager.create_session(
+            max_connections=max_connections, server_port=server_port)
 
-    def add_new_connection_to_session(self, session_id):
-        """Create a new connection to an existing sl4a session.
-
-        Args:
-            session_id: UID of the sl4a session to add connection to.
-
-        Returns:
-            An Android object used to communicate with sl4a on the android
-                device.
-
-        Raises:
-            DoesNotExistError: Raised if the session it's trying to connect to
-            does not exist.
-        """
-        if session_id not in self._droid_sessions:
-            raise DoesNotExistError("Session %d doesn't exist." % session_id)
-        droid = sl4a_client.Sl4aClient(port=self.h_port, uid=session_id)
-        self.log.info("Open s4la session %s", session_id)
-        droid.open(cmd=sl4a_client.Sl4aCommand.CONTINUE)
-        return droid
-
-    def terminate_session(self, session_id):
-        """Terminate a session in sl4a.
-
-        Send terminate signal to sl4a server; stop dispatcher associated with
-        the session. Clear corresponding droids and dispatchers from cache.
-
-        Args:
-            session_id: UID of the sl4a session to terminate.
-        """
-        ed_key = self.serial + str(session_id)
-        if self._event_dispatchers and ed_key in self._event_dispatchers:
-            self.log.info("Clear event dispatcher session %s", session_id)
-            self._event_dispatchers[ed_key].clean_up()
-            del self._event_dispatchers[ed_key]
-        if self._droid_sessions and (session_id in self._droid_sessions):
-            for droid in self._droid_sessions[session_id]:
-                self.log.info("Close sl4a session %s", session_id)
-                droid.closeSl4aSession(timeout=180)
-                droid.close()
-            del self._droid_sessions[session_id]
+        self._sl4a_manager.sessions[session.uid] = session
+        return session.rpc_client
 
     def terminate_all_sessions(self):
         """Terminate all sl4a sessions on the AndroidDevice instance.
 
         Terminate all sessions and clear caches.
         """
-        if self._droid_sessions:
-            session_ids = list(self._droid_sessions.keys())
-            for session_id in session_ids:
-                try:
-                    self.terminate_session(session_id)
-                except Exception as e:
-                    self.log.exception("Failed to terminate session %d: %s",
-                                       session_id, e)
-            if self.h_port:
-                self.adb.remove_tcp_forward(self.h_port)
-                self.h_port = None
+        self._sl4a_manager.terminate_all_sessions()
 
     def run_iperf_client(self,
                          server_host,
@@ -1077,7 +1033,7 @@
         timeout_start = time.time()
         timeout = 15 * 60
 
-        self.adb.wait_for_device(timeout=180)
+        self.adb.wait_for_device(timeout=WAIT_FOR_DEVICE_TIMEOUT)
         while time.time() < timeout_start + timeout:
             try:
                 completed = self.adb.getprop("sys.boot_completed")
@@ -1109,50 +1065,12 @@
             self.fastboot.reboot()
             return
         self.terminate_all_sessions()
-        self.log.info("Reboot")
+        self.log.info("Rebooting")
         self.adb.reboot()
         self.wait_for_boot_completion()
         self.root_adb()
-        if self.is_waiting_for_unlock_pin() and stop_at_lock_screen:
+        if stop_at_lock_screen and self.is_screen_lock_enabled():
             return
-        if not self.ensure_screen_on():
-            self.log.error("User window cannot come up")
-            raise AndroidDeviceError("User window cannot come up")
-        self.ensure_screen_on()
-        self.start_services(self.skip_sl4a)
-
-    def fastboot_wipe(self):
-        """Wipe the device in fastboot mode.
-
-        Pull sl4a apk from device. Terminate all sl4a sessions,
-        Reboot the device to bootloader, wipe the device by fastboot.
-        Reboot the device. wait for device to complete booting
-        Re-intall and start an sl4a session.
-
-        """
-        #Pull sl4a apk from device
-        out = self.adb.shell("pm path com.googlecode.android_scripting")
-        result = re.search(r"package:(.*)", out)
-        if not result:
-            self.log.error("Couldn't find sl4a apk")
-        else:
-            sl4a_apk = result.group(1)
-            self.log.info("Get sl4a apk from %s", sl4a_apk)
-            self.pull_files([sl4a_apk], "/tmp/")
-        has_adb_log = self.is_adb_logcat_on
-        self.terminate_all_sessions()
-        self.log.info("Reboot to bootloader")
-        self.adb.reboot_bootloader(ignore_status=True)
-        self.log.info("Wipe in fastboot")
-        self.fastboot._w()
-        self.fastboot.reboot()
-        self.log.info("Reboot")
-        self.wait_for_boot_completion()
-        self.root_adb()
-        if result:
-            self.log.info("Re-install sl4a")
-            self.adb.install("-r /tmp/base.apk")
-            time.sleep(10)
         self.start_services(self.skip_sl4a)
 
     def search_logcat(self, matching_string):
@@ -1169,6 +1087,11 @@
               "time_stamp": "2017-05-03 17:39:29.898",
               "datetime_obj": datetime object}]
         """
+        cmd_option = '-b all -v year -d'
+        if begin_time:
+            log_begin_time = acts_logger.epoch_to_log_line_timestamp(
+                begin_time)
+            cmd_option = '%s -t "%s"' % (cmd_option, log_begin_time)
         out = self.adb.logcat(
             '-b all -d | grep "%s"' % matching_string, ignore_status=True)
         if not out: return []
@@ -1176,7 +1099,7 @@
         logs = re.findall(r'(\S+\s\S+)(.*%s.*)' % re.escape(matching_string),
                           out)
         for log in logs:
-            time_stamp = "%s-%s" % (datetime.now().year, log[0])
+            time_stamp = log[0]
             time_obj = datetime.strptime(time_stamp, "%Y-%m-%d %H:%M:%S.%f")
             result.append({
                 "log_message": "".join(log),
@@ -1237,8 +1160,10 @@
     def get_my_current_focus_window(self):
         """Get the current focus window on screen"""
         output = self.adb.shell(
-            'dumpsys window windows | grep -E mCurrentFocus')
-        if not output or "mCurrentFocus=null" in output:
+            'dumpsys window windows | grep -E mCurrentFocus',
+            ignore_status=True)
+        if not output or "not found" in output or "Can't find" in output or (
+                "mCurrentFocus=null" in output):
             result = ''
         else:
             result = output.split(' ')[-1].strip("}")
@@ -1247,8 +1172,10 @@
 
     def get_my_current_focus_app(self):
         """Get the current focus application"""
-        output = self.adb.shell('dumpsys window windows | grep -E mFocusedApp')
-        if not output or "mFocusedApp=null" in output:
+        output = self.adb.shell(
+            'dumpsys window windows | grep -E mFocusedApp', ignore_status=True)
+        if not output or "not found" in output or "Can't find" in output or (
+                "mFocusedApp=null" in output):
             result = ''
         else:
             result = output.split(' ')[-2]
@@ -1259,8 +1186,7 @@
         current_window = self.get_my_current_focus_window()
         if window_name:
             return window_name in current_window
-        return current_window and "CryptKeeper" not in current_window and (
-            "FallbackHome" not in self.get_my_current_focus_app())
+        return current_window and ENCRYPTION_WINDOW not in current_window
 
     def wait_for_window_ready(self,
                               window_name=None,
@@ -1268,10 +1194,10 @@
                               check_duration=60):
         elapsed_time = 0
         while elapsed_time < check_duration:
-            time.sleep(check_interval)
-            elapsed_time += check_interval
             if self.is_window_ready(window_name=window_name):
                 return True
+            time.sleep(check_interval)
+            elapsed_time += check_interval
         self.log.info("Current focus window is %s",
                       self.get_my_current_focus_window())
         return False
@@ -1281,8 +1207,7 @@
 
     def is_screen_awake(self):
         """Check if device screen is in sleep mode"""
-        output = self.adb.shell("dumpsys power | grep mWakefulness=")
-        return "Awake" in output
+        return "Awake" in self.adb.shell("dumpsys power | grep mWakefulness=")
 
     def is_screen_emergency_dialer(self):
         """Check if device screen is in emergency dialer mode"""
@@ -1296,43 +1221,53 @@
         """Check if device screen is in emergency dialer mode"""
         return "setupwizard" in self.get_my_current_focus_app()
 
+    def is_screen_lock_enabled(self):
+        """Check if screen lock is enabled"""
+        cmd = ("sqlite3 /data/system/locksettings.db .dump"
+               " | grep lockscreen.password_type | grep -v alternate")
+        out = self.adb.shell(cmd, ignore_status=True)
+        if "unable to open" in out:
+            self.root_adb()
+            out = self.adb.shell(cmd, ignore_status=True)
+        if ",0,'0'" not in out:
+            self.log.info("Screen lock is enabled")
+            return True
+        return False
+
     def is_waiting_for_unlock_pin(self):
         """Check if device is waiting for unlock pin to boot up"""
-        if "CryptKeeper" in self.get_my_current_focus_window():
+        current_window = self.get_my_current_focus_window()
+        current_app = self.get_my_current_focus_app()
+        if ENCRYPTION_WINDOW in current_window:
             self.log.info("Device is in CrpytKeeper window")
             return True
-        if "FallbackHome" in self.get_my_current_focus_app():
+        if "StatusBar" in current_window and (
+            (not current_app) or "FallbackHome" in current_app):
             self.log.info("Device is locked")
             return True
         return False
 
     def ensure_screen_on(self):
         """Ensure device screen is powered on"""
-        if not self.is_screen_awake():
-            self.wakeup_screen()
-        if self.is_screen_emergency_dialer(
-        ) or self.is_screen_in_call_activity():
-            # Send two BACK keycodes to get out notification and dialer
-            self.send_keycode("BACK")
-            self.send_keycode("BACK")
-            time.sleep(2)
-        if self.is_waiting_for_unlock_pin():
-            self.unlock_screen()
-            time.sleep(2)
-            if self.is_waiting_for_unlock_pin():
+        if self.is_screen_lock_enabled():
+            for _ in range(2):
                 self.unlock_screen()
-            return self.wait_for_window_ready()
-        elif self.device_password and self.get_my_current_focus_window(
-        ) == "StatusBar":
-            self.unlock_screen()
-        return self.is_window_ready()
+                time.sleep(1)
+                if self.is_waiting_for_unlock_pin():
+                    self.unlock_screen(password=DEFAULT_DEVICE_PASSWORD)
+                    time.sleep(1)
+                if not self.is_waiting_for_unlock_pin(
+                ) and self.wait_for_window_ready():
+                    return True
+            return False
+        else:
+            self.wakeup_screen()
+            return True
 
     def wakeup_screen(self):
-        self.send_keycode("WAKEUP")
-        current_window = self.get_my_current_focus_window()
-        if "NexusLauncherActivity" not in current_window or (
-                "StatusBar" not in current_window):
-            self.send_keycode("MENU")
+        if not self.is_screen_awake():
+            self.log.info("Screen is not awake, wake it up")
+            self.send_keycode("WAKEUP")
 
     def go_to_sleep(self):
         self.send_keycode("SLEEP")
@@ -1341,13 +1276,19 @@
         self.send_keycode("NUMPAD_%s" % number)
 
     def unlock_screen(self, password=None):
-        password = password or self.device_password or "1111"
-        self.log.info("Unlocking screen with pin %s", password)
-        self.wakeup_screen()
-        self.send_keycode("DEL")
-        for number in password:
-            self.send_keycode_number_pad(number)
-        self.send_keycode("ENTER")
+        self.log.info("Unlocking with %s", password or "swipe up")
+        # Bring device to SLEEP so that unlock process can start fresh
+        self.send_keycode("SLEEP")
+        time.sleep(1)
+        self.send_keycode("WAKEUP")
+        if ENCRYPTION_WINDOW not in self.get_my_current_focus_app():
+            self.send_keycode("MENU")
+        if password:
+            self.send_keycode("DEL")
+            for number in password:
+                self.send_keycode_number_pad(number)
+            self.send_keycode("ENTER")
+            self.send_keycode("BACK")
 
     def exit_setup_wizard(self):
         self.adb.shell(
diff --git a/acts/framework/acts/controllers/native.py b/acts/framework/acts/controllers/native.py
index aa987b1..86b8523 100644
--- a/acts/framework/acts/controllers/native.py
+++ b/acts/framework/acts/controllers/native.py
@@ -14,7 +14,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
-from acts.controllers.sl4a_client import Sl4aClient
+from acts.controllers.sl4a_lib.rpc_connection import RpcConnection
 import json
 import os
 import socket
@@ -33,7 +33,7 @@
 
 
 class SL4NProtocolError(SL4NException):
-    """Raised when there is some error in exchanging data with server on device."""
+    """Raised when there is an error exchanging data with the device server."""
     NO_RESPONSE_FROM_HANDSHAKE = "No response from handshake."
     NO_RESPONSE_FROM_SERVER = "No response from server."
     MISMATCHED_API_ID = "Mismatched API id."
@@ -46,7 +46,7 @@
         i += 1
 
 
-class NativeAndroid(Sl4aClient):
+class NativeAndroid(RpcConnection):
     COUNTER = IDCounter()
 
     def _rpc(self, method, *args):
diff --git a/acts/framework/acts/controllers/relay_lib/dongles.py b/acts/framework/acts/controllers/relay_lib/dongles.py
new file mode 100644
index 0000000..518caad
--- /dev/null
+++ b/acts/framework/acts/controllers/relay_lib/dongles.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+import time
+import enum
+from acts.controllers.relay_lib.generic_relay_device import GenericRelayDevice
+from acts.controllers.relay_lib.relay import SynchronizeRelays
+from acts.controllers.relay_lib.errors import RelayConfigError
+from acts.controllers.relay_lib.helpers import validate_key
+
+# Necessary timeout inbetween commands
+CMD_TIMEOUT = 1.2
+# Pairing mode activation wait time
+PAIRING_MODE_WAIT_TIME = 6
+SINGLE_ACTION_SHORT_WAIT_TIME = 0.6
+SINGLE_ACTION_LONG_WAIT_TIME = 2.0
+MISSING_RELAY_MSG = 'Relay config for Three button  "%s" missing relay "%s".'
+
+
+class Buttons(enum.Enum):
+    ACTION = 'Action'
+    NEXT = 'Next'
+    PREVIOUS = 'Previous'
+
+
+class SingleButtonDongle(GenericRelayDevice):
+    """A Bluetooth dongle with one generic button Normally action.
+
+    Wraps the button presses, as well as the special features like pairing.
+    """
+
+    def __init__(self, config, relay_rig):
+        GenericRelayDevice.__init__(self, config, relay_rig)
+
+        self.mac_address = validate_key('mac_address', config, str,
+                                        'SingleButtonDongle')
+
+        self.ensure_config_contains_relay(Buttons.ACTION.value)
+
+    def ensure_config_contains_relay(self, relay_name):
+        """Throws an error if the relay does not exist."""
+        if relay_name not in self.relays:
+            raise RelayConfigError(MISSING_RELAY_MSG % (self.name, relay_name))
+
+    def setup(self):
+        """Sets all relays to their default state (off)."""
+        GenericRelayDevice.setup(self)
+
+    def clean_up(self):
+        """Sets all relays to their default state (off)."""
+        GenericRelayDevice.clean_up(self)
+
+    def enter_pairing_mode(self):
+        """Enters pairing mode. Blocks the thread until pairing mode is set.
+
+        Holds down the 'ACTION' buttons for PAIRING_MODE_WAIT_TIME seconds.
+        """
+        self.relays[Buttons.ACTION.value].set_nc_for(
+            seconds=PAIRING_MODE_WAIT_TIME)
+
+    def press_play_pause(self):
+        """Briefly presses the Action button."""
+        self.relays[Buttons.ACTION.value].set_nc_for(
+            seconds=SINGLE_ACTION_SHORT_WAIT_TIME)
+
+    def press_vr_mode(self):
+        """Long press the Action button."""
+        self.relays[Buttons.ACTION.value].set_nc_for(
+            seconds=SINGLE_ACTION_LONG_WAIT_TIME)
+
+
+class ThreeButtonDongle(GenericRelayDevice):
+    """A Bluetooth dongle with three generic buttons Normally action, next, and
+     previous.
+
+    Wraps the button presses, as well as the special features like pairing.
+    """
+
+    def __init__(self, config, relay_rig):
+        GenericRelayDevice.__init__(self, config, relay_rig)
+
+        self.mac_address = validate_key('mac_address', config, str,
+                                        'ThreeButtonDongle')
+
+        for button in Buttons:
+            self.ensure_config_contains_relay(button.value)
+
+    def ensure_config_contains_relay(self, relay_name):
+        """Throws an error if the relay does not exist."""
+        if relay_name not in self.relays:
+            raise RelayConfigError(MISSING_RELAY_MSG % (self.name, relay_name))
+
+    def setup(self):
+        """Sets all relays to their default state (off)."""
+        GenericRelayDevice.setup(self)
+
+    def clean_up(self):
+        """Sets all relays to their default state (off)."""
+        GenericRelayDevice.clean_up(self)
+
+    def enter_pairing_mode(self):
+        """Enters pairing mode. Blocks the thread until pairing mode is set.
+
+        Holds down the 'ACTION' buttons for a little over 5 seconds.
+        """
+        self.relays[Buttons.ACTION.value].set_nc_for(
+            seconds=PAIRING_MODE_WAIT_TIME)
+
+    def press_play_pause(self):
+        """Briefly presses the Action button."""
+        self.relays[Buttons.ACTION.value].set_nc_for(
+            seconds=SINGLE_ACTION_SHORT_WAIT_TIME)
+        time.sleep(CMD_TIMEOUT)
+
+    def press_vr_mode(self):
+        """Long press the Action button."""
+        self.relays[Buttons.ACTION.value].set_nc_for(
+            seconds=SINGLE_ACTION_LONG_WAIT_TIME)
+        time.sleep(CMD_TIMEOUT)
+
+    def press_next(self):
+        """Briefly presses the Next button."""
+        self.relays[Buttons.NEXT.value].set_nc_for(
+            seconds=SINGLE_ACTION_SHORT_WAIT_TIME)
+        time.sleep(CMD_TIMEOUT)
+
+    def press_previous(self):
+        """Briefly presses the Previous button."""
+        self.relays[Buttons.PREVIOUS.value].set_nc_for(
+            seconds=SINGLE_ACTION_SHORT_WAIT_TIME)
+        time.sleep(CMD_TIMEOUT)
diff --git a/acts/framework/acts/controllers/relay_lib/relay_rig.py b/acts/framework/acts/controllers/relay_lib/relay_rig.py
index 15b3626..3f4ca05 100644
--- a/acts/framework/acts/controllers/relay_lib/relay_rig.py
+++ b/acts/framework/acts/controllers/relay_lib/relay_rig.py
@@ -20,6 +20,8 @@
 from acts.controllers.relay_lib.fugu_remote import FuguRemote
 from acts.controllers.relay_lib.sony_xb2_speaker import SonyXB2Speaker
 from acts.controllers.relay_lib.ak_xb10_speaker import AkXB10Speaker
+from acts.controllers.relay_lib.dongles import SingleButtonDongle
+from acts.controllers.relay_lib.dongles import ThreeButtonDongle
 
 
 class RelayRig:
@@ -52,6 +54,8 @@
         'FuguRemote': lambda x, rig: FuguRemote(x, rig),
         'SonyXB2Speaker': lambda x, rig: SonyXB2Speaker(x, rig),
         'AkXB10Speaker': lambda x, rig: AkXB10Speaker(x, rig),
+        'SingleButtonDongle': lambda x, rig: SingleButtonDongle(x, rig),
+        'ThreeButtonDongle': lambda x, rig: ThreeButtonDongle(x, rig),
     }
 
     def __init__(self, config):
diff --git a/acts/framework/acts/controllers/sl4a_client.py b/acts/framework/acts/controllers/sl4a_client.py
deleted file mode 100644
index dd9c8f8..0000000
--- a/acts/framework/acts/controllers/sl4a_client.py
+++ /dev/null
@@ -1,385 +0,0 @@
-#/usr/bin/env python3.4
-#
-# Copyright (C) 2009 Google Inc.
-#
-# 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.
-"""
-JSON RPC interface to android scripting engine.
-"""
-
-from builtins import str
-
-import json
-import logging
-import os
-import socket
-import sys
-import threading
-import time
-
-from acts.controllers import adb
-
-HOST = os.environ.get('SL4A_HOST_ADDRESS', None)
-PORT = os.environ.get('SL4A_HOST_PORT', 9999)
-DEFAULT_DEVICE_SIDE_PORT = 8080
-
-UNKNOWN_UID = -1
-
-MAX_SL4A_START_RETRY = 3
-MAX_SL4A_WAIT_TIME = 30
-
-_SL4A_LAUNCH_CMD = (
-    "am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER "
-    "--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT {} "
-    "com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher")
-
-
-class Sl4aException(Exception):
-    pass
-
-
-class Sl4aStartError(Sl4aException):
-    """Raised when sl4a is not able to be started."""
-
-
-class Sl4aApiError(Sl4aException):
-    """Raised when remote API reports an error."""
-
-
-class Sl4aProtocolError(Sl4aException):
-    """Raised when there is some error in exchanging data with server on device."""
-    NO_RESPONSE_FROM_HANDSHAKE = "No response from handshake."
-    NO_RESPONSE_FROM_SERVER = "No response from server."
-    MISMATCHED_API_ID = "Mismatched API id."
-
-
-def start_sl4a(adb_proxy,
-               device_side_port=DEFAULT_DEVICE_SIDE_PORT,
-               wait_time=MAX_SL4A_WAIT_TIME,
-               retries=MAX_SL4A_START_RETRY):
-    """Starts sl4a server on the android device.
-
-    Args:
-        adb_proxy: adb.AdbProxy, The adb proxy to use to start sl4a
-        device_side_port: int, The port number to open on the device side.
-        wait_time: float, The time to wait for sl4a to come up before raising
-                   an error.
-        retries: number of sl4a start command retries.
-
-    Raises:
-        Sl4aException: Raised when SL4A was not able to be started.
-    """
-    if not is_sl4a_installed(adb_proxy):
-        raise Sl4aStartError("SL4A is not installed on %s" % adb_proxy.serial)
-    for _ in range(retries):
-        adb_proxy.shell(_SL4A_LAUNCH_CMD.format(device_side_port))
-        for _ in range(wait_time):
-            time.sleep(1)
-            if is_sl4a_running(adb_proxy):
-                logging.debug("SL4A is running")
-                return
-    raise Sl4aStartError("SL4A failed to start on %s." % adb_proxy.serial)
-
-
-def stop_sl4a(adb_proxy):
-    """Kills any running instance of sl4a.
-
-    Kills any running instance of sl4a. If no instance is running then nothing
-    is done.
-
-    Args:
-        adb_proxy: adb.AdbProxy, The adb proxy to use for checking.
-    """
-    adb_proxy.shell(
-        'am force-stop com.googlecode.android_scripting', ignore_status=True)
-
-
-def is_sl4a_installed(adb_proxy):
-    """Checks if sl4a is installed by querying the package path of sl4a.
-
-    Args:
-        adb: adb.AdbProxy, The adb proxy to use for checking install.
-
-    Returns:
-        True if sl4a is installed, False otherwise.
-    """
-    try:
-        if adb_proxy.shell("pm path com.googlecode.android_scripting"):
-            return True
-    except adb.AdbError as e:
-        if not e.stderr:
-            return False
-        raise
-    return False
-
-
-def is_sl4a_running(adb_proxy, use_new_ps=True):
-    """Checks if the sl4a app is running on an android device.
-
-    Args:
-        adb_proxy: adb.AdbProxy, The adb proxy to use for checking.
-        use_new_ps: Newer versions of ps allow for the flag -A to show all
-                    processes. But older versions will interpert this as a pid.
-                    When true this will use the newer version and fall back to
-                    the old on failure.
-
-    Returns:
-        True if the sl4a app is running, False otherwise.
-    """
-    # Grep for process with a preceding S which means it is truly started.
-    try:
-        if use_new_ps:
-            out = adb_proxy.shell(
-                'ps -A | grep "S com.googlecode.android_scripting"')
-        else:
-            out = adb_proxy.shell(
-                'ps | grep "S com.googlecode.android_scripting"')
-    except Exception as e:
-        logging.error("is_sl4a_running with exception %s", e)
-        out = None
-    if not out:
-        if use_new_ps:
-            out = adb_proxy.shell('ps -A | grep "bad pid"', ignore_status=True)
-            if 'bad pid' in str(out):
-                return is_sl4a_running(adb_proxy, use_new_ps=False)
-        return False
-    return True
-
-
-class Sl4aCommand(object):
-    """Commands that can be invoked on the sl4a client.
-
-    INIT: Initializes a new sessions in sl4a.
-    CONTINUE: Creates a connection.
-    """
-    INIT = 'initiate'
-    CONTINUE = 'continue'
-
-
-class Sl4aClient(object):
-    """A sl4a client that is connected to remotely.
-
-    Connects to a remove device running sl4a. Before opening a connection
-    a port forward must be setup to go over usb. This be done using
-    adb.tcp_forward(). This is calling the shell command
-    adb forward <local> remote>. Once the port has been forwarded it can be
-    used in this object as the port of communication.
-
-    Attributes:
-        port: int, The host port to communicate through.
-        addr: str, The host address who is communicating to the device (usually
-                   localhost).
-        client: file, The socket file used to communicate.
-        uid: int, The sl4a uid of this session.
-        conn: socket.Socket, The socket connection to the remote client.
-    """
-
-    _SOCKET_TIMEOUT = 60
-
-    def __init__(self, port=PORT, addr=HOST, uid=UNKNOWN_UID):
-        """
-        Args:
-            port: int, The port this client should connect to.
-            addr: str, The address this client should connect to.
-            uid: int, The uid of the session to join, or UNKNOWN_UID to start a
-                 new session.
-        """
-        self.port = port
-        self.addr = addr
-        self._lock = threading.Lock()
-        self._counter = self._id_counter()
-        self.client = None  # prevent close errors on connect failure
-        self.uid = uid
-        self.conn = None
-
-    def __del__(self):
-        self.close()
-
-    def _id_counter(self):
-        i = 0
-        while True:
-            yield i
-            i += 1
-
-    def open(self, connection_timeout=60, cmd=Sl4aCommand.INIT):
-        """Opens a connection to the remote client.
-
-        Opens a connection to a remote client with sl4a. The connection will
-        error out if it takes longer than the connection_timeout time. Once
-        connected if the socket takes longer than _SOCKET_TIMEOUT to respond
-        the connection will be closed.
-
-        Args:
-            connection_timeout: int, The time to wait for the connection to come
-                                up.
-            cmd: Sl4aCommand, The command to use for creating the connection.
-
-        Raises:
-            IOError: Raised when the socket times out from io error
-            socket.timeout: Raised when the socket waits to long for connection.
-            Sl4aProtocolError: Raised when there is an error in the protocol.
-        """
-        if connection_timeout:
-            time_left = connection_timeout
-        else:
-            time_left = None
-
-        start_time = time.time()
-
-        def get_time_left():
-            if connection_timeout:
-                return connection_timeout - (time.time() - start_time)
-            # Assume system default if none is given.
-            return socket.getdefaulttimeout()
-
-        while True:
-            try:
-                self.conn = socket.create_connection((self.addr, self.port),
-                                                     max(1, get_time_left()))
-                self.conn.settimeout(self._SOCKET_TIMEOUT)
-                self.client = self.conn.makefile(mode="brw")
-
-                # Some devices will allow a connection but then disconnect
-                # instead of failing on create connection. The first command
-                # Will be error handled to make sure this does not happen.
-                resp = self._cmd(cmd, self.uid)
-                break
-            except (socket.timeout) as e:
-                logging.warning(
-                    "Sl4aClient failed to open socket connection: %s", e)
-                raise
-            except (socket.error, IOError) as e:
-                # TODO: optimize to only forgive some errors here
-                # error values are OS-specific so this will require
-                # additional tuning to fail faster
-                time_left = get_time_left()
-                if time_left <= 0:
-                    logging.warning(
-                        "Sl4aClient failed to create socket connection: %s", e)
-                    raise
-                time.sleep(1)
-
-        if not resp:
-            raise Sl4aProtocolError(
-                Sl4aProtocolError.NO_RESPONSE_FROM_HANDSHAKE)
-        result = json.loads(str(resp, encoding="utf8"))
-        if result['status']:
-            self.uid = result['uid']
-        else:
-            self.uid = UNKNOWN_UID
-
-        return self  # Allow the idiom Sl4aClient(...).open()
-
-    def close(self):
-        """Close the connection to the remote client."""
-        if self.conn is not None:
-            self.conn.close()
-            self.conn = None
-
-    def _cmd(self, command, uid=None):
-        """Send a command to sl4a.
-
-        Given a command name, this will package the command and send it to
-        sl4a.
-
-        Args:
-            command: str, The name of the command to execute.
-            uid: int, the uid of the session to send the command to.
-
-        Returns:
-            The line that was written back.
-        """
-        if not uid:
-            uid = self.uid
-        self.client.write(
-            json.dumps({
-                'cmd': command,
-                'uid': uid
-            }).encode("utf8") + b'\n')
-        self.client.flush()
-        return self.client.readline()
-
-    def _rpc(self, method, *args, **kwargs):
-        """Sends an rpc to sl4a.
-
-        Sends an rpc call to sl4a using this clients connection.
-
-        Args:
-            method: str, The name of the method to execute.
-            args: any, The args to send to sl4a.
-            kwargs: timeout: timeout for the RPC call.
-
-        Returns:
-            The result of the rpc.
-
-        Raises:
-            Sl4aProtocolError: Something went wrong with the sl4a protocol.
-            Sl4aApiError: The rpc went through, however executed with errors.
-        """
-        timeout = kwargs.get("timeout")
-        retries = kwargs.get("retries", 3)
-        with self._lock:
-            apiid = next(self._counter)
-        if timeout:
-            self.conn.settimeout(timeout)
-        data = {'id': apiid, 'method': method, 'params': args}
-        request = json.dumps(data)
-        for i in range(1, retries + 1):
-            try:
-                self.client.write(request.encode("utf8") + b'\n')
-                self.client.flush()
-                response = self.client.readline()
-            except BrokenPipeError as e:
-                if i < retries:
-                    logging.warning("RPC method %s on iteration %s error: %s",
-                                    method, i, e)
-                    continue
-                else:
-                    logging.error("RPC method %s fail on iteration %s: %s",
-                                  method, i, e)
-                    raise
-            if not response:
-                if i < retries:
-                    logging.warning(
-                        "No response for RPC method %s on iteration %s",
-                        method, i)
-                    continue
-                else:
-                    logging.error(
-                        "No response for RPC method %s on iteration %s",
-                        method, i)
-                    raise Sl4aProtocolError(
-                        Sl4aProtocolError.NO_RESPONSE_FROM_SERVER)
-            else:
-                break
-        result = json.loads(str(response, encoding="utf8"))
-        if timeout:
-            self.conn.settimeout(self._SOCKET_TIMEOUT)
-        if result['error']:
-            logging.error("RPC method %s with error %s", method,
-                          result['error'])
-            raise Sl4aApiError("RPC call %s failed with error %s" %
-                               (method, result['error']))
-        if result['id'] != apiid:
-            logging.error("RPC method %s with mismatched api id %s", method,
-                          result['id'])
-            raise Sl4aProtocolError(Sl4aProtocolError.MISMATCHED_API_ID)
-        return result['result']
-
-    def __getattr__(self, name):
-        """Wrapper for python magic to turn method calls into RPC calls."""
-
-        def rpc_call(*args, **kwargs):
-            return self._rpc(name, *args, **kwargs)
-
-        return rpc_call
diff --git a/acts/framework/acts/controllers/sl4a_lib/__init__.py b/acts/framework/acts/controllers/sl4a_lib/__init__.py
new file mode 100644
index 0000000..9727988
--- /dev/null
+++ b/acts/framework/acts/controllers/sl4a_lib/__init__.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
diff --git a/acts/framework/acts/controllers/sl4a_lib/error_reporter.py b/acts/framework/acts/controllers/sl4a_lib/error_reporter.py
new file mode 100644
index 0000000..96aea2a
--- /dev/null
+++ b/acts/framework/acts/controllers/sl4a_lib/error_reporter.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import logging
+import re
+import threading
+import time
+
+from acts import utils
+
+
+class ErrorLogger(logging.LoggerAdapter):
+    """A logger for a given error report."""
+
+    def __init__(self, label):
+        self.label = label
+        super(ErrorLogger, self).__init__(logging.getLogger(), {})
+
+    def process(self, msg, kwargs):
+        """Transforms a log message to be in a given format."""
+        return '[Error Report|%s] %s' % (self.label, msg), kwargs
+
+
+class ErrorReporter(object):
+    """A class that reports errors and diagnoses possible points of failure.
+
+    Attributes:
+        max_reports: The maximum number of reports that should be reported.
+            Defaulted to 1 to prevent multiple reports from reporting at the
+            same time over one another.
+        name: The name of the report to be used in the error logs.
+    """
+
+    def __init__(self, name, max_reports=1):
+        """Creates an error report.
+
+        Args:
+            name: The name of the error report.
+            max_reports: Sets the maximum number of reports to this value.
+        """
+        self.name = name
+        self.max_reports = max_reports
+        self._ticket_number = 0
+        self._ticket_lock = threading.Lock()
+        self._current_request_count = 0
+        self._accept_requests = True
+
+    def create_error_report(self, sl4a_manager, sl4a_session, rpc_connection):
+        """Creates an error report, if possible.
+
+        Returns:
+            False iff a report cannot be created.
+        """
+        if not self._accept_requests:
+            return False
+
+        self._current_request_count += 1
+
+        try:
+            ticket = self._get_report_ticket()
+            if not ticket:
+                return False
+
+            report = ErrorLogger('%s|%s' % (self.name, ticket))
+
+            (self.report_on_adb(sl4a_manager.adb, report)
+             and self.report_device_processes(sl4a_manager.adb, report) and
+             self.report_sl4a_state(rpc_connection, sl4a_manager.adb, report)
+             and self.report_sl4a_session(sl4a_manager, sl4a_session, report))
+
+            return True
+        finally:
+            self._current_request_count -= 1
+
+    def report_on_adb(self, adb, report):
+        """Creates an error report for ADB. Returns false if ADB has failed."""
+        adb_uptime = utils.get_process_uptime('adb')
+        if adb_uptime:
+            report.info('The adb daemon has an uptime of %s '
+                        '([[dd-]hh:]mm:ss).' % adb_uptime)
+        else:
+            report.warning('The adb daemon (on the host machine) is not '
+                           'running. All forwarded ports have been removed.')
+            return False
+
+        devices_output = adb.devices()
+        if adb.serial not in devices_output:
+            report.warning(
+                'This device cannot be found by ADB. The device may have shut '
+                'down or disconnected.')
+            return False
+        elif re.findall(r'%s\s+offline' % adb.serial, devices_output):
+            report.warning(
+                'The device is marked as offline in ADB. We are no longer able '
+                'to access the device.')
+            return False
+        else:
+            report.info(
+                'The device is online and accessible through ADB calls.')
+        return True
+
+    def report_device_processes(self, adb, report):
+        """Creates an error report for the device's required processes.
+
+        Returns:
+            False iff user-apks cannot be communicated with over tcp.
+        """
+        zygote_uptime = utils.get_device_process_uptime(adb, 'zygote')
+        if zygote_uptime:
+            report.info(
+                'Zygote has been running for %s ([[dd-]hh:]mm:ss). If this '
+                'value is low, the phone may have recently crashed.' %
+                zygote_uptime)
+        else:
+            report.warning(
+                'Zygote has been killed. It is likely the Android Runtime has '
+                'crashed. Check the bugreport/logcat for more information.')
+            return False
+
+        netd_uptime = utils.get_device_process_uptime(adb, 'netd')
+        if netd_uptime:
+            report.info(
+                'Netd has been running for %s ([[dd-]hh:]mm:ss). If this '
+                'value is low, the phone may have recently crashed.' %
+                zygote_uptime)
+        else:
+            report.warning(
+                'Netd has been killed. The Android Runtime may have crashed. '
+                'Check the bugreport/logcat for more information.')
+            return False
+
+        adbd_uptime = utils.get_device_process_uptime(adb, 'adbd')
+        if netd_uptime:
+            report.info(
+                'Adbd has been running for %s ([[dd-]hh:]mm:ss). If this '
+                'value is low, the phone may have recently crashed.' %
+                adbd_uptime)
+        else:
+            report.warning('Adbd is not running.')
+            return False
+        return True
+
+    def report_sl4a_state(self, rpc_connection, adb, report):
+        """Creates an error report for the state of SL4A."""
+        report.info(
+            'Diagnosing Failure over connection %s.' % rpc_connection.ports)
+
+        ports = rpc_connection.ports
+        forwarded_ports_output = adb.forward('--list')
+
+        expected_output = '%s tcp:%s tcp:%s' % (adb.serial,
+                                                ports.forwarded_port,
+                                                ports.server_port)
+        if expected_output not in forwarded_ports_output:
+            formatted_output = re.sub(
+                '^', '    ', forwarded_ports_output, flags=re.MULTILINE)
+            report.warning(
+                'The forwarded port for the failed RpcConnection is missing.\n'
+                'Expected:\n    %s\nBut found:\n%s' % (expected_output,
+                                                       formatted_output))
+            return False
+        else:
+            report.info('The connection port has been properly forwarded to '
+                        'the device.')
+
+        sl4a_uptime = utils.get_device_process_uptime(
+            adb, 'com.googlecode.android_scripting')
+        if sl4a_uptime:
+            report.info(
+                'SL4A has been running for %s ([[dd-]hh:]mm:ss). If this '
+                'value is lower than the test case, it must have been '
+                'restarted during the test.' % sl4a_uptime)
+        else:
+            report.warning(
+                'The SL4A scripting service is not running. SL4A may have '
+                'crashed, or have been terminated by the Android Runtime.')
+            return False
+        return True
+
+    def report_sl4a_session(self, sl4a_manager, session, report):
+        """Reports the state of an SL4A session."""
+        if session.server_port not in sl4a_manager.sl4a_ports_in_use:
+            report.warning('SL4A server port %s not found in set of open '
+                           'ports %s' % (session.server_port,
+                                         sl4a_manager.sl4a_ports_in_use))
+            return False
+
+        if session not in sl4a_manager.sessions.values():
+            report.warning('SL4A session %s over port %s is not managed by '
+                           'the SL4A Manager. This session is already dead.' %
+                           (session.uid, session.server_port))
+            return False
+        return True
+
+    def finalize_reports(self):
+        self._accept_requests = False
+        while self._current_request_count > 0:
+            # Wait for other threads to finish.
+            time.sleep(.1)
+
+    def _get_report_ticket(self):
+        """Returns the next ticket, or none if all tickets have been used."""
+        with self._ticket_lock:
+            self._ticket_number += 1
+            ticket_number = self._ticket_number
+
+        if ticket_number <= self.max_reports:
+            return ticket_number
+        else:
+            return None
diff --git a/acts/framework/acts/controllers/event_dispatcher.py b/acts/framework/acts/controllers/sl4a_lib/event_dispatcher.py
similarity index 68%
rename from acts/framework/acts/controllers/event_dispatcher.py
rename to acts/framework/acts/controllers/sl4a_lib/event_dispatcher.py
index 32fba72..91141c0 100644
--- a/acts/framework/acts/controllers/event_dispatcher.py
+++ b/acts/framework/acts/controllers/sl4a_lib/event_dispatcher.py
@@ -15,43 +15,60 @@
 #   limitations under the License.
 
 from concurrent.futures import ThreadPoolExecutor
-import logging
 import queue
 import re
-import socket
 import threading
 import time
-import traceback
+
+from acts import logger
+from acts.controllers.sl4a_lib import rpc_client
 
 
 class EventDispatcherError(Exception):
-    pass
+    """The base class for all EventDispatcher exceptions."""
 
 
 class IllegalStateError(EventDispatcherError):
-    """Raise when user tries to put event_dispatcher into an illegal state.
-    """
+    """Raise when user tries to put event_dispatcher into an illegal state."""
 
 
 class DuplicateError(EventDispatcherError):
-    """Raise when a duplicate is being created and it shouldn't.
-    """
+    """Raise when two event handlers have been assigned to an event name."""
 
 
 class EventDispatcher:
-    """Class managing events for an sl4a connection.
+    """A class for managing the events for an SL4A Session.
+
+    Attributes:
+        _serial: The serial of the device.
+        _rpc_client: The rpc client for that session.
+        _started: A bool that holds whether or not the event dispatcher is
+                  running.
+        _executor: The thread pool executor for running event handlers and
+                   polling.
+        _event_dict: A dictionary of str eventName = Queue<Event> eventQueue
+        _handlers: A dictionary of str eventName => (lambda, args) handler
+        _lock: A lock that prevents multiple reads/writes to the event queues.
+        log: The EventDispatcher's logger.
     """
 
     DEFAULT_TIMEOUT = 60
 
-    def __init__(self, droid):
-        self.droid = droid
-        self.started = False
-        self.executor = None
-        self.poller = None
-        self.event_dict = {}
-        self.handlers = {}
-        self.lock = threading.RLock()
+    def __init__(self, serial, rpc_client):
+        self._serial = serial
+        self._rpc_client = rpc_client
+        self._started = False
+        self._executor = None
+        self._event_dict = {}
+        self._handlers = {}
+        self._lock = threading.RLock()
+
+        def _log_formatter(message):
+            """Defines the formatting used in the logger."""
+            return '[E Dispatcher|%s|%s] %s' % (self._serial,
+                                                self._rpc_client.uid, message)
+
+        self.log = logger.create_logger(_log_formatter)
 
     def poll_events(self):
         """Continuously polls all types of events from sl4a.
@@ -61,40 +78,41 @@
         corresponding event immediately upon event discovery, and the event
         won't be stored. If exceptions occur, stop the dispatcher and return
         """
-        while self.started:
-            event_obj = None
-            event_name = None
+        while self._started:
             try:
-                event_obj = self.droid.eventWait(50000)
-            except Exception as e:
-                if self.started:
-                    logging.error("Exception %s happened during polling.", e)
-                    logging.info(traceback.format_exc())
-                    raise
+                event_obj = self._rpc_client.eventWait(50000)
+            except rpc_client.Sl4aConnectionError as e:
+                if self._rpc_client.is_alive:
+                    self.log.warning('Closing due to closed session.')
+                    break
+                else:
+                    self.log.warning('Closing due to error: %s.' % e)
+                    self.close()
+                    raise e
             if not event_obj:
                 continue
             elif 'name' not in event_obj:
-                logging.error("Received Malformed event {}".format(event_obj))
+                self.log.error('Received Malformed event {}'.format(event_obj))
                 continue
             else:
                 event_name = event_obj['name']
             # if handler registered, process event
-            if event_name in self.handlers:
+            if event_name == 'EventDispatcherShutdown':
+                self.log.debug('Received shutdown signal.')
+                # closeSl4aSession has been called, which closes the event
+                # dispatcher. Stop execution on this polling thread.
+                return
+            if event_name in self._handlers:
                 self.handle_subscribed_event(event_obj, event_name)
-            if event_name == "EventDispatcherShutdown":
-                # closeSl4aSession() has been called, which breaks the
-                # connection with SL4A. Close the event dispatcher on the ACTS
-                # side.
-                break
             else:
-                self.lock.acquire()
-                if event_name in self.event_dict:  # otherwise, cache event
-                    self.event_dict[event_name].put(event_obj)
+                self._lock.acquire()
+                if event_name in self._event_dict:  # otherwise, cache event
+                    self._event_dict[event_name].put(event_obj)
                 else:
                     q = queue.Queue()
                     q.put(event_obj)
-                    self.event_dict[event_name] = q
-                self.lock.release()
+                    self._event_dict[event_name] = q
+                self._lock.release()
 
     def register_handler(self, handler, event_name, args):
         """Registers an event handler.
@@ -112,17 +130,17 @@
             DuplicateError: Raised if attempts to register more than one
                 handler for one type of event.
         """
-        if self.started:
-            raise IllegalStateError(("Can't register service after polling is"
-                                     " started"))
-        self.lock.acquire()
+        if self._started:
+            raise IllegalStateError('Cannot register service after polling is '
+                                    'started.')
+        self._lock.acquire()
         try:
-            if event_name in self.handlers:
+            if event_name in self._handlers:
                 raise DuplicateError(
                     'A handler for {} already exists'.format(event_name))
-            self.handlers[event_name] = (handler, args)
+            self._handlers[event_name] = (handler, args)
         finally:
-            self.lock.release()
+            self._lock.release()
 
     def start(self):
         """Starts the event dispatcher.
@@ -133,32 +151,24 @@
             IllegalStateError: Can't start a dispatcher again when it's already
                 running.
         """
-        if not self.started:
-            self.started = True
-            self.executor = ThreadPoolExecutor(max_workers=32)
-            self.poller = self.executor.submit(self.poll_events)
+        if not self._started:
+            self._started = True
+            self._executor = ThreadPoolExecutor(max_workers=32)
+            self._executor.submit(self.poll_events)
         else:
             raise IllegalStateError("Dispatcher is already started.")
 
-    def clean_up(self):
-        """Clean up and release resources after the event dispatcher polling
-        loop has been broken.
+    def close(self):
+        """Clean up and release resources.
 
-        The following things happen:
-        1. Clear all events and flags.
-        2. Close the sl4a client the event_dispatcher object holds.
-        3. Shut down executor without waiting.
+        This function should only be called after a
+        rpc_client.closeSl4aSession() call.
         """
-        uid = self.droid.uid
-        if not self.started:
+        if not self._started:
             return
-        self.started = False
+        self._started = False
+        self._executor.shutdown(wait=True)
         self.clear_all_events()
-        self.droid.close()
-        self.poller.set_result("Done")
-        # The polling thread is guaranteed to finish after a max of 60 seconds,
-        # so we don't wait here.
-        self.executor.shutdown(wait=False)
 
     def pop_event(self, event_name, timeout=DEFAULT_TIMEOUT):
         """Pop an event from its queue.
@@ -179,15 +189,15 @@
             IllegalStateError: Raised if pop is called before the dispatcher
                 starts polling.
         """
-        if not self.started:
+        if not self._started:
             raise IllegalStateError(
-                "Dispatcher needs to be started before popping.")
+                'Dispatcher needs to be started before popping.')
 
         e_queue = self.get_event_q(event_name)
 
         if not e_queue:
-            raise TypeError(
-                "Failed to get an event queue for {}".format(event_name))
+            raise IllegalStateError(
+                'Failed to get an event queue for {}'.format(event_name))
 
         try:
             # Block for timeout
@@ -223,6 +233,8 @@
             timeout: Number of seconds to wait.
             *args: Optional positional args passed to predicate().
             **kwargs: Optional keyword args passed to predicate().
+                consume_ignored_events: Whether or not to consume events while
+                    searching for the desired event. Defaults to True if unset.
 
         Returns:
             The event that satisfies the predicate.
@@ -232,18 +244,25 @@
                 found before time out.
         """
         deadline = time.time() + timeout
-
+        ignored_events = []
+        consume_events = kwargs.pop('consume_ignored_events', True)
         while True:
             event = None
             try:
                 event = self.pop_event(event_name, 1)
+                if not consume_events:
+                    ignored_events.append(event)
             except queue.Empty:
                 pass
 
             if event and predicate(event, *args, **kwargs):
+                for ignored_event in ignored_events:
+                    self.get_event_q(event_name).put(ignored_event)
                 return event
 
             if time.time() > deadline:
+                for ignored_event in ignored_events:
+                    self.get_event_q(event_name).put(ignored_event)
                 raise queue.Empty(
                     'Timeout after {}s waiting for event: {}'.format(
                         timeout, event_name))
@@ -272,12 +291,12 @@
                 starts polling.
             queue.Empty: Raised if no event was found before time out.
         """
-        if not self.started:
+        if not self._started:
             raise IllegalStateError(
                 "Dispatcher needs to be started before popping.")
         deadline = time.time() + timeout
         while True:
-            #TODO: fix the sleep loop
+            # TODO: fix the sleep loop
             results = self._match_and_pop(regex_pattern)
             if len(results) != 0 or time.time() > deadline:
                 break
@@ -293,16 +312,16 @@
         match (in a sense of regular expression) regex_pattern.
         """
         results = []
-        self.lock.acquire()
-        for name in self.event_dict.keys():
+        self._lock.acquire()
+        for name in self._event_dict.keys():
             if re.match(regex_pattern, name):
-                q = self.event_dict[name]
+                q = self._event_dict[name]
                 if q:
                     try:
                         results.append(q.get(False))
-                    except:
+                    except queue.Empty:
                         pass
-        self.lock.release()
+        self._lock.release()
         return results
 
     def get_event_q(self, event_name):
@@ -310,21 +329,15 @@
 
         If no event of this name has been polled, wait for one to.
 
-        Returns:
-            queue: A queue storing all the events of the specified name.
-                None if timed out.
-            timeout: Number of seconds to wait for the operation.
-
-        Raises:
-            queue.Empty: Raised if the queue does not exist and timeout has
-                passed.
+        Returns: A queue storing all the events of the specified name.
         """
-        self.lock.acquire()
-        if not event_name in self.event_dict or self.event_dict[event_name] is None:
-            self.event_dict[event_name] = queue.Queue()
-        self.lock.release()
+        self._lock.acquire()
+        if (event_name not in self._event_dict
+                or self._event_dict[event_name] is None):
+            self._event_dict[event_name] = queue.Queue()
+        self._lock.release()
 
-        event_queue = self.event_dict[event_name]
+        event_queue = self._event_dict[event_name]
         return event_queue
 
     def handle_subscribed_event(self, event_obj, event_name):
@@ -337,8 +350,8 @@
             event_obj: Json object of the event.
             event_name: Name of the event to call handler for.
         """
-        handler, args = self.handlers[event_name]
-        self.executor.submit(handler, event_obj, *args)
+        handler, args = self._handlers[event_name]
+        self._executor.submit(handler, event_obj, *args)
 
     def _handle(self, event_handler, event_name, user_args, event_timeout,
                 cond, cond_timeout):
@@ -380,9 +393,9 @@
                 If blocking call worker.result() is triggered, the handler
                 needs to return something to unblock.
         """
-        worker = self.executor.submit(self._handle, event_handler, event_name,
-                                      user_args, event_timeout, cond,
-                                      cond_timeout)
+        worker = self._executor.submit(self._handle, event_handler, event_name,
+                                       user_args, event_timeout, cond,
+                                       cond_timeout)
         return worker
 
     def pop_all(self, event_name):
@@ -401,19 +414,19 @@
             IllegalStateError: Raised if pop is called before the dispatcher
                 starts polling.
         """
-        if not self.started:
+        if not self._started:
             raise IllegalStateError(("Dispatcher needs to be started before "
                                      "popping."))
         results = []
         try:
-            self.lock.acquire()
+            self._lock.acquire()
             while True:
-                e = self.event_dict[event_name].get(block=False)
+                e = self._event_dict[event_name].get(block=False)
                 results.append(e)
         except (queue.Empty, KeyError):
             return results
         finally:
-            self.lock.release()
+            self._lock.release()
 
     def clear_events(self, event_name):
         """Clear all events of a particular name.
@@ -421,17 +434,17 @@
         Args:
             event_name: Name of the events to be popped.
         """
-        self.lock.acquire()
+        self._lock.acquire()
         try:
             q = self.get_event_q(event_name)
             q.queue.clear()
         except queue.Empty:
             return
         finally:
-            self.lock.release()
+            self._lock.release()
 
     def clear_all_events(self):
         """Clear all event queues and their cached events."""
-        self.lock.acquire()
-        self.event_dict.clear()
-        self.lock.release()
+        self._lock.acquire()
+        self._event_dict.clear()
+        self._lock.release()
diff --git a/acts/framework/acts/controllers/sl4a_lib/rpc_client.py b/acts/framework/acts/controllers/sl4a_lib/rpc_client.py
new file mode 100644
index 0000000..ee12cef
--- /dev/null
+++ b/acts/framework/acts/controllers/sl4a_lib/rpc_client.py
@@ -0,0 +1,288 @@
+import json
+import threading
+
+import time
+from concurrent import futures
+
+from acts import logger
+
+SOCKET_TIMEOUT = 60
+
+# The Session UID when a UID has not been received yet.
+UNKNOWN_UID = -1
+
+
+class Sl4aException(Exception):
+    """The base class for all SL4A exceptions."""
+
+
+class Sl4aStartError(Sl4aException):
+    """Raised when sl4a is not able to be started."""
+
+
+class Sl4aApiError(Sl4aException):
+    """Raised when remote API reports an error."""
+
+
+class Sl4aConnectionError(Sl4aException):
+    """An error raised upon failure to connect to SL4A."""
+
+
+class Sl4aProtocolError(Sl4aException):
+    """Raised when there an error in exchanging data with server on device."""
+    NO_RESPONSE_FROM_HANDSHAKE = 'No response from handshake.'
+    NO_RESPONSE_FROM_SERVER = 'No response from server.'
+    MISMATCHED_API_ID = 'Mismatched API id.'
+
+
+class MissingSl4AError(Sl4aException):
+    """An error raised when an Sl4aClient is created without SL4A installed."""
+
+
+class RpcClient(object):
+    """An RPC client capable of processing multiple RPCs concurrently.
+
+    Attributes:
+        _free_connections: A list of all idle RpcConnections.
+        _working_connections: A list of all working RpcConnections.
+        _lock: A lock used for accessing critical memory.
+        max_connections: The maximum number of RpcConnections at a time.
+            Increasing or decreasing the number of max connections does NOT
+            modify the thread pool size being used for self.future RPC calls.
+        _log: The logger for this RpcClient.
+    """
+    """The default value for the maximum amount of connections for a client."""
+    DEFAULT_MAX_CONNECTION = 15
+
+    class AsyncClient(object):
+        """An object that allows RPC calls to be called asynchronously.
+
+        Attributes:
+            _rpc_client: The RpcClient to use when making calls.
+            _executor: The ThreadPoolExecutor used to keep track of workers
+        """
+
+        def __init__(self, rpc_client):
+            self._rpc_client = rpc_client
+            self._executor = futures.ThreadPoolExecutor(
+                max_workers=max(rpc_client.max_connections - 2, 1))
+
+        def rpc(self, name, *args, **kwargs):
+            future = self._executor.submit(name, *args, **kwargs)
+            return future
+
+        def __getattr__(self, name):
+            """Wrapper for python magic to turn method calls into RPC calls."""
+
+            def rpc_call(*args, **kwargs):
+                future = self._executor.submit(
+                    self._rpc_client.__getattr__(name), *args, **kwargs)
+                return future
+
+            return rpc_call
+
+    def __init__(self,
+                 uid,
+                 serial,
+                 on_error_callback,
+                 _create_connection_func,
+                 max_connections=None):
+        """Creates a new RpcClient object.
+
+        Args:
+            uid: The session uid this client is a part of.
+            serial: The serial of the Android device. Used for logging.
+            on_error_callback: A callback for when a connection error is raised.
+            _create_connection_func: A reference to the function that creates a
+                new session.
+            max_connections: The maximum number of connections the RpcClient
+                can have.
+        """
+        self._serial = serial
+        self.on_error = on_error_callback
+        self._create_connection_func = _create_connection_func
+        self._free_connections = [self._create_connection_func(uid)]
+
+        self.uid = self._free_connections[0].uid
+        self._lock = threading.Lock()
+
+        def _log_formatter(message):
+            """Formats the message to be logged."""
+            return '[RPC Service|%s|%s] %s' % (self._serial, self.uid, message)
+
+        self._log = logger.create_logger(_log_formatter)
+
+        self._working_connections = []
+        if max_connections is None:
+            self.max_connections = RpcClient.DEFAULT_MAX_CONNECTION
+        else:
+            self.max_connections = max_connections
+
+        self._async_client = RpcClient.AsyncClient(self)
+        self.is_alive = True
+
+    def terminate(self):
+        """Terminates all connections to the SL4A server."""
+        if len(self._working_connections) > 0:
+            self._log.warning(
+                '%s connections are still active, and waiting on '
+                'responses.Closing these connections now.' % len(
+                    self._working_connections))
+        connections = self._free_connections + self._working_connections
+        for connection in connections:
+            self._log.debug(
+                'Closing connection over ports %s' % connection.ports)
+            connection.close()
+        self._free_connections = []
+        self._working_connections = []
+        self.is_alive = False
+
+    def _get_free_connection(self):
+        """Returns a free connection to be used for an RPC call.
+
+        This function also adds the client to the working set to prevent
+        multiple users from obtaining the same client.
+        """
+        while True:
+            if len(self._free_connections) > 0:
+                with self._lock:
+                    # Check if another thread grabbed the remaining connection.
+                    # while we were waiting for the lock.
+                    if len(self._free_connections) == 0:
+                        continue
+                    client = self._free_connections.pop()
+                    self._working_connections.append(client)
+                    return client
+
+            client_count = (
+                len(self._free_connections) + len(self._working_connections))
+            if client_count < self.max_connections:
+                with self._lock:
+                    client_count = (len(self._free_connections) +
+                                    len(self._working_connections))
+                    if client_count < self.max_connections:
+                        client = self._create_connection_func(self.uid)
+                        self._working_connections.append(client)
+                        return client
+            time.sleep(.01)
+
+    def _release_working_connection(self, connection):
+        """Marks a working client as free.
+
+        Args:
+            connection: The client to mark as free.
+        Raises:
+            A ValueError if the client is not a known working connection.
+        """
+        # We need to keep this code atomic because the client count is based on
+        # the length of the free and working connection list lengths.
+        with self._lock:
+            self._working_connections.remove(connection)
+            self._free_connections.append(connection)
+
+    def rpc(self, method, *args, timeout=None, retries=1):
+        """Sends an rpc to sl4a.
+
+        Sends an rpc call to sl4a over this RpcClient's corresponding session.
+
+        Args:
+            method: str, The name of the method to execute.
+            args: any, The args to send to sl4a.
+            timeout: The amount of time to wait for a response.
+            retries: Misnomer, is actually the number of tries.
+
+        Returns:
+            The result of the rpc.
+
+        Raises:
+            Sl4aProtocolError: Something went wrong with the sl4a protocol.
+            Sl4aApiError: The rpc went through, however executed with errors.
+        """
+        connection = self._get_free_connection()
+        ticket = connection.get_new_ticket()
+
+        if timeout:
+            connection.set_timeout(timeout)
+        data = {'id': ticket, 'method': method, 'params': args}
+        request = json.dumps(data)
+        response = ''
+        try:
+            for i in range(1, retries + 1):
+                connection.send_request(request)
+                self._log.debug('Sent: %s' % request)
+
+                response = connection.get_response()
+                self._log.debug('Received: %s', response)
+                if not response:
+                    if i < retries:
+                        self._log.warning(
+                            'No response for RPC method %s on iteration %s',
+                            method, i)
+                        continue
+                    else:
+                        self.on_error(connection)
+                        raise Sl4aProtocolError(
+                            Sl4aProtocolError.NO_RESPONSE_FROM_SERVER)
+                else:
+                    break
+        except BrokenPipeError as e:
+            if self.is_alive:
+                self._log.error('Exception %s happened while communicating to '
+                                'SL4A.', e)
+                self.on_error(connection)
+            else:
+                self._log.warning('The connection was killed during cleanup:')
+                self._log.warning(e)
+            raise Sl4aConnectionError(e)
+        finally:
+            if timeout:
+                connection.set_timeout(SOCKET_TIMEOUT)
+            self._release_working_connection(connection)
+        result = json.loads(str(response, encoding='utf8'))
+
+        if result['error']:
+            err_msg = 'RPC call %s to device failed with error %s' % (
+                method, result['error'])
+            self._log.error(err_msg)
+            raise Sl4aApiError(err_msg)
+        if result['id'] != ticket:
+            self._log.error('RPC method %s with mismatched api id %s', method,
+                            result['id'])
+            raise Sl4aProtocolError(Sl4aProtocolError.MISMATCHED_API_ID)
+        return result['result']
+
+    @property
+    def future(self):
+        """Returns a magic function that returns a future running an RPC call.
+
+        This function effectively allows the idiom:
+
+        >>> rpc_client = RpcClient(...)
+        >>> # returns after call finishes
+        >>> rpc_client.someRpcCall()
+        >>> # Immediately returns a reference to the RPC's future, running
+        >>> # the lengthy RPC call on another thread.
+        >>> future = rpc_client.future.someLengthyRpcCall()
+        >>> rpc_client.doOtherThings()
+        >>> ...
+        >>> # Wait for and get the returned value of the lengthy RPC.
+        >>> # Can specify a timeout as well.
+        >>> value = future.result()
+
+        The number of concurrent calls to this method is limited to
+        (max_connections - 2), to prevent future calls from exhausting all free
+        connections.
+        """
+        return self._async_client
+
+    def __getattr__(self, name):
+        """Wrapper for python magic to turn method calls into RPC calls."""
+
+        def rpc_call(*args, **kwargs):
+            return self.rpc(name, *args, **kwargs)
+
+        if not self.is_alive:
+            raise Sl4aStartError(
+                'This SL4A session has already been terminated. You must '
+                'create a new session to continue.')
+        return rpc_call
diff --git a/acts/framework/acts/controllers/sl4a_lib/rpc_connection.py b/acts/framework/acts/controllers/sl4a_lib/rpc_connection.py
new file mode 100644
index 0000000..69a7d65
--- /dev/null
+++ b/acts/framework/acts/controllers/sl4a_lib/rpc_connection.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import json
+import socket
+import threading
+
+from acts import logger
+from acts.controllers.sl4a_lib import rpc_client
+
+# The Session UID when a UID has not been received yet.
+UNKNOWN_UID = -1
+
+
+class Sl4aConnectionCommand(object):
+    """Commands that can be invoked on the sl4a client.
+
+    INIT: Initializes a new sessions in sl4a.
+    CONTINUE: Creates a connection.
+    """
+    INIT = 'initiate'
+    CONTINUE = 'continue'
+
+
+class RpcConnection(object):
+    """A single RPC Connection thread.
+
+    Attributes:
+        _client_socket: The socket this connection uses.
+        _socket_file: The file created over the _client_socket.
+        _ticket_counter: The counter storing the current ticket number.
+        _ticket_lock: A lock on the ticket counter to prevent ticket collisions.
+        adb: A reference to the AdbProxy of the AndroidDevice. Used for logging.
+        log: The logger for this RPC Client.
+        ports: The Sl4aPorts object that stores the ports this connection uses.
+        uid: The SL4A session ID.
+    """
+
+    def __init__(self, adb, ports, client_socket, socket_fd, uid=UNKNOWN_UID):
+        self._client_socket = client_socket
+        self._socket_file = socket_fd
+        self._ticket_counter = 0
+        self._ticket_lock = threading.Lock()
+        self.adb = adb
+        self.uid = uid
+
+        def _log_formatter(message):
+            """Defines the formatting used in the logger."""
+            return '[SL4A Client|%s|%s] %s' % (self.adb.serial, self.uid,
+                                               message)
+
+        self.log = logger.create_logger(_log_formatter)
+
+        self.ports = ports
+        self.set_timeout(rpc_client.SOCKET_TIMEOUT)
+
+    def open(self):
+        if self.uid != UNKNOWN_UID:
+            start_command = Sl4aConnectionCommand.CONTINUE
+        else:
+            start_command = Sl4aConnectionCommand.INIT
+
+        self._initiate_handshake(start_command)
+
+    def _initiate_handshake(self, start_command):
+        """Establishes a connection with the SL4A server.
+
+        Args:
+            start_command: The command to send. See Sl4aConnectionCommand.
+        """
+        try:
+            resp = self._cmd(start_command)
+        except socket.timeout as e:
+            self.log.error('Failed to open socket connection: %s', e)
+            raise
+        if not resp:
+            raise rpc_client.Sl4aProtocolError(
+                rpc_client.Sl4aProtocolError.NO_RESPONSE_FROM_HANDSHAKE)
+        result = json.loads(str(resp, encoding='utf8'))
+        if result['status']:
+            self.uid = result['uid']
+        else:
+            self.log.warning(
+                'UID not received for connection %s.' % self.ports)
+            self.uid = UNKNOWN_UID
+        self.log.debug('Created connection over: %s.' % self.ports)
+
+    def _cmd(self, command):
+        """Sends an session protocol command to SL4A to establish communication.
+
+        Args:
+            command: The name of the command to execute.
+
+        Returns:
+            The line that was written back.
+        """
+        self.send_request(json.dumps({'cmd': command, 'uid': self.uid}))
+        return self.get_response()
+
+    def get_new_ticket(self):
+        """Returns a ticket for a new request."""
+        with self._ticket_lock:
+            self._ticket_counter += 1
+            ticket = self._ticket_counter
+        return ticket
+
+    def set_timeout(self, timeout):
+        """Sets the socket's wait for response timeout."""
+        self._client_socket.settimeout(timeout)
+
+    def send_request(self, request):
+        """Sends a request over the connection."""
+        self._socket_file.write(request.encode('utf8') + b'\n')
+        self._socket_file.flush()
+
+    def get_response(self):
+        """Returns the first response sent back to the client."""
+        data = self._socket_file.readline()
+        return data
+
+    def close(self):
+        """Closes the connection gracefully."""
+        self._client_socket.close()
+        self.adb.remove_tcp_forward(self.ports.forwarded_port)
diff --git a/acts/framework/acts/controllers/sl4a_lib/sl4a_manager.py b/acts/framework/acts/controllers/sl4a_lib/sl4a_manager.py
new file mode 100644
index 0000000..d55ba38
--- /dev/null
+++ b/acts/framework/acts/controllers/sl4a_lib/sl4a_manager.py
@@ -0,0 +1,282 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import threading
+
+import time
+
+from acts import logger
+from acts.controllers.sl4a_lib import rpc_client
+from acts.controllers.sl4a_lib import sl4a_session
+from acts.controllers.sl4a_lib import error_reporter
+
+FIND_PORT_RETRIES = 3
+
+_SL4A_LAUNCH_SERVER_CMD = (
+    'am startservice -a com.googlecode.android_scripting.action.LAUNCH_SERVER '
+    '--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT %s '
+    'com.googlecode.android_scripting/.service.ScriptingLayerService')
+
+_SL4A_CLOSE_SERVER_CMD = (
+    'am startservice -a com.googlecode.android_scripting.action.KILL_PROCESS '
+    '--ei com.googlecode.android_scripting.extra.PROXY_PORT %s '
+    'com.googlecode.android_scripting/.service.ScriptingLayerService')
+
+# The command for finding SL4A's server port as root.
+_SL4A_ROOT_FIND_PORT_CMD = (
+    # Get all open, listening ports, and their process names
+    'ss -l -p -n | '
+    # Find all open TCP ports for SL4A
+    'grep "tcp.*droid_scripting" | '
+    # Shorten all whitespace to a single space character
+    'tr -s " " | '
+    # Grab the 5th column (which is server:port)
+    'cut -d " " -f 5 |'
+    # Only grab the port
+    'sed s/.*://g')
+
+# The command for finding SL4A's server port without root.
+_SL4A_USER_FIND_PORT_CMD = (
+    # Get all open, listening ports, and their process names
+    'ss -l -p -n | '
+    # Find all open ports exposed to the public. This can produce false
+    # positives since users cannot read the process associated with the port.
+    'grep -e "tcp.*::ffff:127\.0\.0\.1:" | '
+    # Shorten all whitespace to a single space character
+    'tr -s " " | '
+    # Grab the 5th column (which is server:port)
+    'cut -d " " -f 5 |'
+    # Only grab the port
+    'sed s/.*://g')
+
+# The command that begins the SL4A ScriptingLayerService.
+_SL4A_START_SERVICE_CMD = (
+    'am startservice '
+    'com.googlecode.android_scripting/.service.ScriptingLayerService')
+
+# Maps device serials to their SL4A Manager. This is done to prevent multiple
+# Sl4aManagers from existing for the same device.
+_all_sl4a_managers = {}
+
+
+def create_sl4a_manager(adb):
+    """Creates and returns an SL4AManager for the given device.
+
+    Args:
+        adb: A reference to the device's AdbProxy.
+    """
+    if adb.serial in _all_sl4a_managers:
+        _all_sl4a_managers[adb.serial].log.warning(
+            'Attempted to return multiple SL4AManagers on the same device. '
+            'Returning pre-existing SL4AManager instead.')
+        return _all_sl4a_managers[adb.serial]
+    else:
+        manager = Sl4aManager(adb)
+        _all_sl4a_managers[adb.serial] = manager
+        return manager
+
+
+class Sl4aManager(object):
+    """A manager for SL4A Clients to a given AndroidDevice.
+
+    SL4A is a single APK that can host multiple RPC servers at a time. This
+    class manages each server connection over ADB, and will gracefully
+    terminate the apk during cleanup.
+
+    Attributes:
+        _listen_for_port_lock: A lock for preventing multiple threads from
+            potentially mixing up requested ports.
+        _sl4a_ports: A set of all known SL4A server ports in use.
+        adb: A reference to the AndroidDevice's AdbProxy.
+        log: The logger for this object.
+        sessions: A dictionary of session_ids to sessions.
+    """
+
+    def __init__(self, adb):
+        self._listen_for_port_lock = threading.Lock()
+        self._sl4a_ports = set()
+        self.adb = adb
+        self.log = logger.create_logger(
+            lambda msg: '[SL4A Manager|%s] %s' % (adb.serial, msg))
+        self.sessions = {}
+        self._started = False
+        self.error_reporter = error_reporter.ErrorReporter(
+            'SL4A %s' % adb.serial)
+
+    @property
+    def sl4a_ports_in_use(self):
+        """Returns a list of all server ports used by SL4A servers."""
+        return set([session.server_port for session in self.sessions.values()])
+
+    def diagnose_failure(self, session, connection):
+        """Diagnoses all potential known reasons SL4A can fail.
+
+        Assumes the failure happened on an RPC call, which verifies the state
+        of ADB/device."""
+        self.error_reporter.create_error_report(self, session, connection)
+
+    def start_sl4a_server(self, device_port, try_interval=.01):
+        """Opens a server socket connection on SL4A.
+
+        Args:
+            device_port: The expected port for SL4A to open on. Note that in
+                many cases, this will be different than the port returned by
+                this method.
+            try_interval: The amount of seconds between attempts at finding an
+                opened port on the AndroidDevice.
+
+        Returns:
+            The port number on the device the SL4A server is open on.
+
+        Raises:
+            Sl4aConnectionError if SL4A's opened port cannot be found.
+        """
+        # Launch a server through SL4A.
+        self.adb.shell(_SL4A_LAUNCH_SERVER_CMD % device_port)
+
+        # There is a small chance that the server has not come up yet by the
+        # time the launch command has finished. Try to read get the listening
+        # port again after a small amount of time.
+        for _ in range(FIND_PORT_RETRIES):
+            port = self._get_open_listening_port()
+            if port is None:
+                time.sleep(try_interval)
+            else:
+                return port
+
+        raise rpc_client.Sl4aConnectionError(
+            'Unable to find a valid open port for a new server connection. '
+            'Expected port: %s. Open ports: %s' % (device_port,
+                                                   self._sl4a_ports))
+
+    def _get_all_ports_command(self):
+        """Returns the list of all ports from the command to get ports."""
+        is_root = True
+        if not self.adb.is_root():
+            is_root = self.adb.ensure_root()
+
+        if is_root:
+            return _SL4A_ROOT_FIND_PORT_CMD
+        else:
+            # TODO(markdr): When root is unavailable, search logcat output for
+            #               the port the server has opened.
+            self.log.warning('Device cannot be put into root mode. SL4A '
+                             'server connections cannot be verified.')
+            return _SL4A_USER_FIND_PORT_CMD
+
+    def _get_open_listening_port(self):
+        """Returns any open, listening port found for SL4A.
+
+        Will return none if no port is found.
+        """
+        possible_ports = self.adb.shell(self._get_all_ports_command()).split()
+        self.log.debug('SL4A Ports found: %s' % possible_ports)
+
+        # Acquire the lock. We lock this method because if multiple threads
+        # attempt to get a server at the same time, they can potentially find
+        # the same port as being open, and both attempt to connect to it.
+        with self._listen_for_port_lock:
+            for port in possible_ports:
+                if port not in self._sl4a_ports:
+                    self._sl4a_ports.add(port)
+                    return int(port)
+        return None
+
+    def is_sl4a_installed(self):
+        """Returns True if SL4A is installed on the AndroidDevice."""
+        return bool(
+            self.adb.shell(
+                'pm path com.googlecode\.android_scripting',
+                ignore_status=True))
+
+    def start_sl4a_service(self):
+        """Starts the SL4A Service on the device.
+
+        For starting an RPC server, use start_sl4a_server() instead.
+        """
+        # Verify SL4A is installed.
+        if not self._started:
+            self._started = True
+            if not self.is_sl4a_installed():
+                raise rpc_client.MissingSl4AError(
+                    'SL4A is not installed on device %s' % self.adb.serial)
+            if self.adb.shell(
+                    'ps | grep "S com.googlecode.android_scripting"'):
+                # Close all SL4A servers not opened by this manager.
+                ports = self.adb.shell(self._get_all_ports_command()).split()
+                for port in ports:
+                    self.adb.shell(_SL4A_CLOSE_SERVER_CMD % port)
+            else:
+                # Start the service if it is not up already.
+                self.adb.shell(_SL4A_START_SERVICE_CMD)
+
+    def obtain_sl4a_server(self, server_port):
+        """Obtain an SL4A server port.
+
+        If the port is open and valid, return it. Otherwise, open an new server
+        with the hinted server_port.
+        """
+        if server_port not in self.sl4a_ports_in_use:
+            return self.start_sl4a_server(server_port)
+        else:
+            return server_port
+
+    def create_session(self,
+                       max_connections=None,
+                       client_port=0,
+                       server_port=None):
+        """Creates an SL4A server with the given ports if possible.
+
+        The ports are not guaranteed to be available for use. If the port
+        asked for is not available, this will be logged, and the port will
+        be randomized.
+
+        Args:
+            client_port: The port on the host machine
+            server_port: The port on the Android device.
+            max_connections: The max number of client connections for the
+                session.
+
+        Returns:
+            A new Sl4aServer instance.
+        """
+        if server_port is None:
+            # If a session already exists, use the same server.
+            if len(self.sessions) > 0:
+                server_port = self.sessions[sorted(
+                    self.sessions.keys())[0]].server_port
+            # Otherwise, open a new server on a random port.
+            else:
+                server_port = 0
+        self.start_sl4a_service()
+        session = sl4a_session.Sl4aSession(
+            self.adb,
+            client_port,
+            server_port,
+            self.obtain_sl4a_server,
+            self.diagnose_failure,
+            max_connections=max_connections)
+        self.sessions[session.uid] = session
+        return session
+
+    def terminate_all_sessions(self):
+        """Terminates all SL4A sessions gracefully."""
+        self.error_reporter.finalize_reports()
+        for _, session in self.sessions.items():
+            session.terminate()
+        self.sessions = {}
+        for port in self._sl4a_ports:
+            self.adb.shell(_SL4A_CLOSE_SERVER_CMD % port)
+        self._sl4a_ports = set()
diff --git a/acts/framework/acts/controllers/sl4a_lib/sl4a_ports.py b/acts/framework/acts/controllers/sl4a_lib/sl4a_ports.py
new file mode 100644
index 0000000..aeb4969
--- /dev/null
+++ b/acts/framework/acts/controllers/sl4a_lib/sl4a_ports.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+class Sl4aPorts(object):
+    """A container for the three ports needed for an SL4A connection.
+
+    Attributes:
+        client_port: The port on the host associated with the SL4A client
+        forwarded_port: The port forwarded to the Android device.
+        server_port: The port on the device associated with the SL4A server.
+    """
+
+    def __init__(self, client_port=0, forwarded_port=0, server_port=0):
+        self.client_port = client_port
+        self.forwarded_port = forwarded_port
+        self.server_port = server_port
+
+    def __str__(self):
+        return '(%s, %s, %s)' % (self.client_port, self.forwarded_port,
+                                 self.server_port)
diff --git a/acts/framework/acts/controllers/sl4a_lib/sl4a_session.py b/acts/framework/acts/controllers/sl4a_lib/sl4a_session.py
new file mode 100644
index 0000000..e36754f
--- /dev/null
+++ b/acts/framework/acts/controllers/sl4a_lib/sl4a_session.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import socket
+import threading
+
+import errno
+
+from acts import logger
+from acts.controllers.sl4a_lib import event_dispatcher
+from acts.controllers.sl4a_lib import rpc_connection
+from acts.controllers.sl4a_lib import rpc_client
+from acts.controllers.sl4a_lib import sl4a_ports
+
+SOCKET_TIMEOUT = 60
+
+# The SL4A Session UID when a UID has not been received yet.
+UNKNOWN_UID = -1
+
+
+class Sl4aSession(object):
+    """An object that tracks the state of an SL4A Session.
+
+    Attributes:
+        _event_dispatcher: The EventDispatcher instance, if any, for this
+            session.
+        _terminate_lock: A lock that prevents race conditions for multiple
+            threads calling terminate()
+        _terminated: A bool that stores whether or not this session has been
+            terminated. Terminated sessions cannot be restarted.
+        adb: A reference to the AndroidDevice's AdbProxy.
+        log: The logger for this Sl4aSession
+        server_port: The SL4A server port this session is established on.
+        uid: The uid that corresponds the the SL4A Server's session id. This
+            value is only unique during the lifetime of the SL4A apk.
+    """
+
+    def __init__(self,
+                 adb,
+                 host_port,
+                 device_port,
+                 get_server_port_func,
+                 on_error_callback,
+                 max_connections=None):
+        """Creates an SL4A Session.
+
+        Args:
+            adb: A reference to the adb proxy
+            get_server_port_func: A lambda (int) that returns the corrected
+                server port. The int passed in hints at which port to use, if
+                possible.
+            host_port: The port the host machine uses to connect to the SL4A
+                server for its first connection.
+            device_port: The SL4A server port to be used as a hint for which
+                SL4A server to connect to.
+        """
+        self._event_dispatcher = None
+        self._terminate_lock = threading.Lock()
+        self._terminated = False
+        self.adb = adb
+
+        def _log_formatter(message):
+            return '[SL4A Session|%s|%s] %s' % (self.adb.serial, self.uid,
+                                                message)
+
+        self.log = logger.create_logger(_log_formatter)
+
+        self.server_port = device_port
+        self.uid = UNKNOWN_UID
+        self.obtain_server_port = get_server_port_func
+        self._on_error_callback = on_error_callback
+
+        connection_creator = self._rpc_connection_creator(host_port)
+        self.rpc_client = rpc_client.RpcClient(
+            self.uid,
+            self.adb.serial,
+            self.diagnose_failure,
+            connection_creator,
+            max_connections=max_connections)
+
+    def _rpc_connection_creator(self, host_port):
+        def create_client(uid):
+            return self._create_rpc_connection(
+                ports=sl4a_ports.Sl4aPorts(host_port, 0, self.server_port),
+                uid=uid)
+
+        return create_client
+
+    @property
+    def is_alive(self):
+        return not self._terminated
+
+    def _create_rpc_connection(self, ports=None, uid=UNKNOWN_UID):
+        """Creates an RPC Connection with the specified ports.
+
+        Args:
+            ports: A Sl4aPorts object or a tuple of (host/client_port,
+                   forwarded_port, device/server_port). If any of these are
+                   zero, the OS will determine their values during connection.
+
+                   Note that these ports are only suggestions. If they are not
+                   available, the a different port will be selected.
+            uid: The UID of the SL4A Session. To create a new session, use
+                 UNKNOWN_UID.
+        Returns:
+            An Sl4aClient.
+        """
+        if ports is None:
+            ports = sl4a_ports.Sl4aPorts(0, 0, 0)
+        # Open a new server if a server cannot be inferred.
+        ports.server_port = self.obtain_server_port(ports.server_port)
+        self.server_port = ports.server_port
+        # Forward the device port to the host.
+        ports.forwarded_port = int(self.adb.tcp_forward(0, ports.server_port))
+        client_socket, fd = self._create_client_side_connection(ports)
+        client = rpc_connection.RpcConnection(
+            self.adb, ports, client_socket, fd, uid=uid)
+        client.open()
+        if uid == UNKNOWN_UID:
+            self.uid = client.uid
+        return client
+
+    def diagnose_failure(self, connection):
+        """Diagnoses any problems related to the SL4A session."""
+        self._on_error_callback(self, connection)
+
+    def get_event_dispatcher(self):
+        """Returns the EventDispatcher for this Sl4aSession."""
+        if self._event_dispatcher is None:
+            self._event_dispatcher = event_dispatcher.EventDispatcher(
+                self, self.rpc_client)
+        return self._event_dispatcher
+
+    def _create_client_side_connection(self, ports):
+        """Creates and connects the client socket to the forward device port.
+
+        Args:
+            ports: A Sl4aPorts object or a tuple of (host_port,
+            forwarded_port, device_port).
+
+        Returns:
+            A tuple of (socket, socket_file_descriptor).
+        """
+        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        client_socket.settimeout(SOCKET_TIMEOUT)
+        client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        if ports.client_port != 0:
+            try:
+                client_socket.bind((socket.gethostname(), ports.client_port))
+            except OSError as e:
+                # If the port is in use, log and ask for any open port.
+                if e.errno == errno.EADDRINUSE:
+                    self.log.warning(
+                        'Port %s is already in use on the host. '
+                        'Generating a random port.' % ports.client_port)
+                    ports.client_port = 0
+                    return self._create_client_side_connection(ports)
+                raise
+
+        # Verify and obtain the port opened by SL4A.
+        try:
+            # Connect to the port that has been forwarded to the device.
+            client_socket.connect(('localhost', ports.forwarded_port))
+        except socket.timeout:
+            raise rpc_client.Sl4aConnectionError(
+                'SL4A has not connected over the specified port within the '
+                'timeout of %s seconds.' % SOCKET_TIMEOUT)
+        except socket.error as e:
+            # In extreme, unlikely cases, a socket error with
+            # errno.EADDRNOTAVAIL can be raised when a desired host_port is
+            # taken by a separate program between the bind and connect calls.
+            # Note that if host_port is set to zero, there is no bind before
+            # the connection is made, so this error will never be thrown.
+            if e.errno == errno.EADDRNOTAVAIL:
+                ports.client_port = 0
+                return self._create_client_side_connection(ports)
+            raise
+        ports.client_port = client_socket.getsockname()[1]
+        return client_socket, client_socket.makefile(mode='brw')
+
+    def terminate(self):
+        """Terminates the session.
+
+        The return of process execution is blocked on completion of all events
+        being processed by handlers in the Event Dispatcher.
+        """
+        with self._terminate_lock:
+            if not self._terminated:
+                self.log.debug('Terminating Session.')
+                self.rpc_client.closeSl4aSession()
+                # Must be set after closeSl4aSession so the rpc_client does not
+                # think the session has closed.
+                self._terminated = True
+                if self._event_dispatcher:
+                    self._event_dispatcher.close()
+                self.rpc_client.terminate()
diff --git a/acts/framework/acts/controllers/sl4a_types.py b/acts/framework/acts/controllers/sl4a_lib/sl4a_types.py
similarity index 100%
rename from acts/framework/acts/controllers/sl4a_types.py
rename to acts/framework/acts/controllers/sl4a_lib/sl4a_types.py
diff --git a/acts/framework/acts/libs/ota/ota_runners/ota_runner.py b/acts/framework/acts/libs/ota/ota_runners/ota_runner.py
index 776c900..45bc4c6 100644
--- a/acts/framework/acts/libs/ota/ota_runners/ota_runner.py
+++ b/acts/framework/acts/libs/ota/ota_runners/ota_runner.py
@@ -14,7 +14,6 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-import logging
 import time
 
 SL4A_SERVICE_SETUP_TIME = 5
@@ -33,23 +32,38 @@
         self.serial = self.android_device.serial
 
     def _update(self):
-        logging.info('Stopping services.')
+        log = self.android_device.log
+        old_info = self.android_device.adb.getprop('ro.build.fingerprint')
+        log.info('Starting Update. Beginning build info: %s', old_info)
+        log.info('Stopping services.')
         self.android_device.stop_services()
-        logging.info('Beginning tool.')
+        log.info('Beginning tool.')
         self.ota_tool.update(self)
-        logging.info('Tool finished. Waiting for boot completion.')
+        log.info('Tool finished. Waiting for boot completion.')
         self.android_device.wait_for_boot_completion()
-        logging.info('Boot completed. Rooting adb.')
+        new_info = self.android_device.adb.getprop('ro.build.fingerprint')
+        if not old_info or old_info == new_info:
+            raise OtaError('The device was not updated to a new build. '
+                           'Previous build: %s. New build: %s' % (old_info,
+                                                                  new_info))
+        log.info('Boot completed. Rooting adb.')
         self.android_device.root_adb()
-        logging.info('Root complete. Installing new SL4A.')
-        output = self.android_device.adb.install('-r %s' % self.get_sl4a_apk())
-        logging.info('SL4A install output: %s' % output)
-        time.sleep(SL4A_SERVICE_SETUP_TIME)
-        logging.info('Starting services.')
+        log.info('Root complete.')
+        if self.android_device.skip_sl4a:
+            self.android_device.log.info("Skipping SL4A install.")
+        else:
+            for _ in range(3):
+                self.android_device.log.info("Re-installing SL4A.")
+                self.android_device.adb.install(
+                    "-r -g %s" % self.get_sl4a_apk(), ignore_status=True)
+                time.sleep(SL4A_SERVICE_SETUP_TIME)
+                if self.android_device.is_sl4a_installed():
+                    break
+        log.info('Starting services.')
         self.android_device.start_services()
-        logging.info('Services started. Running ota tool cleanup.')
+        log.info('Services started. Running ota tool cleanup.')
         self.ota_tool.cleanup(self)
-        logging.info('Cleanup complete.')
+        log.info('Cleanup complete.')
 
     def can_update(self):
         """Whether or not an update package is available for the device."""
diff --git a/acts/framework/acts/libs/ota/ota_tools/adb_sideload_ota_tool.py b/acts/framework/acts/libs/ota/ota_tools/adb_sideload_ota_tool.py
index f94a762..df21bc5 100644
--- a/acts/framework/acts/libs/ota/ota_tools/adb_sideload_ota_tool.py
+++ b/acts/framework/acts/libs/ota/ota_tools/adb_sideload_ota_tool.py
@@ -40,10 +40,9 @@
         logging.info('Sideloading ota package')
         package_path = ota_runner.get_ota_package()
         logging.info('Running adb sideload with package "%s"' % package_path)
-        sideload_result = ota_runner.android_device.adb.sideload(
+        ota_runner.android_device.adb.sideload(
             package_path, timeout=PUSH_TIMEOUT)
-        logging.info('Sideload output: %s' % sideload_result)
         logging.info('Sideload complete. Waiting for device to come back up.')
         ota_runner.android_device.adb.wait_for_recovery()
-        ota_runner.android_device.adb.reboot()
+        ota_runner.android_device.reboot(stop_at_lock_screen=True)
         logging.info('Device is up. Update complete.')
diff --git a/acts/framework/acts/libs/ota/ota_tools/update_device_ota_tool.py b/acts/framework/acts/libs/ota/ota_tools/update_device_ota_tool.py
index 978842f..c9c9dd7 100644
--- a/acts/framework/acts/libs/ota/ota_tools/update_device_ota_tool.py
+++ b/acts/framework/acts/libs/ota/ota_tools/update_device_ota_tool.py
@@ -50,7 +50,7 @@
         logging.info('Output: %s' % result.stdout)
 
         logging.info('Rebooting device for update to go live.')
-        ota_runner.android_device.adb.reboot()
+        ota_runner.android_device.reboot(stop_at_lock_screen=True)
         logging.info('Reboot sent.')
 
     def __del__(self):
diff --git a/acts/framework/acts/logger.py b/acts/framework/acts/logger.py
index 5d7fed9..92f1e5e 100755
--- a/acts/framework/acts/logger.py
+++ b/acts/framework/acts/logger.py
@@ -22,12 +22,13 @@
 import re
 import sys
 
+from acts import tracelogger
 from acts.utils import create_dir
 
 log_line_format = "%(asctime)s.%(msecs).03d %(levelname)s %(message)s"
 # The micro seconds are added by the format string above,
 # so the time format does not include ms.
-log_line_time_format = "%m-%d %H:%M:%S"
+log_line_time_format = "%Y-%m-%d %H:%M:%S"
 log_line_timestamp_len = 18
 
 logline_timestamp_re = re.compile("\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d")
@@ -44,10 +45,10 @@
         minute, second, microsecond.
     """
     date, time = t.split(' ')
-    month, day = date.split('-')
+    year, month, day = date.split('-')
     h, m, s = time.split(':')
     s, ms = s.split('.')
-    return (month, day, h, m, s, ms)
+    return year, month, day, h, m, s, ms
 
 
 def is_valid_logline_timestamp(timestamp):
@@ -86,7 +87,7 @@
 
 def epoch_to_log_line_timestamp(epoch_time):
     """Converts an epoch timestamp in ms to log line timestamp format, which
-    is readible for humans.
+    is readable for humans.
 
     Args:
         epoch_time: integer, an epoch timestamp in ms.
@@ -97,7 +98,7 @@
     """
     s, ms = divmod(epoch_time, 1000)
     d = datetime.datetime.fromtimestamp(s)
-    return d.strftime("%m-%d %H:%M:%S.") + str(ms)
+    return d.strftime("%Y-%m-%d %H:%M:%S.") + str(ms)
 
 
 def get_log_line_timestamp(delta=None):
@@ -112,7 +113,7 @@
     Returns:
         A timestamp in log line format with an offset.
     """
-    return _get_timestamp("%m-%d %H:%M:%S.%f", delta)
+    return _get_timestamp("%Y-%m-%d %H:%M:%S.%f", delta)
 
 
 def get_log_file_timestamp(delta=None):
@@ -125,9 +126,9 @@
         delta: Number of seconds to offset from current time; can be negative.
 
     Returns:
-        A timestamp in log filen name format with an offset.
+        A timestamp in log file name format with an offset.
     """
-    return _get_timestamp("%m-%d-%Y_%H-%M-%S-%f", delta)
+    return _get_timestamp("%Y-%m-%d-%Y_%H-%M-%S-%f", delta)
 
 
 def _setup_test_logger(log_path, prefix=None, filename=None):
@@ -167,7 +168,8 @@
     fh_info = logging.FileHandler(os.path.join(log_path, 'test_run_info.txt'))
     fh_info.setFormatter(f_formatter)
     fh_info.setLevel(logging.INFO)
-    fh_error = logging.FileHandler(os.path.join(log_path, 'test_run_error.txt'))
+    fh_error = logging.FileHandler(
+        os.path.join(log_path, 'test_run_error.txt'))
     fh_error.setFormatter(f_formatter)
     fh_error.setLevel(logging.WARNING)
     log.addHandler(ch)
@@ -233,3 +235,23 @@
     norm_tp = norm_tp.replace(':', '-')
     return norm_tp
 
+
+class LoggerAdapter(logging.LoggerAdapter):
+    """A LoggerAdapter class that takes in a lambda for transforming logs."""
+
+    def __init__(self, logging_lambda):
+        self.logging_lambda = logging_lambda
+        super(LoggerAdapter, self).__init__(logging.getLogger(), {})
+
+    def process(self, msg, kwargs):
+        return self.logging_lambda(msg), kwargs
+
+
+def create_logger(logging_lambda=lambda message: message):
+    """Returns a logger with logging defined by a given lambda.
+
+    Args:
+        logging_lambda: A lambda of the form:
+            >>> lambda log_message: return 'string'
+    """
+    return tracelogger.TraceLogger(LoggerAdapter(logging_lambda))
diff --git a/acts/framework/acts/records.py b/acts/framework/acts/records.py
index efff674..a36ccb3 100644
--- a/acts/framework/acts/records.py
+++ b/acts/framework/acts/records.py
@@ -41,7 +41,7 @@
     RECORD_RESULT = "Result"
     RECORD_UID = "UID"
     RECORD_EXTRAS = "Extras"
-    RECORD_EXTRA_ERRORS = "Extra Errors"
+    RECORD_ADDITIONAL_ERRORS = "Extra Errors"
     RECORD_DETAILS = "Details"
     TEST_RESULT_PASS = "PASS"
     TEST_RESULT_FAIL = "FAIL"
@@ -74,7 +74,7 @@
         self.result = None
         self.extras = None
         self.details = None
-        self.extra_errors = {}
+        self.additional_errors = {}
 
     def test_begin(self):
         """Call this when the test case it records begins execution.
@@ -97,7 +97,7 @@
         self.end_time = utils.get_current_epoch_time()
         self.log_end_time = logger.epoch_to_log_line_timestamp(self.end_time)
         self.result = result
-        if self.extra_errors:
+        if self.additional_errors:
             self.result = TestResultEnums.TEST_RESULT_UNKNOWN
         if isinstance(e, signals.TestSignal):
             self.details = e.details
@@ -161,7 +161,7 @@
             e: An exception object.
         """
         self.result = TestResultEnums.TEST_RESULT_UNKNOWN
-        self.extra_errors[tag] = str(e)
+        self.additional_errors[tag] = str(e)
 
     def __str__(self):
         d = self.to_dict()
@@ -191,7 +191,7 @@
         d[TestResultEnums.RECORD_UID] = self.uid
         d[TestResultEnums.RECORD_EXTRAS] = self.extras
         d[TestResultEnums.RECORD_DETAILS] = self.details
-        d[TestResultEnums.RECORD_EXTRA_ERRORS] = self.extra_errors
+        d[TestResultEnums.RECORD_ADDITIONAL_ERRORS] = self.additional_errors
         return d
 
     def json_str(self):
@@ -235,6 +235,8 @@
         self.blocked = []
         self.unknown = []
         self.controller_info = {}
+        self.post_run_data = {}
+        self.extras = {}
 
     def __add__(self, r):
         """Overrides '+' operator for TestResult class.
@@ -275,6 +277,15 @@
             return
         self.controller_info[name] = info
 
+    def set_extra_data(self, name, info):
+        try:
+            json.dumps(info)
+        except TypeError:
+            logging.warning("Controller info for %s is not JSON serializable! "
+                            "Coercing it to string." % name)
+            info = str(info)
+        self.extras[name] = info
+
     def add_record(self, record):
         """Adds a test record to test result.
 
@@ -297,16 +308,6 @@
             self.executed.append(record)
             self.unknown.append(record)
 
-    def add_controller_info(self, name, info):
-        try:
-            json.dumps(info)
-        except TypeError:
-            logging.warning(("Controller info for %s is not JSON serializable!"
-                             " Coercing it to string.") % name)
-            self.controller_info[name] = str(info)
-            return
-        self.controller_info[name] = info
-
     @property
     def is_all_pass(self):
         """True if no tests failed or threw errors, False otherwise."""
@@ -335,6 +336,7 @@
         d["ControllerInfo"] = self.controller_info
         d["Results"] = [record.to_dict() for record in self.executed]
         d["Summary"] = self.summary_dict()
+        d["Extras"] = self.extras
         json_str = json.dumps(d, indent=4, sort_keys=True)
         return json_str
 
diff --git a/acts/framework/acts/test_runner.py b/acts/framework/acts/test_runner.py
index aa81976..70b3400 100644
--- a/acts/framework/acts/test_runner.py
+++ b/acts/framework/acts/test_runner.py
@@ -15,6 +15,7 @@
 #   limitations under the License.
 
 from future import standard_library
+
 standard_library.install_aliases()
 
 import argparse
@@ -176,8 +177,6 @@
         self.log: The logger object used throughout this test run.
         self.controller_registry: A dictionary that holds the controller
                                   objects used in a test run.
-        self.controller_destructors: A dictionary that holds the controller
-                                     distructors. Keys are controllers' names.
         self.test_classes: A dictionary where we can look up the test classes
                            by name to instantiate.
         self.run_list: A list of tuples specifying what tests to run.
@@ -203,7 +202,6 @@
         logger.setup_test_logger(self.log_path, self.testbed_name)
         self.log = logging.getLogger()
         self.controller_registry = {}
-        self.controller_destructors = {}
         if self.test_configs.get(keys.Config.key_random.value):
             test_case_iterations = self.test_configs.get(
                 keys.Config.key_test_case_iterations.value, 10)
@@ -277,9 +275,6 @@
         and import the corresponding controller module from acts.controllers
         package.
 
-        TODO(angli): Remove this when all scripts change to explicitly declare
-                     controller dependency.
-
         Returns:
             A list of controller modules.
         """
@@ -318,7 +313,28 @@
                     "Controller interface %s in %s cannot be null." %
                     (attr, module.__name__))
 
-    def register_controller(self, module, required=True):
+    @staticmethod
+    def get_module_reference_name(a_module):
+        """Returns the module's reference name.
+
+        This is largely for backwards compatibility with log parsing. If the
+        module defines ACTS_CONTROLLER_REFERENCE_NAME, it will return that
+        value, or the module's submodule name.
+
+        Args:
+            a_module: Any module. Ideally, a controller module.
+        Returns:
+            A string corresponding to the module's name.
+        """
+        if hasattr(a_module, 'ACTS_CONTROLLER_REFERENCE_NAME'):
+            return a_module.ACTS_CONTROLLER_REFERENCE_NAME
+        else:
+            return a_module.__name__.split('.')[-1]
+
+    def register_controller(self,
+                            controller_module,
+                            required=True,
+                            builtin=False):
         """Registers an ACTS controller module for a test run.
 
         An ACTS controller module is a Python lib that can be used to control
@@ -355,18 +371,27 @@
                     A list of json serializable objects, each represents the
                     info of a controller object. The order of the info object
                     should follow that of the input objects.
-
+            def get_post_job_info(controller_list):
+                [Optional] Returns information about the controller after the
+                test has run. This info is sent to test_run_summary.json's
+                "Extras" key.
+                Args:
+                    The list of controller objects created by the module
+                Returns:
+                    A (name, data) tuple.
         Registering a controller module declares a test class's dependency the
         controller. If the module config exists and the module matches the
         controller interface, controller objects will be instantiated with
         corresponding configs. The module should be imported first.
 
         Args:
-            module: A module that follows the controller module interface.
+            controller_module: A module that follows the controller module
+                interface.
             required: A bool. If True, failing to register the specified
-                      controller module raises exceptions. If False, returns
-                      None upon failures.
-
+                controller module raises exceptions. If False, returns None upon
+                failures.
+            builtin: Specifies that the module is a builtin controller module in
+                ACTS. If true, adds itself to test_run_info.
         Returns:
             A list of controller objects instantiated from controller_module, or
             None.
@@ -378,51 +403,45 @@
             the controller module has already been registered or any other error
             occurred in the registration process.
         """
-        TestRunner.verify_controller_module(module)
-        try:
-            # If this is a builtin controller module, use the default ref name.
-            module_ref_name = module.ACTS_CONTROLLER_REFERENCE_NAME
-            builtin = True
-        except AttributeError:
-            # Or use the module's name
-            builtin = False
-            module_ref_name = module.__name__.split('.')[-1]
-        if module_ref_name in self.controller_registry:
+        TestRunner.verify_controller_module(controller_module)
+        module_ref_name = self.get_module_reference_name(controller_module)
+
+        if controller_module in self.controller_registry:
             raise signals.ControllerError(
-                ("Controller module %s has already been registered. It can not"
-                 " be registered again.") % module_ref_name)
+                "Controller module %s has already been registered. It can not "
+                "be registered again." % module_ref_name)
         # Create controller objects.
-        create = module.create
-        module_config_name = module.ACTS_CONTROLLER_CONFIG_NAME
+        module_config_name = controller_module.ACTS_CONTROLLER_CONFIG_NAME
         if module_config_name not in self.testbed_configs:
             if required:
                 raise signals.ControllerError(
                     "No corresponding config found for %s" %
                     module_config_name)
-            self.log.warning(
-                "No corresponding config found for optional controller %s",
-                module_config_name)
+            else:
+                self.log.warning(
+                    "No corresponding config found for optional controller %s",
+                    module_config_name)
             return None
         try:
             # Make a deep copy of the config to pass to the controller module,
             # in case the controller module modifies the config internally.
             original_config = self.testbed_configs[module_config_name]
             controller_config = copy.deepcopy(original_config)
-            objects = create(controller_config)
+            controllers = controller_module.create(controller_config)
         except:
             self.log.exception(
                 "Failed to initialize objects for controller %s, abort!",
                 module_config_name)
             raise
-        if not isinstance(objects, list):
+        if not isinstance(controllers, list):
             raise signals.ControllerError(
                 "Controller module %s did not return a list of objects, abort."
                 % module_ref_name)
-        self.controller_registry[module_ref_name] = objects
+        self.controller_registry[controller_module] = controllers
         # Collect controller information and write to test result.
         # Implementation of "get_info" is optional for a controller module.
-        if hasattr(module, "get_info"):
-            controller_info = module.get_info(objects)
+        if hasattr(controller_module, "get_info"):
+            controller_info = controller_module.get_info(controllers)
             self.log.info("Controller %s: %s", module_config_name,
                           controller_info)
             self.results.add_controller_info(module_config_name,
@@ -430,29 +449,32 @@
         else:
             self.log.warning("No controller info obtained for %s",
                              module_config_name)
+
         # TODO(angli): After all tests move to register_controller, stop
         # tracking controller objs in test_run_info.
         if builtin:
-            self.test_run_info[module_ref_name] = objects
-        self.log.debug("Found %d objects for controller %s",
-                       len(objects), module_config_name)
-        destroy_func = module.destroy
-        self.controller_destructors[module_ref_name] = destroy_func
-        return objects
+            self.test_run_info[module_ref_name] = controllers
+        self.log.debug("Found %d objects for controller %s", len(controllers),
+                       module_config_name)
+        return controllers
 
     def unregister_controllers(self):
         """Destroy controller objects and clear internal registry.
 
         This will be called at the end of each TestRunner.run call.
         """
-        for name, destroy in self.controller_destructors.items():
+        for controller_module, controllers in self.controller_registry.items():
+            name = self.get_module_reference_name(controller_module)
+            if hasattr(controller_module, 'get_post_job_info'):
+                self.log.debug('Getting post job info for %s', name)
+                name, value = controller_module.get_post_job_info(controllers)
+                self.results.set_extra_data(name, value)
             try:
-                self.log.debug("Destroying %s.", name)
-                destroy(self.controller_registry[name])
+                self.log.debug('Destroying %s.', name)
+                controller_module.destroy(controllers)
             except:
                 self.log.exception("Exception occurred destroying %s.", name)
         self.controller_registry = {}
-        self.controller_destructors = {}
 
     def parse_config(self, test_configs):
         """Parses the test configuration and unpacks objects and parameters
@@ -519,7 +541,8 @@
             test_cls = self.test_classes[test_cls_name]
         except KeyError:
             self.log.info(
-                "Cannot find test class %s skipping for now." % test_cls_name)
+                "Cannot find test class %s or classes matching pattern, "
+                "skipping for now." % test_cls_name)
             record = records.TestResultRecord("*all*", test_cls_name)
             record.test_skip(signals.TestSkip("Test class does not exist."))
             self.results.add_record(record)
@@ -536,6 +559,7 @@
                 cls_result = test_cls_instance.run(test_cases,
                                                    test_case_iterations)
                 self.results += cls_result
+                self._write_results_json_str()
             except signals.TestAbortAll as e:
                 self.results += e.results
                 raise e
@@ -544,7 +568,7 @@
         """Executes test cases.
 
         This will instantiate controller and test classes, and execute test
-        classes. This can be called multiple times to repeatly execute the
+        classes. This can be called multiple times to repeatedly execute the
         requested test cases.
 
         A call to TestRunner.stop should eventually happen to conclude the life
@@ -578,7 +602,7 @@
                 # Import and register the built-in controller modules specified
                 # in testbed config.
                 for module in self._import_builtin_controllers():
-                    self.register_controller(module)
+                    self.register_controller(module, builtin=True)
                 self.run_test_class(test_cls_name, test_case_names)
             except signals.TestAbortAll as e:
                 self.log.warning(
diff --git a/acts/framework/acts/test_utils/bt/BluetoothCarHfpBaseTest.py b/acts/framework/acts/test_utils/bt/BluetoothCarHfpBaseTest.py
index 9a6b2b4..4c0e929 100644
--- a/acts/framework/acts/test_utils/bt/BluetoothCarHfpBaseTest.py
+++ b/acts/framework/acts/test_utils/bt/BluetoothCarHfpBaseTest.py
@@ -22,6 +22,7 @@
 import time
 from queue import Empty
 
+from acts.keys import Config
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.bt_test_utils import pair_pri_to_sec
 from acts.test_utils.tel.tel_test_utils import ensure_phones_default_state
@@ -59,7 +60,7 @@
         if not "sim_conf_file" in self.user_params.keys():
             self.log.error("Missing mandatory user config \"sim_conf_file\"!")
             return False
-        sim_conf_file = self.user_params["sim_conf_file"]
+        sim_conf_file = self.user_params["sim_conf_file"][0]
         if not os.path.isfile(sim_conf_file):
             sim_conf_file = os.path.join(
                 self.user_params[Config.key_config_path], sim_conf_file)
diff --git a/acts/framework/acts/test_utils/bt/BtFunhausBaseTest.py b/acts/framework/acts/test_utils/bt/BtFunhausBaseTest.py
index 85fdba3..5fb302a 100644
--- a/acts/framework/acts/test_utils/bt/BtFunhausBaseTest.py
+++ b/acts/framework/acts/test_utils/bt/BtFunhausBaseTest.py
@@ -119,7 +119,7 @@
         :return: None
         """
         self.ad.droid.mediaPlayOpen("file:///sdcard/Music/{}".format(
-            self.music_file_to_play))
+            self.music_file_to_play.split("/")[-1]))
         self.ad.droid.mediaPlaySetLooping(True)
         self.ad.log.info("Music is now playing.")
 
diff --git a/acts/framework/acts/test_utils/bt/BtMetricsBaseTest.py b/acts/framework/acts/test_utils/bt/BtMetricsBaseTest.py
index 66acc84..3964ca4 100644
--- a/acts/framework/acts/test_utils/bt/BtMetricsBaseTest.py
+++ b/acts/framework/acts/test_utils/bt/BtMetricsBaseTest.py
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 import os
+import time
 
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.libs.proto.proto_utils import compile_import_proto, parse_proto_to_ascii
diff --git a/acts/framework/acts/test_utils/bt/bt_test_utils.py b/acts/framework/acts/test_utils/bt/bt_test_utils.py
index d2f3553..5658408 100644
--- a/acts/framework/acts/test_utils/bt/bt_test_utils.py
+++ b/acts/framework/acts/test_utils/bt/bt_test_utils.py
@@ -51,6 +51,7 @@
 from acts.test_utils.bt.bt_constants import bt_default_timeout
 from acts.test_utils.bt.bt_constants import bt_discovery_timeout
 from acts.test_utils.bt.bt_constants import bt_profile_states
+from acts.test_utils.bt.bt_constants import bt_profile_constants
 from acts.test_utils.bt.bt_constants import bt_rfcomm_uuids
 from acts.test_utils.bt.bt_constants import bt_scan_mode_types
 from acts.test_utils.bt.bt_constants import btsnoop_last_log_path_on_device
@@ -545,6 +546,7 @@
     if droid.bluetoothCheckState() is True:
         droid.bluetoothToggleState(False)
         if droid.bluetoothCheckState() is True:
+            log.error("Failed to toggle Bluetooth off.")
             return False
     return True
 
@@ -819,7 +821,7 @@
         True of connection is successful, false if unsuccessful.
     """
     # Check if we support all profiles.
-    supported_profiles = [i.value for i in BluetoothProfile]
+    supported_profiles = bt_profile_constants.values()
     for profile in profiles_set:
         if profile not in supported_profiles:
             pri_ad.log.info("Profile {} is not supported list {}".format(
@@ -911,7 +913,7 @@
         False on Failure
     """
     # Sanity check to see if all the profiles in the given set is supported
-    supported_profiles = [i.value for i in BluetoothProfile]
+    supported_profiles = bt_profile_constants.values()
     for profile in profiles_list:
         if profile not in supported_profiles:
             pri_ad.log.info("Profile {} is not in supported list {}".format(
@@ -1257,3 +1259,4 @@
     if addr in {d['address'] for d in devices}:
         return True
     return False
+
diff --git a/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py b/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
index 5f3fe9d..e8e6ad1 100644
--- a/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
+++ b/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
@@ -35,13 +35,16 @@
     initial_set_up_for_subid_infomation
 from acts.test_utils.tel.tel_test_utils import abort_all_tests
 from acts.test_utils.tel.tel_test_utils import check_qxdm_logger_always_on
+from acts.test_utils.tel.tel_test_utils import is_sim_locked
 from acts.test_utils.tel.tel_test_utils import ensure_phones_default_state
 from acts.test_utils.tel.tel_test_utils import ensure_phones_idle
+from acts.test_utils.tel.tel_test_utils import print_radio_info
 from acts.test_utils.tel.tel_test_utils import refresh_droid_config
 from acts.test_utils.tel.tel_test_utils import setup_droid_properties
 from acts.test_utils.tel.tel_test_utils import set_phone_screen_on
 from acts.test_utils.tel.tel_test_utils import set_phone_silent_mode
 from acts.test_utils.tel.tel_test_utils import set_qxdm_logger_always_on
+from acts.test_utils.tel.tel_test_utils import unlock_sim
 from acts.test_utils.tel.tel_defines import PRECISE_CALL_STATE_LISTEN_LEVEL_BACKGROUND
 from acts.test_utils.tel.tel_defines import PRECISE_CALL_STATE_LISTEN_LEVEL_FOREGROUND
 from acts.test_utils.tel.tel_defines import PRECISE_CALL_STATE_LISTEN_LEVEL_RINGING
@@ -59,6 +62,7 @@
         self.logger_sessions = []
 
         for ad in self.android_devices:
+            ad.qxdm_log = True
             if getattr(ad, "qxdm_always_on", False):
                 #this is only supported on 2017 devices
                 ad.log.info("qxdm_always_on is set in config file")
@@ -68,21 +72,9 @@
                     set_qxdm_logger_always_on(ad, mask)
                 else:
                     ad.log.info("qxdm always on is already set")
-
-            #The puk and pin should be provided in testbed config file.
-            #"AndroidDevice": [{"serial": "84B5T15A29018214",
-            #                   "adb_logcat_param": "-b all",
-            #                   "puk": "12345678",
-            #                   "puk_pin": "1234"}]
-            if hasattr(ad, 'puk'):
-                if not hasattr(ad, 'puk_pin'):
-                    abort_all_tests(ad.log, "puk_pin is not provided")
-                ad.log.info("Enter PUK code and pin")
-                if not ad.droid.telephonySupplyPuk(ad.puk, ad.puk_pin):
-                    abort_all_tests(
-                        ad.log,
-                        "Puk and puk_pin provided in testbed config do NOT work"
-                    )
+            print_radio_info(ad)
+            if not unlock_sim(ad):
+                abort_all_tests(ad.log, "unable to unlock SIM")
 
         self.skip_reset_between_cases = self.user_params.get(
             "skip_reset_between_cases", True)
@@ -97,17 +89,21 @@
             self.test_id = test_id
             log_string = "[Test ID] %s" % test_id
             self.log.info(log_string)
+            no_crash = True
             try:
                 for ad in self.android_devices:
-                    ad.droid.logI("Started %s" % log_string)
+                    if getattr(ad, "droid"):
+                        ad.droid.logI("Started %s" % log_string)
                 # TODO: b/19002120 start QXDM Logging
                 result = fn(self, *args, **kwargs)
                 for ad in self.android_devices:
-                    ad.droid.logI("Finished %s" % log_string)
+                    if getattr(ad, "droid"):
+                        ad.droid.logI("Finished %s" % log_string)
                     new_crash = ad.check_crash_report(self.test_name,
                                                       self.begin_time, result)
-                    if new_crash:
+                    if self.user_params.get("check_crash", True) and new_crash:
                         ad.log.error("Find new crash reports %s", new_crash)
+                        no_crash = False
                 if not result and self.user_params.get("telephony_auto_rerun"):
                     self.teardown_test()
                     # re-run only once, if re-run pass, mark as pass
@@ -115,7 +111,8 @@
                     self.log.info(log_string)
                     self.setup_test()
                     for ad in self.android_devices:
-                        ad.droid.logI("Rerun Started %s" % log_string)
+                        if getattr(ad, "droid"):
+                            ad.droid.logI("Rerun Started %s" % log_string)
                     result = fn(self, *args, **kwargs)
                     if result is True:
                         self.log.info("Rerun passed.")
@@ -128,7 +125,7 @@
                         # still be considered a failure for reporting purposes.
                         self.log.info("Rerun indeterminate.")
                         result = False
-                return result
+                return result and no_crash
             except (TestSignal, TestAbortClass, TestAbortAll):
                 raise
             except Exception as e:
@@ -149,6 +146,8 @@
         if not sim_conf_file:
             self.log.info("\"sim_conf_file\" is not provided test bed config!")
         else:
+            if isinstance(sim_conf_file, list):
+                sim_conf_file = sim_conf_file[0]
             # If the sim_conf_file is not a full path, attempt to find it
             # relative to the config file.
             if not os.path.isfile(sim_conf_file):
@@ -157,7 +156,6 @@
                 if not os.path.isfile(sim_conf_file):
                     self.log.error("Unable to load user config %s ",
                                    sim_conf_file)
-                    return False
 
         setattr(self, "diag_logger",
                 self.register_controller(
@@ -180,13 +178,6 @@
                 ad.adb.shell("am start --ei EXTRA_LAUNCH_CARRIER_APP 0 -n "
                              "\"com.google.android.wfcactivation/"
                              ".VzwEmergencyAddressActivity\"")
-            # Start telephony monitor
-            if not ad.is_apk_running("com.google.telephonymonitor"):
-                ad.log.info("TelephonyMonitor is not running, start it now")
-                ad.adb.shell(
-                    'am broadcast -a '
-                    'com.google.gservices.intent.action.GSERVICES_OVERRIDE -e '
-                    '"ce.telephony_monitor_enable" "true"')
             # Sub ID setup
             initial_set_up_for_subid_infomation(self.log, ad)
             if "enable_wifi_verbose_logging" in self.user_params:
@@ -198,8 +189,6 @@
             # Set chrome browser start with no-first-run verification and
             # disable-fre. Give permission to read from and write to storage.
             for cmd in (
-                    "am start -n com.google.android.setupwizard/."
-                    "SetupWizardExitActivity",
                     "pm disable com.android.cellbroadcastreceiver",
                     "pm grant com.android.chrome "
                     "android.permission.READ_EXTERNAL_STORAGE",
@@ -211,6 +200,27 @@
                     '--disable-fre" > /data/local/tmp/chrome-command-line'):
                 ad.adb.shell(cmd)
 
+            # Curl for 2016/7 devices
+            try:
+                if int(ad.adb.getprop("ro.product.first_api_level")) >= 25:
+                    out = ad.adb.shell("/data/curl --version")
+                    if not out or "not found" in out:
+                        tel_data = self.user_params.get("tel_data", "tel_data")
+                        if isinstance(tel_data, list):
+                            tel_data = tel_data[0]
+                        curl_file_path = os.path.join(tel_data, "curl")
+                        if not os.path.isfile(curl_file_path):
+                            curl_file_path = os.path.join(
+                                self.user_params[Config.key_config_path],
+                                curl_file_path)
+                        if os.path.isfile(curl_file_path):
+                            ad.log.info("Pushing Curl to /data dir")
+                            ad.adb.push("%s /data" % (curl_file_path))
+                            ad.adb.shell(
+                                "chmod 777 /data/curl", ignore_status=True)
+            except Exception:
+                ad.log.info("Failed to push curl on this device")
+
             # Ensure that a test class starts from a consistent state that
             # improves chances of valid network selection and facilitates
             # logging.
@@ -236,6 +246,7 @@
     def teardown_class(self):
         try:
             for ad in self.android_devices:
+                ad.droid.disableDevicePassword()
                 if "enable_wifi_verbose_logging" in self.user_params:
                     ad.droid.wifiEnableVerboseLogging(
                         WIFI_VERBOSE_LOGGING_DISABLED)
@@ -251,7 +262,8 @@
 
         if self.skip_reset_between_cases:
             ensure_phones_idle(self.log, self.android_devices)
-        ensure_phones_default_state(self.log, self.android_devices)
+        else:
+            ensure_phones_default_state(self.log, self.android_devices)
 
     def teardown_test(self):
         return True
diff --git a/acts/framework/acts/test_utils/tel/anritsu_utils.py b/acts/framework/acts/test_utils/tel/anritsu_utils.py
index 5a30895..1755198 100644
--- a/acts/framework/acts/test_utils/tel/anritsu_utils.py
+++ b/acts/framework/acts/test_utils/tel/anritsu_utils.py
@@ -33,6 +33,7 @@
 from acts.controllers.anritsu_lib.md8475a import TestPowerControl
 from acts.controllers.anritsu_lib.md8475a import TestMeasurement
 from acts.controllers.anritsu_lib.md8475a import Switch
+from acts.controllers.anritsu_lib.md8475a import BtsPacketRate
 from acts.test_utils.tel.tel_defines import CALL_TEARDOWN_PHONE
 from acts.test_utils.tel.tel_defines import CALL_TEARDOWN_REMOTE
 from acts.test_utils.tel.tel_defines import MAX_WAIT_TIME_CALL_DROP
@@ -126,9 +127,11 @@
 
 # Default Cell Parameters
 DEFAULT_OUTPUT_LEVEL = -30
-DEFAULT_INPUT_LEVEL = 10  # apply to LTE & WCDMA only
-DEFAULT_LTE_BAND = 2
+# apply to LTE & WCDMA only to reduce UE transmit power if path loss
+DEFAULT_INPUT_LEVEL = -10
+DEFAULT_LTE_BAND = [2, 4]
 DEFAULT_WCDMA_BAND = 1
+DEFAULT_WCDMA_PACKET_RATE = BtsPacketRate.WCDMA_DLHSAUTO_REL7_ULHSAUTO
 DEFAULT_GSM_BAND = GSM_BAND_GSM850
 DEFAULT_CDMA1X_BAND = 0
 DEFAULT_CDMA1X_CH = 356
@@ -331,6 +334,7 @@
     bts.lac = get_wcdma_lac(user_params, cell_no)
     bts.output_level = DEFAULT_OUTPUT_LEVEL
     bts.input_level = DEFAULT_INPUT_LEVEL
+    bts.packet_rate = DEFAULT_WCDMA_PACKET_RATE
 
 
 def _init_gsm_bts(bts, user_params, cell_no, sim_card):
@@ -583,6 +587,34 @@
     return [lte_bts, cdma1x_bts]
 
 
+def set_system_model_lte_evdo(anritsu_handle, user_params, sim_card):
+    """ Configures Anritsu system for LTE and EVDO simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Lte and 1x BTS objects
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.LTE, BtsTechnology.EVDO)
+    # setting BTS parameters
+    lte_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    evdo_bts = anritsu_handle.get_BTS(BtsNumber.BTS2)
+    _init_lte_bts(lte_bts, user_params, CELL_1, sim_card)
+    _init_evdo_bts(evdo_bts, user_params, CELL_2, sim_card)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    pdn2 = anritsu_handle.get_PDN(PDN_NO_2)
+    pdn3 = anritsu_handle.get_PDN(PDN_NO_3)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDR_1, UE_IPV6_ADDR_1, True)
+    _init_PDN(anritsu_handle, pdn2, UE_IPV4_ADDR_2, UE_IPV6_ADDR_2, False)
+    _init_PDN(anritsu_handle, pdn3, UE_IPV4_ADDR_3, UE_IPV6_ADDR_3, True)
+    vnid1 = anritsu_handle.get_IMS(DEFAULT_VNID)
+    _init_IMS(anritsu_handle, vnid1)
+    return [lte_bts, evdo_bts]
+
+
 def set_system_model_wcdma_gsm(anritsu_handle, user_params, sim_card):
     """ Configures Anritsu system for WCDMA and GSM simulation
 
@@ -893,6 +925,242 @@
             log.error(str(e))
 
 
+def handover_tc(log,
+                anritsu_handle,
+                wait_time=0,
+                timeout=60,
+                s_bts=BtsNumber.BTS1,
+                t_bts=BtsNumber.BTS2):
+    """ Setup and perform a handover test case in MD8475A
+
+    Args:
+        anritsu_handle: Anritsu object.
+        s_bts: Serving (originating) BTS
+        t_bts: Target (destination) BTS
+        wait_time: time to wait before handover
+
+    Returns:
+        True for success False for failure
+    """
+    log.info("Starting HO test case procedure")
+    time.sleep(wait_time)
+    ho_tc = anritsu_handle.get_AnritsuTestCases()
+    ho_tc.procedure = TestProcedure.PROCEDURE_HO
+    ho_tc.bts_direction = (s_bts, t_bts)
+    ho_tc.power_control = TestPowerControl.POWER_CONTROL_DISABLE
+    ho_tc.measurement_LTE = TestMeasurement.MEASUREMENT_DISABLE
+    anritsu_handle.start_testcase()
+    status = anritsu_handle.get_testcase_status()
+    timer = 0
+    while status == "0":
+        time.sleep(1)
+        status = anritsu_handle.get_testcase_status()
+        timer += 1
+        if timer > timeout:
+            return "Handover Test Case time out in {} sec!".format(timeout)
+    return status
+
+
+def make_ims_call(log,
+                  ad,
+                  anritsu_handle,
+                  callee_number,
+                  is_emergency=False,
+                  check_ims_reg=True,
+                  check_ims_calling=True,
+                  mo=True,
+                  ims_virtual_network_id=DEFAULT_IMS_VIRTUAL_NETWORK_ID):
+    """ Makes a MO call after IMS registred
+
+    Args:
+        ad: Android device object.
+        anritsu_handle: Anritsu object.
+        callee_number: Number to be called.
+        check_ims_reg: check if Anritsu cscf server state is "SIPIDLE".
+        check_ims_calling: check if Anritsu cscf server state is "CALLING".
+        mo: Mobile originated call
+        ims_virtual_network_id: ims virtual network id.
+
+    Returns:
+        True for success False for failure
+    """
+
+    try:
+        # confirm ims registration
+        if check_ims_reg:
+            if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                            ims_virtual_network_id,
+                                            ImsCscfStatus.SIPIDLE.value):
+                raise _CallSequenceException("IMS/CSCF status is not idle.")
+        if mo:  # make MO call
+            log.info("Making Call to " + callee_number)
+            if not initiate_call(log, ad, callee_number, is_emergency):
+                raise _CallSequenceException("Initiate call failed.")
+            if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                            ims_virtual_network_id,
+                                            ImsCscfStatus.CALLING.value):
+                raise _CallSequenceException(
+                    "Phone IMS status is not calling.")
+        else:  # make MT call
+            log.info("Making IMS Call to UE from MD8475A...")
+            anritsu_handle.ims_cscf_call_action(ims_virtual_network_id,
+                                                ImsCscfCall.MAKE.value)
+            if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                            ims_virtual_network_id,
+                                            ImsCscfStatus.RINGING.value):
+                raise _CallSequenceException(
+                    "Phone IMS status is not ringing.")
+            # answer the call on the UE
+            if not wait_and_answer_call(log, ad):
+                raise _CallSequenceException("UE Answer call Fail")
+
+        if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                        ims_virtual_network_id,
+                                        ImsCscfStatus.CONNECTED.value):
+            raise _CallSequenceException(
+                "MD8475A IMS status is not connected.")
+        return True
+
+    except _CallSequenceException as e:
+        log.error(e)
+        return False
+
+
+def tear_down_call(log,
+                   ad,
+                   anritsu_handle,
+                   ims_virtual_network_id=DEFAULT_IMS_VIRTUAL_NETWORK_ID):
+    """ Check and End a VoLTE call
+
+    Args:
+        ad: Android device object.
+        anritsu_handle: Anritsu object.
+        ims_virtual_network_id: ims virtual network id.
+
+    Returns:
+        True for success False for failure
+    """
+    try:
+        # end the call from phone
+        log.info("Disconnecting the call from DUT")
+        if not hangup_call(log, ad):
+            raise _CallSequenceException("Error in Hanging-Up Call on DUT.")
+        # confirm if CSCF status is back to idle
+        if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                        ims_virtual_network_id,
+                                        ImsCscfStatus.SIPIDLE.value):
+            raise _CallSequenceException("IMS/CSCF status is not idle.")
+        return True
+
+    except _CallSequenceException as e:
+        log.error(e)
+        return False
+    finally:
+        try:
+            if ad.droid.telecomIsInCall():
+                ad.droid.telecomEndCall()
+        except Exception as e:
+            log.error(str(e))
+
+
+# This procedure is for VoLTE mobility test cases
+def ims_call_ho(log,
+                ad,
+                anritsu_handle,
+                callee_number,
+                is_emergency=False,
+                check_ims_reg=True,
+                check_ims_calling=True,
+                mo=True,
+                wait_time_in_volte=WAIT_TIME_IN_CALL_FOR_IMS,
+                ims_virtual_network_id=DEFAULT_IMS_VIRTUAL_NETWORK_ID):
+    """ Makes a MO call after IMS registred, then handover
+
+    Args:
+        ad: Android device object.
+        anritsu_handle: Anritsu object.
+        callee_number: Number to be called.
+        check_ims_reg: check if Anritsu cscf server state is "SIPIDLE".
+        check_ims_calling: check if Anritsu cscf server state is "CALLING".
+        mo: Mobile originated call
+        wait_time_in_volte: Time for phone in VoLTE call, not used for SRLTE
+        ims_virtual_network_id: ims virtual network id.
+
+    Returns:
+        True for success False for failure
+    """
+
+    try:
+        # confirm ims registration
+        if check_ims_reg:
+            if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                            ims_virtual_network_id,
+                                            ImsCscfStatus.SIPIDLE.value):
+                raise _CallSequenceException("IMS/CSCF status is not idle.")
+        if mo:  # make MO call
+            log.info("Making Call to " + callee_number)
+            if not initiate_call(log, ad, callee_number, is_emergency):
+                raise _CallSequenceException("Initiate call failed.")
+            if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                            ims_virtual_network_id,
+                                            ImsCscfStatus.CALLING.value):
+                raise _CallSequenceException(
+                    "Phone IMS status is not calling.")
+        else:  # make MT call
+            log.info("Making IMS Call to UE from MD8475A...")
+            anritsu_handle.ims_cscf_call_action(ims_virtual_network_id,
+                                                ImsCscfCall.MAKE.value)
+            if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                            ims_virtual_network_id,
+                                            ImsCscfStatus.RINGING.value):
+                raise _CallSequenceException(
+                    "Phone IMS status is not ringing.")
+            # answer the call on the UE
+            if not wait_and_answer_call(log, ad):
+                raise _CallSequenceException("UE Answer call Fail")
+
+        if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                        ims_virtual_network_id,
+                                        ImsCscfStatus.CONNECTED.value):
+            raise _CallSequenceException("Phone IMS status is not connected.")
+        log.info(
+            "Wait for {} seconds before handover".format(wait_time_in_volte))
+        time.sleep(wait_time_in_volte)
+
+        # Once VoLTE call is connected, then Handover
+        log.info("Starting handover procedure...")
+        result = handover_tc(anritsu_handle, BtsNumber.BTS1, BtsNumber.BTS2)
+        log.info("Handover procedure ends with result code {}".format(result))
+        log.info(
+            "Wait for {} seconds after handover".format(wait_time_in_volte))
+        time.sleep(wait_time_in_volte)
+
+        # check if the phone stay in call
+        if not ad.droid.telecomIsInCall():
+            raise _CallSequenceException("Call ended before delay_in_call.")
+        # end the call from phone
+        log.info("Disconnecting the call from DUT")
+        if not hangup_call(log, ad):
+            raise _CallSequenceException("Error in Hanging-Up Call on DUT.")
+        # confirm if CSCF status is back to idle
+        if not wait_for_ims_cscf_status(log, anritsu_handle,
+                                        ims_virtual_network_id,
+                                        ImsCscfStatus.SIPIDLE.value):
+            raise _CallSequenceException("IMS/CSCF status is not idle.")
+
+        return True
+
+    except _CallSequenceException as e:
+        log.error(e)
+        return False
+    finally:
+        try:
+            if ad.droid.telecomIsInCall():
+                ad.droid.telecomEndCall()
+        except Exception as e:
+            log.error(str(e))
+
+
 # This procedure is for SRLTE CSFB and SRVCC test cases
 def ims_call_cs_teardown(
         log,
@@ -1450,7 +1718,8 @@
     transmission_mode = user_params.get(key, DEFAULT_T_MODE)
     return transmission_mode
 
-def get_dl_antennas(user_params, cell_no):
+
+def get_dl_antenna(user_params, cell_no):
     """ Returns the DL ANTENNA to be used from the user specified parameters
         or default value
 
@@ -1466,6 +1735,7 @@
     dl_antenna = user_params.get(key, DEFAULT_DL_ANTENNA)
     return dl_antenna
 
+
 def get_lte_band(user_params, cell_no):
     """ Returns the LTE BAND to be used from the user specified parameters
         or default value
@@ -1479,8 +1749,8 @@
         LTE BAND to be used
     """
     key = "cell{}_lte_band".format(cell_no)
-    lte_band = user_params.get(key, DEFAULT_LTE_BAND)
-    return lte_band
+    band = DEFAULT_LTE_BAND[cell_no - 1]
+    return user_params.get(key, band)
 
 
 def get_wcdma_band(user_params, cell_no):
diff --git a/acts/framework/acts/test_utils/tel/tel_test_utils.py b/acts/framework/acts/test_utils/tel/tel_test_utils.py
index d02307a..f7bbe07 100644
--- a/acts/framework/acts/test_utils/tel/tel_test_utils.py
+++ b/acts/framework/acts/test_utils/tel/tel_test_utils.py
@@ -28,8 +28,7 @@
 from queue import Empty
 from acts.asserts import abort_all
 from acts.controllers.adb import AdbError
-from acts.controllers.android_device import AndroidDevice
-from acts.controllers.event_dispatcher import EventDispatcher
+from acts.controllers.sl4a_lib.event_dispatcher import EventDispatcher
 from acts.test_utils.tel.tel_defines import AOSP_PREFIX
 from acts.test_utils.tel.tel_defines import CARRIER_UNKNOWN
 from acts.test_utils.tel.tel_defines import COUNTRY_CODE_LIST
@@ -80,6 +79,7 @@
 from acts.test_utils.tel.tel_defines import SERVICE_STATE_IN_SERVICE
 from acts.test_utils.tel.tel_defines import SERVICE_STATE_OUT_OF_SERVICE
 from acts.test_utils.tel.tel_defines import SERVICE_STATE_POWER_OFF
+from acts.test_utils.tel.tel_defines import SIM_STATE_PIN_REQUIRED
 from acts.test_utils.tel.tel_defines import SIM_STATE_READY
 from acts.test_utils.tel.tel_defines import WAIT_TIME_SUPPLY_PUK_CODE
 from acts.test_utils.tel.tel_defines import TELEPHONY_STATE_IDLE
@@ -181,6 +181,10 @@
     return ad.adb.getprop("gsm.sim.operator.alpha")
 
 
+def get_plmn_by_adb(ad):
+    return ad.adb.getprop("gsm.sim.operator.numeric")
+
+
 def get_sub_id_by_adb(ad):
     return ad.adb.shell("service call iphonesubinfo 5")
 
@@ -199,16 +203,22 @@
             log.warning("Failed to load %s!", sim_filename)
 
     sub_id = get_sub_id_by_adb(ad)
-    if iccid in sim_data and sim_data[iccid].get("phone_num"):
+    iccid = get_iccid_by_adb(ad)
+    ad.log.info("iccid = %s", iccid)
+    if sim_data.get(iccid) and sim_data[iccid].get("phone_num"):
         phone_number = phone_number_formatter(sim_data[iccid]["phone_num"])
     else:
         phone_number = get_phone_number_by_adb(ad)
+        if not phone_number and hasattr(ad, phone_number):
+            phone_number = ad.phone_number
     if not phone_number:
-        abort_all_tests(ad.log, "Failed to find valid phone number")
+        ad.log.error("Failed to find valid phone number for %s", iccid)
+        abort_all_tests("Failed to find valid phone number for %s" % ad.serial)
     sim_record = {
         'phone_num': phone_number,
         'iccid': get_iccid_by_adb(ad),
-        'sim_operator_name': get_operator_by_adb(ad)
+        'sim_operator_name': get_operator_by_adb(ad),
+        'operator': operator_name_from_plmn_id(get_plmn_by_adb(ad))
     }
     device_props = {'subscription': {sub_id: sim_record}}
     ad.log.info("subId %s SIM record: %s", sub_id, sim_record)
@@ -237,6 +247,7 @@
             log.warning("Failed to load %s!", sim_filename)
     if not ad.cfg["subscription"]:
         abort_all_tests(ad.log, "No Valid SIMs found in device")
+    result = False
     for sub_id, sub_info in ad.cfg["subscription"].items():
         sub_info["operator"] = get_operator_name(log, ad, sub_id)
         iccid = sub_info["iccid"]
@@ -244,31 +255,46 @@
             ad.log.warn("Unable to find ICC-ID for SIM")
             continue
         if not sub_info["phone_num"]:
-            if iccid not in sim_data or not sim_data[iccid].get("phone_num"):
-                abort_all_tests(
-                    ad.log,
-                    "Failed to find valid phone number for ICCID %s in %s" %
-                    (iccid, sim_filename))
-            else:
+            if iccid in sim_data and sim_data[iccid].get("phone_num"):
                 sub_info["phone_num"] = sim_data[iccid]["phone_num"]
-        elif sim_data.get(iccid) and sim_data[iccid].get("phone_num"):
-            if not check_phone_number_match(sub_info["phone_num"],
-                                            sim_data[iccid]["phone_num"]):
-                ad.log.error(
-                    "ICCID %s phone number is %s in %s, does not match "
-                    "the number %s retrieved from the phone", iccid,
-                    sim_data[iccid]["phone_num"], sim_filename,
-                    sub_info["phone_num"])
-            sub_info["phone_num"] = sim_data[iccid]["phone_num"]
-        if not hasattr(ad, 'roaming') and sub_info["sim_plmn"] != sub_info[
-                "network_plmn"] and (
-                    sub_info["sim_operator_name"].strip() not in sub_info[
-                        "network_operator_name"].strip()):
+                ad.phone_number = sub_info["phone_num"]
+                result = True
+            else:
+                phone_number = get_phone_number_by_secret_code(
+                    ad, sub_info["sim_operator_name"])
+                if phone_number:
+                    sub_info["phone_num"] = sim_data[iccid]["phone_num"]
+                    ad.phone_number = sub_info["phone_num"]
+                    result = True
+                else:
+                    ad.log.error(
+                        "Unable to retrieve phone number for sub %s iccid %s"
+                        " from device or testbed config or sim_file %s",
+                        sim_filename, sub_id, iccid)
+        else:
+            result = True
+            if sim_data.get(iccid) and sim_data[iccid].get("phone_num"):
+                if not check_phone_number_match(sub_info["phone_num"],
+                                                sim_data[iccid]["phone_num"]):
+                    ad.log.warning(
+                        "ICCID %s phone number is %s in %s, does not match "
+                        "the number %s retrieved from the phone", iccid,
+                        sim_data[iccid]["phone_num"], sim_filename,
+                        sub_info["phone_num"])
+                    sub_info["phone_num"] = sim_data[iccid]["phone_num"]
+        if not hasattr(
+                ad, 'roaming'
+        ) and sub_info["sim_plmn"] != sub_info["network_plmn"] and (
+                sub_info["sim_operator_name"].strip() not in
+                sub_info["network_operator_name"].strip()):
             ad.log.info("roaming is not enabled, enable it")
             setattr(ad, 'roaming', True)
+        ad.log.info("SubId %s info: %s", sub_id, sorted(sub_info.items()))
     data_roaming = getattr(ad, 'roaming', False)
     if get_cell_data_roaming_state_by_adb(ad) != data_roaming:
         set_cell_data_roaming_state_by_adb(ad, data_roaming)
+    if not result:
+        abort_all_tests(ad.log, "Failed to find valid phone number")
 
     ad.log.debug("cfg = %s", ad.cfg)
 
@@ -322,16 +348,34 @@
             sim_record[
                 "sim_country"] = droid.telephonyGetSimCountryIsoForSubscription(
                     sub_id)
-            sim_record["phone_num"] = phone_number_formatter(
-                droid.telephonyGetLine1NumberForSubscription(sub_id))
+            phone_number = droid.telephonyGetLine1NumberForSubscription(sub_id)
+            if not phone_number and getattr(ad, "phone_number", None):
+                phone_number = ad.phone_number
+            sim_record["phone_num"] = phone_number_formatter(phone_number)
             sim_record[
                 "phone_tag"] = droid.telephonyGetLine1AlphaTagForSubscription(
                     sub_id)
             cfg['subscription'][sub_id] = sim_record
-            ad.log.info("SubId %s SIM record: %s", sub_id, sim_record)
+            ad.log.debug("SubId %s SIM record: %s", sub_id, sim_record)
     setattr(ad, 'cfg', cfg)
 
 
+def get_phone_number_by_secret_code(ad, operator):
+    if "T-Mobile" in operator:
+        ad.droid.telecomDialNumber("#686#")
+        ad.send_keycode("ENTER")
+        for _ in range(12):
+            output = ad.search_logcat("mobile number")
+            if output:
+                result = re.findall(r"mobile number is (\S+)",
+                                    output[-1]["log_message"])
+                ad.send_keycode("BACK")
+                return result[0]
+            else:
+                time.sleep(5)
+    return ""
+
+
 def get_slot_index_from_subid(log, ad, sub_id):
     try:
         info = ad.droid.subscriptionGetSubInfoForSubscriber(sub_id)
@@ -1080,7 +1124,8 @@
                   callee_number,
                   emergency=False,
                   timeout=MAX_WAIT_TIME_CALL_INITIATION,
-                  checking_interval=5):
+                  checking_interval=5,
+                  wait_time_betwn_call_initcheck=0):
     """Make phone call from caller to callee.
 
     Args:
@@ -1104,6 +1149,9 @@
         else:
             ad.droid.telecomCallNumber(callee_number)
 
+        # Sleep time for stress test b/64915613
+        time.sleep(wait_time_betwn_call_initcheck)
+
         # Verify OFFHOOK event
         checking_retries = int(timeout / checking_interval)
         for i in range(checking_retries):
@@ -1206,6 +1254,9 @@
             if check_call_state_connected_by_adb(ad):
                 ad.log.info("Call to %s is connected", callee_number)
                 return True
+            if check_call_state_idle_by_adb(ad):
+                ad.log.info("Call to %s failed", callee_number)
+                return False
         ad.log.info("Make call to %s failed", callee_number)
         return False
     except Exception as e:
@@ -1515,7 +1566,8 @@
                         verify_caller_func=None,
                         verify_callee_func=None,
                         wait_time_in_call=WAIT_TIME_IN_CALL,
-                        incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+                        incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND,
+                        extra_sleep=0):
     """ Call process, including make a phone call from caller,
     accept from callee, and hang up. The call is on default voice subscription
 
@@ -1536,6 +1588,7 @@
             if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
             if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
             else, do nothing.
+        extra_sleep: for stress test only - b/64915613
 
     Returns:
         True if call process without any error.
@@ -1547,7 +1600,7 @@
     return call_setup_teardown_for_subscription(
         log, ad_caller, ad_callee, subid_caller, subid_callee, ad_hangup,
         verify_caller_func, verify_callee_func, wait_time_in_call,
-        incall_ui_display)
+        incall_ui_display, extra_sleep)
 
 
 def call_setup_teardown_for_subscription(
@@ -1560,7 +1613,8 @@
         verify_caller_func=None,
         verify_callee_func=None,
         wait_time_in_call=WAIT_TIME_IN_CALL,
-        incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+        incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND,
+        extra_sleep=0):
     """ Call process, including make a phone call from caller,
     accept from callee, and hang up. The call is on specified subscription
 
@@ -1583,6 +1637,7 @@
             if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
             if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
             else, do nothing.
+        extra_sleep: for stress test only - b/64915613
 
     Returns:
         True if call process without any error.
@@ -1604,7 +1659,11 @@
                        callee_number, wait_time_in_call)
 
     try:
-        if not initiate_call(log, ad_caller, callee_number):
+        if not initiate_call(
+                log,
+                ad_caller,
+                callee_number,
+                wait_time_betwn_call_initcheck=extra_sleep):
             ad_caller.log.error("Initiate call failed.")
             result = False
             return False
@@ -1677,6 +1736,8 @@
         If no error happen, return phone number in expected format.
         Else, return None.
     """
+    if not input_string:
+        return ""
     # make sure input_string is 10 digital
     # Remove white spaces, dashes, dots
     input_string = input_string.replace(" ", "").replace("-", "").replace(
@@ -1801,7 +1862,7 @@
 def active_file_download_task(log, ad, file_name="5MB"):
     if not hasattr(ad, "curl_capable"):
         try:
-            out = ad.adb.shell("curl --version")
+            out = ad.adb.shell("/data/curl --version")
             if not out or "not found" in out:
                 setattr(ad, "curl_capable", False)
                 ad.log.info("curl is unavailable, use chrome to download file")
@@ -1828,7 +1889,7 @@
     if not file_size:
         log.warning("file_name %s for download is not available", file_name)
         return False
-    timeout = min(max(file_size / 100000, 300), 3600)
+    timeout = min(max(file_size / 100000, 600), 3600)
     output_path = "/sdcard/Download/" + file_name + ".zip"
     if not ad.curl_capable:
         return (http_file_download_by_chrome, (ad, url, file_size, True,
@@ -1843,7 +1904,7 @@
     return task[0](*task[1])
 
 
-def verify_internet_connection(log, ad):
+def verify_internet_connection(log, ad, retries=1):
     """Verify internet connection by ping test.
 
     Args:
@@ -1851,8 +1912,12 @@
         ad: Android Device Object.
 
     """
-    ad.log.info("Verify internet connection")
-    return adb_shell_ping(ad, count=5, timeout=60, loss_tolerance=40)
+    for i in range(retries):
+        ad.log.info("Verify internet connection - attempt %d", i + 1)
+        result = adb_shell_ping(ad, count=5, timeout=60, loss_tolerance=40)
+        if result:
+            return True
+    return False
 
 
 def iperf_test_by_adb(log,
@@ -1929,7 +1994,7 @@
     file_directory, file_name = _generate_file_directory_and_file_name(
         url, out_path)
     file_path = os.path.join(file_directory, file_name)
-    curl_cmd = "curl"
+    curl_cmd = "/data/curl"
     if limit_rate:
         curl_cmd += " --limit-rate %s" % limit_rate
     if retry:
@@ -1979,8 +2044,13 @@
     file_path = os.path.join(file_directory, file_name)
     # Remove pre-existing file
     ad.force_stop_apk("com.android.chrome")
-    ad.adb.shell("rm %s" % file_path, ignore_status=True)
-    ad.adb.shell("rm %s.crdownload" % file_path, ignore_status=True)
+    file_to_be_delete = os.path.join(file_directory, "*%s*" % file_name)
+    ad.adb.shell("rm -f %s" % file_to_be_delete)
+    ad.adb.shell("rm -rf /sdcard/Download/.*")
+    ad.adb.shell("rm -f /sdcard/Download/.*")
+    total_rx_bytes_before = ad.droid.getTotalRxBytes()
+    mobile_rx_bytes_before = ad.droid.getMobileRxBytes()
+    subscriber_mobile_data_usage_before = get_mobile_data_usage(ad)
     ad.ensure_screen_on()
     ad.log.info("Download %s with timeout %s", url, timeout)
     open_url_by_adb(ad, url)
@@ -2030,8 +2100,8 @@
                                  check.
         timeout: timeout for file download to complete.
     """
-    file_folder, file_name = _generate_file_directory_and_file_name(url,
-                                                                    out_path)
+    file_folder, file_name = _generate_file_directory_and_file_name(
+        url, out_path)
     file_path = os.path.join(file_folder, file_name)
     try:
         ad.log.info("Download file from %s to %s by sl4a RPC call", url,
@@ -2072,8 +2142,7 @@
                 connection_type, connection_type_string_in_event, cur_type)
             return False
 
-    if 'isConnected' in _event['data'] and _event['data'][
-            'isConnected'] == target_state:
+    if 'isConnected' in _event['data'] and _event['data']['isConnected'] == target_state:
         return True
     return False
 
@@ -2100,8 +2169,8 @@
         False if failed.
     """
     sub_id = get_default_data_sub_id(ad)
-    return wait_for_cell_data_connection_for_subscription(log, ad, sub_id,
-                                                          state, timeout_value)
+    return wait_for_cell_data_connection_for_subscription(
+        log, ad, sub_id, state, timeout_value)
 
 
 def _is_data_connection_state_match(log, ad, expected_data_connection_state):
@@ -2465,7 +2534,9 @@
     """
     # TODO: b/26293960 No framework API available to set IMS by SubId.
     if not ad.droid.imsIsEnhanced4gLteModeSettingEnabledByPlatform():
-        raise TelTestUtilsError("VoLTE not supported by platform.")
+        ad.log.info("VoLTE not supported by platform.")
+        raise TelTestUtilsError(
+            "VoLTE not supported by platform %s." % ad.serial)
     current_state = ad.droid.imsIsEnhanced4gLteModeSettingEnabledByUser()
     if new_state is None:
         new_state = not current_state
@@ -2787,7 +2858,9 @@
         if not is_ims_registered(log, ad):
             ad.log.info("VoLTE is Available, but IMS is not registered.")
             return False
-        return True
+        else:
+            ad.log.info("IMS is registered")
+            return True
 
 
 def is_video_enabled(log, ad):
@@ -3169,6 +3242,7 @@
         result = False
         ad_rx.ed.clear_all_events()
         ad_rx.droid.smsStartTrackingIncomingSmsMessage()
+        time.sleep(0.1)  #sleep 100ms after starting event tracking
         try:
             ad_tx.droid.smsSendTextMessage(phonenumber_rx, text, True)
 
@@ -3312,7 +3386,6 @@
                                    MAX_WAIT_TIME_SMS_SENT_SUCCESS)
             except Empty:
                 log.warning("No sent_success event.")
-                return False
 
             if not wait_for_matching_mms(log, ad_rx, phonenumber_tx, message):
                 return False
@@ -3886,13 +3959,11 @@
     Phone not in airplane mode.
     """
     result = True
-
     if not toggle_airplane_mode(log, ad, False, False):
         ad.log.error("Fail to turn off airplane mode")
         result = False
-    set_wifi_to_default(log, ad)
-
     try:
+        set_wifi_to_default(log, ad)
         if ad.droid.telecomIsInCall():
             ad.droid.telecomEndCall()
             if not wait_for_droid_not_in_call(log, ad):
@@ -3902,21 +3973,22 @@
         data_roaming = getattr(ad, 'roaming', False)
         if get_cell_data_roaming_state_by_adb(ad) != data_roaming:
             set_cell_data_roaming_state_by_adb(ad, data_roaming)
+        if not wait_for_not_network_rat(
+                log, ad, RAT_FAMILY_WLAN, voice_or_data=NETWORK_SERVICE_DATA):
+            ad.log.error("%s still in %s", NETWORK_SERVICE_DATA,
+                         RAT_FAMILY_WLAN)
+            result = False
+
+        if check_subscription and not ensure_phone_subscription(log, ad):
+            ad.log.error("Unable to find a valid subscription!")
+            result = False
     except Exception as e:
         ad.log.error("%s failure, toggle APM instead", e)
-        toggle_airplane_mode(log, ad, True, False)
-        toggle_airplane_mode(log, ad, False, False)
-        ad.droid.telephonyToggleDataConnection(True)
-        set_wfc_mode(log, ad, WFC_MODE_DISABLED)
-
-    if not wait_for_not_network_rat(
-            log, ad, RAT_FAMILY_WLAN, voice_or_data=NETWORK_SERVICE_DATA):
-        ad.log.error("%s still in %s", NETWORK_SERVICE_DATA, RAT_FAMILY_WLAN)
-        result = False
-
-    if check_subscription and not ensure_phone_subscription(log, ad):
-        ad.log.error("Unable to find a valid subscription!")
-        result = False
+        toggle_airplane_mode_by_adb(log, ad, True)
+        toggle_airplane_mode_by_adb(log, ad, False)
+        ad.send_keycode("ENDCALL")
+        ad.adb.shell("settings put global wfc_ims_enabled 0")
+        ad.adb.shell("settings put global mobile_data 1")
 
     return result
 
@@ -3954,8 +4026,7 @@
         False if wifi is not connected to wifi_ssid
     """
     wifi_info = ad.droid.wifiGetConnectionInfo()
-    if wifi_info["supplicant_state"] == "completed" and wifi_info[
-            "SSID"] == wifi_ssid:
+    if wifi_info["supplicant_state"] == "completed" and wifi_info["SSID"] == wifi_ssid:
         ad.log.info("Wifi is connected to %s", wifi_ssid)
         return True
     else:
@@ -4433,8 +4504,8 @@
     try:
         return (
             (network_callback_id == event['data'][NetworkCallbackContainer.ID])
-            and (network_callback_event == event['data'][
-                NetworkCallbackContainer.NETWORK_CALLBACK_EVENT]))
+            and (network_callback_event == event['data']
+                 [NetworkCallbackContainer.NETWORK_CALLBACK_EVENT]))
     except KeyError:
         return False
 
@@ -4577,14 +4648,13 @@
 
     """
     ad.log.debug("Ensuring no tcpdump is running in background")
-    try:
-        ad.adb.shell("killall -9 tcpdump")
-    except AdbError:
-        ad.log.warn("Killing existing tcpdump processes failed")
+    ad.adb.shell("killall -9 tcpdump")
     begin_time = epoch_to_log_line_timestamp(get_current_epoch_time())
     begin_time = normalize_log_line_timestamp(begin_time)
-    file_name = "/sdcard/tcpdump{}{}{}.pcap".format(ad.serial, test_name,
-                                                    begin_time)
+
+    file_name = "/sdcard/tcpdump/tcpdump_%s_%s_%s.pcap" % (ad.serial,
+                                                           test_name,
+                                                           begin_time)
     ad.log.info("tcpdump file is %s", file_name)
     if mask == "all":
         cmd = "adb -s {} shell tcpdump -i any -s0 -w {}" . \
@@ -4615,3 +4685,234 @@
         ad.adb.pull("{} {}".format(tcpdump_file, tcpdump_path))
     ad.adb.shell("rm -rf {}".format(tcpdump_file))
     return True
+
+
+def fastboot_wipe(ad, skip_setup_wizard=True):
+    """Wipe the device in fastboot mode.
+
+    Pull sl4a apk from device. Terminate all sl4a sessions,
+    Reboot the device to bootloader, wipe the device by fastboot.
+    Reboot the device. wait for device to complete booting
+    Re-intall and start an sl4a session.
+    """
+    status = True
+    # Pull sl4a apk from device
+    out = ad.adb.shell("pm path com.googlecode.android_scripting")
+    result = re.search(r"package:(.*)", out)
+    if not result:
+        ad.log.error("Couldn't find sl4a apk")
+    else:
+        sl4a_apk = result.group(1)
+        ad.log.info("Get sl4a apk from %s", sl4a_apk)
+        ad.pull_files([sl4a_apk], "/tmp/")
+    ad.stop_services()
+    ad.log.info("Reboot to bootloader")
+    ad.adb.reboot_bootloader(ignore_status=True)
+    ad.log.info("Wipe in fastboot")
+    try:
+        ad.fastboot._w()
+    except Exception as e:
+        ad.log.error(e)
+        status = False
+    for _ in range(2):
+        try:
+            ad.log.info("Reboot in fastboot")
+            ad.fastboot.reboot()
+            ad.wait_for_boot_completion()
+            break
+        except Exception as e:
+            ad.log.error("Exception error %s", e)
+    ad.root_adb()
+    if not ad.ensure_screen_on():
+        ad.log.error("User window cannot come up")
+    if result:
+        # Try to reinstall for three times as the device might not be
+        # ready to apk install shortly after boot complete.
+        for _ in range(3):
+            if ad.is_sl4a_installed():
+                break
+            ad.log.info("Re-install sl4a")
+            ad.adb.install("-r /tmp/base.apk")
+            time.sleep(10)
+    ad.start_services(ad.skip_sl4a, skip_setup_wizard=skip_setup_wizard)
+    return status
+
+
+def unlocking_device(ad, device_password=None):
+    """First unlock device attempt, required after reboot"""
+    ad.unlock_screen(device_password)
+    time.sleep(2)
+    ad.adb.wait_for_device(timeout=180)
+    if not ad.is_waiting_for_unlock_pin():
+        return True
+    else:
+        ad.unlock_screen(device_password)
+        time.sleep(2)
+        ad.adb.wait_for_device(timeout=180)
+        if ad.wait_for_window_ready():
+            return True
+    ad.log.error("Unable to unlock to user window")
+    return False
+
+
+def refresh_sl4a_session(ad):
+    try:
+        ad.droid.logI("Checking SL4A connection")
+        ad.log.info("Existing sl4a session is active")
+    except:
+        ad.terminate_all_sessions()
+        ad.ensure_screen_on()
+        ad.log.info("Open new sl4a connection")
+        droid, ed = ad.get_droid()
+        ed.start()
+
+
+def reset_device_password(ad, device_password=None):
+    # Enable or Disable Device Password per test bed config
+    unlock_sim(ad)
+    screen_lock = ad.is_screen_lock_enabled()
+    if device_password:
+        refresh_sl4a_session(ad)
+        ad.droid.setDevicePassword(device_password)
+        time.sleep(2)
+        if screen_lock:
+            # existing password changed
+            return
+        else:
+            # enable device password and log in for the first time
+            ad.log.info("Enable device password")
+            ad.adb.wait_for_device(timeout=180)
+    else:
+        if not screen_lock:
+            # no existing password, do not set password
+            return
+        else:
+            # password is enabled on the device
+            # need to disable the password and log in on the first time
+            # with unlocking with a swipe
+            ad.log.info("Disable device password")
+            refresh_sl4a_session(ad)
+            ad.droid.disableDevicePassword()
+            time.sleep(2)
+            ad.adb.wait_for_device(timeout=180)
+    refresh_sl4a_session(ad)
+    if not ad.is_adb_logcat_on:
+        ad.start_adb_logcat()
+
+
+def is_sim_locked(ad):
+    try:
+        return ad.droid.telephonyGetSimState() == SIM_STATE_PIN_REQUIRED
+    except:
+        return ad.adb.getprop("gsm.sim.state") == SIM_STATE_PIN_REQUIRED
+
+
+def unlock_sim(ad):
+    #The puk and pin can be provided in testbed config file.
+    #"AndroidDevice": [{"serial": "84B5T15A29018214",
+    #                   "adb_logcat_param": "-b all",
+    #                   "puk": "12345678",
+    #                   "puk_pin": "1234"}]
+    if not is_sim_locked(ad):
+        return True
+    puk_pin = getattr(ad, "puk_pin", "1111")
+    try:
+        if not hasattr(ad, 'puk'):
+            ad.log.info("Enter SIM pin code")
+            result = ad.droid.telephonySupplyPin(puk_pin)
+        else:
+            ad.log.info("Enter PUK code and pin")
+            result = ad.droid.telephonySupplyPuk(ad.puk, puk_pin)
+    except:
+        # if sl4a is not available, use adb command
+        ad.unlock_screen(puk_pin)
+        if is_sim_locked(ad):
+            ad.unlock_screen(puk_pin)
+    time.sleep(30)
+    return not is_sim_locked(ad)
+
+
+def send_dialer_secret_code(ad, secret_code):
+    """Send dialer secret code.
+
+    ad: android device controller
+    secret_code: the secret code to be sent to dialer. the string between
+                 code prefix *#*# and code postfix #*#*. *#*#<xxx>#*#*
+    """
+    action = 'android.provider.Telephony.SECRET_CODE'
+    uri = 'android_secret_code://%s' % secret_code
+    intent = ad.droid.makeIntent(
+        action,
+        uri,
+        None,  # type
+        None,  # extras
+        None,  # categories,
+        None,  # packagename,
+        None,  # classname,
+        0x01000000)  # flags
+    ad.log.info('Issuing dialer secret dialer code: %s', secret_code)
+    ad.droid.sendBroadcastIntent(intent)
+
+
+def system_file_push(ad, src_file_path, dst_file_path):
+    """Push system file on a device.
+
+    Push system file need to change some system setting and remount.
+    """
+    cmd = "%s %s" % (src_file_path, dst_file_path)
+    out = ad.adb.push(cmd, timeout=300, ignore_status=True)
+    skip_sl4a = True if "sl4a.apk" in src_file_path else False
+    if "Read-only file system" in out:
+        ad.log.info("Change read-only file system")
+        ad.adb.disable_verity()
+        ad.reboot(skip_sl4a)
+        ad.adb.remount()
+        out = ad.adb.push(cmd, timeout=300, ignore_status=True)
+        if "Read-only file system" in out:
+            ad.reboot(skip_sl4a)
+            out = ad.adb.push(cmd, timeout=300, ignore_status=True)
+            if "error" in out:
+                ad.log.error("%s failed with %s", cmd, out)
+                return False
+            else:
+                ad.log.info("push %s succeed")
+                if skip_sl4a: ad.reboot(skip_sl4a)
+                return True
+        else:
+            return True
+    elif "error" in out:
+        return False
+    else:
+        return True
+
+
+def flash_radio(ad, file_path, skip_setup_wizard=True):
+    """Flash radio image."""
+    ad.stop_services()
+    ad.log.info("Reboot to bootloader")
+    ad.adb.reboot_bootloader(ignore_status=True)
+    ad.log.info("Flash radio in fastboot")
+    try:
+        ad.fastboot.flash("radio %s" % file_path, timeout=300)
+    except Exception as e:
+        ad.log.error(e)
+        status = False
+    for _ in range(2):
+        try:
+            ad.log.info("Reboot in fastboot")
+            ad.fastboot.reboot()
+            ad.wait_for_boot_completion()
+            break
+        except Exception as e:
+            ad.log.error("Exception error %s", e)
+    ad.root_adb()
+    if not ad.ensure_screen_on():
+        ad.log.error("User window cannot come up")
+    ad.start_services(ad.skip_sl4a, skip_setup_wizard=skip_setup_wizard)
+
+
+def print_radio_info(ad, extra_msg=""):
+    for prop in ("gsm.version.baseband", "persist.radio.ver_info",
+                 "persist.radio.cnv.ver_info", "persist.radio.ci_status"):
+        output = ad.adb.getprop(prop)
+        if output: ad.log.info("%s%s = %s", extra_msg, prop, output)
diff --git a/acts/framework/acts/test_utils/wifi/aware/AwareBaseTest.py b/acts/framework/acts/test_utils/wifi/aware/AwareBaseTest.py
index d95a3eb..0b9192d 100644
--- a/acts/framework/acts/test_utils/wifi/aware/AwareBaseTest.py
+++ b/acts/framework/acts/test_utils/wifi/aware/AwareBaseTest.py
@@ -38,20 +38,26 @@
     self.unpack_userparams(required_params)
 
     for ad in self.android_devices:
+      asserts.skip_if(
+          not ad.droid.doesDeviceSupportWifiAwareFeature(),
+          "Device under test does not support Wi-Fi Aware - skipping test")
       wutils.wifi_toggle_state(ad, True)
       ad.droid.wifiP2pClose()
       aware_avail = ad.droid.wifiIsAwareAvailable()
       if not aware_avail:
         self.log.info('Aware not available. Waiting ...')
         autils.wait_for_event(ad, aconsts.BROADCAST_WIFI_AWARE_AVAILABLE)
-      ad.ed.pop_all(aconsts.BROADCAST_WIFI_AWARE_AVAILABLE) # clear-out extras
+      ad.ed.clear_all_events()
       ad.aware_capabilities = autils.get_aware_capabilities(ad)
       self.reset_device_parameters(ad)
       self.reset_device_statistics(ad)
       self.set_power_mode_parameters(ad)
 
+
   def teardown_test(self):
     for ad in self.android_devices:
+      if not ad.droid.doesDeviceSupportWifiAwareFeature():
+        return
       ad.droid.wifiP2pClose()
       ad.droid.wifiAwareDestroyAll()
       self.reset_device_parameters(ad)
diff --git a/acts/framework/acts/test_utils/wifi/aware/aware_test_utils.py b/acts/framework/acts/test_utils/wifi/aware/aware_test_utils.py
index 9ed704f..4438064 100644
--- a/acts/framework/acts/test_utils/wifi/aware/aware_test_utils.py
+++ b/acts/framework/acts/test_utils/wifi/aware/aware_test_utils.py
@@ -342,6 +342,21 @@
       extras=out)
   return res.group(1).upper().replace(':', '')
 
+def get_ipv6_addr(device, interface):
+  """Get the IPv6 address of the specified interface. Uses ifconfig and parses
+  its output. Returns a None if the interface does not have an IPv6 address
+  (indicating it is not UP).
+
+  Args:
+    device: Device on which to query the interface IPv6 address.
+    interface: Name of the interface for which to obtain the IPv6 address.
+  """
+  out = device.adb.shell("ifconfig %s" % interface)
+  res = re.match(".*inet6 addr: (\S+)/.*", out , re.S)
+  if not res:
+    return None
+  return res.group(1)
+
 #########################################################
 # Aware primitives
 #########################################################
@@ -357,6 +372,26 @@
   network_req = {"TransportType": 5, "NetworkSpecifier": ns}
   return dut.droid.connectivityRequestWifiAwareNetwork(network_req)
 
+def get_network_specifier(dut, id, dev_type, peer_mac, sec):
+  """Create a network specifier for the device based on the security
+  configuration.
+
+  Args:
+    dut: device
+    id: session ID
+    dev_type: device type - Initiator or Responder
+    peer_mac: the discovery MAC address of the peer
+    sec: security configuration
+  """
+  if sec is None:
+    return dut.droid.wifiAwareCreateNetworkSpecifierOob(
+        id, dev_type, peer_mac)
+  if isinstance(sec, str):
+    return dut.droid.wifiAwareCreateNetworkSpecifierOob(
+        id, dev_type, peer_mac, sec)
+  return dut.droid.wifiAwareCreateNetworkSpecifierOob(
+      id, dev_type, peer_mac, None, sec)
+
 def configure_dw(device, is_default, is_24_band, value):
   """Use the command-line API to configure the DW (discovery window) setting
 
@@ -460,6 +495,23 @@
   config[aconsts.DISCOVERY_KEY_TERM_CB_ENABLED] = term_cb_enable
   return config
 
+def attach_with_identity(dut):
+  """Start an Aware session (attach) and wait for confirmation and identity
+  information (mac address).
+
+  Args:
+    dut: Device under test
+  Returns:
+    id: Aware session ID.
+    mac: Discovery MAC address of this device.
+  """
+  id = dut.droid.wifiAwareAttach(True)
+  wait_for_event(dut, aconsts.EVENT_CB_ON_ATTACHED)
+  event = wait_for_event(dut, aconsts.EVENT_CB_ON_IDENTITY_CHANGED)
+  mac = event["data"]["mac"]
+
+  return id, mac
+
 def create_discovery_pair(p_dut,
                           s_dut,
                           p_config,
diff --git a/acts/framework/acts/test_utils/wifi/wifi_constants.py b/acts/framework/acts/test_utils/wifi/wifi_constants.py
index 8c9f978..1ded7db 100644
--- a/acts/framework/acts/test_utils/wifi/wifi_constants.py
+++ b/acts/framework/acts/test_utils/wifi/wifi_constants.py
@@ -16,6 +16,7 @@
 
 # Constants for Wifi related events.
 WIFI_CONNECTED = "WifiNetworkConnected"
+WIFI_DISCONNECTED = "WifiNetworkDisconnected"
 SUPPLICANT_CON_CHANGED = "SupplicantConnectionChanged"
 WIFI_FORGET_NW_SUCCESS = "WifiManagerForgetNetworkOnSuccess"
 
diff --git a/acts/framework/acts/utils.py b/acts/framework/acts/utils.py
index 802e0c1..62c5d58 100755
--- a/acts/framework/acts/utils.py
+++ b/acts/framework/acts/utils.py
@@ -31,9 +31,12 @@
 import zipfile
 
 from acts.controllers import adb
+from acts import tracelogger
 
 # File name length is limited to 255 chars on some OS, so we need to make sure
 # the file names we output fits within the limit.
+from acts.libs.proc import job
+
 MAX_FILENAME_LEN = 255
 
 
@@ -886,4 +889,22 @@
     for dirpath, dirnames, filenames in os.walk(path):
         for filename in filenames:
             total += os.path.getsize(os.path.join(dirpath, filename))
-    return total
\ No newline at end of file
+    return total
+
+
+def get_process_uptime(process):
+    """Returns the runtime in [[dd-]hh:]mm:ss, or '' if not running."""
+    pid = job.run('pidof %s' % process, ignore_status=True).stdout
+    runtime = ''
+    if pid:
+        runtime = job.run('ps -o etime= -p "%s"' % pid).stdout
+    return runtime
+
+
+def get_device_process_uptime(adb, process):
+    """Returns the uptime of a device process."""
+    pid = adb.shell('pidof %s' % process, ignore_status=True)
+    runtime = ''
+    if pid:
+        runtime = adb.shell('ps -o etime= -p "%s"' % pid)
+    return runtime
diff --git a/acts/framework/sample_config_2.json b/acts/framework/sample_config_2.json
new file mode 100644
index 0000000..6960c2b
--- /dev/null
+++ b/acts/framework/sample_config_2.json
@@ -0,0 +1,20 @@
+{
+    "_description": "This is an example skeleton test configuration file.",
+    "testbed": {
+        "Enterprise-D": {
+            "_description": "Sample testbed with two android devices",
+            "AndroidDevice": ["<serial>", "<serial>"]
+        },
+        "Enterprise-E": {
+            "_description": "Sample testbed with two android devices",
+            "AndroidDevice": [{"serial": "<serial>", "label": "caller"},
+                              {"serial": "<serial>", "label": "callee", "whatever": "anything"}]
+        },
+        "SampleTestBed": {
+            "_description": "Sample testbed with no devices"
+        }
+    },
+    "logpath": "/tmp/logs",
+    "testpaths": ["../tests/sample"],
+    "custom_param1": {"favorite_food": "Icecream!"}
+}
diff --git a/acts/framework/tests/acts_android_device_test.py b/acts/framework/tests/acts_android_device_test.py
index c42275d..02aa485 100755
--- a/acts/framework/tests/acts_android_device_test.py
+++ b/acts/framework/tests/acts_android_device_test.py
@@ -21,24 +21,18 @@
 import tempfile
 import unittest
 
-from acts import base_test
+from acts import logger
 from acts.controllers import android_device
 
 # Mock log path for a test run.
 MOCK_LOG_PATH = "/tmp/logs/MockTest/xx-xx-xx_xx-xx-xx/"
-# The expected result of the cat adb operation.
-MOCK_ADB_LOGCAT_CAT_RESULT = [
-    "02-29 14:02:21.456  4454  Something\n",
-    "02-29 14:02:21.789  4454  Something again\n"
-]
-# A mockd piece of adb logcat output.
-MOCK_ADB_LOGCAT = ("02-29 14:02:19.123  4454  Nothing\n"
-                   "%s"
-                   "02-29 14:02:22.123  4454  Something again and again\n"
-                   ) % ''.join(MOCK_ADB_LOGCAT_CAT_RESULT)
 # Mock start and end time of the adb cat.
-MOCK_ADB_LOGCAT_BEGIN_TIME = "02-29 14:02:20.123"
-MOCK_ADB_LOGCAT_END_TIME = "02-29 14:02:22.000"
+MOCK_ADB_LOGCAT_BEGIN_TIME = "1970-01-02 21:03:20.123"
+MOCK_ADB_LOGCAT_END_TIME = "1970-01-02 21:22:02.000"
+MOCK_ADB_EPOCH_BEGIN_TIME = 191000123
+
+MOCK_SERIAL = 1
+MOCK_BUILD_ID = "ABC1.123456.007"
 
 
 def get_mock_ads(num):
@@ -88,7 +82,9 @@
 
     def getprop(self, params):
         if params == "ro.build.id":
-            return "AB42"
+            return MOCK_BUILD_ID
+        elif params == "ro.build.version.incremental":
+            return "123456789"
         elif params == "ro.build.type":
             return "userdebug"
         elif params == "ro.build.product" or params == "ro.product.name":
@@ -253,20 +249,40 @@
     @mock.patch('acts.controllers.adb.AdbProxy', return_value=MockAdbProxy(1))
     @mock.patch(
         'acts.controllers.fastboot.FastbootProxy',
-        return_value=MockFastbootProxy(1))
-    def test_AndroidDevice_build_info(self, MockFastboot, MockAdbProxy):
+        return_value=MockFastbootProxy(MOCK_SERIAL))
+    def test_AndroidDevice_build_info_release(self, MockFastboot,
+                                              MockAdbProxy):
         """Verifies the AndroidDevice object's basic attributes are correctly
         set after instantiation.
         """
         ad = android_device.AndroidDevice(serial=1)
         build_info = ad.build_info
-        self.assertEqual(build_info["build_id"], "AB42")
+        self.assertEqual(build_info["build_id"], "ABC1.123456.007")
         self.assertEqual(build_info["build_type"], "userdebug")
 
     @mock.patch('acts.controllers.adb.AdbProxy', return_value=MockAdbProxy(1))
     @mock.patch(
         'acts.controllers.fastboot.FastbootProxy',
-        return_value=MockFastbootProxy(1))
+        return_value=MockFastbootProxy(MOCK_SERIAL))
+    def test_AndroidDevice_build_info_dev(self, MockFastboot, MockAdbProxy):
+        """Verifies the AndroidDevice object's basic attributes are correctly
+        set after instantiation.
+        """
+        global MOCK_BUILD_ID
+        ad = android_device.AndroidDevice(serial=1)
+        old_mock_build_id = MOCK_BUILD_ID
+        MOCK_BUILD_ID = "ABC-MR1"
+        build_info = ad.build_info
+        self.assertEqual(build_info["build_id"], "123456789")
+        self.assertEqual(build_info["build_type"], "userdebug")
+        MOCK_BUILD_ID = old_mock_build_id
+
+    @mock.patch(
+        'acts.controllers.adb.AdbProxy',
+        return_value=MockAdbProxy(MOCK_SERIAL))
+    @mock.patch(
+        'acts.controllers.fastboot.FastbootProxy',
+        return_value=MockFastbootProxy(MOCK_SERIAL))
     @mock.patch('acts.utils.create_dir')
     @mock.patch('acts.utils.exe_cmd')
     def test_AndroidDevice_take_bug_report(self, exe_mock, create_dir_mock,
@@ -351,7 +367,7 @@
                                          "AndroidDevice%s" % ad.serial,
                                          "adblog,fakemodel,%s.txt" % ad.serial)
         creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
-        adb_cmd = 'adb -s %s logcat -T 1 -v threadtime -b all >> %s'
+        adb_cmd = 'adb -s %s logcat -T 1 -v year -b all >> %s'
         start_proc_mock.assert_called_with(adb_cmd % (ad.serial,
                                                       expected_log_path))
         self.assertEqual(ad.adb_logcat_file_path, expected_log_path)
@@ -398,52 +414,42 @@
                                          "AndroidDevice%s" % ad.serial,
                                          "adblog,fakemodel,%s.txt" % ad.serial)
         creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
-        adb_cmd = 'adb -s %s logcat -T 1 -v threadtime -b radio >> %s'
+        adb_cmd = 'adb -s %s logcat -T 1 -v year -b radio >> %s'
         start_proc_mock.assert_called_with(adb_cmd % (ad.serial,
                                                       expected_log_path))
         self.assertEqual(ad.adb_logcat_file_path, expected_log_path)
+    @mock.patch(
+        'acts.controllers.adb.AdbProxy',
+        return_value=MockAdbProxy(MOCK_SERIAL))
+    def test_get_apk_process_id_process_cannot_find(self, adb_proxy):
+        ad = android_device.AndroidDevice(serial=MOCK_SERIAL)
+        ad.adb.return_value = "does_not_contain_value"
+        self.assertEqual(None, ad.get_package_pid("some_package"))
 
-    @mock.patch('acts.controllers.adb.AdbProxy', return_value=MockAdbProxy(1))
     @mock.patch(
-        'acts.controllers.fastboot.FastbootProxy',
-        return_value=MockFastbootProxy(1))
-    @mock.patch('acts.utils.start_standing_subprocess', return_value="process")
-    @mock.patch('acts.utils.stop_standing_subprocess')
+        'acts.controllers.adb.AdbProxy',
+        return_value=MockAdbProxy(MOCK_SERIAL))
+    def test_get_apk_process_id_process_exists_second_try(self, adb_proxy):
+        ad = android_device.AndroidDevice(serial=MOCK_SERIAL)
+        ad.adb.return_multiple = True
+        ad.adb.return_value = ["", "system 1 2 3 4  S com.some_package"]
+        self.assertEqual(1, ad.get_package_pid("some_package"))
+
     @mock.patch(
-        'acts.logger.get_log_line_timestamp',
-        return_value=MOCK_ADB_LOGCAT_END_TIME)
-    @mock.patch('acts.utils._assert_subprocess_running')
-    def test_AndroidDevice_cat_adb_log(
-            self, check_proc_mock, mock_timestamp_getter, stop_proc_mock,
-            start_proc_mock, FastbootProxy, MockAdbProxy):
-        """Verifies that AndroidDevice.cat_adb_log loads the correct adb log
-        file, locates the correct adb log lines within the given time range,
-        and writes the lines to the correct output file.
-        """
-        mock_serial = 1
-        ad = android_device.AndroidDevice(serial=mock_serial)
-        # Expect error if attempted to cat adb log before starting adb logcat.
-        expected_msg = ("Attempting to cat adb log when none has been "
-                        "collected on Android device .*")
-        with self.assertRaisesRegex(android_device.AndroidDeviceError,
-                                    expected_msg):
-            ad.cat_adb_log("some_test", MOCK_ADB_LOGCAT_BEGIN_TIME)
-        ad.start_adb_logcat()
-        # Direct the log path of the ad to a temp dir to avoid racing.
-        ad.log_path = os.path.join(self.tmp_dir, ad.log_path)
-        mock_adb_log_path = os.path.join(ad.log_path, "adblog,%s,%s.txt" %
-                                         (ad.model, ad.serial))
-        with open(mock_adb_log_path, 'w') as f:
-            f.write(MOCK_ADB_LOGCAT)
-        ad.cat_adb_log("some_test", MOCK_ADB_LOGCAT_BEGIN_TIME)
-        cat_file_path = os.path.join(
-            ad.log_path, "AdbLogExcerpts",
-            ("some_test,02-29 14:02:20.123,%s,%s.txt") % (ad.model, ad.serial))
-        with open(cat_file_path, 'r') as f:
-            actual_cat = f.read()
-        self.assertEqual(actual_cat, ''.join(MOCK_ADB_LOGCAT_CAT_RESULT))
-        # Stops adb logcat.
-        ad.stop_adb_logcat()
+        'acts.controllers.adb.AdbProxy',
+        return_value=MockAdbProxy(MOCK_SERIAL))
+    def test_get_apk_process_id_bad_return(self, adb_proxy):
+        ad = android_device.AndroidDevice(serial=MOCK_SERIAL)
+        ad.adb.return_value = "bad_return_index_error"
+        self.assertEqual(None, ad.get_package_pid("some_package"))
+
+    @mock.patch(
+        'acts.controllers.adb.AdbProxy',
+        return_value=MockAdbProxy(MOCK_SERIAL))
+    def test_get_apk_process_id_bad_return(self, adb_proxy):
+        ad = android_device.AndroidDevice(serial=MOCK_SERIAL)
+        ad.adb.return_value = "bad return value error"
+        self.assertEqual(None, ad.get_package_pid("some_package"))
 
 
 if __name__ == "__main__":
diff --git a/acts/framework/tests/acts_base_class_test.py b/acts/framework/tests/acts_base_class_test.py
index 75bd2e2..7ee3e85 100755
--- a/acts/framework/tests/acts_base_class_test.py
+++ b/acts/framework/tests/acts_base_class_test.py
@@ -53,9 +53,10 @@
     def test_current_test_case_name(self):
         class MockBaseTest(base_test.BaseTestClass):
             def test_func(self):
-                asserts.assert_true(self.current_test_name == "test_func", (
-                    "Got "
-                    "unexpected test name %s.") % self.current_test_name)
+                asserts.assert_true(
+                        self.current_test_name == "test_func",
+                        ("Got "
+                         "unexpected test name %s.") % self.current_test_name)
 
         bt_cls = MockBaseTest(self.mock_test_cls_configs)
         bt_cls.run(test_names=["test_func"])
@@ -281,7 +282,7 @@
         self.assertIsNone(actual_record.details)
         self.assertIsNone(actual_record.extras)
         expected_extra_error = {"teardown_test": MSG_EXPECTED_EXCEPTION}
-        self.assertEqual(actual_record.extra_errors, expected_extra_error)
+        self.assertEqual(actual_record.additional_errors, expected_extra_error)
         expected_summary = (
             "Blocked 0, ControllerInfo {}, Executed 1, Failed 0, Passed 0, "
             "Requested 1, Skipped 0, Unknown 1")
@@ -465,7 +466,7 @@
         bt_cls = MockBaseTest(self.mock_test_cls_configs)
         bt_cls.run()
         actual_record = bt_cls.results.unknown[0]
-        self.assertIn('_on_fail', actual_record.extra_errors)
+        self.assertIn('_on_fail', actual_record.additional_errors)
         self.assertEqual(actual_record.test_name, self.mock_test_name)
         self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
         self.assertIsNone(actual_record.extras)
@@ -488,7 +489,7 @@
         bt_cls.run()
         actual_record = bt_cls.results.unknown[0]
         expected_extra_error = {'_on_pass': expected_msg}
-        self.assertEqual(actual_record.extra_errors, expected_extra_error)
+        self.assertEqual(actual_record.additional_errors, expected_extra_error)
         self.assertEqual(actual_record.test_name, self.mock_test_name)
         self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
         self.assertIsNone(actual_record.extras)
@@ -511,7 +512,7 @@
         self.assertEqual(actual_record.test_name, self.mock_test_name)
         self.assertEqual(actual_record.details, "Test Body Exception.")
         self.assertIsNone(actual_record.extras)
-        self.assertEqual(actual_record.extra_errors["teardown_test"],
+        self.assertEqual(actual_record.additional_errors["teardown_test"],
                          "Details=This is an expected exception., Extras=None")
         expected_summary = (
             "Blocked 0, ControllerInfo {}, Executed 1, Failed 0, Passed 0, "
@@ -535,7 +536,7 @@
         self.assertEqual(actual_record.test_name, self.mock_test_name)
         self.assertEqual(actual_record.details, "Test Passed!")
         self.assertIsNone(actual_record.extras)
-        self.assertEqual(actual_record.extra_errors["teardown_test"],
+        self.assertEqual(actual_record.additional_errors["teardown_test"],
                          "Details=This is an expected exception., Extras=None")
         expected_summary = (
             "Blocked 0, ControllerInfo {}, Executed 1, Failed 0, Passed 0, "
@@ -557,8 +558,9 @@
         self.assertEqual(actual_record.test_name, self.mock_test_name)
         self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
         self.assertEqual(actual_record.extras, MOCK_EXTRA)
-        self.assertEqual(actual_record.extra_errors,
-                         {'_on_pass': MSG_EXPECTED_EXCEPTION})
+        self.assertEqual(actual_record.additional_errors, {
+            '_on_pass': MSG_EXPECTED_EXCEPTION
+        })
         expected_summary = (
             "Blocked 0, ControllerInfo {}, Executed 1, Failed 0, Passed 0, "
             "Requested 1, Skipped 0, Unknown 1")
@@ -579,8 +581,9 @@
         self.assertEqual(actual_record.test_name, self.mock_test_name)
         self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
         self.assertEqual(actual_record.extras, MOCK_EXTRA)
-        self.assertEqual(actual_record.extra_errors,
-                         {'_on_fail': MSG_EXPECTED_EXCEPTION})
+        self.assertEqual(actual_record.additional_errors, {
+            '_on_fail': MSG_EXPECTED_EXCEPTION
+        })
         expected_summary = (
             "Blocked 0, ControllerInfo {}, Executed 1, Failed 0, Passed 0, "
             "Requested 1, Skipped 0, Unknown 1")
@@ -603,9 +606,10 @@
         self.assertEqual(bt_cls.results.passed[0].test_name, "test_1")
         self.assertEqual(bt_cls.results.failed[0].details,
                          MSG_EXPECTED_EXCEPTION)
-        self.assertEqual(bt_cls.results.summary_str(), (
-            "Blocked 0, ControllerInfo {}, Executed 2, Failed 1, Passed 1, "
-            "Requested 3, Skipped 0, Unknown 0"))
+        self.assertEqual(
+            bt_cls.results.summary_str(),
+            ("Blocked 0, ControllerInfo {}, Executed 2, Failed 1, Passed 1, "
+             "Requested 3, Skipped 0, Unknown 0"))
 
     def test_uncaught_exception(self):
         class MockBaseTest(base_test.BaseTestClass):
diff --git a/acts/framework/tests/acts_import_test_utils_test.py b/acts/framework/tests/acts_import_test_utils_test.py
index 4cfa93d..2171b38 100755
--- a/acts/framework/tests/acts_import_test_utils_test.py
+++ b/acts/framework/tests/acts_import_test_utils_test.py
@@ -46,7 +46,6 @@
 from acts.test_utils.tel import tel_video_utils
 from acts.test_utils.tel import tel_voice_utils
 
-from acts.test_utils.wifi import wifi_aware_const
 from acts.test_utils.wifi import wifi_constants
 from acts.test_utils.wifi import wifi_test_utils
 
diff --git a/acts/framework/tests/acts_logger_test.py b/acts/framework/tests/acts_logger_test.py
index 8d26778..dd18ae7 100755
--- a/acts/framework/tests/acts_logger_test.py
+++ b/acts/framework/tests/acts_logger_test.py
@@ -25,7 +25,7 @@
 
     def test_epoch_to_log_line_timestamp(self):
         actual_stamp = logger.epoch_to_log_line_timestamp(1469134262116)
-        self.assertEqual("07-21 13:51:02.116", actual_stamp)
+        self.assertEqual("2016-07-21 13:51:02.116", actual_stamp)
 
 
 if __name__ == "__main__":
diff --git a/acts/framework/tests/acts_records_test.py b/acts/framework/tests/acts_records_test.py
index bc0a250..cbf6561 100755
--- a/acts/framework/tests/acts_records_test.py
+++ b/acts/framework/tests/acts_records_test.py
@@ -48,11 +48,12 @@
         d[records.TestResultEnums.RECORD_EXTRAS] = extras
         d[records.TestResultEnums.RECORD_BEGIN_TIME] = record.begin_time
         d[records.TestResultEnums.RECORD_END_TIME] = record.end_time
-        d[records.TestResultEnums.RECORD_LOG_BEGIN_TIME] = record.log_begin_time
+        d[records.TestResultEnums.
+            RECORD_LOG_BEGIN_TIME] = record.log_begin_time
         d[records.TestResultEnums.RECORD_LOG_END_TIME] = record.log_end_time
         d[records.TestResultEnums.RECORD_UID] = None
         d[records.TestResultEnums.RECORD_CLASS] = None
-        d[records.TestResultEnums.RECORD_EXTRA_ERRORS] = {}
+        d[records.TestResultEnums.RECORD_ADDITIONAL_ERRORS] = {}
         actual_d = record.to_dict()
         self.assertDictEqual(actual_d, d)
         # Verify that these code paths do not cause crashes and yield non-empty
diff --git a/acts/framework/tests/acts_sl4a_client_test.py b/acts/framework/tests/acts_sl4a_client_test.py
deleted file mode 100755
index 1754d4f..0000000
--- a/acts/framework/tests/acts_sl4a_client_test.py
+++ /dev/null
@@ -1,255 +0,0 @@
-#!/usr/bin/env python3.4
-#
-#   Copyright 2016 - The Android Open Source Project
-#
-#   Licensed under the Apache License, Version 2.0 (the "License");
-#   you may not use this file except in compliance with the License.
-#   You may obtain a copy of the License at
-#
-#       http://www.apache.org/licenses/LICENSE-2.0
-#
-#   Unless required by applicable law or agreed to in writing, software
-#   distributed under the License is distributed on an "AS IS" BASIS,
-#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#   See the License for the specific language governing permissions and
-#   limitations under the License.
-
-from builtins import str
-
-import json
-import mock
-import socket
-import unittest
-
-from acts.controllers import sl4a_client
-
-MOCK_RESP = b'{"id": 0, "result": 123, "error": null, "status": 1, "uid": 1}'
-MOCK_RESP_TEMPLATE = '{"id": %d, "result": 123, "error": null, "status": 1, "uid": 1}'
-MOCK_RESP_UNKWN_STATUS = b'{"id": 0, "result": 123, "error": null, "status": 0}'
-MOCK_RESP_WITH_ERROR = b'{"id": 0, "error": 1, "status": 1, "uid": 1}'
-
-
-class MockSocketFile(object):
-    def __init__(self, resp):
-        self.resp = resp
-        self.last_write = None
-
-    def write(self, msg):
-        self.last_write = msg
-
-    def readline(self):
-        return self.resp
-
-    def flush(self):
-        pass
-
-
-class ActsSl4aClientTest(unittest.TestCase):
-    """This test class has unit tests for the implementation of everything
-    under acts.controllers.android, which is the RPC client module for sl4a.
-    """
-
-    def setup_mock_socket_file(self, mock_create_connection):
-        """Sets up a fake socket file from the mock connection.
-
-        Args:
-            mock_create_connection: The mock method for creating a method.
-
-        Returns:
-            The mock file that will be injected into the code.
-        """
-        fake_file = MockSocketFile(MOCK_RESP)
-        fake_conn = mock.MagicMock()
-        fake_conn.makefile.return_value = fake_file
-        mock_create_connection.return_value = fake_conn
-
-        return fake_file
-
-    @mock.patch('socket.create_connection')
-    def test_open_timeout_io_error(self, mock_create_connection):
-        """Test socket timeout with io error
-
-        Test that if the net socket gives an io error, then the sl4a client
-        will eventually exit with an IOError.
-        """
-        mock_create_connection.side_effect = IOError()
-
-        with self.assertRaises(IOError):
-            client = sl4a_client.Sl4aClient()
-            client.open(connection_timeout=0.1)
-
-    @mock.patch('socket.create_connection')
-    def test_open_timeout(self, mock_create_connection):
-        """Test socket timeout
-
-        Test that a timeout exception will be raised if the socket gives a
-        timeout.
-        """
-        mock_create_connection.side_effect = socket.timeout
-
-        with self.assertRaises(socket.timeout):
-            client = sl4a_client.Sl4aClient()
-            client.open(connection_timeout=0.1)
-
-    @mock.patch('socket.create_connection')
-    def test_handshake_error(self, mock_create_connection):
-        """Test error in sl4a handshake
-
-        Test that if there is an error in the sl4a handshake then a protocol
-        error will be raised.
-        """
-        fake_conn = mock.MagicMock()
-        fake_conn.makefile.return_value = MockSocketFile(None)
-        mock_create_connection.return_value = fake_conn
-
-        with self.assertRaises(sl4a_client.Sl4aProtocolError):
-            client = sl4a_client.Sl4aClient()
-            client.open()
-
-    @mock.patch('socket.create_connection')
-    def test_open_handshake(self, mock_create_connection):
-        """Test sl4a client handshake
-
-        Test that at the end of a handshake with no errors the client object
-        has the correct parameters.
-        """
-        fake_conn = mock.MagicMock()
-        fake_conn.makefile.return_value = MockSocketFile(MOCK_RESP)
-        mock_create_connection.return_value = fake_conn
-
-        client = sl4a_client.Sl4aClient()
-        client.open()
-
-        self.assertEqual(client.uid, 1)
-
-    @mock.patch('socket.create_connection')
-    def test_open_handshake_unknown_status(self, mock_create_connection):
-        """Test handshake with unknown status response
-
-        Test that when the handshake is given an unknown status then the client
-        will not be given a uid.
-        """
-        fake_conn = mock.MagicMock()
-        fake_conn.makefile.return_value = MockSocketFile(
-            MOCK_RESP_UNKWN_STATUS)
-        mock_create_connection.return_value = fake_conn
-
-        client = sl4a_client.Sl4aClient()
-        client.open()
-
-        self.assertEqual(client.uid, sl4a_client.UNKNOWN_UID)
-
-    @mock.patch('socket.create_connection')
-    def test_open_no_response(self, mock_create_connection):
-        """Test handshake no response
-
-        Test that if a handshake recieves no response then it will give a
-        protocol error.
-        """
-        fake_file = self.setup_mock_socket_file(mock_create_connection)
-
-        client = sl4a_client.Sl4aClient()
-        client.open()
-
-        fake_file.resp = None
-
-        with self.assertRaises(
-                sl4a_client.Sl4aProtocolError,
-                msg=sl4a_client.Sl4aProtocolError.NO_RESPONSE_FROM_HANDSHAKE):
-            client.some_rpc(1, 2, 3)
-
-    @mock.patch('socket.create_connection')
-    def test_rpc_error_response(self, mock_create_connection):
-        """Test rpc that is given an error response
-
-        Test that when an rpc recieves a reponse with an error will raised
-        an api error.
-        """
-        fake_file = self.setup_mock_socket_file(mock_create_connection)
-
-        client = sl4a_client.Sl4aClient()
-        client.open()
-
-        fake_file.resp = MOCK_RESP_WITH_ERROR
-
-        with self.assertRaises(sl4a_client.Sl4aApiError, msg=1):
-            client.some_rpc(1, 2, 3)
-
-    @mock.patch('socket.create_connection')
-    def test_rpc_id_mismatch(self, mock_create_connection):
-        """Test rpc that returns a different id than expected
-
-        Test that if an rpc returns with an id that is different than what
-        is expected will give a protocl error.
-        """
-        fake_file = self.setup_mock_socket_file(mock_create_connection)
-
-        client = sl4a_client.Sl4aClient()
-        client.open()
-
-        fake_file.resp = (MOCK_RESP_TEMPLATE % 52).encode('utf8')
-
-        with self.assertRaises(
-                sl4a_client.Sl4aProtocolError,
-                msg=sl4a_client.Sl4aProtocolError.MISMATCHED_API_ID):
-            client.some_rpc(1, 2, 3)
-
-    @mock.patch('socket.create_connection')
-    def test_rpc_no_response(self, mock_create_connection):
-        """Test rpc that does not get a reponse
-
-        Test that when an rpc does not get a response it throws a protocol
-        error.
-        """
-        fake_file = self.setup_mock_socket_file(mock_create_connection)
-
-        client = sl4a_client.Sl4aClient()
-        client.open()
-
-        fake_file.resp = None
-
-        with self.assertRaises(
-                sl4a_client.Sl4aProtocolError,
-                msg=sl4a_client.Sl4aProtocolError.NO_RESPONSE_FROM_SERVER):
-            client.some_rpc(1, 2, 3)
-
-    @mock.patch('socket.create_connection')
-    def test_rpc_send_to_socket(self, mock_create_connection):
-        """Test rpc sending and recieving
-
-        Tests that when an rpc is sent and received the corrent data
-        is used.
-        """
-        fake_file = self.setup_mock_socket_file(mock_create_connection)
-
-        client = sl4a_client.Sl4aClient()
-        client.open()
-
-        result = client.some_rpc(1, 2, 3)
-        self.assertEqual(result, 123)
-
-        expected = {'id': 0, 'method': 'some_rpc', 'params': [1, 2, 3]}
-        actual = json.loads(fake_file.last_write.decode('utf-8'))
-
-        self.assertEqual(expected, actual)
-
-    @mock.patch('socket.create_connection')
-    def test_rpc_call_increment_counter(self, mock_create_connection):
-        """Test rpc counter
-
-        Test that with each rpc call the counter is incremented by 1.
-        """
-        fake_file = self.setup_mock_socket_file(mock_create_connection)
-
-        client = sl4a_client.Sl4aClient()
-        client.open()
-
-        for i in range(0, 10):
-            fake_file.resp = (MOCK_RESP_TEMPLATE % i).encode('utf-8')
-            client.some_rpc()
-
-        self.assertEqual(next(client._counter), 10)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/acts/framework/tests/acts_test_runner_test.py b/acts/framework/tests/acts_test_runner_test.py
index 2e2809e..451ee98 100755
--- a/acts/framework/tests/acts_test_runner_test.py
+++ b/acts/framework/tests/acts_test_runner_test.py
@@ -74,11 +74,10 @@
         tr = test_runner.TestRunner(mock_test_config, self.mock_run_list)
         tr.register_controller(mock_controller)
         registered_name = "mock_controller"
-        self.assertTrue(registered_name in tr.controller_registry)
-        mock_ctrlrs = tr.controller_registry[registered_name]
+        self.assertTrue(mock_controller in tr.controller_registry)
+        mock_ctrlrs = tr.controller_registry[mock_controller]
         self.assertEqual(mock_ctrlrs[0].magic, "magic1")
         self.assertEqual(mock_ctrlrs[1].magic, "magic2")
-        self.assertTrue(tr.controller_destructors[registered_name])
         expected_msg = "Controller module .* has already been registered."
         with self.assertRaisesRegexp(signals.ControllerError, expected_msg):
             tr.register_controller(mock_controller)
@@ -114,17 +113,16 @@
                 "magic1", "magic2"
             ]
             tr = test_runner.TestRunner(mock_test_config, self.mock_run_list)
-            tr.register_controller(mock_controller)
+            tr.register_controller(mock_controller, builtin=True)
             self.assertTrue(mock_ref_name in tr.test_run_info)
-            self.assertTrue(mock_ref_name in tr.controller_registry)
+            self.assertTrue(mock_controller in tr.controller_registry)
             mock_ctrlrs = tr.test_run_info[mock_ctrlr_ref_name]
             self.assertEqual(mock_ctrlrs[0].magic, "magic1")
             self.assertEqual(mock_ctrlrs[1].magic, "magic2")
-            self.assertTrue(tr.controller_destructors[mock_ctrlr_ref_name])
             expected_msg = "Controller module .* has already been registered."
             with self.assertRaisesRegexp(signals.ControllerError,
                                          expected_msg):
-                tr.register_controller(mock_controller)
+                tr.register_controller(mock_controller, builtin=True)
         finally:
             delattr(mock_controller, "ACTS_CONTROLLER_REFERENCE_NAME")
 
@@ -172,16 +170,14 @@
             "magic": "Magic2"
         }]
         mock_test_config[tb_key][mock_ctrlr_config_name] = my_config
-        tr = test_runner.TestRunner(mock_test_config, [('IntegrationTest',
-                                                        None)])
+        tr = test_runner.TestRunner(mock_test_config,
+                                    [('IntegrationTest', None)])
         tr.run()
         self.assertFalse(tr.controller_registry)
-        self.assertFalse(tr.controller_destructors)
         self.assertTrue(mock_test_config[tb_key][mock_ctrlr_config_name][0])
         tr.run()
         tr.stop()
         self.assertFalse(tr.controller_registry)
-        self.assertFalse(tr.controller_destructors)
         results = tr.results.summary_dict()
         self.assertEqual(results["Requested"], 2)
         self.assertEqual(results["Executed"], 2)
@@ -236,12 +232,12 @@
             "serial": "1",
             "skip_sl4a": True
         }]
-        tr = test_runner.TestRunner(mock_test_config, [(
-            'IntegrationTest', None), ('IntegrationTest', None)])
+        tr = test_runner.TestRunner(mock_test_config,
+                                    [('IntegrationTest', None),
+                                     ('IntegrationTest', None)])
         tr.run()
         tr.stop()
         self.assertFalse(tr.controller_registry)
-        self.assertFalse(tr.controller_destructors)
         results = tr.results.summary_dict()
         self.assertEqual(results["Requested"], 2)
         self.assertEqual(results["Executed"], 2)
diff --git a/acts/framework/tests/acts_unittest_suite.py b/acts/framework/tests/acts_unittest_suite.py
index 02a9c6e..3b68d08 100755
--- a/acts/framework/tests/acts_unittest_suite.py
+++ b/acts/framework/tests/acts_unittest_suite.py
@@ -23,7 +23,6 @@
 import acts_host_utils_test
 import acts_logger_test
 import acts_records_test
-import acts_sl4a_client_test
 import acts_test_runner_test
 import acts_utils_test
 
@@ -34,10 +33,8 @@
         acts_base_class_test.ActsBaseClassTest,
         acts_test_runner_test.ActsTestRunnerTest,
         acts_android_device_test.ActsAndroidDeviceTest,
-        acts_records_test.ActsRecordsTest,
-        acts_sl4a_client_test.ActsSl4aClientTest,
-        acts_utils_test.ActsUtilsTest, acts_logger_test.ActsLoggerTest,
-        acts_host_utils_test.ActsHostUtilsTest
+        acts_records_test.ActsRecordsTest, acts_utils_test.ActsUtilsTest,
+        acts_logger_test.ActsLoggerTest, acts_host_utils_test.ActsHostUtilsTest
     ]
 
     loader = unittest.TestLoader()
diff --git a/acts/framework/tests/controllers/__init__.py b/acts/framework/tests/controllers/__init__.py
new file mode 100644
index 0000000..9727988
--- /dev/null
+++ b/acts/framework/tests/controllers/__init__.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
diff --git a/acts/framework/tests/controllers/sl4a_lib/__init__.py b/acts/framework/tests/controllers/sl4a_lib/__init__.py
new file mode 100644
index 0000000..9727988
--- /dev/null
+++ b/acts/framework/tests/controllers/sl4a_lib/__init__.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
diff --git a/acts/framework/tests/controllers/sl4a_lib/rpc_client_test.py b/acts/framework/tests/controllers/sl4a_lib/rpc_client_test.py
new file mode 100644
index 0000000..dbaa2ab
--- /dev/null
+++ b/acts/framework/tests/controllers/sl4a_lib/rpc_client_test.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import unittest
+
+import mock
+
+from acts.controllers.sl4a_lib import rpc_client
+
+
+class BreakoutError(Exception):
+    """Thrown to prove program execution."""
+
+
+class RpcClientTest(unittest.TestCase):
+    """Tests the rpc_client.RpcClient class."""
+
+    def test_terminate_warn_on_working_connections(self):
+        """Tests rpc_client.RpcClient.terminate().
+
+        Tests that if some connections are still working, we log this before
+        closing the connections.
+        """
+        session = mock.Mock()
+
+        client = rpc_client.RpcClient(session.uid, session.adb.serial,
+                                      lambda _: mock.Mock(),
+                                      lambda _: mock.Mock())
+        client._log = mock.Mock()
+        client._working_connections = [mock.Mock()]
+
+        client.terminate()
+
+        self.assertTrue(client._log.warning.called)
+
+    def test_terminate_closes_all_connections(self):
+        """Tests rpc_client.RpcClient.terminate().
+
+        Tests that all free and working connections have been closed.
+        """
+        session = mock.Mock()
+
+        client = rpc_client.RpcClient(session.uid, session.adb.serial,
+                                      lambda _: mock.Mock(),
+                                      lambda _: mock.Mock())
+        client._log = mock.Mock()
+        working_connections = [mock.Mock() for _ in range(3)]
+        free_connections = [mock.Mock() for _ in range(3)]
+        client._free_connections = free_connections
+        client._working_connections = working_connections
+
+        client.terminate()
+
+        for connection in working_connections + free_connections:
+            self.assertTrue(connection.close.called)
+
+    def test_get_free_connection_get_available_client(self):
+        """Tests rpc_client.RpcClient._get_free_connection().
+
+        Tests that an available client is returned if one exists.
+        """
+
+        def fail_on_wrong_execution():
+            self.fail('The program is not executing the expected path. '
+                      'Tried to return an available free client, ended up '
+                      'sleeping to wait for client instead.')
+
+        session = mock.Mock()
+
+        client = rpc_client.RpcClient(session.uid, session.adb.serial,
+                                      lambda _: mock.Mock(),
+                                      lambda _: mock.Mock())
+        expected_connection = mock.Mock()
+        client._free_connections = [expected_connection]
+        client._lock = mock.MagicMock()
+
+        with mock.patch('time.sleep') as sleep_mock:
+            sleep_mock.side_effect = fail_on_wrong_execution
+
+            connection = client._get_free_connection()
+
+        self.assertEqual(connection, expected_connection)
+        self.assertTrue(expected_connection in client._working_connections)
+        self.assertEqual(len(client._free_connections), 0)
+
+    def test_get_free_connection_continues_upon_connection_taken(self):
+        """Tests rpc_client.RpcClient._get_free_connection().
+
+        Tests that if the free connection is taken while trying to acquire the
+        lock to reserve it, the thread gives up the lock and tries again.
+        """
+
+        def empty_list():
+            client._free_connections.clear()
+
+        def fail_on_wrong_execution():
+            self.fail('The program is not executing the expected path. '
+                      'Tried to return an available free client, ended up '
+                      'sleeping to wait for client instead.')
+
+        session = mock.Mock()
+
+        client = rpc_client.RpcClient(session.uid, session.adb.serial,
+                                      lambda _: mock.Mock(),
+                                      lambda _: mock.Mock())
+        client._free_connections = mock.Mock()
+        client._lock = mock.MagicMock()
+        client._lock.acquire.side_effect = empty_list
+        client._free_connections = [mock.Mock()]
+
+        with mock.patch('time.sleep') as sleep_mock:
+            sleep_mock.side_effect = fail_on_wrong_execution
+
+            try:
+                client._get_free_connection()
+            except IndexError:
+                self.fail('Tried to pop free connection when another thread'
+                          'has taken it.')
+        # Assert that the lock has been freed.
+        self.assertEqual(client._lock.acquire.call_count,
+                         client._lock.release.call_count)
+
+    def test_get_free_connection_sleep(self):
+        """Tests rpc_client.RpcClient._get_free_connection().
+
+        Tests that if the free connection is taken, it will wait for a new one.
+        """
+
+        session = mock.Mock()
+
+        client = rpc_client.RpcClient(session.uid, session.adb.serial,
+                                      lambda _: mock.Mock(),
+                                      lambda _: mock.Mock())
+        client._free_connections = []
+        client.max_connections = 0
+        client._lock = mock.MagicMock()
+        client._free_connections = []
+
+        with mock.patch('time.sleep') as sleep_mock:
+            sleep_mock.side_effect = BreakoutError()
+            try:
+                client._get_free_connection()
+            except BreakoutError:
+                # Assert that the lock has been freed.
+                self.assertEqual(client._lock.acquire.call_count,
+                                 client._lock.release.call_count)
+                # Asserts that the sleep has been called.
+                self.assertTrue(sleep_mock.called)
+                # Asserts that no changes to connections happened
+                self.assertEqual(len(client._free_connections), 0)
+                self.assertEqual(len(client._working_connections), 0)
+                return True
+        self.fail('Failed to hit sleep case')
+
+    def test_release_working_connection(self):
+        """Tests rpc_client.RpcClient._release_working_connection.
+
+        Tests that the working connection is moved into the free connections.
+        """
+        session = mock.Mock()
+        client = rpc_client.RpcClient(session.uid, session.adb.serial,
+                                      lambda _: mock.Mock(),
+                                      lambda _: mock.Mock())
+
+        connection = mock.Mock()
+        client._working_connections = [connection]
+        client._free_connections = []
+        client._release_working_connection(connection)
+
+        self.assertTrue(connection in client._free_connections)
+        self.assertFalse(connection in client._working_connections)
+
+    def test_future(self):
+        """Tests rpc_client.RpcClient.future.
+
+        """
+        session = mock.Mock()
+        client = rpc_client.RpcClient(session.uid, session.adb.serial,
+                                      lambda _: mock.Mock(),
+                                      lambda _: mock.Mock())
+
+        self.assertEqual(client.future, client._async_client)
+
+    def test_getattr(self):
+        """Tests rpc_client.RpcClient.__getattr__.
+
+        Tests that the name, args, and kwargs are correctly passed to self.rpc.
+        """
+        session = mock.Mock()
+        client = rpc_client.RpcClient(session.uid, session.adb.serial,
+                                      lambda _: mock.Mock(),
+                                      lambda _: mock.Mock())
+        client.rpc = mock.MagicMock()
+        fn = client.fake_function_please_do_not_be_implemented
+
+        fn('arg1', 'arg2', kwarg1=1, kwarg2=2)
+        client.rpc.assert_called_with(
+            'fake_function_please_do_not_be_implemented',
+            'arg1',
+            'arg2',
+            kwarg1=1,
+            kwarg2=2)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/acts/framework/tests/controllers/sl4a_lib/rpc_connection_test.py b/acts/framework/tests/controllers/sl4a_lib/rpc_connection_test.py
new file mode 100755
index 0000000..ce1d1a6
--- /dev/null
+++ b/acts/framework/tests/controllers/sl4a_lib/rpc_connection_test.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2016 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import mock
+import unittest
+
+from acts.controllers.sl4a_lib import rpc_client, rpc_connection
+
+MOCK_RESP = b'{"id": 0, "result": 123, "error": null, "status": 1, "uid": 1}'
+MOCK_RESP_UNKNOWN_UID = b'{"id": 0, "result": 123, "error": null, "status": 0}'
+MOCK_RESP_WITH_ERROR = b'{"id": 0, "error": 1, "status": 1, "uid": 1}'
+
+
+class MockSocketFile(object):
+    def __init__(self, resp):
+        self.resp = resp
+        self.last_write = None
+
+    def write(self, msg):
+        self.last_write = msg
+
+    def readline(self):
+        return self.resp
+
+    def flush(self):
+        pass
+
+
+class RpcConnectionTest(unittest.TestCase):
+    """This test class has unit tests for the implementation of everything
+    under acts.controllers.android, which is the RPC client module for sl4a.
+    """
+
+    @staticmethod
+    def mock_rpc_connection(response=MOCK_RESP,
+                            uid=rpc_connection.UNKNOWN_UID):
+        """Sets up a faked socket file from the mock connection."""
+        fake_file = MockSocketFile(response)
+        fake_conn = mock.MagicMock()
+        fake_conn.makefile.return_value = fake_file
+        adb = mock.Mock()
+        ports = mock.Mock()
+
+        return rpc_connection.RpcConnection(
+            adb, ports, fake_conn, fake_file, uid=uid)
+
+    def test_open_chooses_init_on_unknown_uid(self):
+        """Tests rpc_connection.RpcConnection.open().
+
+        Tests that open uses the init start command when the uid is unknown.
+        """
+
+        def pass_on_init(start_command):
+            if not start_command == rpc_connection.Sl4aConnectionCommand.INIT:
+                self.fail(
+                    'Must call "init". Called "%s" instead.' % start_command)
+
+        connection = self.mock_rpc_connection()
+        connection._initiate_handshake = pass_on_init
+        connection.open()
+
+    def test_open_chooses_continue_on_known_uid(self):
+        """Tests rpc_connection.RpcConnection.open().
+
+        Tests that open uses the continue start command when the uid is known.
+        """
+
+        def pass_on_continue(start_command):
+            if start_command != rpc_connection.Sl4aConnectionCommand.CONTINUE:
+                self.fail('Must call "continue". Called "%s" instead.' %
+                          start_command)
+
+        connection = self.mock_rpc_connection(uid=1)
+        connection._initiate_handshake = pass_on_continue
+        connection.open()
+
+    def test_initiate_handshake_returns_uid(self):
+        """Tests rpc_connection.RpcConnection._initiate_handshake().
+
+        Test that at the end of a handshake with no errors the client object
+        has the correct parameters.
+        """
+        connection = self.mock_rpc_connection()
+        connection._initiate_handshake(
+            rpc_connection.Sl4aConnectionCommand.INIT)
+
+        self.assertEqual(connection.uid, 1)
+
+    def test_initiate_handshake_returns_unknown_status(self):
+        """Tests rpc_connection.RpcConnection._initiate_handshake().
+
+        Test that when the handshake is given an unknown uid then the client
+        will not be given a uid.
+        """
+        connection = self.mock_rpc_connection(MOCK_RESP_UNKNOWN_UID)
+        connection._initiate_handshake(
+            rpc_connection.Sl4aConnectionCommand.INIT)
+
+        self.assertEqual(connection.uid, rpc_client.UNKNOWN_UID)
+
+    def test_initiate_handshake_no_response(self):
+        """Tests rpc_connection.RpcConnection._initiate_handshake().
+
+        Test that if a handshake receives no response then it will give a
+        protocol error.
+        """
+        connection = self.mock_rpc_connection(b'')
+
+        with self.assertRaises(
+                rpc_client.Sl4aProtocolError,
+                msg=rpc_client.Sl4aProtocolError.NO_RESPONSE_FROM_HANDSHAKE):
+            connection._initiate_handshake(
+                rpc_connection.Sl4aConnectionCommand.INIT)
+
+    def test_cmd_properly_formatted(self):
+        """Tests rpc_connection.RpcConnection._cmd().
+
+        Tests that the command sent is properly formatted.
+        """
+        connection = self.mock_rpc_connection(MOCK_RESP)
+        connection._cmd('test')
+        self.assertIn(
+            connection._socket_file.last_write,
+            [b'{"cmd": "test", "uid": -1}\n', b'{"uid": -1, "cmd": "test"}\n'])
+
+    def test_get_new_ticket(self):
+        """Tests rpc_connection.RpcConnection.get_new_ticket().
+
+        Tests that a new number is always given for get_new_ticket().
+        """
+        connection = self.mock_rpc_connection(MOCK_RESP)
+        self.assertEqual(connection.get_new_ticket() + 1,
+                         connection.get_new_ticket())
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/acts/framework/tests/controllers/sl4a_lib/sl4a_manager_test.py b/acts/framework/tests/controllers/sl4a_lib/sl4a_manager_test.py
new file mode 100644
index 0000000..ded5f65
--- /dev/null
+++ b/acts/framework/tests/controllers/sl4a_lib/sl4a_manager_test.py
@@ -0,0 +1,481 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import mock
+import unittest
+
+from acts.controllers.sl4a_lib import sl4a_manager
+from acts.controllers.sl4a_lib import rpc_client
+
+
+class Sl4aManagerFactoryTest(unittest.TestCase):
+    """Tests the sl4a_manager module-level functions."""
+
+    def setUp(self):
+        """Clears the Sl4aManager cache."""
+        sl4a_manager._all_sl4a_managers = {}
+
+    def test_create_manager(self):
+        """Tests sl4a_manager.create_sl4a_manager().
+
+        Tests that a new Sl4aManager is returned without an error.
+        """
+        adb = mock.Mock()
+        adb.serial = 'SERIAL'
+        sl4a_man = sl4a_manager.create_sl4a_manager(adb)
+        self.assertEqual(sl4a_man.adb, adb)
+
+    def test_create_sl4a_manager_return_already_created_manager(self):
+        """Tests sl4a_manager.create_sl4a_manager().
+
+        Tests that a second call to create_sl4a_manager() does not create a
+        new Sl4aManager, and returns the first created Sl4aManager instead.
+        """
+        adb = mock.Mock()
+        adb.serial = 'SERIAL'
+        first_manager = sl4a_manager.create_sl4a_manager(adb)
+
+        adb_same_serial = mock.Mock()
+        adb_same_serial.serial = 'SERIAL'
+        second_manager = sl4a_manager.create_sl4a_manager(adb)
+
+        self.assertEqual(first_manager, second_manager)
+
+    def test_create_sl4a_manager_multiple_devices_with_one_manager_each(self):
+        """Tests sl4a_manager.create_sl4a_manager().
+
+        Tests that when create_s4l4a_manager() is called for different devices,
+        each device gets its own Sl4aManager object.
+        """
+        adb_1 = mock.Mock()
+        adb_1.serial = 'SERIAL'
+        first_manager = sl4a_manager.create_sl4a_manager(adb_1)
+
+        adb_2 = mock.Mock()
+        adb_2.serial = 'DIFFERENT_SERIAL_NUMBER'
+        second_manager = sl4a_manager.create_sl4a_manager(adb_2)
+
+        self.assertNotEqual(first_manager, second_manager)
+
+
+class Sl4aManagerTest(unittest.TestCase):
+    """Tests the sl4a_manager.Sl4aManager class."""
+    FIND_PORT_RETRIES = 0
+    _SL4A_LAUNCH_SERVER_CMD = ''
+    _SL4A_CLOSE_SERVER_CMD = ''
+    _SL4A_ROOT_FIND_PORT_CMD = ''
+    _SL4A_USER_FIND_PORT_CMD = ''
+    _SL4A_START_SERVICE_CMD = ''
+
+    @classmethod
+    def setUpClass(cls):
+        # Copy all module constants before testing begins.
+        Sl4aManagerTest.FIND_PORT_RETRIES = \
+            sl4a_manager.FIND_PORT_RETRIES
+        Sl4aManagerTest._SL4A_LAUNCH_SERVER_CMD = \
+            sl4a_manager._SL4A_LAUNCH_SERVER_CMD
+        Sl4aManagerTest._SL4A_CLOSE_SERVER_CMD = \
+            sl4a_manager._SL4A_CLOSE_SERVER_CMD
+        Sl4aManagerTest._SL4A_ROOT_FIND_PORT_CMD = \
+            sl4a_manager._SL4A_ROOT_FIND_PORT_CMD
+        Sl4aManagerTest._SL4A_USER_FIND_PORT_CMD = \
+            sl4a_manager._SL4A_USER_FIND_PORT_CMD
+        Sl4aManagerTest._SL4A_START_SERVICE_CMD = \
+            sl4a_manager._SL4A_START_SERVICE_CMD
+
+    def setUp(self):
+        # Restore all module constants at the beginning of each test case.
+        sl4a_manager.FIND_PORT_RETRIES = \
+            Sl4aManagerTest.FIND_PORT_RETRIES
+        sl4a_manager._SL4A_LAUNCH_SERVER_CMD = \
+            Sl4aManagerTest._SL4A_LAUNCH_SERVER_CMD
+        sl4a_manager._SL4A_CLOSE_SERVER_CMD = \
+            Sl4aManagerTest._SL4A_CLOSE_SERVER_CMD
+        sl4a_manager._SL4A_ROOT_FIND_PORT_CMD = \
+            Sl4aManagerTest._SL4A_ROOT_FIND_PORT_CMD
+        sl4a_manager._SL4A_USER_FIND_PORT_CMD = \
+            Sl4aManagerTest._SL4A_USER_FIND_PORT_CMD
+        sl4a_manager._SL4A_START_SERVICE_CMD = \
+            Sl4aManagerTest._SL4A_START_SERVICE_CMD
+
+        # Reset module data at the beginning of each test.
+        sl4a_manager._all_sl4a_managers = {}
+
+    def test_sl4a_ports_in_use(self):
+        """Tests sl4a_manager.Sl4aManager.sl4a_ports_in_use
+
+        Tests to make sure all server ports are returned with no duplicates.
+        """
+        adb = mock.Mock()
+        manager = sl4a_manager.Sl4aManager(adb)
+        session_1 = mock.Mock()
+        session_1.server_port = 12345
+        manager.sessions[1] = session_1
+        session_2 = mock.Mock()
+        session_2.server_port = 15973
+        manager.sessions[2] = session_2
+        session_3 = mock.Mock()
+        session_3.server_port = 12345
+        manager.sessions[3] = session_3
+        session_4 = mock.Mock()
+        session_4.server_port = 67890
+        manager.sessions[4] = session_4
+        session_5 = mock.Mock()
+        session_5.server_port = 75638
+        manager.sessions[5] = session_5
+
+        returned_ports = manager.sl4a_ports_in_use
+
+        # No duplicated ports.
+        self.assertEqual(len(returned_ports), len(set(returned_ports)))
+        # One call for each session
+        self.assertSetEqual(set(returned_ports), {12345, 15973, 67890, 75638})
+
+    def test_start_sl4a_server_uses_all_retries(self):
+        """Tests sl4a_manager.Sl4aManager.start_sl4a_server().
+
+        Tests to ensure that _start_sl4a_server retries and successfully returns
+        a port.
+        """
+        adb = mock.Mock()
+        adb.shell = lambda _, **kwargs: ''
+
+        side_effects = []
+        expected_port = 12345
+        for _ in range(sl4a_manager.FIND_PORT_RETRIES - 1):
+            side_effects.append(None)
+        side_effects.append(expected_port)
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        manager._get_open_listening_port = mock.Mock(side_effect=side_effects)
+        try:
+            found_port = manager.start_sl4a_server(0, try_interval=0)
+            self.assertTrue(found_port)
+        except rpc_client.Sl4aConnectionError:
+            self.fail('start_sl4a_server failed to respect FIND_PORT_RETRIES.')
+
+    def test_start_sl4a_server_fails_all_retries(self):
+        """Tests sl4a_manager.Sl4aManager.start_sl4a_server().
+
+        Tests to ensure that start_sl4a_server throws an error if all retries
+        fail.
+        """
+        adb = mock.Mock()
+        adb.shell = lambda _, **kwargs: ''
+
+        side_effects = []
+        for _ in range(sl4a_manager.FIND_PORT_RETRIES):
+            side_effects.append(None)
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        manager._get_open_listening_port = mock.Mock(side_effect=side_effects)
+        try:
+            manager.start_sl4a_server(0, try_interval=0)
+            self.fail('Sl4aConnectionError was not thrown.')
+        except rpc_client.Sl4aConnectionError:
+            pass
+
+    def test_get_all_ports_command_uses_root_cmd(self):
+        """Tests sl4a_manager.Sl4aManager._get_all_ports_command().
+
+        Tests that _get_all_ports_command calls the root command when root is
+        available.
+        """
+        adb = mock.Mock()
+        adb.is_root = lambda: True
+        command = 'ngo45hke3b4vie3mv5ni93,vfu3j'
+        sl4a_manager._SL4A_ROOT_FIND_PORT_CMD = command
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        self.assertEqual(manager._get_all_ports_command(), command)
+
+    def test_get_all_ports_command_escalates_to_root(self):
+        """Tests sl4a_manager.Sl4aManager._call_get_ports_command().
+
+        Tests that _call_get_ports_command calls the root command when adb is
+        user but can escalate to root.
+        """
+        adb = mock.Mock()
+        adb.is_root = lambda: False
+        adb.ensure_root = lambda: True
+        command = 'ngo45hke3b4vie3mv5ni93,vfu3j'
+        sl4a_manager._SL4A_ROOT_FIND_PORT_CMD = command
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        self.assertEqual(manager._get_all_ports_command(), command)
+
+    def test_get_all_ports_command_uses_user_cmd(self):
+        """Tests sl4a_manager.Sl4aManager._call_get_ports_command().
+
+        Tests that _call_get_ports_command calls the user command when root is
+        unavailable.
+        """
+        adb = mock.Mock()
+        adb.is_root = lambda: False
+        adb.ensure_root = lambda: False
+        command = 'ngo45hke3b4vie3mv5ni93,vfu3j'
+        sl4a_manager._SL4A_USER_FIND_PORT_CMD = command
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        self.assertEqual(manager._get_all_ports_command(), command)
+
+    def test_get_open_listening_port_no_port_found(self):
+        """Tests sl4a_manager.Sl4aManager._get_open_listening_port().
+
+        Tests to ensure None is returned if no open port is found.
+        """
+        adb = mock.Mock()
+        adb.shell = lambda _: ''
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        self.assertIsNone(manager._get_open_listening_port())
+
+    def test_get_open_listening_port_no_new_port_found(self):
+        """Tests sl4a_manager.Sl4aManager._get_open_listening_port().
+
+        Tests to ensure None is returned if the ports returned have all been
+        marked as in used.
+        """
+        adb = mock.Mock()
+        adb.shell = lambda _: '12345 67890'
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        manager._sl4a_ports = {'12345', '67890'}
+        self.assertIsNone(manager._get_open_listening_port())
+
+    def test_get_open_listening_port_port_is_avaiable(self):
+        """Tests sl4a_manager.Sl4aManager._get_open_listening_port().
+
+        Tests to ensure a port is returned if a port is found and has not been
+        marked as used.
+        """
+        adb = mock.Mock()
+        adb.shell = lambda _: '12345 67890'
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        manager._sl4a_ports = {'12345'}
+        self.assertEqual(manager._get_open_listening_port(), 67890)
+
+    def test_is_sl4a_installed_is_true(self):
+        """Tests sl4a_manager.Sl4aManager.is_sl4a_installed().
+
+        Tests is_sl4a_installed() returns true when pm returns data
+        """
+        adb = mock.Mock()
+        adb.shell = lambda _, **kwargs: 'asdf'
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        self.assertTrue(manager.is_sl4a_installed())
+
+    def test_is_sl4a_installed_is_false(self):
+        """Tests sl4a_manager.Sl4aManager.is_sl4a_installed().
+
+        Tests is_sl4a_installed() returns true when pm returns data
+        """
+        adb = mock.Mock()
+        adb.shell = lambda _, **kwargs: ''
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        self.assertFalse(manager.is_sl4a_installed())
+
+    def test_start_sl4a_throws_error_on_sl4a_not_installed(self):
+        """Tests sl4a_manager.Sl4aManager.start_sl4a_service().
+
+        Tests that a MissingSl4aError is thrown when SL4A is not installed.
+        """
+        adb = mock.Mock()
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        manager.is_sl4a_installed = lambda: False
+        try:
+            manager.start_sl4a_service()
+            self.fail('An error should have been thrown.')
+        except rpc_client.MissingSl4AError:
+            pass
+
+    def test_start_sl4a_starts_sl4a_if_not_running(self):
+        """Tests sl4a_manager.Sl4aManager.start_sl4a_service().
+
+        Tests that SL4A is started if it was not already running.
+        """
+        adb = mock.Mock()
+        adb.shell = mock.Mock(side_effect=['', ''])
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        manager.is_sl4a_installed = lambda: True
+        try:
+            manager.start_sl4a_service()
+        except rpc_client.MissingSl4AError:
+            self.fail('An error should not have been thrown.')
+        adb.shell.assert_called_with(sl4a_manager._SL4A_START_SERVICE_CMD)
+
+    def test_start_sl4a_does_not_start_sl4a_if_sl4a_is_running(self):
+        """Tests sl4a_manager.Sl4aManager.start_sl4a_service().
+
+        Tests that SL4A is not started if it is already running.
+        """
+
+        def fail_on_start_service(command):
+            if command == sl4a_manager._SL4A_START_SERVICE_CMD:
+                self.fail(
+                    'Called start command when SL4A was already started.')
+            return 'SL4A has already started.'
+
+        adb = mock.Mock()
+        adb.shell = fail_on_start_service
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        manager.is_sl4a_installed = lambda: True
+        try:
+            manager.start_sl4a_service()
+        except rpc_client.MissingSl4AError:
+            self.fail('An error should not have been thrown.')
+
+    def test_create_session_uses_oldest_server_port(self):
+        """Tests sl4a_manager.Sl4aManager.create_session().
+
+        Tests that when no port is given, the oldest server port opened is used
+        as the server port for a new session. The oldest server port can be
+        found by getting the oldest session's server port.
+        """
+        adb = mock.Mock()
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        # Ignore starting SL4A.
+        manager.start_sl4a_service = lambda: None
+
+        session_1 = mock.Mock()
+        session_1.server_port = 12345
+        session_2 = mock.Mock()
+        session_2.server_port = 67890
+        session_3 = mock.Mock()
+        session_3.server_port = 67890
+
+        manager.sessions[3] = session_3
+        manager.sessions[1] = session_1
+        manager.sessions[2] = session_2
+
+        with mock.patch.object(
+                rpc_client.RpcClient, '__init__', return_value=None):
+            created_session = manager.create_session()
+
+        self.assertEqual(created_session.server_port, session_1.server_port)
+
+    def test_create_session_uses_random_port_when_no_session_exists(self):
+        """Tests sl4a_manager.Sl4aManager.create_session().
+
+        Tests that when no port is given, and no SL4A server exists, the server
+        port for the session is set to 0.
+        """
+        adb = mock.Mock()
+
+        manager = sl4a_manager.create_sl4a_manager(adb)
+        # Ignore starting SL4A.
+        manager.start_sl4a_service = lambda: None
+
+        with mock.patch.object(
+                rpc_client.RpcClient, '__init__', return_value=None):
+            created_session = manager.create_session()
+
+        self.assertEqual(created_session.server_port, 0)
+
+    def test_terminate_all_session_call_terminate_on_all_sessions(self):
+        """Tests sl4a_manager.Sl4aManager.terminate_all_sessions().
+
+        Tests to see that the manager has called terminate on all sessions.
+        """
+        called_terminate_on = list()
+
+        def called_on(session):
+            called_terminate_on.append(session)
+
+        adb = mock.Mock()
+        manager = sl4a_manager.Sl4aManager(adb)
+
+        session_1 = mock.Mock()
+        session_1.terminate = lambda *args, **kwargs: called_on(session_1)
+        manager.sessions[1] = session_1
+        session_4 = mock.Mock()
+        session_4.terminate = lambda *args, **kwargs: called_on(session_4)
+        manager.sessions[4] = session_4
+        session_5 = mock.Mock()
+        session_5.terminate = lambda *args, **kwargs: called_on(session_5)
+        manager.sessions[5] = session_5
+
+        manager.terminate_all_sessions()
+        # No duplicates calls to terminate.
+        self.assertEqual(
+            len(called_terminate_on), len(set(called_terminate_on)))
+        # One call for each session
+        self.assertSetEqual(
+            set(called_terminate_on), {session_1, session_4, session_5})
+
+    def test_terminate_all_session_close_each_server(self):
+        """Tests sl4a_manager.Sl4aManager.terminate_all_sessions().
+
+        Tests to see that the manager has called terminate on all sessions.
+        """
+        closed_ports = list()
+
+        def close(command):
+            closed_ports.append(command)
+
+        adb = mock.Mock()
+        adb.shell = close
+        sl4a_manager._SL4A_CLOSE_SERVER_CMD = '%s'
+        ports_to_close = {'12345', '67890', '24680', '13579'}
+
+        manager = sl4a_manager.Sl4aManager(adb)
+        manager._sl4a_ports = set(ports_to_close)
+        manager.terminate_all_sessions()
+
+        # No duplicate calls to close port
+        self.assertEqual(len(closed_ports), len(set(closed_ports)))
+        # One call for each port
+        self.assertSetEqual(ports_to_close, set(closed_ports))
+
+    def test_obtain_sl4a_server_starts_new_server(self):
+        """Tests sl4a_manager.Sl4aManager.obtain_sl4a_server().
+
+        Tests that a new server can be returned if the server does not exist.
+        """
+        adb = mock.Mock()
+        manager = sl4a_manager.Sl4aManager(adb)
+        manager.start_sl4a_server = mock.Mock()
+
+        manager.obtain_sl4a_server(0)
+
+        self.assertTrue(manager.start_sl4a_server.called)
+
+    @mock.patch(
+        'acts.controllers.sl4a_lib.sl4a_manager.Sl4aManager.sl4a_ports_in_use',
+        new_callable=mock.PropertyMock)
+    def test_obtain_sl4a_server_returns_existing_server(
+            self, sl4a_ports_in_use):
+        """Tests sl4a_manager.Sl4aManager.obtain_sl4a_server().
+
+        Tests that an existing server is returned if it is already opened.
+        """
+        adb = mock.Mock()
+        manager = sl4a_manager.Sl4aManager(adb)
+        manager.start_sl4a_server = mock.Mock()
+        sl4a_ports_in_use.return_value = [12345]
+
+        ret = manager.obtain_sl4a_server(12345)
+
+        self.assertFalse(manager.start_sl4a_server.called)
+        self.assertEqual(12345, ret)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/acts/framework/tests/controllers/sl4a_lib/sl4a_session_test.py b/acts/framework/tests/controllers/sl4a_lib/sl4a_session_test.py
new file mode 100644
index 0000000..d16fb99
--- /dev/null
+++ b/acts/framework/tests/controllers/sl4a_lib/sl4a_session_test.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import errno
+import mock
+from socket import timeout
+from socket import error as socket_error
+import unittest
+from mock import patch
+
+from acts.controllers.sl4a_lib import sl4a_ports
+from acts.controllers.sl4a_lib import sl4a_session
+from acts.controllers.sl4a_lib import rpc_client
+
+
+class Sl4aSessionTest(unittest.TestCase):
+    """Tests the sl4a_session.Sl4aSession class."""
+
+    def test_is_alive_true_on_not_terminated(self):
+        """Tests sl4a_session.Sl4aSession.is_alive.
+
+        Tests that the session is_alive when it has not been terminated.
+        """
+        session = mock.Mock()
+        session._terminated = False
+        session.is_alive = sl4a_session.Sl4aSession.is_alive
+        self.assertNotEqual(session._terminated, session.is_alive)
+
+    def test_is_alive_false_on_terminated(self):
+        """Tests sl4a_session.Sl4aSession.is_alive.
+
+        Tests that the session is_alive when it has not been terminated.
+        """
+        session = mock.Mock()
+        session._terminated = True
+        session.is_alive = sl4a_session.Sl4aSession.is_alive
+        self.assertNotEqual(session._terminated, session.is_alive)
+
+    @patch('acts.controllers.sl4a_lib.event_dispatcher.EventDispatcher')
+    def test_get_event_dispatcher_create_on_none(self, _):
+        """Tests sl4a_session.Sl4aSession.get_event_dispatcher.
+
+        Tests that a new event_dispatcher is created if one does not exist.
+        """
+        session = mock.Mock()
+        session._event_dispatcher = None
+        ed = sl4a_session.Sl4aSession.get_event_dispatcher(session)
+        self.assertTrue(session._event_dispatcher is not None)
+        self.assertEqual(session._event_dispatcher, ed)
+
+    def test_get_event_dispatcher_returns_existing_event_dispatcher(self):
+        """Tests sl4a_session.Sl4aSession.get_event_dispatcher.
+
+        Tests that the existing event_dispatcher is returned.
+        """
+        session = mock.Mock()
+        session._event_dispatcher = 'Something that is not None'
+        ed = sl4a_session.Sl4aSession.get_event_dispatcher(session)
+        self.assertEqual(session._event_dispatcher, ed)
+
+    def test_create_client_side_connection_hint_already_in_use(self):
+        """Tests sl4a_session.Sl4aSession._create_client_side_connection().
+
+        Tests that if the hinted port is already in use, the function will
+        call itself with a hinted port of 0 (random).
+        """
+        session = mock.Mock()
+        session._create_client_side_connection = mock.Mock()
+        with mock.patch('socket.socket') as socket:
+            # Throw an error when trying to bind to the hinted port.
+            error = OSError()
+            error.errno = errno.EADDRINUSE
+            socket_instance = mock.Mock()
+            socket_instance.bind = mock.Mock()
+            socket_instance.bind.side_effect = error
+            socket.return_value = socket_instance
+
+            sl4a_session.Sl4aSession._create_client_side_connection(
+                session, sl4a_ports.Sl4aPorts(1, 2, 3))
+
+        fn = session._create_client_side_connection
+        self.assertEqual(fn.call_count, 1)
+        # Asserts that the 1st argument (Sl4aPorts) sent to the function
+        # has a client port of 0.
+        self.assertEqual(fn.call_args_list[0][0][0].client_port, 0)
+
+    def test_create_client_side_connection_catches_timeout(self):
+        """Tests sl4a_session.Sl4aSession._create_client_side_connection().
+
+        Tests that the function will raise an Sl4aConnectionError upon timeout.
+        """
+        session = mock.Mock()
+        session._create_client_side_connection = mock.Mock()
+        error = timeout()
+        with mock.patch('socket.socket') as socket:
+            # Throw an error when trying to bind to the hinted port.
+            socket_instance = mock.Mock()
+            socket_instance.connect = mock.Mock()
+            socket_instance.connect.side_effect = error
+            socket.return_value = socket_instance
+
+            with self.assertRaises(rpc_client.Sl4aConnectionError):
+                sl4a_session.Sl4aSession._create_client_side_connection(
+                    session, sl4a_ports.Sl4aPorts(0, 2, 3))
+
+    def test_create_client_side_connection_hint_taken_during_fn(self):
+        """Tests sl4a_session.Sl4aSession._create_client_side_connection().
+
+        Tests that the function will call catch an EADDRNOTAVAIL OSError and
+        call itself again, this time with a hinted port of 0 (random).
+        """
+        session = mock.Mock()
+        session._create_client_side_connection = mock.Mock()
+        error = socket_error()
+        error.errno = errno.EADDRNOTAVAIL
+        with mock.patch('socket.socket') as socket:
+            # Throw an error when trying to bind to the hinted port.
+            socket_instance = mock.Mock()
+            socket_instance.connect = mock.Mock()
+            socket_instance.connect.side_effect = error
+            socket.return_value = socket_instance
+
+            sl4a_session.Sl4aSession._create_client_side_connection(
+                session, sl4a_ports.Sl4aPorts(0, 2, 3))
+
+        fn = session._create_client_side_connection
+        self.assertEqual(fn.call_count, 1)
+        # Asserts that the 1st argument (Sl4aPorts) sent to the function
+        # has a client port of 0.
+        self.assertEqual(fn.call_args_list[0][0][0].client_port, 0)
+
+    def test_create_client_side_connection_re_raises_uncaught_errors(self):
+        """Tests sl4a_session.Sl4aSession._create_client_side_connection().
+
+        Tests that the function will re-raise any socket error that does not
+        have errno.EADDRNOTAVAIL.
+        """
+        session = mock.Mock()
+        session._create_client_side_connection = mock.Mock()
+        error = socket_error()
+        # Some error that isn't EADDRNOTAVAIL
+        error.errno = errno.ESOCKTNOSUPPORT
+        with mock.patch('socket.socket') as socket:
+            # Throw an error when trying to bind to the hinted port.
+            socket_instance = mock.Mock()
+            socket_instance.connect = mock.Mock()
+            socket_instance.connect.side_effect = error
+            socket.return_value = socket_instance
+
+            with self.assertRaises(socket_error):
+                sl4a_session.Sl4aSession._create_client_side_connection(
+                    session, sl4a_ports.Sl4aPorts(0, 2, 3))
+
+    def test_terminate_only_closes_if_not_terminated(self):
+        """Tests sl4a_session.Sl4aSession.terminate()
+
+        Tests that terminate only runs termination steps if the session has not
+        already been terminated.
+        """
+        session = mock.Mock()
+        session._terminate_lock = mock.MagicMock()
+        session._terminated = True
+        sl4a_session.Sl4aSession.terminate(session)
+
+        self.assertFalse(session._event_dispatcher.close.called)
+        self.assertFalse(session.rpc_client.terminate.called)
+
+    def test_terminate_closes_session_first(self):
+        """Tests sl4a_session.Sl4aSession.terminate()
+
+        Tests that terminate only runs termination steps if the session has not
+        already been terminated.
+        """
+        session = mock.Mock()
+        session._terminate_lock = mock.MagicMock()
+        session._terminated = True
+        sl4a_session.Sl4aSession.terminate(session)
+
+        self.assertFalse(session._event_dispatcher.close.called)
+        self.assertFalse(session.rpc_client.terminate.called)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/acts/framework/tests/controllers/sl4a_lib/test_suite.py b/acts/framework/tests/controllers/sl4a_lib/test_suite.py
new file mode 100755
index 0000000..add8d6c
--- /dev/null
+++ b/acts/framework/tests/controllers/sl4a_lib/test_suite.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#!/usr/bin/env python3.4
+#
+#   Copyright 2016 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import sys
+import unittest
+
+from tests.controllers.sl4a_lib import rpc_client_test
+from tests.controllers.sl4a_lib import rpc_connection_test
+from tests.controllers.sl4a_lib import sl4a_manager_test
+from tests.controllers.sl4a_lib import sl4a_session_test
+
+
+def compile_suite():
+    test_classes_to_run = [
+        rpc_client_test.RpcClientTest,
+        rpc_connection_test.RpcConnectionTest,
+        sl4a_manager_test.Sl4aManagerFactoryTest,
+        sl4a_manager_test.Sl4aManagerTest,
+        sl4a_session_test.Sl4aSessionTest,
+    ]
+    loader = unittest.TestLoader()
+
+    suites_list = []
+    for test_class in test_classes_to_run:
+        suite = loader.loadTestsFromTestCase(test_class)
+        suites_list.append(suite)
+
+    big_suite = unittest.TestSuite(suites_list)
+    return big_suite
+
+
+if __name__ == "__main__":
+    # This is the entry point for running all SL4A Lib unit tests.
+    runner = unittest.TextTestRunner()
+    results = runner.run(compile_suite())
+    sys.exit(not results.wasSuccessful())
diff --git a/acts/tests/google/ble/api/BleAdvertiseApiTest.py b/acts/tests/google/ble/api/BleAdvertiseApiTest.py
index 0eefa1c..0deed94 100644
--- a/acts/tests/google/ble/api/BleAdvertiseApiTest.py
+++ b/acts/tests/google/ble/api/BleAdvertiseApiTest.py
@@ -20,7 +20,7 @@
 then other test suites utilising Ble Advertisements will also fail.
 """
 
-from acts.controllers import sl4a_client
+from acts.controllers.sl4a_lib import rpc_client
 from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.bt_test_utils import adv_fail
@@ -77,8 +77,8 @@
         if adv_mode != exp_adv_mode:
             test_result = False
             self.log.debug("exp filtering mode: {},"
-                           " found filtering mode: {}".format(exp_adv_mode,
-                                                              adv_mode))
+                           " found filtering mode: {}".format(
+                               exp_adv_mode, adv_mode))
         if tx_power_level != exp_tx_power_level:
             test_result = False
             self.log.debug("exp tx power level: {},"
@@ -366,8 +366,9 @@
         self.log.debug("Step 1: Setup environment.")
         droid = self.ad_dut.droid
         exp_adv_tx_power = (ble_advertise_settings_tx_powers['low'])
-        self.log.debug("Step 2: Set the filtering settings object's value to ".
-                       format(exp_adv_tx_power))
+        self.log.debug(
+            "Step 2: Set the filtering settings object's value to ".format(
+                exp_adv_tx_power))
         return self.verify_adv_settings_tx_power_level(droid, exp_adv_tx_power)
 
     @BluetoothBaseTest.bt_test_wrap
@@ -397,8 +398,9 @@
         self.log.debug("Step 1: Setup environment.")
         droid = self.ad_dut.droid
         exp_adv_tx_power = ble_advertise_settings_tx_powers['ultra_low']
-        self.log.debug("Step 2: Set the filtering settings object's value to ".
-                       format(exp_adv_tx_power))
+        self.log.debug(
+            "Step 2: Set the filtering settings object's value to ".format(
+                exp_adv_tx_power))
         return self.verify_adv_settings_tx_power_level(droid, exp_adv_tx_power)
 
     @BluetoothBaseTest.bt_test_wrap
@@ -618,8 +620,8 @@
         exp_service_uuids = ["0"]
         self.log.debug("Step 2: Set the filtering data service uuids to " +
                        str(exp_service_uuids))
-        return self.verify_invalid_adv_data_service_uuids(droid,
-                                                          exp_service_uuids)
+        return self.verify_invalid_adv_data_service_uuids(
+            droid, exp_service_uuids)
 
     @BluetoothBaseTest.bt_test_wrap
     @test_tracker_info(uuid='51d634e7-6271-4cc0-a57b-3c1b632a7db6')
@@ -1030,7 +1032,7 @@
         droid.bleStartBleAdvertising(advcallback, adv_data, adv_settings)
         try:
             ed.pop_event(adv_fail.format(advcallback))
-        except sl4a_client.Sl4aApiError:
+        except rpc_client.Sl4aApiError:
             self.log.info("{} event was not found.".format(
                 adv_fail.format(advcallback)))
             return False
@@ -1107,8 +1109,9 @@
             self.log.debug("exp value: {}, Actual value: {}".format(
                 exp_service_uuids, service_uuids))
             return False
-        self.log.debug("Advertise Data's service uuids {}, value test Passed.".
-                       format(exp_service_uuids))
+        self.log.debug(
+            "Advertise Data's service uuids {}, value test Passed.".format(
+                exp_service_uuids))
         return True
 
     def verify_adv_data_service_data(self, droid, exp_service_data_uuid,
@@ -1151,8 +1154,8 @@
                            ", Actual value: " + str(manu_specific_data))
             return False
         self.log.debug("Advertise Data's manu id: " + str(exp_manu_id) +
-                       ", manu's specific data: " + str(
-                           exp_manu_specific_data) + "  value test Passed.")
+                       ", manu's specific data: " +
+                       str(exp_manu_specific_data) + "  value test Passed.")
         return True
 
     def verify_adv_data_include_tx_power_level(self, droid,
@@ -1204,10 +1207,10 @@
             self.log.debug("Set Advertise settings invalid filtering mode "
                            "passed with input as {}".format(exp_adv_mode))
             return False
-        except sl4a_client.Sl4aApiError:
-            self.log.debug("Set Advertise settings invalid filtering mode "
-                           "failed successfully with input as {}".format(
-                               exp_adv_mode))
+        except rpc_client.Sl4aApiError:
+            self.log.debug(
+                "Set Advertise settings invalid filtering mode "
+                "failed successfully with input as {}".format(exp_adv_mode))
             return True
 
     def verify_invalid_adv_settings_tx_power_level(self, droid,
@@ -1218,10 +1221,10 @@
             self.log.debug("Set Advertise settings invalid tx power level " +
                            " with input as {}".format(exp_adv_tx_power))
             return False
-        except sl4a_client.Sl4aApiError:
-            self.log.debug("Set Advertise settings invalid tx power level "
-                           "failed successfullywith input as {}".format(
-                               exp_adv_tx_power))
+        except rpc_client.Sl4aApiError:
+            self.log.debug(
+                "Set Advertise settings invalid tx power level "
+                "failed successfullywith input as {}".format(exp_adv_tx_power))
             return True
 
     def verify_invalid_adv_data_service_uuids(self, droid, exp_service_uuids):
@@ -1231,10 +1234,10 @@
             self.log.debug("Set Advertise Data service uuids " +
                            " with input as {}".format(exp_service_uuids))
             return False
-        except sl4a_client.Sl4aApiError:
-            self.log.debug("Set Advertise Data invalid service uuids failed "
-                           "successfully with input as {}".format(
-                               exp_service_uuids))
+        except rpc_client.Sl4aApiError:
+            self.log.debug(
+                "Set Advertise Data invalid service uuids failed "
+                "successfully with input as {}".format(exp_service_uuids))
             return True
 
     def verify_invalid_adv_data_service_data(
@@ -1247,10 +1250,10 @@
                            ", service data: {}".format(exp_service_data_uuid,
                                                        exp_service_data))
             return False
-        except sl4a_client.Sl4aApiError:
-            self.log.debug("Set Advertise Data service data uuid: " + str(
-                exp_service_data_uuid) + ", service data: " + str(
-                    exp_service_data) + " failed successfully.")
+        except rpc_client.Sl4aApiError:
+            self.log.debug("Set Advertise Data service data uuid: " +
+                           str(exp_service_data_uuid) + ", service data: " +
+                           str(exp_service_data) + " failed successfully.")
             return True
 
     def verify_invalid_adv_data_manu_id(self, droid, exp_manu_id,
@@ -1259,11 +1262,11 @@
             droid.bleAddAdvertiseDataManufacturerId(exp_manu_id,
                                                     exp_manu_specific_data)
             droid.bleBuildAdvertiseData()
-            self.log.debug("Set Advertise Data manu id: " + str(exp_manu_id) +
-                           ", manu specific data: " + str(
-                               exp_manu_specific_data))
+            self.log.debug(
+                "Set Advertise Data manu id: " + str(exp_manu_id) +
+                ", manu specific data: " + str(exp_manu_specific_data))
             return False
-        except sl4a_client.Sl4aApiError:
+        except rpc_client.Sl4aApiError:
             self.log.debug("Set Advertise Data manu id: {},"
                            " manu specific data: {},".format(
                                exp_manu_id, exp_manu_specific_data))
diff --git a/acts/tests/google/ble/api/BleScanApiTest.py b/acts/tests/google/ble/api/BleScanApiTest.py
index f7bece3..8e743fd 100644
--- a/acts/tests/google/ble/api/BleScanApiTest.py
+++ b/acts/tests/google/ble/api/BleScanApiTest.py
@@ -20,7 +20,7 @@
 then other test suites utilising Ble Scanner will also fail.
 """
 
-from acts.controllers import sl4a_client
+from acts.controllers.sl4a_lib import rpc_client
 from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.bt_constants import ble_scan_settings_callback_types
@@ -82,53 +82,53 @@
         if 'ScanSettings' in input.keys():
             try:
                 droid.bleSetScanSettingsCallbackType(input['ScanSettings'][0])
-                droid.bleSetScanSettingsReportDelayMillis(input['ScanSettings']
-                                                          [1])
+                droid.bleSetScanSettingsReportDelayMillis(
+                    input['ScanSettings'][1])
                 droid.bleSetScanSettingsScanMode(input['ScanSettings'][2])
                 droid.bleSetScanSettingsResultType(input['ScanSettings'][3])
-            except sl4a_client.Sl4aApiError as error:
+            except rpc_client.Sl4aApiError as error:
                 self.log.debug("Set Scan Settings failed with: ".format(error))
                 return False
         if 'ScanFilterDeviceName' in input.keys():
             try:
                 droid.bleSetScanFilterDeviceName(input['ScanFilterDeviceName'])
-            except sl4a_client.Sl4aApiError as error:
+            except rpc_client.Sl4aApiError as error:
                 self.log.debug("Set Scan Filter Device Name failed with: {}"
                                .format(error))
                 return False
         if 'ScanFilterDeviceAddress' in input.keys():
             try:
-                droid.bleSetScanFilterDeviceAddress(input[
-                    'ScanFilterDeviceAddress'])
-            except sl4a_client.Sl4aApiError as error:
+                droid.bleSetScanFilterDeviceAddress(
+                    input['ScanFilterDeviceAddress'])
+            except rpc_client.Sl4aApiError as error:
                 self.log.debug("Set Scan Filter Device Address failed with: {}"
                                .format(error))
                 return False
-        if ('ScanFilterManufacturerDataId' in input.keys() and
-                'ScanFilterManufacturerDataMask' in input.keys()):
+        if ('ScanFilterManufacturerDataId' in input.keys()
+                and 'ScanFilterManufacturerDataMask' in input.keys()):
             try:
                 droid.bleSetScanFilterManufacturerData(
                     input['ScanFilterManufacturerDataId'],
                     input['ScanFilterManufacturerData'],
                     input['ScanFilterManufacturerDataMask'])
-            except sl4a_client.Sl4aApiError as error:
+            except rpc_client.Sl4aApiError as error:
                 self.log.debug("Set Scan Filter Manufacturer info with data "
                                "mask failed with: {}".format(error))
                 return False
-        if ('ScanFilterManufacturerDataId' in input.keys() and
-                'ScanFilterManufacturerData' in input.keys() and
-                'ScanFilterManufacturerDataMask' not in input.keys()):
+        if ('ScanFilterManufacturerDataId' in input.keys()
+                and 'ScanFilterManufacturerData' in input.keys()
+                and 'ScanFilterManufacturerDataMask' not in input.keys()):
             try:
                 droid.bleSetScanFilterManufacturerData(
                     input['ScanFilterManufacturerDataId'],
                     input['ScanFilterManufacturerData'])
-            except sl4a_client.Sl4aApiError as error:
+            except rpc_client.Sl4aApiError as error:
                 self.log.debug(
                     "Set Scan Filter Manufacturer info failed with: "
                     "{}".format(error))
                 return False
-        if ('ScanFilterServiceUuid' in input.keys() and
-                'ScanFilterServiceMask' in input.keys()):
+        if ('ScanFilterServiceUuid' in input.keys()
+                and 'ScanFilterServiceMask' in input.keys()):
 
             droid.bleSetScanFilterServiceUuid(input['ScanFilterServiceUuid'],
                                               input['ScanFilterServiceMask'])
@@ -161,42 +161,41 @@
                                                  device_name_filter))
             return False
         if device_address_filter != input['ScanFilterDeviceAddress']:
-            self.log.debug("Scan Filter address name did not match. expected: "
-                           "{}, found: {}".format(input[
-                               'ScanFilterDeviceAddress'],
-                                                  device_address_filter))
+            self.log.debug(
+                "Scan Filter address name did not match. expected: "
+                "{}, found: {}".format(input['ScanFilterDeviceAddress'],
+                                       device_address_filter))
             return False
         if manufacturer_id != input['ScanFilterManufacturerDataId']:
             self.log.debug("Scan Filter manufacturer data id did not match. "
-                           "expected: {}, found: {}".format(input[
-                               'ScanFilterManufacturerDataId'],
-                                                            manufacturer_id))
+                           "expected: {}, found: {}".format(
+                               input['ScanFilterManufacturerDataId'],
+                               manufacturer_id))
             return False
         if manufacturer_data != input['ScanFilterManufacturerData']:
             self.log.debug("Scan Filter manufacturer data did not match. "
-                           "expected: {}, found: {}".format(input[
-                               'ScanFilterManufacturerData'],
-                                                            manufacturer_data))
+                           "expected: {}, found: {}".format(
+                               input['ScanFilterManufacturerData'],
+                               manufacturer_data))
             return False
         if 'ScanFilterManufacturerDataMask' in input.keys():
             manufacturer_data_mask = droid.bleGetScanFilterManufacturerDataMask(
                 filter_list, scan_filter_index)
-            if manufacturer_data_mask != input[
-                    'ScanFilterManufacturerDataMask']:
+            if manufacturer_data_mask != input['ScanFilterManufacturerDataMask']:
                 self.log.debug(
                     "Manufacturer data mask did not match. expected:"
-                    " {}, found: {}".format(input[
-                        'ScanFilterManufacturerDataMask'],
-                                            manufacturer_data_mask))
+                    " {}, found: {}".format(
+                        input['ScanFilterManufacturerDataMask'],
+                        manufacturer_data_mask))
 
                 return False
-        if ('ScanFilterServiceUuid' in input.keys() and
-                'ScanFilterServiceMask' in input.keys()):
+        if ('ScanFilterServiceUuid' in input.keys()
+                and 'ScanFilterServiceMask' in input.keys()):
 
             expected_service_uuid = input['ScanFilterServiceUuid']
             expected_service_mask = input['ScanFilterServiceMask']
-            service_uuid = droid.bleGetScanFilterServiceUuid(filter_list,
-                                                             scan_filter_index)
+            service_uuid = droid.bleGetScanFilterServiceUuid(
+                filter_list, scan_filter_index)
             service_mask = droid.bleGetScanFilterServiceUuidMask(
                 filter_list, scan_filter_index)
             if service_uuid != expected_service_uuid.lower():
@@ -262,8 +261,8 @@
         Priority: 0
         """
         input = {}
-        test_result = self.validate_scan_settings_helper(input,
-                                                         self.ad_dut.droid)
+        test_result = self.validate_scan_settings_helper(
+            input, self.ad_dut.droid)
         if not test_result:
             self.log.error("Could not setup ble scanner.")
             return test_result
@@ -330,8 +329,8 @@
             ble_scan_settings_callback_types['first_match'], 0,
             ble_scan_settings_modes['low_power'],
             ble_scan_settings_result_types['full'])
-        test_result = self.validate_scan_settings_helper(input,
-                                                         self.ad_dut.droid)
+        test_result = self.validate_scan_settings_helper(
+            input, self.ad_dut.droid)
         return test_result
 
     @BluetoothBaseTest.bt_test_wrap
@@ -360,8 +359,8 @@
             ble_scan_settings_callback_types['match_lost'], 0,
             ble_scan_settings_modes['low_power'],
             ble_scan_settings_result_types['full'])
-        test_result = self.validate_scan_settings_helper(input,
-                                                         self.ad_dut.droid)
+        test_result = self.validate_scan_settings_helper(
+            input, self.ad_dut.droid)
         return test_result
 
     @BluetoothBaseTest.bt_test_wrap
@@ -387,8 +386,8 @@
         input = {}
         input["ScanSettings"] = (-1, 0, ble_scan_settings_modes['low_power'],
                                  ble_scan_settings_result_types['full'])
-        test_result = self.validate_scan_settings_helper(input,
-                                                         self.ad_dut.droid)
+        test_result = self.validate_scan_settings_helper(
+            input, self.ad_dut.droid)
         return not test_result
 
     @BluetoothBaseTest.bt_test_wrap
@@ -417,8 +416,8 @@
             ble_scan_settings_callback_types['all_matches'], 0,
             ble_scan_settings_modes['low_power'],
             ble_scan_settings_result_types['full'])
-        test_result = self.validate_scan_settings_helper(input,
-                                                         self.ad_dut.droid)
+        test_result = self.validate_scan_settings_helper(
+            input, self.ad_dut.droid)
         return test_result
 
     @BluetoothBaseTest.bt_test_wrap
@@ -877,8 +876,8 @@
         """
         input = {}
         input['ScanFilterDeviceAddress'] = ""
-        test_result = self.validate_scan_settings_helper(input,
-                                                         self.ad_dut.droid)
+        test_result = self.validate_scan_settings_helper(
+            input, self.ad_dut.droid)
         return not test_result
 
     @BluetoothBaseTest.bt_test_wrap
diff --git a/acts/tests/google/ble/api/GattApiTest.py b/acts/tests/google/ble/api/GattApiTest.py
index 06095de..aa0b194 100644
--- a/acts/tests/google/ble/api/GattApiTest.py
+++ b/acts/tests/google/ble/api/GattApiTest.py
@@ -17,7 +17,7 @@
 Test script to exercise Gatt Apis.
 """
 
-from acts.controllers import sl4a_client
+from acts.controllers.sl4a_lib import rpc_client
 from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.bt_test_utils import setup_multiple_devices_for_bt_test
@@ -114,7 +114,7 @@
         invalid_callback_index = -1
         try:
             self.ad.droid.gattServerOpenGattServer(invalid_callback_index)
-        except sl4a_client.Sl4aApiError as e:
+        except rpc_client.Sl4aApiError as e:
             self.log.info("Failed successfully with exception: {}.".format(e))
             return True
         return False
diff --git a/acts/tests/google/ble/scan/BleBackgroundScanTest.py b/acts/tests/google/ble/scan/BleBackgroundScanTest.py
index 115b960..3cb2842 100644
--- a/acts/tests/google/ble/scan/BleBackgroundScanTest.py
+++ b/acts/tests/google/ble/scan/BleBackgroundScanTest.py
@@ -24,11 +24,13 @@
 from acts.test_utils.bt.bt_test_utils import bluetooth_off
 from acts.test_utils.bt.bt_test_utils import bluetooth_on
 from acts.test_utils.bt.bt_test_utils import cleanup_scanners_and_advertisers
+from acts.test_utils.bt.bt_test_utils import enable_bluetooth
 from acts.test_utils.bt.bt_test_utils import generate_ble_advertise_objects
 from acts.test_utils.bt.bt_test_utils import generate_ble_scan_objects
 from acts.test_utils.bt.bt_constants import bluetooth_le_off
 from acts.test_utils.bt.bt_constants import bluetooth_le_on
 from acts.test_utils.bt.bt_constants import bt_adapter_states
+from acts.test_utils.bt.bt_constants import ble_scan_settings_modes
 from acts.test_utils.bt.bt_constants import scan_result
 
 import time
@@ -49,10 +51,9 @@
         self.adv_ad = self.android_devices[1]
 
     def setup_test(self):
-        if (self.scn_ad.droid.bluetoothGetLeState() ==
-                bt_adapter_states['off']):
-            self.scn_ad.droid.bluetoothEnableBLE()
-            self.scn_ad.ed.pop_event(bluetooth_le_on)
+        # Always start tests with Bluetooth enabled and BLE disabled.
+        enable_bluetooth(self.scn_ad.droid, self.scn_ad.ed)
+        self.scn_ad.droid.bluetoothDisableBLE()
         for a in self.android_devices:
             a.ed.clear_all_events()
         return True
@@ -65,21 +66,13 @@
         self.active_scan_callback_list = []
 
     def _setup_generic_advertisement(self):
+        self.adv_ad.droid.bleSetAdvertiseDataIncludeDeviceName(True)
         adv_callback, adv_data, adv_settings = generate_ble_advertise_objects(
             self.adv_ad.droid)
         self.adv_ad.droid.bleStartBleAdvertising(adv_callback, adv_data,
                                                  adv_settings)
         self.active_adv_callback_list.append(adv_callback)
 
-    def _verify_no_events_found(self, event_name):
-        try:
-            self.scn_ad.ed.pop_event(event_name, self.default_timeout)
-            self.log.error("Found an event when none was expected.")
-            return False
-        except Empty:
-            self.log.info("No scan result found as expected.")
-            return True
-
     @BluetoothBaseTest.bt_test_wrap
     @test_tracker_info(uuid='4d13c3a8-1805-44ef-a92a-e385540767f1')
     def test_background_scan(self):
@@ -105,8 +98,15 @@
         TAGS: LE, Advertising, Scanning, Background Scanning
         Priority: 0
         """
-        import time
+        self.scn_ad.droid.bluetoothEnableBLE()
         self._setup_generic_advertisement()
+        self.scn_ad.droid.bleSetScanSettingsScanMode(ble_scan_settings_modes[
+            'low_latency'])
+        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
+            self.scn_ad.droid)
+        self.scn_ad.droid.bleSetScanFilterDeviceName(
+            self.adv_ad.droid.bluetoothGetLocalName())
+        self.scn_ad.droid.bleBuildScanFilter(filter_list)
         self.scn_ad.droid.bluetoothToggleState(False)
         try:
             self.scn_ad.ed.pop_event(bluetooth_off, self.default_timeout)
@@ -114,22 +114,6 @@
             self.log.error("Bluetooth Off event not found. Expected {}".format(
                 bluetooth_off))
             return False
-        self.scn_ad.droid.bluetoothDisableBLE()
-        try:
-            self.scn_ad.ed.pop_event(bluetooth_off, self.default_timeout)
-        except Empty:
-            self.log.error("Bluetooth Off event not found. Expected {}".format(
-                bluetooth_off))
-            return False
-        self.scn_ad.droid.bluetoothEnableBLE()
-        try:
-            self.scn_ad.ed.pop_event(bluetooth_off, self.default_timeout * 2)
-        except Empty:
-            self.log.error("Bluetooth On event not found. Expected {}".format(
-                bluetooth_on))
-            return False
-        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
-            self.scn_ad.droid)
         self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
                                           scan_callback)
         expected_event = scan_result.format(scan_callback)
@@ -168,15 +152,21 @@
         """
         self._setup_generic_advertisement()
         self.scn_ad.droid.bluetoothEnableBLE()
+        self.scn_ad.droid.bleSetScanSettingsScanMode(ble_scan_settings_modes[
+            'low_latency'])
+        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
+            self.scn_ad.droid)
+        self.scn_ad.droid.bleSetScanFilterDeviceName(
+            self.adv_ad.droid.bluetoothGetLocalName())
+        self.scn_ad.droid.bleBuildScanFilter(filter_list)
         self.scn_ad.droid.bluetoothToggleState(False)
         try:
             self.scn_ad.ed.pop_event(bluetooth_off, self.default_timeout)
         except Empty:
+            self.log.info(self.scn_ad.droid.bluetoothCheckState())
             self.log.error("Bluetooth Off event not found. Expected {}".format(
                 bluetooth_off))
             return False
-        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
-            self.scn_ad.droid)
         try:
             self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
                                               scan_callback)
@@ -187,9 +177,6 @@
                 self.log.error("Scan Result event not found. Expected {}".
                                format(expected_event))
                 return False
-            self.log.info("Was able to start background scan even though ble "
-                          "was disabled.")
-            return False
         except Exception:
             self.log.info(
                 "Was not able to start a background scan as expected.")
@@ -226,39 +213,22 @@
         """
         ble_state_error_msg = "Bluetooth LE State not OK {}. Expected {} got {}"
         # Enable BLE always available (effectively enabling BT in location)
-        self.scn_ad.adb.shell(
-            "shell settings put global ble_scan_always_enabled 1")
-
+        self.scn_ad.droid.bluetoothEnableBLE()
         self.scn_ad.droid.bluetoothToggleState(False)
         try:
             self.scn_ad.ed.pop_event(bluetooth_off, self.default_timeout)
         except Empty:
             self.log.error("Bluetooth Off event not found. Expected {}".format(
                 bluetooth_off))
+            self.log.info(self.scn_ad.droid.bluetoothCheckState())
             return False
 
         # Sleep because LE turns off after the bluetooth off event fires
         time.sleep(self.default_timeout)
         state = self.scn_ad.droid.bluetoothGetLeState()
-        if state != bt_adapter_states['off']:
-            self.log.error(
-                ble_state_error_msg.format("after BT Disable",
-                                           bt_adapter_states['off'], state))
-            return False
-
-        # TODO: BleStateChangedOn got generated as we shut off bluetooth above?
-        self.scn_ad.ed.clear_all_events()
-        result = self.scn_ad.droid.bluetoothEnableBLE()
-        try:
-            self.scn_ad.ed.pop_event(bluetooth_le_on, self.default_timeout)
-        except Empty:
-            self.log.error("Bluetooth LE On event not found. Expected {}".
-                           format(bluetooth_le_on))
-            return False
-        state = self.scn_ad.droid.bluetoothGetLeState()
         if state != bt_adapter_states['ble_on']:
             self.log.error(
-                ble_state_error_msg.format("before Airplane Mode OFF",
+                ble_state_error_msg.format("after BT Disable",
                                            bt_adapter_states['ble_on'], state))
             return False
 
diff --git a/acts/tests/google/ble/scan/BleScanScreenStateTest.py b/acts/tests/google/ble/scan/BleScanScreenStateTest.py
new file mode 100644
index 0000000..b6c17f6
--- /dev/null
+++ b/acts/tests/google/ble/scan/BleScanScreenStateTest.py
@@ -0,0 +1,555 @@
+#/usr/bin/env python3.4
+#
+# 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.
+"""
+This test script exercises different scan filters with different screen states.
+"""
+
+import concurrent
+import json
+import pprint
+import time
+
+from queue import Empty
+from acts.test_decorators import test_tracker_info
+from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
+from acts.test_utils.bt.bt_constants import adv_succ
+from acts.test_utils.bt.bt_constants import ble_advertise_settings_modes
+from acts.test_utils.bt.bt_constants import ble_scan_settings_modes
+from acts.test_utils.bt.bt_constants import bt_default_timeout
+from acts.test_utils.bt.bt_constants import scan_result
+from acts.test_utils.bt.bt_test_utils import batch_scan_result
+from acts.test_utils.bt.bt_test_utils import generate_ble_advertise_objects
+from acts.test_utils.bt.bt_test_utils import generate_ble_scan_objects
+from acts.test_utils.bt.bt_test_utils import reset_bluetooth
+
+
+class BleScanScreenStateTest(BluetoothBaseTest):
+    advertise_callback = -1
+    max_concurrent_scans = 28
+    scan_callback = -1
+    shorter_scan_timeout = 2
+
+    def __init__(self, controllers):
+        BluetoothBaseTest.__init__(self, controllers)
+        self.scn_ad = self.android_devices[0]
+        self.adv_ad = self.android_devices[1]
+
+    def _setup_generic_advertisement(self):
+        self.adv_ad.droid.bleSetAdvertiseSettingsAdvertiseMode(
+            ble_advertise_settings_modes['low_latency'])
+        self.advertise_callback, advertise_data, advertise_settings = (
+            generate_ble_advertise_objects(self.adv_ad.droid))
+        self.adv_ad.droid.bleStartBleAdvertising(
+            self.advertise_callback, advertise_data, advertise_settings)
+        try:
+            self.adv_ad.ed.pop_event(adv_succ.format(self.advertise_callback))
+        except Empty:
+            self.log.error("Failed to start advertisement.")
+            return False
+        return True
+
+    def _setup_scan_with_no_filters(self):
+        filter_list, scan_settings, self.scan_callback = \
+            generate_ble_scan_objects(self.scn_ad.droid)
+        self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
+                                          self.scan_callback)
+
+    def _scan_found_results(self):
+        try:
+            self.scn_ad.ed.pop_event(
+                scan_result.format(self.scan_callback), bt_default_timeout)
+            self.log.info("Found an advertisement.")
+        except Empty:
+            self.log.info("Did not find an advertisement.")
+            return False
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='9b695819-e5a8-48b3-87a0-f90422998bf9')
+    def test_scan_no_filters_screen_on(self):
+        """Test LE scanning is successful with no filters and screen on.
+
+        Test LE scanning is successful with no filters and screen on. Scan
+        results should be found.
+
+        Steps:
+        1. Setup advertisement
+        2. Turn on screen
+        3. Start scanner without filters
+        4. Verify scan results are found
+        5. Teardown advertisement and scanner
+
+        Expected Result:
+        Scan results should be found.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, Screen
+        Priority: 2
+        """
+        # Step 1
+        if not self._setup_generic_advertisement():
+            return False
+
+        # Step 2
+        self.scn_ad.droid.wakeUpNow()
+
+        # Step 3
+        self._setup_scan_with_no_filters()
+
+        # Step 4
+        if not self._scan_found_results():
+            return False
+
+        # Step 5
+        self.adv_ad.droid.bleStopBleAdvertising(self.advertise_callback)
+        self.scn_ad.droid.bleStopBleScan(self.scan_callback)
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='38fb6959-f07b-4501-814b-81a498e3efc4')
+    def test_scan_no_filters_screen_off(self):
+        """Test LE scanning is successful with no filters and screen off.
+
+        Test LE scanning is successful with no filters and screen off. No scan
+        results should be found.
+
+        Steps:
+        1. Setup advertisement
+        2. Turn off screen
+        3. Start scanner without filters
+        4. Verify no scan results are found
+        5. Teardown advertisement and scanner
+
+        Expected Result:
+        No scan results should be found.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, Screen
+        Priority: 1
+        """
+        # Step 1
+        if not self._setup_generic_advertisement():
+            return False
+
+        # Step 2
+        self.scn_ad.droid.goToSleepNow()
+        # Give the device time to go to sleep
+        time.sleep(2)
+
+        # Step 3
+        self._setup_scan_with_no_filters()
+
+        # Step 4
+        if self._scan_found_results():
+            return False
+
+        # Step 5
+        self.adv_ad.droid.bleStopBleAdvertising(self.advertise_callback)
+        self.scn_ad.droid.bleStopBleScan(self.scan_callback)
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='7186ef2f-096a-462e-afde-b0e3d4ecdd83')
+    def test_scan_filters_works_with_screen_off(self):
+        """Test LE scanning is successful with filters and screen off.
+
+        Test LE scanning is successful with no filters and screen off. No scan
+        results should be found.
+
+        Steps:
+        1. Setup advertisement
+        2. Turn off screen
+        3. Start scanner with filters
+        4. Verify scan results are found
+        5. Teardown advertisement and scanner
+
+        Expected Result:
+        Scan results should be found.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, Screen
+        Priority: 1
+        """
+        # Step 1
+        adv_device_name = self.adv_ad.droid.bluetoothGetLocalName()
+        print(adv_device_name)
+        self.adv_ad.droid.bleSetAdvertiseDataIncludeDeviceName(True)
+        if not self._setup_generic_advertisement():
+            return False
+
+        # Step 2
+        self.scn_ad.droid.goToSleepNow()
+
+        # Step 3
+        self.scn_ad.droid.bleSetScanFilterDeviceName(adv_device_name)
+        filter_list, scan_settings, self.scan_callback = generate_ble_scan_objects(
+            self.scn_ad.droid)
+        self.scn_ad.droid.bleBuildScanFilter(filter_list)
+        self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
+                                          self.scan_callback)
+
+        # Step 4
+        if not self._scan_found_results():
+            return False
+
+        # Step 5
+        self.adv_ad.droid.bleStopBleAdvertising(self.advertise_callback)
+        self.scn_ad.droid.bleStopBleScan(self.scan_callback)
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='02cd6dca-149e-439b-8427-a2edc7864265')
+    def test_scan_no_filters_screen_off_then_turn_on(self):
+        """Test start LE scan with no filters while screen is off then turn on.
+
+        Test that a scan without filters will not return results while the
+        screen is off but will return results when the screen turns on.
+
+        Steps:
+        1. Setup advertisement
+        2. Turn off screen
+        3. Start scanner without filters
+        4. Verify no scan results are found
+        5. Turn screen on
+        6. Verify scan results are found
+        7. Teardown advertisement and scanner
+
+        Expected Result:
+        Scan results should only come in when the screen is on.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, Screen
+        Priority: 2
+        """
+        # Step 1
+        if not self._setup_generic_advertisement():
+            return False
+
+        # Step 2
+        self.scn_ad.droid.goToSleepNow()
+        # Give the device time to go to sleep
+        time.sleep(2)
+
+        # Step 3
+        self._setup_scan_with_no_filters()
+
+        # Step 4
+        if self._scan_found_results():
+            return False
+
+        # Step 5
+        self.scn_ad.droid.wakeUpNow()
+
+        # Step 6
+        if not self._scan_found_results():
+            return False
+
+        # Step 7
+        self.adv_ad.droid.bleStopBleAdvertising(self.advertise_callback)
+        self.scn_ad.droid.bleStopBleScan(self.scan_callback)
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='eb9fc373-f5e8-4a55-9750-02b7a11893d1')
+    def test_scan_no_filters_screen_on_then_turn_off(self):
+        """Test start LE scan with no filters while screen is on then turn off.
+
+        Test that a scan without filters will not return results while the
+        screen is off but will return results when the screen turns on.
+
+        Steps:
+        1. Setup advertisement
+        2. Turn off screen
+        3. Start scanner without filters
+        4. Verify no scan results are found
+        5. Turn screen on
+        6. Verify scan results are found
+        7. Teardown advertisement and scanner
+
+        Expected Result:
+        Scan results should only come in when the screen is on.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, Screen
+        Priority: 2
+        """
+        # Step 1
+        if not self._setup_generic_advertisement():
+            return False
+
+        # Step 2
+        self.scn_ad.droid.wakeUpNow()
+
+        # Step 3
+        self._setup_scan_with_no_filters()
+
+        # Step 4
+        if not self._scan_found_results():
+            return False
+
+        # Step 5
+        self.scn_ad.droid.goToSleepNow()
+        # Give the device time to go to sleep
+        time.sleep(2)
+        self.scn_ad.ed.clear_all_events()
+
+        # Step 6
+        if self._scan_found_results():
+            return False
+
+        # Step 7
+        self.adv_ad.droid.bleStopBleAdvertising(self.advertise_callback)
+        self.scn_ad.droid.bleStopBleScan(self.scan_callback)
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='41d90e11-b0a8-4eed-bff1-c19678920762')
+    def test_scan_no_filters_screen_toggling(self):
+        """Test start LE scan with no filters and test screen toggling.
+
+        Test that a scan without filters will not return results while the
+        screen is off and return results while the screen is on.
+
+        Steps:
+        1. Setup advertisement
+        2. Turn off screen
+        3. Start scanner without filters
+        4. Verify no scan results are found
+        5. Turn screen on
+        6. Verify scan results are found
+        7. Repeat steps 1-6 10 times
+        7. Teardown advertisement and scanner
+
+        Expected Result:
+        Scan results should only come in when the screen is on.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, Screen
+        Priority: 3
+        """
+        iterations = 10
+        # Step 1
+        if not self._setup_generic_advertisement():
+            return False
+
+        for i in range(iterations):
+            self.log.info("Starting iteration {}".format(i + 1))
+            # Step 2
+            self.scn_ad.droid.goToSleepNow()
+            # Give the device time to go to sleep
+            time.sleep(2)
+            self.scn_ad.ed.clear_all_events()
+
+            # Step 3
+            self._setup_scan_with_no_filters()
+
+            # Step 4
+            if self._scan_found_results():
+                return False
+
+            # Step 5
+            self.scn_ad.droid.wakeUpNow()
+
+            # Step 6
+            if not self._scan_found_results():
+                return False
+
+        # Step 7
+        self.adv_ad.droid.bleStopBleAdvertising(self.advertise_callback)
+        self.scn_ad.droid.bleStopBleScan(self.scan_callback)
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='7a2fe7ef-b15f-4e93-a2f0-40e2f7d9cbcb')
+    def test_opportunistic_scan_no_filters_screen_off_then_on(self):
+        """Test opportunistic scanning does not find results with screen off.
+
+        Test LE scanning is successful with no filters and screen off. No scan
+        results should be found.
+
+        Steps:
+        1. Setup advertisement
+        2. Turn off screen
+        3. Start opportunistic scan without filters
+        4. Start scan without filters
+        5. Verify no scan results are found on either scan instance
+        6. Wake up phone
+        7. Verify scan results on each scan instance
+        8. Teardown advertisement and scanner
+
+        Expected Result:
+        No scan results should be found.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, Screen
+        Priority: 1
+        """
+        # Step 1
+        if not self._setup_generic_advertisement():
+            return False
+
+        # Step 2
+        self.scn_ad.droid.goToSleepNow()
+        # Give the device time to go to sleep
+        time.sleep(2)
+
+        # Step 3
+        self.scn_ad.droid.bleSetScanSettingsScanMode(ble_scan_settings_modes[
+            'opportunistic'])
+        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
+            self.scn_ad.droid)
+        self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
+                                          scan_callback)
+
+        # Step 4
+        filter_list2, scan_settings2, scan_callback2 = generate_ble_scan_objects(
+            self.scn_ad.droid)
+        self.scn_ad.droid.bleStartBleScan(filter_list2, scan_settings2,
+                                          scan_callback2)
+
+        # Step 5
+        try:
+            self.scn_ad.ed.pop_event(
+                scan_result.format(scan_callback), self.shorter_scan_timeout)
+            self.log.error("Found an advertisement on opportunistic scan.")
+            return False
+        except Empty:
+            self.log.info("Did not find an advertisement.")
+        try:
+            self.scn_ad.ed.pop_event(
+                scan_result.format(scan_callback2), self.shorter_scan_timeout)
+            self.log.error("Found an advertisement on scan instance.")
+            return False
+        except Empty:
+            self.log.info("Did not find an advertisement.")
+
+        # Step 6
+        self.scn_ad.droid.wakeUpNow()
+
+        # Step 7
+        try:
+            self.scn_ad.ed.pop_event(
+                scan_result.format(scan_callback), self.shorter_scan_timeout)
+            self.log.info("Found an advertisement on opportunistic scan.")
+        except Empty:
+            self.log.error(
+                "Did not find an advertisement on opportunistic scan.")
+            return False
+        try:
+            self.scn_ad.ed.pop_event(
+                scan_result.format(scan_callback2), self.shorter_scan_timeout)
+            self.log.info("Found an advertisement on scan instance.")
+        except Empty:
+            self.log.info("Did not find an advertisement.")
+            return False
+
+        # Step 8
+        self.adv_ad.droid.bleStopBleAdvertising(self.advertise_callback)
+        self.scn_ad.droid.bleStopBleScan(scan_callback)
+        self.scn_ad.droid.bleStopBleScan(scan_callback2)
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='406f1a2e-160f-4fb2-8a87-6403996df36e')
+    def test_max_scan_no_filters_screen_off_then_turn_on(self):
+        """Test start max scans with no filters while screen is off then turn on
+
+        Test that max LE scan without filters will not return results while the
+        screen is off but will return results when the screen turns on.
+
+        Steps:
+        1. Setup advertisement
+        2. Turn off screen
+        3. Start scanner without filters and verify no scan results
+        4. Turn screen on
+        5. Verify scan results are found on each scan callback
+        6. Teardown advertisement and all scanner
+
+        Expected Result:
+        Scan results should only come in when the screen is on.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, Screen
+        Priority: 2
+        """
+        # Step 1
+        if not self._setup_generic_advertisement():
+            return False
+
+        # Step 2
+        self.scn_ad.droid.goToSleepNow()
+        # Give the device time to go to sleep
+        time.sleep(2)
+
+        # Step 3
+        scan_callback_list = []
+        for _ in range(self.max_concurrent_scans):
+            filter_list, scan_settings, scan_callback = \
+                generate_ble_scan_objects(self.scn_ad.droid)
+            self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
+                                              scan_callback)
+            scan_callback_list.append(scan_callback)
+            try:
+                self.scn_ad.ed.pop_event(
+                    scan_result.format(self.scan_callback),
+                    self.shorter_scan_timeout)
+                self.log.info("Found an advertisement.")
+                return False
+            except Empty:
+                self.log.info("Did not find an advertisement.")
+
+        # Step 4
+        self.scn_ad.droid.wakeUpNow()
+
+        # Step 5
+        for callback in scan_callback_list:
+            try:
+                self.scn_ad.ed.pop_event(
+                    scan_result.format(callback), self.shorter_scan_timeout)
+                self.log.info("Found an advertisement.")
+            except Empty:
+                self.log.info("Did not find an advertisement.")
+                return False
+
+        # Step 7
+        self.adv_ad.droid.bleStopBleAdvertising(self.advertise_callback)
+        for callback in scan_callback_list:
+            self.scn_ad.droid.bleStopBleScan(callback)
+        return True
diff --git a/acts/tests/google/bt/audio_lab/ThreeButtonDongleTest.py b/acts/tests/google/bt/audio_lab/ThreeButtonDongleTest.py
new file mode 100644
index 0000000..11d5dc0
--- /dev/null
+++ b/acts/tests/google/bt/audio_lab/ThreeButtonDongleTest.py
@@ -0,0 +1,199 @@
+#/usr/bin/env python3.4
+#
+# 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.
+"""
+Test script to test various ThreeButtonDongle devices
+"""
+import time
+
+from acts.controllers.relay_lib.relay import SynchronizeRelays
+from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
+from acts.test_utils.bt.bt_test_utils import clear_bonded_devices
+
+
+class ThreeButtonDongleTest(BluetoothBaseTest):
+    iterations = 10
+
+    def __init__(self, controllers):
+        BluetoothBaseTest.__init__(self, controllers)
+        self.dut = self.android_devices[0]
+        self.dongle = self.relay_devices[0]
+        self.log.info("Target dongle is {}".format(self.dongle.name))
+
+    def setup_test(self):
+        super(BluetoothBaseTest, self).setup_test()
+        self.dongle.setup()
+        return True
+
+    def teardown_test(self):
+        super(BluetoothBaseTest, self).teardown_test()
+        self.dongle.clean_up()
+        clear_bonded_devices(self.dut)
+        return True
+
+    def _pair_devices(self):
+        self.dut.droid.bluetoothStartPairingHelper(False)
+        self.dongle.enter_pairing_mode()
+
+        self.dut.droid.bluetoothBond(self.dongle.mac_address)
+
+        end_time = time.time() + 20
+        self.dut.log.info("Verifying devices are bonded")
+        while time.time() < end_time:
+            bonded_devices = self.dut.droid.bluetoothGetBondedDevices()
+
+            for d in bonded_devices:
+                if d['address'] == self.dongle.mac_address:
+                    self.dut.log.info("Successfully bonded to device.")
+                    self.log.info("Bonded devices:\n{}".format(bonded_devices))
+                return True
+        self.dut.log.info("Failed to bond devices.")
+        return False
+
+    @BluetoothBaseTest.bt_test_wrap
+    def test_pairing(self):
+        """Test pairing between a three button dongle and an Android device.
+
+        Test the dongle can be paired to Android device.
+
+        Steps:
+        1. Find the MAC address of remote controller from relay config file.
+        2. Start the device paring process.
+        3. Enable remote controller in pairing mode.
+        4. Verify the remote is paired.
+
+        Expected Result:
+          Remote controller is paired.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: Bluetooth, bonding, relay
+        Priority: 3
+        """
+        if not self._pair_devices():
+            return False
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    def test_pairing_multiple_iterations(self):
+        """Test pairing between a three button dongle and an Android device.
+
+        Test the dongle can be paired to Android device.
+
+        Steps:
+        1. Find the MAC address of remote controller from relay config file.
+        2. Start the device paring process.
+        3. Enable remote controller in pairing mode.
+        4. Verify the remote is paired.
+
+        Expected Result:
+          Remote controller is paired.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: Bluetooth, bonding, relay
+        Priority: 3
+        """
+        for i in range(self.iterations):
+            self.log.info("Testing iteration {}.".format(i))
+            if not self._pair_devices():
+                return False
+            self.log.info("Unbonding devices.")
+            self.dut.droid.bluetoothUnbond(self.dongle.mac_address)
+            # Sleep for relax time for the relay
+            time.sleep(2)
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    def test_next_multiple_iterations(self):
+        """Test pairing for multiple iterations.
+
+        Test the dongle can be paired to Android device.
+
+        Steps:
+        1. Pair devices
+        2. Press the next button on dongle for pre-definied iterations.
+
+        Expected Result:
+          Test is successful
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: Bluetooth, bonding, relay
+        Priority: 3
+        """
+        if not self._pair_devices():
+            return False
+        for _ in range(self.iterations):
+            self.dongle.press_next()
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    def test_play_pause_multiple_iterations(self):
+        """Test play/pause button on a three button dongle.
+
+        Test the dongle can be paired to Android device.
+
+        Steps:
+        1. Pair devices
+        2. Press the next button on dongle for pre-definied iterations.
+
+        Expected Result:
+          Test is successful
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: Bluetooth, bonding, relay
+        Priority: 3
+        """
+        if not self._pair_devices():
+            return False
+        for _ in range(self.iterations):
+            self.dongle.press_play_pause()
+        return True
+
+    @BluetoothBaseTest.bt_test_wrap
+    def test_previous_mulitple_iterations(self):
+        """Test previous button on a three button dongle.
+
+        Test the dongle can be paired to Android device.
+
+        Steps:
+        1. Pair devices
+        2. Press the next button on dongle for pre-definied iterations.
+
+        Expected Result:
+          Test is successful
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: Bluetooth, bonding, relay
+        Priority: 3
+        """
+        if not self._pair_devices():
+            return False
+        for _ in range(100):
+            self.dongle.press_previous()
+        return True
diff --git a/acts/tests/google/bt/car_bt/BtCarBasicFunctionalityTest.py b/acts/tests/google/bt/car_bt/BtCarBasicFunctionalityTest.py
index 49031af..bc6f3d4 100644
--- a/acts/tests/google/bt/car_bt/BtCarBasicFunctionalityTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarBasicFunctionalityTest.py
@@ -20,6 +20,7 @@
 import time
 
 from queue import Empty
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.BtEnum import BluetoothScanModeType
 from acts.test_utils.bt.bt_test_utils import check_device_supported_profiles
@@ -42,7 +43,7 @@
     def setup_class(self):
         return setup_multiple_devices_for_bt_test(self.android_devices)
 
-    #@BluetoothTest(UUID=b52a032a-3438-4b84-863f-c46a969882a4)
+    @test_tracker_info(uuid='b52a032a-3438-4b84-863f-c46a969882a4')
     @BluetoothBaseTest.bt_test_wrap
     def test_if_support_a2dp_sink_profile(self):
         """ Test that a single device can support A2DP SNK profile.
@@ -59,7 +60,7 @@
             return False
         return True
 
-    #@BluetoothTest(UUID=3c2cb613-6c8a-4ed7-8783-37fb47bff5f2)
+    @test_tracker_info(uuid='3c2cb613-6c8a-4ed7-8783-37fb47bff5f2')
     @BluetoothBaseTest.bt_test_wrap
     def test_if_support_hfp_client_profile(self):
         """ Test that a single device can support HFP HF profile.
@@ -76,7 +77,7 @@
             return False
         return True
 
-    #@BluetoothTest(UUID=c3854e74-33da-4e4d-a9cb-4f5170ef7d10)
+    @test_tracker_info(uuid='c3854e74-33da-4e4d-a9cb-4f5170ef7d10')
     @BluetoothBaseTest.bt_test_wrap
     def test_if_support_pbap_client_profile(self):
         """ Test that a single device can support PBAP PCE profile.
diff --git a/acts/tests/google/bt/car_bt/BtCarHfpConferenceTest.py b/acts/tests/google/bt/car_bt/BtCarHfpConferenceTest.py
index 43f3dfc..60784b8 100644
--- a/acts/tests/google/bt/car_bt/BtCarHfpConferenceTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarHfpConferenceTest.py
@@ -19,6 +19,7 @@
 
 import time
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.BluetoothCarHfpBaseTest import BluetoothCarHfpBaseTest
 from acts.test_utils.bt import BtEnum
@@ -55,7 +56,7 @@
             attempts -= 1
         return connected
 
-    #@BluetoothTest(UUID=a9657693-b534-4625-bf91-69a1d1b9a943)
+    @test_tracker_info(uuid='a9657693-b534-4625-bf91-69a1d1b9a943')
     @BluetoothBaseTest.bt_test_wrap
     def test_multi_way_call_accept(self):
         """
diff --git a/acts/tests/google/bt/car_bt/BtCarHfpConnectionTest.py b/acts/tests/google/bt/car_bt/BtCarHfpConnectionTest.py
index 98aa8f3..a59d17d 100644
--- a/acts/tests/google/bt/car_bt/BtCarHfpConnectionTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarHfpConnectionTest.py
@@ -19,6 +19,7 @@
 
 import time
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.BluetoothCarHfpBaseTest import BluetoothCarHfpBaseTest
 from acts.test_utils.bt import BtEnum
@@ -57,7 +58,7 @@
         self.hf.droid.bluetoothDisconnectConnected(
             self.ag.droid.bluetoothGetLocalAddress())
 
-    #@BluetoothTest(UUID=a6669f9b-fb49-4bd8-aa9c-9d6369e34442)
+    @test_tracker_info(uuid='a6669f9b-fb49-4bd8-aa9c-9d6369e34442')
     @BluetoothBaseTest.bt_test_wrap
     def test_call_transfer_disconnect_connect(self):
         """
@@ -96,8 +97,9 @@
             return False
 
         # Now connect the devices.
-        if not bt_test_utils.connect_pri_to_sec(self.hf, self.ag, set(
-            [BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
+        if not bt_test_utils.connect_pri_to_sec(
+                self.hf, self.ag,
+                set([BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
             self.log.error("Could not connect HF and AG {} {}".format(
                 self.hf.serial, self.ag.serial))
             return False
@@ -116,7 +118,7 @@
 
         return ret
 
-    #@BluetoothTest(UUID=97727b64-a590-4d84-a257-1facd8aafd16)
+    @test_tracker_info(uuid='97727b64-a590-4d84-a257-1facd8aafd16')
     @BluetoothBaseTest.bt_test_wrap
     def test_call_transfer_off_on(self):
         """
@@ -140,8 +142,9 @@
         Priority: 1
         """
         # Connect HF & AG
-        if not bt_test_utils.connect_pri_to_sec(self.hf, self.ag, set(
-            [BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
+        if not bt_test_utils.connect_pri_to_sec(
+                self.hf, self.ag,
+                set([BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
             self.log.error("Could not connect HF and AG {} {}".format(
                 self.hf.serial, self.ag.serial))
             return False
@@ -195,7 +198,7 @@
 
         return ret
 
-    #@BluetoothTest(UUID=95f76e2c-1cdd-4a7c-8e26-863b4c4242be)
+    @test_tracker_info(uuid='95f76e2c-1cdd-4a7c-8e26-863b4c4242be')
     @BluetoothBaseTest.bt_test_wrap
     def test_call_transfer_connect_disconnect_connect(self):
         """
@@ -221,8 +224,9 @@
         Priority: 1
         """
         # Now connect the devices.
-        if not bt_test_utils.connect_pri_to_sec(self.hf, self.ag, set(
-            [BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
+        if not bt_test_utils.connect_pri_to_sec(
+                self.hf, self.ag,
+                set([BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
             self.log.error("Could not connect HF and AG {} {}".format(
                 self.hf.serial, self.ag.serial))
             return False
@@ -267,8 +271,9 @@
             return False
 
         # Now connect the devices.
-        if not bt_test_utils.connect_pri_to_sec(self.hf, self.ag, set(
-            [BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
+        if not bt_test_utils.connect_pri_to_sec(
+                self.hf, self.ag,
+                set([BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
             self.log.error("Could not connect HF and AG {} {}".format(
                 self.hf.serial, self.ag.serial))
             # Additional profile connection check for b/
diff --git a/acts/tests/google/bt/car_bt/BtCarHfpFuzzTest.py b/acts/tests/google/bt/car_bt/BtCarHfpFuzzTest.py
index f6166b1..9408f37 100644
--- a/acts/tests/google/bt/car_bt/BtCarHfpFuzzTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarHfpFuzzTest.py
@@ -20,6 +20,7 @@
 
 import time
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.BluetoothCarHfpBaseTest import BluetoothCarHfpBaseTest
 from acts.test_utils.bt import BtEnum
@@ -52,17 +53,19 @@
         # Delay set contains the delay between dial and hangup for a call.
         # We keep very small delays to significantly large ones to stress test
         # various kind of timing issues.
-        self.delay_set = [0.1,
-                          0.2,
-                          0.3,
-                          0.4,
-                          0.5,  # Very short delays
-                          1.0,
-                          2.0,
-                          3.0,
-                          4.0,
-                          5.0,  # Med delays
-                          10.0]  # Large delays
+        self.delay_set = [
+            0.1,
+            0.2,
+            0.3,
+            0.4,
+            0.5,  # Very short delays
+            1.0,
+            2.0,
+            3.0,
+            4.0,
+            5.0,  # Med delays
+            10.0
+        ]  # Large delays
 
     def dial_a_hangup_b_quick(self, a, b, delay=0, ph=""):
         """
@@ -120,7 +123,7 @@
 
         return True
 
-    #@BluetoothTest(UUID=32022c74-fdf3-44c4-9e82-e518bdcce667)
+    @test_tracker_info(uuid='32022c74-fdf3-44c4-9e82-e518bdcce667')
     @BluetoothBaseTest.bt_test_wrap
     def test_fuzz_outgoing_hf(self):
         """
@@ -161,7 +164,7 @@
         # above).
         return self.stabilize_and_check_sanity()
 
-    #@BluetoothTest(UUID=bc6d52b2-4acc-461e-ad55-fad5a5ecb091)
+    @test_tracker_info(uuid='bc6d52b2-4acc-461e-ad55-fad5a5ecb091')
     @BluetoothBaseTest.bt_test_wrap
     def test_fuzz_outgoing_ag(self):
         """
@@ -202,7 +205,7 @@
         # above).
         return self.stabilize_and_check_sanity()
 
-    #@BluetoothTest(UUID=d834384a-38d5-4260-bfd5-98f8207c04f5)
+    @test_tracker_info(uuid='d834384a-38d5-4260-bfd5-98f8207c04f5')
     @BluetoothBaseTest.bt_test_wrap
     def test_fuzz_dial_hf_hangup_ag(self):
         """
@@ -243,7 +246,7 @@
         # above).
         return self.stabilize_and_check_sanity()
 
-    #@BluetoothTest(UUID=6de1a8ab-3cb0-4594-a9bb-d882a3414836)
+    @test_tracker_info(uuid='6de1a8ab-3cb0-4594-a9bb-d882a3414836')
     @BluetoothBaseTest.bt_test_wrap
     def test_fuzz_dial_ag_hangup_hf(self):
         """
diff --git a/acts/tests/google/bt/car_bt/BtCarHfpTest.py b/acts/tests/google/bt/car_bt/BtCarHfpTest.py
index 63a092c..3bdfc56 100644
--- a/acts/tests/google/bt/car_bt/BtCarHfpTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarHfpTest.py
@@ -18,6 +18,7 @@
 """
 
 import time
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.BluetoothCarHfpBaseTest import BluetoothCarHfpBaseTest
 from acts.test_utils.bt import BtEnum
@@ -37,21 +38,22 @@
         if not super(BtCarHfpTest, self).setup_class():
             return False
         # Disable the A2DP profile.
-        bt_test_utils.set_profile_priority(
-            self.hf, self.ag, [BtEnum.BluetoothProfile.PBAP_CLIENT.value,
-                               BtEnum.BluetoothProfile.A2DP_SINK.value],
-            BtEnum.BluetoothPriorityLevel.PRIORITY_OFF)
+        bt_test_utils.set_profile_priority(self.hf, self.ag, [
+            BtEnum.BluetoothProfile.PBAP_CLIENT.value,
+            BtEnum.BluetoothProfile.A2DP_SINK.value
+        ], BtEnum.BluetoothPriorityLevel.PRIORITY_OFF)
         bt_test_utils.set_profile_priority(
             self.hf, self.ag, [BtEnum.BluetoothProfile.HEADSET_CLIENT.value],
             BtEnum.BluetoothPriorityLevel.PRIORITY_ON)
 
-        if not bt_test_utils.connect_pri_to_sec(self.hf, self.ag, set(
-            [BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
+        if not bt_test_utils.connect_pri_to_sec(
+                self.hf, self.ag,
+                set([BtEnum.BluetoothProfile.HEADSET_CLIENT.value])):
             self.log.error("Failed to connect.")
             return False
         return True
 
-    #@BluetoothTest(UUID=4ce2195a-b70a-4584-912e-cbd20d20e19d)
+    @test_tracker_info(uuid='4ce2195a-b70a-4584-912e-cbd20d20e19d')
     @BluetoothBaseTest.bt_test_wrap
     def test_default_calling_account(self):
         """
@@ -86,11 +88,11 @@
             return False
         if not acc_component_id.startswith(BLUETOOTH_PKG_NAME):
             self.hf.log.error("Component name does not start with pkg name {}".
-                          format(selected_acc))
+                              format(selected_acc))
             return False
         return True
 
-    #@BluetoothTest(UUID=e579009d-05f3-4236-a698-5de8c11d73a9)
+    @test_tracker_info(uuid='e579009d-05f3-4236-a698-5de8c11d73a9')
     @BluetoothBaseTest.bt_test_wrap
     def test_outgoing_call_hf(self):
         """
@@ -114,7 +116,7 @@
         """
         return self.dial_a_hangup_b(self.hf, self.hf)
 
-    #@BluetoothTest(UUID=c9d5f9cd-f275-4adf-b212-c2e9a70d4cac)
+    @test_tracker_info(uuid='c9d5f9cd-f275-4adf-b212-c2e9a70d4cac')
     @BluetoothBaseTest.bt_test_wrap
     def test_outgoing_call_ag(self):
         """
@@ -138,7 +140,7 @@
         """
         return self.dial_a_hangup_b(self.ag, self.ag)
 
-    #@BluetoothTest(UUID=908c199b-ca65-4694-821d-1b864ee3fe69)
+    @test_tracker_info(uuid='908c199b-ca65-4694-821d-1b864ee3fe69')
     @BluetoothBaseTest.bt_test_wrap
     def test_outgoing_dial_ag_hangup_hf(self):
         """
@@ -162,7 +164,7 @@
         """
         return self.dial_a_hangup_b(self.ag, self.hf)
 
-    #@BluetoothTest(UUID=5d1d52c7-51d8-4c82-b437-2e91a6220db3)
+    @test_tracker_info(uuid='5d1d52c7-51d8-4c82-b437-2e91a6220db3')
     @BluetoothBaseTest.bt_test_wrap
     def test_outgoing_dial_hf_hangup_ag(self):
         """
@@ -186,7 +188,7 @@
         """
         return self.dial_a_hangup_b(self.hf, self.ag)
 
-    #@BluetoothTest(UUID=a718e238-7e31-40c9-a45b-72081210cc73)
+    @test_tracker_info(uuid='a718e238-7e31-40c9-a45b-72081210cc73')
     @BluetoothBaseTest.bt_test_wrap
     def test_incoming_dial_re_hangup_re(self):
         """
diff --git a/acts/tests/google/bt/car_bt/BtCarMapMceTest.py b/acts/tests/google/bt/car_bt/BtCarMapMceTest.py
index f8248f3..ba8760a 100644
--- a/acts/tests/google/bt/car_bt/BtCarMapMceTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarMapMceTest.py
@@ -21,6 +21,7 @@
 import queue
 
 import acts
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.BluetoothCarHfpBaseTest import BluetoothCarHfpBaseTest
 from acts.test_utils.bt import bt_test_utils
@@ -93,12 +94,14 @@
             return False
         return True
 
+    @test_tracker_info(uuid='0858347a-e649-4f18-85b6-6990cc311dee')
     @BluetoothBaseTest.bt_test_wrap
     def test_send_message(self):
         bt_test_utils.connect_pri_to_sec(
             self.MCE, self.MSE, set([BtEnum.BluetoothProfile.MAP_MCE.value]))
         return self.send_message([self.REMOTE])
 
+    @test_tracker_info(uuid='b25caa53-3c7f-4cfa-a0ec-df9a8f925fe5')
     @BluetoothBaseTest.bt_test_wrap
     def test_receive_message(self):
         bt_test_utils.connect_pri_to_sec(
@@ -114,6 +117,7 @@
         self.MCE.log.info(receivedMessage['data'])
         return True
 
+    @test_tracker_info(uuid='5b7b3ded-0a1a-470f-b119-9a03bc092805')
     @BluetoothBaseTest.bt_test_wrap
     def test_send_message_failure_no_cellular(self):
         if not toggle_airplane_mode_by_adb(self.log, self.MSE, True):
@@ -123,10 +127,12 @@
             self.MCE, self.MSE, set([BtEnum.BluetoothProfile.MAP_MCE.value]))
         return not self.send_message([self.REMOTE])
 
+    @test_tracker_info(uuid='19444142-1d07-47dc-860b-f435cba46fca')
     @BluetoothBaseTest.bt_test_wrap
     def test_send_message_failure_no_map_connection(self):
         return not self.send_message([self.REMOTE])
 
+    @test_tracker_info(uuid='c7e569c0-9f6c-49a4-8132-14bc544ccb53')
     @BluetoothBaseTest.bt_test_wrap
     def test_send_message_failure_no_bluetooth(self):
         if not toggle_airplane_mode_by_adb(self.log, self.MSE, True):
@@ -139,6 +145,7 @@
             self.MCE.log.info("Failed to connect as expected")
         return not self.send_message([self.REMOTE])
 
+    @test_tracker_info(uuid='8cdb4a54-3f18-482f-be3d-acda9c4cbeed')
     @BluetoothBaseTest.bt_test_wrap
     def test_disconnect_failure_send_message(self):
         connected = bt_test_utils.connect_pri_to_sec(
@@ -157,6 +164,7 @@
         return connected and disconnected and not self.send_message(
             [self.REMOTE])
 
+    @test_tracker_info(uuid='2d79a896-b1c1-4fb7-9924-db8b5c698be5')
     @BluetoothBaseTest.bt_test_wrap
     def manual_test_send_message_to_contact(self):
         bt_test_utils.connect_pri_to_sec(
@@ -170,6 +178,7 @@
                 selected_contact['data'], "Don't Text and Drive!")
         return False
 
+    @test_tracker_info(uuid='8ce9a7dd-3b5e-4aee-a897-30740e2439c3')
     @BluetoothBaseTest.bt_test_wrap
     def test_send_message_to_multiple_phones(self):
         bt_test_utils.connect_pri_to_sec(
diff --git a/acts/tests/google/bt/car_bt/BtCarMediaConnectionTest.py b/acts/tests/google/bt/car_bt/BtCarMediaConnectionTest.py
index b99a7c3..7865bc1 100644
--- a/acts/tests/google/bt/car_bt/BtCarMediaConnectionTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarMediaConnectionTest.py
@@ -19,6 +19,7 @@
 
 import time
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt import bt_test_utils
 from acts.test_utils.car import car_bt_utils
@@ -77,7 +78,7 @@
                 return True
         return False
 
-    #@BluetoothTest(UUID=1934c0d5-3fa3-43e5-a91f-2c8a4424f5cd)
+    @test_tracker_info(uuid='1934c0d5-3fa3-43e5-a91f-2c8a4424f5cd')
     @BluetoothBaseTest.bt_test_wrap
     def test_a2dp_connect_disconnect_from_src(self):
         """
@@ -101,8 +102,9 @@
         if (car_media_utils.is_a2dp_connected(self.log, self.SNK, self.SRC)):
             self.log.info("Already Connected")
         else:
-            if (not bt_test_utils.connect_pri_to_sec(self.SRC, self.SNK, set(
-                [BtEnum.BluetoothProfile.A2DP.value]))):
+            if (not bt_test_utils.connect_pri_to_sec(
+                    self.SRC, self.SNK,
+                    set([BtEnum.BluetoothProfile.A2DP.value]))):
                 return False
 
         result = bt_test_utils.disconnect_pri_from_sec(
@@ -122,7 +124,7 @@
 
         return True
 
-    #@BluetoothTest(UUID=70d30007-540a-4e86-bd75-ab218774350e)
+    @test_tracker_info(uuid='70d30007-540a-4e86-bd75-ab218774350e')
     @BluetoothBaseTest.bt_test_wrap
     def test_a2dp_connect_disconnect_from_snk(self):
         """
@@ -147,8 +149,9 @@
         if car_media_utils.is_a2dp_connected(self.log, self.SNK, self.SRC):
             self.log.info("Already Connected")
         else:
-            if (not bt_test_utils.connect_pri_to_sec(self.SNK, self.SRC, set(
-                [BtEnum.BluetoothProfile.A2DP_SINK.value]))):
+            if (not bt_test_utils.connect_pri_to_sec(
+                    self.SNK, self.SRC,
+                    set([BtEnum.BluetoothProfile.A2DP_SINK.value]))):
                 return False
         # Disconnect
         result = bt_test_utils.disconnect_pri_from_sec(
diff --git a/acts/tests/google/bt/car_bt/BtCarMediaPassthroughTest.py b/acts/tests/google/bt/car_bt/BtCarMediaPassthroughTest.py
index 9585449..05d1737 100644
--- a/acts/tests/google/bt/car_bt/BtCarMediaPassthroughTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarMediaPassthroughTest.py
@@ -20,6 +20,7 @@
 import os
 import time
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt import bt_test_utils
 from acts.test_utils.bt import BtEnum
@@ -62,7 +63,9 @@
                 "Missing mandatory user config \"local_media_path\"!")
             return False
         self.local_media_path = self.user_params["local_media_path"]
-        if not os.path.isdir(self.local_media_path):
+        if type(self.local_media_path) is list:
+            self.log.info("Media ready to push as is.")
+        elif not os.path.isdir(self.local_media_path):
             self.local_media_path = os.path.join(
                 self.user_params[Config.key_config_path],
                 self.local_media_path)
@@ -87,7 +90,11 @@
 
         # Push media files from self.local_media_path to ANDROID_MEDIA_PATH
         # Refer to note in the beginning of file
-        self.TG.adb.push("{} {}".format(self.local_media_path,
+        if type(self.local_media_path) is list:
+            for item in self.local_media_path:
+                self.TG.adb.push("{} {}".format(item, self.android_music_path))
+        else:
+            self.TG.adb.push("{} {}".format(self.local_media_path,
                                         self.android_music_path))
 
         return True
@@ -130,7 +137,7 @@
                     return False
         return True
 
-    #@BluetoothTest(UUID=cf4fae08-f4f6-4e0d-b00a-4f6c41d69ff9)
+    @test_tracker_info(uuid='cf4fae08-f4f6-4e0d-b00a-4f6c41d69ff9')
     @BluetoothBaseTest.bt_test_wrap
     def test_play_pause(self):
         """
@@ -161,7 +168,7 @@
             return False
         return True
 
-    #@BluetoothTest(UUID=15615b26-3a49-4fa0-b369-41962e8de192)
+    @test_tracker_info(uuid='15615b26-3a49-4fa0-b369-41962e8de192')
     @BluetoothBaseTest.bt_test_wrap
     def test_passthrough(self):
         """
@@ -204,6 +211,7 @@
         return True
 
     @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='d4103c82-6d21-486b-bc25-007f988245b9')
     def test_media_metadata(self):
         """
         Test if the metadata matches between the two ends.
@@ -272,6 +280,7 @@
             return False
 
     @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='8f6179db-b800-4ff0-b55f-ee79e009c1a8')
     def test_disconnect_while_media_playing(self):
         """
         Disconnect BT between CT and TG in the middle of a audio streaming session and check
@@ -345,6 +354,7 @@
         return True
 
     @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='46cd95c8-2066-4018-846d-03366796e94f')
     def test_connect_while_media_playing(self):
         """
         BT connect SRC and SNK when the SRC is already playing music and verify SNK strarts streaming
@@ -389,9 +399,9 @@
             car_media_utils.CMD_MEDIA_PLAY)
         # At this point, media should be playing only on phone, not on Car, since they are disconnected
         if not car_media_utils.isMediaSessionActive(
-                self.log, self.TG,
-                PHONE_MEDIA_BROWSER_SERVICE_NAME) or car_media_utils.isMediaSessionActive(
-                    self.log, self.CT, CAR_MEDIA_BROWSER_SERVICE_NAME):
+                self.log, self.TG, PHONE_MEDIA_BROWSER_SERVICE_NAME
+        ) or car_media_utils.isMediaSessionActive(
+                self.log, self.CT, CAR_MEDIA_BROWSER_SERVICE_NAME):
             self.log.error("Media playing in wrong end")
             return False
 
@@ -402,8 +412,9 @@
             return False
 
         # Now connect to Car on Bluetooth
-        if (not bt_test_utils.connect_pri_to_sec(self.SRC, self.SNK, set(
-            [BtEnum.BluetoothProfile.A2DP.value]))):
+        if (not bt_test_utils.connect_pri_to_sec(
+                self.SRC, self.SNK, set(
+                    [BtEnum.BluetoothProfile.A2DP.value]))):
             return False
 
         # Wait for a bit for the information to show up in the car side
diff --git a/acts/tests/google/bt/car_bt/BtCarPairedConnectDisconnectTest.py b/acts/tests/google/bt/car_bt/BtCarPairedConnectDisconnectTest.py
index 635ba86..6a695d6 100644
--- a/acts/tests/google/bt/car_bt/BtCarPairedConnectDisconnectTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarPairedConnectDisconnectTest.py
@@ -27,6 +27,7 @@
 
 import time
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.base_test import BaseTestClass
 from acts.test_utils.bt import bt_test_utils
@@ -56,7 +57,7 @@
             len(devices), 1,
             "pair_pri_to_sec succeeded but no bonded devices.")
 
-    #@BluetoothTest(UUID=b0babf3b-8049-4b64-9125-408efb1bbcd2)
+    @test_tracker_info(uuid='b0babf3b-8049-4b64-9125-408efb1bbcd2')
     @BluetoothBaseTest.bt_test_wrap
     def test_pairing(self):
         """
@@ -82,13 +83,14 @@
             self.car.droid.bluetoothGetLocalAddress(),
             BtEnum.BluetoothPriorityLevel.PRIORITY_OFF.value)
         addr = self.ph.droid.bluetoothGetLocalAddress()
-        if not bt_test_utils.connect_pri_to_sec(self.car, self.ph, set(
-            [BtEnum.BluetoothProfile.A2DP_SINK.value])):
+        if not bt_test_utils.connect_pri_to_sec(
+                self.car, self.ph,
+                set([BtEnum.BluetoothProfile.A2DP_SINK.value])):
             if not bt_test_utils.is_a2dp_snk_device_connected(self.car, addr):
                 return False
         return True
 
-    #@BluetoothTest(UUID=a44f13e2-c012-4292-8dd5-9f32a023e297)
+    @test_tracker_info(uuid='a44f13e2-c012-4292-8dd5-9f32a023e297')
     @BluetoothBaseTest.bt_test_wrap
     def test_connect_disconnect_paired(self):
         """
@@ -114,9 +116,12 @@
         for i in range(NUM_TEST_RUNS):
             self.log.info("Running test [" + str(i) + "/" + str(NUM_TEST_RUNS)
                           + "]")
-            success = bt_test_utils.connect_pri_to_sec(self.car, self.ph, set(
-                [BtEnum.BluetoothProfile.HEADSET_CLIENT.value,
-                 BtEnum.BluetoothProfile.A2DP_SINK.value]))
+            success = bt_test_utils.connect_pri_to_sec(
+                self.car, self.ph,
+                set([
+                    BtEnum.BluetoothProfile.HEADSET_CLIENT.value,
+                    BtEnum.BluetoothProfile.A2DP_SINK.value
+                ]))
 
             # Check if we got connected.
             if not success:
@@ -133,9 +138,10 @@
 
             # Disconnect the devices.
             success = bt_test_utils.disconnect_pri_from_sec(
-                self.car, self.ph,
-                [BtEnum.BluetoothProfile.HEADSET_CLIENT.value,
-                 BtEnum.BluetoothProfile.A2DP_SINK.value])
+                self.car, self.ph, [
+                    BtEnum.BluetoothProfile.HEADSET_CLIENT.value,
+                    BtEnum.BluetoothProfile.A2DP_SINK.value
+                ])
 
             if success is False:
                 self.car.log.info("Disconnect failed.")
@@ -153,4 +159,3 @@
         if failure > 0:
             return False
         return True
-
diff --git a/acts/tests/google/bt/car_bt/BtCarPairingTest.py b/acts/tests/google/bt/car_bt/BtCarPairingTest.py
index 12a58c9..09810d1 100644
--- a/acts/tests/google/bt/car_bt/BtCarPairingTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarPairingTest.py
@@ -19,6 +19,7 @@
 
 import time
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.base_test import BaseTestClass
 from acts.test_utils.bt import bt_test_utils
@@ -37,7 +38,7 @@
         self.car = self.android_devices[0]
         self.ph = self.android_devices[1]
 
-    #@BluetoothTest(UUID=bf56e915-eef7-45cd-b5a6-771f6ef72602)
+    @test_tracker_info(uuid='f56e915-eef7-45cd-b5a6-771f6ef72602')
     @BluetoothBaseTest.bt_test_wrap
     def test_simple_pairing(self):
         """
@@ -89,7 +90,7 @@
             return False
         return True
 
-    #@BluetoothTest(UUID=be4db211-10a0-479a-8958-dff0ccadca1a)
+    @test_tracker_info(uuid='be4db211-10a0-479a-8958-dff0ccadca1a')
     @BluetoothBaseTest.bt_test_wrap
     def test_repairing(self):
         """
diff --git a/acts/tests/google/bt/car_bt/BtCarPbapTest.py b/acts/tests/google/bt/car_bt/BtCarPbapTest.py
index bacf4c4..bc1b930 100644
--- a/acts/tests/google/bt/car_bt/BtCarPbapTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarPbapTest.py
@@ -19,6 +19,7 @@
 import os
 import time
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt.bt_test_utils import setup_multiple_devices_for_bt_test
 from acts.base_test import BaseTestClass
@@ -139,7 +140,7 @@
             self.pce, 0)
         return contacts_added and contacts_removed
 
-    #@BluetoothTest(UUID=7dcdecfc-42d1-4f41-b66e-823c8f161356)
+    @test_tracker_info(uuid='7dcdecfc-42d1-4f41-b66e-823c8f161356')
     @BluetoothBaseTest.bt_test_wrap
     def test_pbap_connect_and_disconnect(self):
         """Test Connectivity
@@ -187,7 +188,7 @@
 
         return True
 
-    #@BluetoothTest(UUID=1733efb9-71af-4956-bd3a-0d3167d94d0c)
+    @test_tracker_info(uuid='1733efb9-71af-4956-bd3a-0d3167d94d0c')
     @BluetoothBaseTest.bt_test_wrap
     def test_contact_download(self):
         """Test Contact Download
@@ -222,7 +223,7 @@
             return False
         return bt_contacts_utils.erase_contacts(self.pce)
 
-    #@BluetoothTest(UUID=99dc6ac6-b7cf-45ce-927b-8c4ebf8ab664)
+    @test_tracker_info(uuid='99dc6ac6-b7cf-45ce-927b-8c4ebf8ab664')
     @BluetoothBaseTest.bt_test_wrap
     def test_modify_phonebook(self):
         """Test Modify Phonebook
@@ -258,7 +259,7 @@
             self.pse, self.contacts_destination_path, PSE_CONTACTS_FILE)
         return self.connect_and_verify(phone_numbers_added)
 
-    #@BluetoothTest(UUID=bbe31bf5-51e8-4175-b266-1c7750e44f5b)
+    @test_tracker_info(uuid='bbe31bf5-51e8-4175-b266-1c7750e44f5b')
     @BluetoothBaseTest.bt_test_wrap
     def test_special_contacts(self):
         """Test Special Contacts
@@ -332,7 +333,7 @@
 
         return self.connect_and_verify(phone_numbers_added)
 
-    #@BluetoothTest(UUID=2aa2bd00-86cc-4f39-a06a-90b17ea5b320)
+    @test_tracker_info(uuid='2aa2bd00-86cc-4f39-a06a-90b17ea5b320')
     @BluetoothBaseTest.bt_test_wrap
     def test_call_log(self):
         """Test Call Log
@@ -399,6 +400,8 @@
 
         return True
 
+    @test_tracker_info(uuid='bb018bf4-5a61-478d-acce-eef88050e489')
+    @BluetoothBaseTest.bt_test_wrap
     def test_multiple_phones(self):
         """Test Multiple Phones
 
diff --git a/acts/tests/google/bt/car_bt/BtCarToggleTest.py b/acts/tests/google/bt/car_bt/BtCarToggleTest.py
index fa2bf23..b903dfe 100644
--- a/acts/tests/google/bt/car_bt/BtCarToggleTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarToggleTest.py
@@ -17,6 +17,7 @@
 This test is used to test basic functionality of bluetooth adapter by turning it ON/OFF.
 """
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 from acts.test_utils.bt import bt_test_utils
 
@@ -31,6 +32,7 @@
     def on_fail(self, test_name, begin_time):
         bt_test_utils.take_btsnoop_logs(self.android_devices, self, test_name)
 
+    @test_tracker_info(uuid='290eb41f-6e66-4dc1-8f3e-55783901d116')
     @BluetoothBaseTest.bt_test_wrap
     def test_bluetooth_reset(self):
         """Test resetting bluetooth.
diff --git a/acts/tests/google/bt/power/SetupBTPairingTest.py b/acts/tests/google/bt/power/SetupBTPairingTest.py
new file mode 100644
index 0000000..4478fd4
--- /dev/null
+++ b/acts/tests/google/bt/power/SetupBTPairingTest.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3.4
+#
+# 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.
+
+"""
+This test script leverages the relay_lib to pair different BT devices. This
+script will be invoked from Tradefed test. The test will first setup pairing
+between BT device and DUT and wait for signal (through socket) from tradefed
+to power down the BT device
+"""
+
+import logging
+import socket
+import sys
+import time
+
+from acts import base_test
+
+class SetupBTPairingTest(base_test.BaseTestClass):
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+
+    def setup_test(self):
+        self.bt_device = self.relay_devices[0]
+
+    def wait_for_test_completion(self):
+        port = int(self.user_params["socket_port"])
+        timeout = float(self.user_params["socket_timeout_secs"])
+
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+        server_address = ('localhost', port)
+        logging.info("Starting server socket on localhost port %s", port)
+        sock.bind(('localhost', port))
+        sock.settimeout(timeout)
+        sock.listen(1)
+        logging.info("Waiting for client socket connection")
+        try:
+            connection, client_address = sock.accept()
+        except socket.timeout:
+            logging.error("Did not receive signal. Shutting down AP")
+        except socket.error:
+            logging.error("Socket connection errored out. Shutting down AP")
+        finally:
+            if connection is not None:
+                connection.close()
+            if sock is not None:
+                sock.shutdown(socket.SHUT_RDWR)
+                sock.close()
+
+
+    def enable_pairing_mode(self):
+        self.bt_device.setup()
+        self.bt_device.power_on()
+        # Wait for a moment between pushing buttons
+        time.sleep(0.25)
+        self.bt_device.enter_pairing_mode()
+
+    def test_bt_pairing(self):
+        req_params = [
+            "RelayDevice", "socket_port", "socket_timeout_secs"
+        ]
+        opt_params = []
+        self.unpack_userparams(
+            req_param_names=req_params, opt_param_names=opt_params)
+        # Setup BT pairing mode
+        self.enable_pairing_mode()
+        # BT pairing mode is turned on
+        self.wait_for_test_completion()
+
+    def teardown_test(self):
+        self.bt_device.power_off()
+        self.bt_device.clean_up()
diff --git a/acts/tests/google/tel/lab/TelLabMobilityTest.py b/acts/tests/google/tel/lab/TelLabMobilityTest.py
new file mode 100644
index 0000000..8ce7b7e
--- /dev/null
+++ b/acts/tests/google/tel/lab/TelLabMobilityTest.py
@@ -0,0 +1,473 @@
+#/usr/bin/env python3.4
+#
+#   Copyright 2016 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+"""
+Sanity tests for voice tests in telephony
+"""
+import time
+
+from acts.controllers.anritsu_lib._anritsu_utils import AnritsuError
+from acts.controllers.anritsu_lib.md8475a import MD8475A
+from acts.controllers.anritsu_lib.md8475a import BtsNumber
+from acts.test_utils.tel.anritsu_utils import WAIT_TIME_ANRITSU_REG_AND_CALL
+from acts.test_utils.tel.anritsu_utils import handover_tc
+from acts.test_utils.tel.anritsu_utils import make_ims_call
+from acts.test_utils.tel.anritsu_utils import tear_down_call
+from acts.test_utils.tel.anritsu_utils import set_system_model_lte_lte
+from acts.test_utils.tel.anritsu_utils import set_system_model_lte_wcdma
+from acts.test_utils.tel.anritsu_utils import set_system_model_lte_gsm
+from acts.test_utils.tel.anritsu_utils import set_system_model_lte_1x
+from acts.test_utils.tel.anritsu_utils import set_system_model_lte_evdo
+from acts.test_utils.tel.anritsu_utils import set_usim_parameters
+from acts.test_utils.tel.tel_defines import CALL_TEARDOWN_PHONE
+from acts.test_utils.tel.tel_defines import RAT_FAMILY_CDMA2000
+from acts.test_utils.tel.tel_defines import RAT_FAMILY_GSM
+from acts.test_utils.tel.tel_defines import RAT_FAMILY_LTE
+from acts.test_utils.tel.tel_defines import RAT_FAMILY_UMTS
+from acts.test_utils.tel.tel_defines import RAT_1XRTT
+from acts.test_utils.tel.tel_defines import NETWORK_MODE_CDMA
+from acts.test_utils.tel.tel_defines import NETWORK_MODE_GSM_ONLY
+from acts.test_utils.tel.tel_defines import NETWORK_MODE_GSM_UMTS
+from acts.test_utils.tel.tel_defines import NETWORK_MODE_LTE_CDMA_EVDO
+from acts.test_utils.tel.tel_defines import NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA
+from acts.test_utils.tel.tel_defines import NETWORK_MODE_LTE_GSM_WCDMA
+from acts.test_utils.tel.tel_defines import WAIT_TIME_IN_CALL
+from acts.test_utils.tel.tel_defines import WAIT_TIME_IN_CALL_FOR_IMS
+from acts.test_utils.tel.tel_test_utils import ensure_network_rat
+from acts.test_utils.tel.tel_test_utils import ensure_phones_idle
+from acts.test_utils.tel.tel_test_utils import toggle_airplane_mode_by_adb
+from acts.test_utils.tel.tel_test_utils import toggle_volte
+from acts.test_utils.tel.tel_test_utils import run_multithread_func
+from acts.test_utils.tel.tel_test_utils import iperf_test_by_adb
+from acts.test_utils.tel.tel_voice_utils import phone_idle_volte
+from acts.test_utils.tel.TelephonyBaseTest import TelephonyBaseTest
+from acts.utils import adb_shell_ping
+from acts.utils import rand_ascii_str
+from acts.controllers import iperf_server
+from acts.utils import exe_cmd
+
+DEFAULT_CALL_NUMBER = "0123456789"
+DEFAULT_PING_DURATION = 5
+WAITTIME_BEFORE_HANDOVER = 20
+WAITTIME_AFTER_HANDOVER = 20
+
+
+class TelLabMobilityTest(TelephonyBaseTest):
+    def __init__(self, controllers):
+        TelephonyBaseTest.__init__(self, controllers)
+        self.ad = self.android_devices[0]
+        self.ad.sim_card = getattr(self.ad, "sim_card", None)
+        self.md8475a_ip_address = self.user_params[
+            "anritsu_md8475a_ip_address"]
+        self.wlan_option = self.user_params.get("anritsu_wlan_option", False)
+        self.voice_call_number = self.user_params.get('voice_call_number',
+                                                      DEFAULT_CALL_NUMBER)
+        self.ip_server = self.iperf_servers[0]
+        self.port_num = self.ip_server.port
+        self.log.info("Iperf Port is %s", self.port_num)
+
+    def setup_class(self):
+        try:
+            self.anritsu = MD8475A(self.md8475a_ip_address, self.log,
+                                   self.wlan_option)
+        except AnritsuError:
+            self.log.error("Error in connecting to Anritsu Simulator")
+            return False
+        return True
+
+    def setup_test(self):
+        try:
+            self.ad.droid.telephonyFactoryReset()
+        except Exception as e:
+            self.ad.log.error(e)
+        toggle_airplane_mode_by_adb(self.log, self.ad, True)
+        self.ad.adb.shell(
+            "setprop net.lte.ims.volte.provisioned 1", ignore_status=True)
+        # get a handle to virtual phone
+        self.virtualPhoneHandle = self.anritsu.get_VirtualPhone()
+        return True
+
+    def teardown_test(self):
+        self.log.info("Stopping Simulation")
+        self.anritsu.stop_simulation()
+        toggle_airplane_mode_by_adb(self.log, self.ad, True)
+        return True
+
+    def teardown_class(self):
+        self.anritsu.disconnect()
+        return True
+
+    def active_handover(self,
+                        set_simulation_func,
+                        phone_setup_func,
+                        phone_idle_func_after_registration=None,
+                        volte=True,
+                        iperf=True,
+                        all_bands=True,
+                        is_wait_for_registration=True,
+                        voice_number=DEFAULT_CALL_NUMBER,
+                        teardown_side=CALL_TEARDOWN_PHONE,
+                        wait_time_in_call=WAIT_TIME_IN_CALL):
+        try:
+            bts = set_simulation_func(self.anritsu, self.user_params,
+                                      self.ad.sim_card)
+            set_usim_parameters(self.anritsu, self.ad.sim_card)
+
+            self.anritsu.start_simulation()
+            self.anritsu.send_command("IMSSTARTVN 1")
+
+            self.ad.droid.telephonyToggleDataConnection(False)
+
+            # turn off all other BTS to ensure UE registers on BTS1
+            sim_model = (self.anritsu.get_simulation_model()).split(",")
+            no_of_bts = len(sim_model)
+            for i in range(2, no_of_bts + 1):
+                self.anritsu.send_command("OUTOFSERVICE OUT,BTS{}".format(i))
+            if phone_setup_func is not None:
+                if not phone_setup_func(self.ad):
+                    self.log.error("phone_setup_func failed.")
+
+            if is_wait_for_registration:
+                self.anritsu.wait_for_registration_state()
+
+            if phone_idle_func_after_registration:
+                if not phone_idle_func_after_registration(self.log, self.ad):
+                    self.log.error("phone_idle_func failed.")
+
+            for i in range(2, no_of_bts + 1):
+                self.anritsu.send_command("OUTOFSERVICE IN,BTS{}".format(i))
+
+            time.sleep(WAIT_TIME_ANRITSU_REG_AND_CALL)
+
+            if iperf:
+                server_ip = self.iperf_setup()
+                if not server_ip:
+                    self.log.error("iperf server can not be reached by ping")
+                    return False
+
+            if volte:
+                if not make_ims_call(self.log, self.ad, self.anritsu,
+                                     voice_number):
+                    self.log.error("Phone {} Failed to make volte call to {}"
+                                   .format(self.ad.serial, voice_number))
+                    return False
+
+            if not iperf:  # VoLTE only
+                result = handover_tc(self.log, self.anritsu,
+                                     WAITTIME_BEFORE_HANDOVER, BtsNumber.BTS1,
+                                     BtsNumber.BTS2)
+                time.sleep(WAITTIME_AFTER_HANDOVER)
+            else:  # with iPerf
+                iperf_task = (self._iperf_task, (
+                    server_ip,
+                    WAITTIME_BEFORE_HANDOVER + WAITTIME_AFTER_HANDOVER - 10))
+                ho_task = (handover_tc,
+                           (self.log, self.anritsu, WAITTIME_BEFORE_HANDOVER,
+                            BtsNumber.BTS1, BtsNumber.BTS2))
+                result = run_multithread_func(self.log, [ho_task, iperf_task])
+                if not result[1]:
+                    self.log.error("iPerf failed.")
+                    return False
+
+            self.log.info("handover test case result code {}.".format(result[
+                0]))
+
+            if volte:
+                # check if the phone stay in call
+                if not self.ad.droid.telecomIsInCall():
+                    self.log.error("Call is already ended in the phone.")
+                    return False
+
+                if not tear_down_call(self.log, self.ad, self.anritsu):
+                    self.log.error("Phone {} Failed to tear down"
+                                   .format(self.ad.serial, voice_number))
+                    return False
+
+            simmodel = self.anritsu.get_simulation_model().split(',')
+            if simmodel[1] == "WCDMA" and iperf:
+                iperf_task = (self._iperf_task, (
+                    server_ip,
+                    WAITTIME_BEFORE_HANDOVER + WAITTIME_AFTER_HANDOVER - 10))
+                ho_task = (handover_tc,
+                           (self.log, self.anritsu, WAITTIME_BEFORE_HANDOVER,
+                            BtsNumber.BTS2, BtsNumber.BTS1))
+                result = run_multithread_func(self.log, [ho_task, iperf_task])
+                if not result[1]:
+                    self.log.error("iPerf failed.")
+                    return False
+                self.log.info("handover test case result code {}.".format(
+                    result[0]))
+
+        except AnritsuError as e:
+            self.log.error("Error in connection with Anritsu Simulator: " +
+                           str(e))
+            return False
+        except Exception as e:
+            self.log.error("Exception during voice call procedure: " + str(e))
+            return False
+        return True
+
+    def iperf_setup(self):
+        # Fetch IP address of the host machine
+        cmd = "|".join(("ifconfig", "grep eth0 -A1", "grep inet",
+                        "cut -d ':' -f2", "cut -d ' ' -f 1"))
+        destination_ip = exe_cmd(cmd)
+        destination_ip = (destination_ip.decode("utf-8")).split("\n")[0]
+        self.log.info("Dest IP is %s", destination_ip)
+
+        if not adb_shell_ping(self.ad, DEFAULT_PING_DURATION, destination_ip):
+            self.log.error("Pings failed to Destination.")
+            return False
+
+        return destination_ip
+
+    def _iperf_task(self, destination_ip, duration):
+        self.log.info("Starting iPerf task")
+        self.ip_server.start()
+        tput_dict = {"Uplink": 0, "Downlink": 0}
+        if iperf_test_by_adb(
+                self.log,
+                self.ad,
+                destination_ip,
+                self.port_num,
+                True,  # reverse = true
+                duration,
+                rate_dict=tput_dict):
+            uplink = tput_dict["Uplink"]
+            downlink = tput_dict["Downlink"]
+            self.ip_server.stop()
+            return True
+        else:
+            self.log.error("iperf failed to Destination.")
+            self.ip_server.stop()
+            return False
+
+    def _phone_setup_lte_wcdma(self, ad):
+        return ensure_network_rat(
+            self.log,
+            ad,
+            NETWORK_MODE_LTE_GSM_WCDMA,
+            RAT_FAMILY_LTE,
+            toggle_apm_after_setting=True)
+
+    def _phone_setup_lte_1x(self, ad):
+        return ensure_network_rat(
+            self.log,
+            ad,
+            NETWORK_MODE_LTE_CDMA_EVDO,
+            RAT_FAMILY_LTE,
+            toggle_apm_after_setting=True)
+
+    def _phone_setup_wcdma(self, ad):
+        return ensure_network_rat(
+            self.log,
+            ad,
+            NETWORK_MODE_GSM_UMTS,
+            RAT_FAMILY_UMTS,
+            toggle_apm_after_setting=True)
+
+    def _phone_setup_gsm(self, ad):
+        return ensure_network_rat(
+            self.log,
+            ad,
+            NETWORK_MODE_GSM_ONLY,
+            RAT_FAMILY_GSM,
+            toggle_apm_after_setting=True)
+
+    def _phone_setup_1x(self, ad):
+        return ensure_network_rat(
+            self.log,
+            ad,
+            NETWORK_MODE_CDMA,
+            RAT_FAMILY_CDMA2000,
+            toggle_apm_after_setting=True)
+
+    def _phone_setup_airplane_mode(self, ad):
+        return toggle_airplane_mode_by_adb(self.log, ad, True)
+
+    def _phone_setup_volte_airplane_mode(self, ad):
+        toggle_volte(self.log, ad, True)
+        return toggle_airplane_mode_by_adb(self.log, ad, True)
+
+    def _phone_setup_volte(self, ad):
+        ad.droid.telephonyToggleDataConnection(True)
+        toggle_volte(self.log, ad, True)
+        return ensure_network_rat(
+            self.log,
+            ad,
+            NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA,
+            RAT_FAMILY_LTE,
+            toggle_apm_after_setting=True)
+
+    """ Tests Begin """
+
+    @TelephonyBaseTest.tel_test_wrap
+    def test_volte_iperf_handover(self):
+        """ Test VoLTE to VoLTE Inter-Freq handover with iPerf data
+        Steps:
+        1. Setup CallBox for 2 LTE cells with 2 different bands.
+        2. Turn on DUT and enable VoLTE. Make an voice call to DEFAULT_CALL_NUMBER.
+        3. Check if VoLTE voice call connected successfully.
+        4. Start iPerf data transfer
+        5. Handover the call to BTS2 and check if the call is still up.
+        6. Check iPerf data throughput
+        7. Tear down the call.
+
+        Expected Results:
+        1. VoLTE Voice call is made successfully.
+        2. After handover, the call is not dropped.
+        3. Tear down call succeed.
+
+        Returns:
+            True if pass; False if fail
+        """
+        return self.active_handover(
+            set_system_model_lte_lte,
+            self._phone_setup_volte,
+            phone_idle_volte,
+            volte=True,
+            iperf=True)
+
+    @TelephonyBaseTest.tel_test_wrap
+    def test_volte_handover(self):
+        """ Test VoLTE to VoLTE Inter-Freq handover without iPerf data
+        Steps:
+        1. Setup CallBox for 2 LTE cells with 2 different bands.
+        2. Turn on DUT and enable VoLTE. Make an voice call to DEFAULT_CALL_NUMBER.
+        3. Check if VoLTE voice call connected successfully.
+        4. Handover the call to BTS2 and check if the call is still up.
+        5. Tear down the call.
+
+        Expected Results:
+        1. VoLTE Voice call is made successfully.
+        2. After handover, the call is not dropped.
+        3. Tear down call succeed.
+
+        Returns:
+            True if pass; False if fail
+        """
+        return self.active_handover(
+            set_system_model_lte_lte,
+            self._phone_setup_volte,
+            phone_idle_volte,
+            volte=True,
+            iperf=False)
+
+    @TelephonyBaseTest.tel_test_wrap
+    def test_iperf_handover(self):
+        """ Test Inter-Freq handover with iPerf data
+        Steps:
+        1. Setup CallBox for 2 LTE cells with 2 different bands.
+        2. Turn on DUT and enable VoLTE.
+        3. Start iPerf data transfer
+        4. Handover the call to BTS2
+        5. Check iPerf data throughput
+
+        Expected Results:
+        1. Data call is made successfully.
+        2. After handover, the data is not dropped.
+
+        Returns:
+            True if pass; False if fail
+        """
+        return self.active_handover(
+            set_system_model_lte_lte,
+            self._phone_setup_volte,
+            phone_idle_volte,
+            volte=False,
+            iperf=True)
+
+    @TelephonyBaseTest.tel_test_wrap
+    def test_volte_iperf_handover_wcdma(self):
+        """ Test VoLTE to VoLTE Inter-Freq handover with iPerf data
+        Steps:
+        1. Setup CallBox for 2 LTE cells with 2 different bands.
+        2. Turn on DUT and enable VoLTE. Make an voice call to DEFAULT_CALL_NUMBER.
+        3. Check if VoLTE voice call connected successfully.
+        4. Start iPerf data transfer
+        5. Handover the call to BTS2 and check if the call is still up.
+        6. Check iPerf data throughput
+        7. Tear down the call.
+
+        Expected Results:
+        1. VoLTE Voice call is made successfully.
+        2. After handover, the call is not dropped.
+        3. Tear down call succeed.
+
+        Returns:
+            True if pass; False if fail
+        """
+        return self.active_handover(
+            set_system_model_lte_wcdma,
+            self._phone_setup_volte,
+            phone_idle_volte,
+            volte=True,
+            iperf=True)
+
+    @TelephonyBaseTest.tel_test_wrap
+    def test_volte_handover_wcdma(self):
+        """ Test VoLTE to VoLTE Inter-Freq handover with iPerf data
+        Steps:
+        1. Setup CallBox for 2 LTE cells with 2 different bands.
+        2. Turn on DUT and enable VoLTE. Make an voice call to DEFAULT_CALL_NUMBER.
+        3. Check if VoLTE voice call connected successfully.
+        4. Start iPerf data transfer
+        5. Handover the call to BTS2 and check if the call is still up.
+        6. Check iPerf data throughput
+        7. Tear down the call.
+
+        Expected Results:
+        1. VoLTE Voice call is made successfully.
+        2. After handover, the call is not dropped.
+        3. Tear down call succeed.
+
+        Returns:
+            True if pass; False if fail
+        """
+        return self.active_handover(
+            set_system_model_lte_wcdma,
+            self._phone_setup_volte,
+            phone_idle_volte,
+            volte=True,
+            iperf=False)
+
+    @TelephonyBaseTest.tel_test_wrap
+    def test_iperf_handover_wcdma(self):
+        """ Test VoLTE to VoLTE Inter-Freq handover with iPerf data
+        Steps:
+        1. Setup CallBox for 2 LTE cells with 2 different bands.
+        2. Turn on DUT and enable VoLTE. Make an voice call to DEFAULT_CALL_NUMBER.
+        3. Check if VoLTE voice call connected successfully.
+        4. Start iPerf data transfer
+        5. Handover the call to BTS2 and check if the call is still up.
+        6. Check iPerf data throughput
+        7. Tear down the call.
+
+        Expected Results:
+        1. VoLTE Voice call is made successfully.
+        2. After handover, the call is not dropped.
+        3. Tear down call succeed.
+
+        Returns:
+            True if pass; False if fail
+        """
+        return self.active_handover(
+            set_system_model_lte_wcdma,
+            self._phone_setup_volte,
+            phone_idle_volte,
+            volte=False,
+            iperf=True)
+
+    """ Tests End """
diff --git a/acts/tests/google/tel/live/TelLiveMobilityStressTest.py b/acts/tests/google/tel/live/TelLiveMobilityStressTest.py
index 7da1491..16d4d95 100644
--- a/acts/tests/google/tel/live/TelLiveMobilityStressTest.py
+++ b/acts/tests/google/tel/live/TelLiveMobilityStressTest.py
@@ -68,7 +68,7 @@
 from TelWifiVoiceTest import ATTEN_NAME_FOR_CELL_4G
 
 import socket
-from acts.controllers.sl4a_client import Sl4aProtocolError
+from acts.controllers.sl4a_lib.rpc_client import Sl4aProtocolError
 
 IGNORE_EXCEPTIONS = (BrokenPipeError, Sl4aProtocolError)
 EXCEPTION_TOLERANCE = 20
diff --git a/acts/tests/google/tel/live/TelLiveRebootStressTest.py b/acts/tests/google/tel/live/TelLiveRebootStressTest.py
index bb36923..bcdbe66 100644
--- a/acts/tests/google/tel/live/TelLiveRebootStressTest.py
+++ b/acts/tests/google/tel/live/TelLiveRebootStressTest.py
@@ -20,7 +20,7 @@
 import collections
 import time
 from acts.test_decorators import test_tracker_info
-from acts.controllers.sl4a_types import Sl4aNetworkInfo
+from acts.controllers.sl4a_lib.sl4a_types import Sl4aNetworkInfo
 from acts.test_utils.tel.TelephonyBaseTest import TelephonyBaseTest
 from acts.test_utils.tel.tel_data_utils import wifi_tethering_setup_teardown
 from acts.test_utils.tel.tel_defines import AOSP_PREFIX
diff --git a/acts/tests/google/tel/live/TelLiveStressTest.py b/acts/tests/google/tel/live/TelLiveStressTest.py
index 62dfcba..6cb0bca 100644
--- a/acts/tests/google/tel/live/TelLiveStressTest.py
+++ b/acts/tests/google/tel/live/TelLiveStressTest.py
@@ -61,8 +61,7 @@
 from acts.utils import get_current_epoch_time
 from acts.utils import rand_ascii_str
 
-import socket
-from acts.controllers.sl4a_client import Sl4aProtocolError
+from acts.controllers.sl4a_lib.rpc_client import Sl4aProtocolError
 
 IGNORE_EXCEPTIONS = (BrokenPipeError, Sl4aProtocolError)
 EXCEPTION_TOLERANCE = 20
diff --git a/acts/tests/google/wifi/WifiRttManagerTest.py b/acts/tests/google/wifi/WifiRttManagerTest.py
index a906c6b..743d895 100644
--- a/acts/tests/google/wifi/WifiRttManagerTest.py
+++ b/acts/tests/google/wifi/WifiRttManagerTest.py
@@ -21,7 +21,7 @@
 import acts.test_utils.wifi.wifi_test_utils as wutils
 import acts.utils
 from acts import asserts
-from acts.controllers import sl4a_client
+from acts.controllers.sl4a_lib import rpc_client
 
 WifiEnums = wutils.WifiEnums
 
@@ -118,11 +118,11 @@
     def invalid_params_logic(self, rtt_params):
         try:
             self.dut.droid.wifiRttStartRanging([rtt_params])
-        except sl4a_client.Sl4aApiError as e:
+        except rpc_client.Sl4aApiError as e:
             e_str = str(e)
-            asserts.assert_true("IllegalArgumentException" in e_str,
-                                "Missing IllegalArgumentException in %s." %
-                                e_str)
+            asserts.assert_true(
+                "IllegalArgumentException" in e_str,
+                "Missing IllegalArgumentException in %s." % e_str)
             msg = "Got expected exception with invalid param %s." % rtt_params
             self.log.info(msg)
 
@@ -286,8 +286,8 @@
                         if not is_d_valid:
                             self.log.warning(
                                 ("Reported distance %.2fm is out of the"
-                                 " acceptable range %.2f±%.2fm.") %
-                                (d, acd, margin))
+                                 " acceptable range %.2f±%.2fm.") % (d, acd,
+                                                                     margin))
                             out_of_range += 1
                         continue
                     # Check if the RTT value is in range.
@@ -407,24 +407,34 @@
     def test_invalid_params(self):
         """Tests the sanity check function in RttManager.
         """
-        param_list = [
-            {RttParam.device_type: 3}, {RttParam.device_type: 1,
-                                        RttParam.request_type: 3}, {
-                                            RttParam.device_type: 1,
-                                            RttParam.request_type: 1,
-                                            RttParam.BSSID: None
-                                        }, {RttParam.BSSID: "xxxxxxxx",
-                                            RttParam.number_burst: 1},
-            {RttParam.number_burst: 0,
-             RttParam.num_samples_per_burst: -1},
-            {RttParam.num_samples_per_burst: 32}, {
-                RttParam.num_samples_per_burst: 5,
-                RttParam.num_retries_per_measurement_frame: -1
-            }, {RttParam.num_retries_per_measurement_frame: 4}, {
-                RttParam.num_retries_per_measurement_frame: 2,
-                RttParam.num_retries_per_FTMR: -1
-            }, {RttParam.num_retries_per_FTMR: 4}
-        ]
+        param_list = [{
+            RttParam.device_type: 3
+        }, {
+            RttParam.device_type: 1,
+            RttParam.request_type: 3
+        }, {
+            RttParam.device_type: 1,
+            RttParam.request_type: 1,
+            RttParam.BSSID: None
+        }, {
+            RttParam.BSSID: "xxxxxxxx",
+            RttParam.number_burst: 1
+        }, {
+            RttParam.number_burst: 0,
+            RttParam.num_samples_per_burst: -1
+        }, {
+            RttParam.num_samples_per_burst: 32
+        }, {
+            RttParam.num_samples_per_burst: 5,
+            RttParam.num_retries_per_measurement_frame: -1
+        }, {
+            RttParam.num_retries_per_measurement_frame: 4
+        }, {
+            RttParam.num_retries_per_measurement_frame: 2,
+            RttParam.num_retries_per_FTMR: -1
+        }, {
+            RttParam.num_retries_per_FTMR: 4
+        }]
         for param in param_list:
             self.invalid_params_logic(param)
         return True
@@ -434,21 +444,20 @@
         devices support device-to-ap RTT.
         """
         model = acts.utils.trim_model_name(self.dut.model)
-        asserts.assert_true(
-            self.dut.droid.wifiIsDeviceToDeviceRttSupported(),
-            "Device to device is supposed to be supported.")
+        asserts.assert_true(self.dut.droid.wifiIsDeviceToDeviceRttSupported(),
+                            "Device to device is supposed to be supported.")
         if any([model in m for m in self.support_models]):
             asserts.assert_true(self.dut.droid.wifiIsDeviceToApRttSupported(),
                                 "%s should support device-to-ap RTT." % model)
             self.log.info("%s supports device-to-ap RTT as expected." % model)
         else:
-            asserts.assert_false(self.dut.droid.wifiIsDeviceToApRttSupported(),
-                                 "%s should not support device-to-ap RTT." %
-                                 model)
-            self.log.info((
-                "%s does not support device-to-ap RTT as expected.") % model)
-            asserts.abort_class("Device %s does not support RTT, abort." %
-                                model)
+            asserts.assert_false(
+                self.dut.droid.wifiIsDeviceToApRttSupported(),
+                "%s should not support device-to-ap RTT." % model)
+            self.log.info(
+                ("%s does not support device-to-ap RTT as expected.") % model)
+            asserts.abort_class(
+                "Device %s does not support RTT, abort." % model)
         return True
 
     def test_capability_check(self):
@@ -458,8 +467,8 @@
         asserts.assert_true(caps, "Unable to get rtt capabilities.")
         self.log.debug("Got rtt capabilities %s" % caps)
         model = acts.utils.trim_model_name(self.dut.model)
-        asserts.assert_true(model in self.rtt_cap_table, "Unknown model %s" %
-                            model)
+        asserts.assert_true(model in self.rtt_cap_table,
+                            "Unknown model %s" % model)
         expected_caps = self.rtt_cap_table[model]
         for k, v in expected_caps.items():
             asserts.assert_true(k in caps, "%s missing in capabilities." % k)
diff --git a/acts/tests/google/wifi/aware/functional/AttachTest.py b/acts/tests/google/wifi/aware/functional/AttachTest.py
index 191a26f..598cca6 100644
--- a/acts/tests/google/wifi/aware/functional/AttachTest.py
+++ b/acts/tests/google/wifi/aware/functional/AttachTest.py
@@ -16,6 +16,7 @@
 
 import time
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.wifi import wifi_test_utils as wutils
 from acts.test_utils.wifi.aware import aware_const as aconsts
 from acts.test_utils.wifi.aware import aware_test_utils as autils
@@ -27,6 +28,7 @@
   def __init__(self, controllers):
     AwareBaseTest.__init__(self, controllers)
 
+  @test_tracker_info(uuid="cdafd1e0-bcf5-4fe8-ae32-f55483db9925")
   def test_attach(self):
     """Functional test case / Attach test cases / attach
 
@@ -38,6 +40,7 @@
     autils.wait_for_event(dut, aconsts.EVENT_CB_ON_ATTACHED)
     autils.fail_on_event(dut, aconsts.EVENT_CB_ON_IDENTITY_CHANGED)
 
+  @test_tracker_info(uuid="82f2a8bc-a62b-49c2-ac8a-fe8460010ba2")
   def test_attach_with_identity(self):
     """Functional test case / Attach test cases / attach with identity callback
 
@@ -49,6 +52,7 @@
     autils.wait_for_event(dut, aconsts.EVENT_CB_ON_ATTACHED)
     autils.wait_for_event(dut, aconsts.EVENT_CB_ON_IDENTITY_CHANGED)
 
+  @test_tracker_info(uuid="d2714d14-f330-47d4-b8e9-ee4d5e5b7ea0")
   def test_attach_multiple_sessions(self):
     """Functional test case / Attach test cases / multiple attach sessions
 
@@ -90,6 +94,7 @@
                          autils.decorate_event(
                              aconsts.EVENT_CB_ON_IDENTITY_CHANGED, id3))
 
+  @test_tracker_info(uuid="b8ea4d02-ae23-42a7-a85e-def52932c858")
   def test_attach_with_no_wifi(self):
     """Function test case / Attach test cases / attempt to attach with wifi off
 
@@ -103,6 +108,7 @@
     dut.droid.wifiAwareAttach()
     autils.wait_for_event(dut, aconsts.EVENT_CB_ON_ATTACH_FAILED)
 
+  @test_tracker_info(uuid="7ffde8e7-a010-4b77-97f5-959f263b5249")
   def test_attach_apm_toggle_attach_again(self):
     """Validates that enabling Airplane mode while Aware is on resets it
     correctly, and allows it to be re-enabled when Airplane mode is then
diff --git a/acts/tests/google/wifi/aware/functional/CapabilitiesTest.py b/acts/tests/google/wifi/aware/functional/CapabilitiesTest.py
index 55e63e9..b9b6108 100644
--- a/acts/tests/google/wifi/aware/functional/CapabilitiesTest.py
+++ b/acts/tests/google/wifi/aware/functional/CapabilitiesTest.py
@@ -15,8 +15,8 @@
 #   limitations under the License.
 
 from acts import asserts
-from acts import signals
 
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.net import connectivity_const as cconsts
 from acts.test_utils.wifi.aware import aware_const as aconsts
 from acts.test_utils.wifi.aware import aware_test_utils as autils
@@ -82,6 +82,7 @@
 
   ###############################
 
+  @test_tracker_info(uuid="45da8a41-6c02-4434-9eb9-aa0a36ff9f65")
   def test_max_discovery_sessions(self):
     """Validate that the device can create as many discovery sessions as are
     indicated in the device capabilities
@@ -148,7 +149,7 @@
     - On discovery set up NDP
 
     Note: the test requires MAX_NDP + 2 devices to be validated. If these are
-    not available it will be skipped (not failed).
+    not available the test will fail.
     """
     dut = self.android_devices[0]
 
@@ -156,10 +157,16 @@
     # same)
     max_ndp = dut.aware_capabilities[aconsts.CAP_MAX_NDP_SESSIONS]
 
-    if len(self.android_devices) < max_ndp + 2:
-      raise signals.TestSkip('Setup does not contain a sufficient number of '
-                             'devices: need %d, have %d' % (max_ndp + 2,
-                             len(self.android_devices)))
+    # get number of attached devices: needs to be max_ndp+2 to allow for max_ndp
+    # NDPs + an additional one expected to fail.
+    # However, will run the test with max_ndp+1 devices to verify that at least
+    # that many NDPs can be created. Will still fail at the end to indicate that
+    # full test was not run.
+    num_peer_devices = min(len(self.android_devices) - 1, max_ndp + 1)
+    asserts.assert_true(
+        num_peer_devices >= max_ndp,
+        'A minimum of %d devices is needed to run the test, have %d' %
+        (max_ndp + 1, len(self.android_devices)))
 
     # attach
     session_id = dut.droid.wifiAwareAttach()
@@ -175,7 +182,7 @@
         expect_success=True)
 
     # loop over other DUTs
-    for i in range(max_ndp + 1):
+    for i in range(num_peer_devices):
       other_dut = self.android_devices[i + 1]
 
       # attach
@@ -250,3 +257,7 @@
             (cconsts.NETWORK_CB_KEY_EVENT,
              cconsts.NETWORK_CB_LINK_PROPERTIES_CHANGED),
             (cconsts.NETWORK_CB_KEY_ID, s_req_key))
+
+    asserts.assert_true(num_peer_devices > max_ndp,
+                        'Needed %d devices to run the test, have %d' %
+                        (max_ndp + 2, len(self.android_devices)))
diff --git a/acts/tests/google/wifi/aware/functional/DataPathTest.py b/acts/tests/google/wifi/aware/functional/DataPathTest.py
index 8414c1d..7e77c79 100644
--- a/acts/tests/google/wifi/aware/functional/DataPathTest.py
+++ b/acts/tests/google/wifi/aware/functional/DataPathTest.py
@@ -16,6 +16,8 @@
 
 import time
 
+from acts import asserts
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.net import connectivity_const as cconsts
 from acts.test_utils.wifi.aware import aware_const as aconsts
 from acts.test_utils.wifi.aware import aware_test_utils as autils
@@ -334,8 +336,10 @@
 
     # Publisher & Subscriber: fail on network formation
     time.sleep(autils.EVENT_NDP_TIMEOUT)
-    autils.fail_on_event(p_dut, cconsts.EVENT_NETWORK_CALLBACK, timeout=0)
-    autils.fail_on_event(s_dut, cconsts.EVENT_NETWORK_CALLBACK, timeout=0)
+    autils.fail_on_event_with_keys(p_dut, cconsts.EVENT_NETWORK_CALLBACK, 0,
+                                   (cconsts.NETWORK_CB_KEY_ID, p_req_key))
+    autils.fail_on_event_with_keys(s_dut, cconsts.EVENT_NETWORK_CALLBACK, 0,
+                                   (cconsts.NETWORK_CB_KEY_ID, s_req_key))
 
     # clean-up
     p_dut.droid.connectivityUnregisterNetworkCallback(p_req_key)
@@ -414,8 +418,10 @@
 
     # Initiator & Responder: fail on network formation
     time.sleep(autils.EVENT_NDP_TIMEOUT)
-    autils.fail_on_event(init_dut, cconsts.EVENT_NETWORK_CALLBACK, timeout=0)
-    autils.fail_on_event(resp_dut, cconsts.EVENT_NETWORK_CALLBACK, timeout=0)
+    autils.fail_on_event_with_keys(init_dut, cconsts.EVENT_NETWORK_CALLBACK, 0,
+                                   (cconsts.NETWORK_CB_KEY_ID, init_req_key))
+    autils.fail_on_event_with_keys(resp_dut, cconsts.EVENT_NETWORK_CALLBACK, 0,
+                                   (cconsts.NETWORK_CB_KEY_ID, resp_req_key))
 
     # clean-up
     resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_key)
@@ -437,6 +443,7 @@
   # peer using the Aware-provided peer handle (as opposed to a MAC address).
   #######################################
 
+  @test_tracker_info(uuid="fa30bedc-d1de-4440-bf25-ec00d10555af")
   def test_ib_unsolicited_passive_open_specific(self):
     """Data-path: in-band, unsolicited/passive, open encryption, specific peer
 
@@ -448,6 +455,7 @@
         encr_type=self.ENCR_TYPE_OPEN,
         use_peer_id=True)
 
+  @test_tracker_info(uuid="57fc9d53-32ae-470f-a8b1-2fe37893687d")
   def test_ib_unsolicited_passive_open_any(self):
     """Data-path: in-band, unsolicited/passive, open encryption, any peer
 
@@ -459,6 +467,7 @@
         encr_type=self.ENCR_TYPE_OPEN,
         use_peer_id=False)
 
+  @test_tracker_info(uuid="93b2a23d-8579-448a-936c-7812929464cf")
   def test_ib_unsolicited_passive_passphrase_specific(self):
     """Data-path: in-band, unsolicited/passive, passphrase, specific peer
 
@@ -470,6 +479,7 @@
         encr_type=self.ENCR_TYPE_PASSPHRASE,
         use_peer_id=True)
 
+  @test_tracker_info(uuid="1736126f-a0ff-4712-acc4-f89b4eef5716")
   def test_ib_unsolicited_passive_passphrase_any(self):
     """Data-path: in-band, unsolicited/passive, passphrase, any peer
 
@@ -481,6 +491,7 @@
         encr_type=self.ENCR_TYPE_PASSPHRASE,
         use_peer_id=False)
 
+  @test_tracker_info(uuid="b9353d5b-3f77-46bf-bfd9-65d56a7c939a")
   def test_ib_unsolicited_passive_pmk_specific(self):
     """Data-path: in-band, unsolicited/passive, PMK, specific peer
 
@@ -492,6 +503,7 @@
         encr_type=self.ENCR_TYPE_PMK,
         use_peer_id=True)
 
+  @test_tracker_info(uuid="06f3b2ab-4a10-4398-83a4-6a23851b1662")
   def test_ib_unsolicited_passive_pmk_any(self):
     """Data-path: in-band, unsolicited/passive, PMK, any peer
 
@@ -503,6 +515,7 @@
         encr_type=self.ENCR_TYPE_PMK,
         use_peer_id=False)
 
+  @test_tracker_info(uuid="0ed7d8b3-a69e-46ba-aeb7-13e507ecf290")
   def test_ib_solicited_active_open_specific(self):
     """Data-path: in-band, solicited/active, open encryption, specific peer
 
@@ -514,6 +527,7 @@
         encr_type=self.ENCR_TYPE_OPEN,
         use_peer_id=True)
 
+  @test_tracker_info(uuid="c7ba6d28-5ef6-45d9-95d5-583ad6d981f3")
   def test_ib_solicited_active_open_any(self):
     """Data-path: in-band, solicited/active, open encryption, any peer
 
@@ -525,6 +539,7 @@
         encr_type=self.ENCR_TYPE_OPEN,
         use_peer_id=False)
 
+  @test_tracker_info(uuid="388cea99-0e2e-49ea-b00e-f3e56b6236e5")
   def test_ib_solicited_active_passphrase_specific(self):
     """Data-path: in-band, solicited/active, passphrase, specific peer
 
@@ -536,6 +551,7 @@
         encr_type=self.ENCR_TYPE_PASSPHRASE,
         use_peer_id=True)
 
+  @test_tracker_info(uuid="fcd3e28a-5eab-4169-8a0c-dc7204dcdc13")
   def test_ib_solicited_active_passphrase_any(self):
     """Data-path: in-band, solicited/active, passphrase, any peer
 
@@ -547,6 +563,7 @@
         encr_type=self.ENCR_TYPE_PASSPHRASE,
         use_peer_id=False)
 
+  @test_tracker_info(uuid="9d4eaad7-ba53-4a06-8ce0-e308daea3309")
   def test_ib_solicited_active_pmk_specific(self):
     """Data-path: in-band, solicited/active, PMK, specific peer
 
@@ -558,6 +575,7 @@
         encr_type=self.ENCR_TYPE_PMK,
         use_peer_id=True)
 
+  @test_tracker_info(uuid="129d850e-c312-4137-a67b-05ae95fe66cc")
   def test_ib_solicited_active_pmk_any(self):
     """Data-path: in-band, solicited/active, PMK, any peer
 
@@ -582,6 +600,7 @@
   # exchange of MAC addresses and then Wi-Fi Aware for data-path.
   #######################################
 
+  @test_tracker_info(uuid="7db17d8c-1dce-4084-b695-215bbcfe7d41")
   def test_oob_open_specific(self):
     """Data-path: out-of-band, open encryption, specific peer
 
@@ -591,6 +610,7 @@
         encr_type=self.ENCR_TYPE_OPEN,
         use_peer_id=True)
 
+  @test_tracker_info(uuid="ad416d89-cb95-4a07-8d29-ee213117450b")
   def test_oob_open_any(self):
     """Data-path: out-of-band, open encryption, any peer
 
@@ -600,6 +620,7 @@
         encr_type=self.ENCR_TYPE_OPEN,
         use_peer_id=False)
 
+  @test_tracker_info(uuid="74937a3a-d524-43e2-8979-4449271cab52")
   def test_oob_passphrase_specific(self):
     """Data-path: out-of-band, passphrase, specific peer
 
@@ -609,6 +630,7 @@
         encr_type=self.ENCR_TYPE_PASSPHRASE,
         use_peer_id=True)
 
+  @test_tracker_info(uuid="afcbdc7e-d3a9-465b-b1da-ce2e42e3941e")
   def test_oob_passphrase_any(self):
     """Data-path: out-of-band, passphrase, any peer
 
@@ -618,6 +640,7 @@
         encr_type=self.ENCR_TYPE_PASSPHRASE,
         use_peer_id=False)
 
+  @test_tracker_info(uuid="0d095031-160a-4537-aab5-41b6ad5d55f8")
   def test_oob_pmk_specific(self):
     """Data-path: out-of-band, PMK, specific peer
 
@@ -627,6 +650,7 @@
         encr_type=self.ENCR_TYPE_PMK,
         use_peer_id=True)
 
+  @test_tracker_info(uuid="e45477bd-66cc-4eb7-88dd-4518c8aa2a74")
   def test_oob_pmk_any(self):
     """Data-path: out-of-band, PMK, any peer
 
@@ -638,6 +662,7 @@
 
   ##############################################################
 
+  @test_tracker_info(uuid="1c2c9805-dc1e-43b5-a1b8-315e8c9a4337")
   def test_passphrase_min(self):
     """Data-path: minimum passphrase length
 
@@ -649,6 +674,7 @@
                                use_peer_id=False,
                                passphrase_to_use=self.PASSPHRASE_MIN)
 
+  @test_tracker_info(uuid="e696e2b9-87a9-4521-b337-61b9efaa2057")
   def test_passphrase_max(self):
     """Data-path: maximum passphrase length
 
@@ -660,70 +686,362 @@
                                use_peer_id=False,
                                passphrase_to_use=self.PASSPHRASE_MAX)
 
+  @test_tracker_info(uuid="533cd44c-ff30-4283-ac28-f71fd7b4f02d")
   def test_negative_mismatch_publisher_peer_id(self):
     """Data-path: failure when publisher peer ID is mismatched"""
     self.run_mismatched_ib_data_path_test(pub_mismatch=True, sub_mismatch=False)
 
+  @test_tracker_info(uuid="682f275e-722a-4f8b-85e7-0dcea9d25532")
   def test_negative_mismatch_subscriber_peer_id(self):
     """Data-path: failure when subscriber peer ID is mismatched"""
     self.run_mismatched_ib_data_path_test(pub_mismatch=False, sub_mismatch=True)
 
+  @test_tracker_info(uuid="7fa82796-7fc9-4d9e-bbbb-84b751788943")
   def test_negative_mismatch_init_mac(self):
     """Data-path: failure when Initiator MAC address mismatch"""
     self.run_mismatched_oob_data_path_test(
         init_mismatch_mac=True,
         resp_mismatch_mac=False)
 
+  @test_tracker_info(uuid="edeae959-4644-44f9-8d41-bdeb5216954e")
   def test_negative_mismatch_resp_mac(self):
     """Data-path: failure when Responder MAC address mismatch"""
     self.run_mismatched_oob_data_path_test(
         init_mismatch_mac=False,
         resp_mismatch_mac=True)
 
+  @test_tracker_info(uuid="91f46949-c47f-49f9-a90f-6fae699613a7")
   def test_negative_mismatch_passphrase(self):
     """Data-path: failure when passphrases mismatch"""
     self.run_mismatched_oob_data_path_test(
         init_encr_type=self.ENCR_TYPE_PASSPHRASE,
         resp_encr_type=self.ENCR_TYPE_PASSPHRASE)
 
+  @test_tracker_info(uuid="01c49c2e-dc92-4a27-bb47-c4fc67617c23")
   def test_negative_mismatch_pmk(self):
     """Data-path: failure when PMK mismatch"""
     self.run_mismatched_oob_data_path_test(
         init_encr_type=self.ENCR_TYPE_PMK,
         resp_encr_type=self.ENCR_TYPE_PMK)
 
+  @test_tracker_info(uuid="4d651797-5fbb-408e-a4b6-a6e1944136da")
   def test_negative_mismatch_open_passphrase(self):
     """Data-path: failure when initiator is open, and responder passphrase"""
     self.run_mismatched_oob_data_path_test(
         init_encr_type=self.ENCR_TYPE_OPEN,
         resp_encr_type=self.ENCR_TYPE_PASSPHRASE)
 
+  @test_tracker_info(uuid="1ae697f4-5987-4187-aeef-1e22d07d4a7c")
   def test_negative_mismatch_open_pmk(self):
     """Data-path: failure when initiator is open, and responder PMK"""
     self.run_mismatched_oob_data_path_test(
         init_encr_type=self.ENCR_TYPE_OPEN,
         resp_encr_type=self.ENCR_TYPE_PMK)
 
+  @test_tracker_info(uuid="f027b1cc-0e7a-4075-b880-5e64b288afbd")
   def test_negative_mismatch_pmk_passphrase(self):
     """Data-path: failure when initiator is pmk, and responder passphrase"""
     self.run_mismatched_oob_data_path_test(
         init_encr_type=self.ENCR_TYPE_PMK,
         resp_encr_type=self.ENCR_TYPE_PASSPHRASE)
 
+  @test_tracker_info(uuid="0819bbd4-72ae-49c4-bd46-5448db2b0a06")
   def test_negative_mismatch_passphrase_open(self):
     """Data-path: failure when initiator is passphrase, and responder open"""
     self.run_mismatched_oob_data_path_test(
         init_encr_type=self.ENCR_TYPE_PASSPHRASE,
         resp_encr_type=self.ENCR_TYPE_OPEN)
 
+  @test_tracker_info(uuid="7ef24f62-8e6b-4732-88a3-80a43584dda4")
   def test_negative_mismatch_pmk_open(self):
     """Data-path: failure when initiator is PMK, and responder open"""
     self.run_mismatched_oob_data_path_test(
         init_encr_type=self.ENCR_TYPE_PMK,
         resp_encr_type=self.ENCR_TYPE_OPEN)
 
+  @test_tracker_info(uuid="7b9c9efc-1c06-465e-8a5e-d6a22ac1da97")
   def test_negative_mismatch_passphrase_pmk(self):
     """Data-path: failure when initiator is passphrase, and responder pmk"""
     self.run_mismatched_oob_data_path_test(
         init_encr_type=self.ENCR_TYPE_PASSPHRASE,
         resp_encr_type=self.ENCR_TYPE_OPEN)
+
+
+  ##########################################################################
+
+  def wait_for_request_responses(self, dut, req_keys, aware_ifs):
+    """Wait for network request confirmation for all request keys.
+
+    Args:
+      dut: Device under test
+      req_keys: (in) A list of the network requests
+      aware_ifs: (out) A list into which to append the network interface
+    """
+    num_events = 0
+    while num_events != len(req_keys):
+      event = autils.wait_for_event(dut, cconsts.EVENT_NETWORK_CALLBACK)
+      if (event["data"][cconsts.NETWORK_CB_KEY_EVENT] ==
+          cconsts.NETWORK_CB_LINK_PROPERTIES_CHANGED):
+        if event["data"][cconsts.NETWORK_CB_KEY_ID] in req_keys:
+          num_events = num_events + 1
+          aware_ifs.append(event["data"][cconsts.NETWORK_CB_KEY_INTERFACE_NAME])
+        else:
+          self.log.info("Received an unexpected connectivity, the revoked "
+                        "network request probably went through -- %s", event)
+
+  @test_tracker_info(uuid="2e325e2b-d552-4890-b470-20b40284395d")
+  def test_multiple_identical_networks(self):
+    """Validate that creating multiple networks between 2 devices, each network
+    with identical configuration is supported over a single NDP.
+
+    Verify that the interface and IPv6 address is the same for all networks.
+    """
+    init_dut = self.android_devices[0]
+    init_dut.pretty_name = "Initiator"
+    resp_dut = self.android_devices[1]
+    resp_dut.pretty_name = "Responder"
+
+    N = 2 # first iteration (must be 2 to give us a chance to cancel the first)
+    M = 5 # second iteration
+
+    init_ids = []
+    resp_ids = []
+
+    # Initiator+Responder: attach and wait for confirmation & identity
+    # create 10 sessions to be used in the different (but identical) NDPs
+    for i in range(N + M):
+      id, init_mac = autils.attach_with_identity(init_dut)
+      init_ids.append(id)
+      id, resp_mac = autils.attach_with_identity(resp_dut)
+      resp_ids.append(id)
+
+    # wait for for devices to synchronize with each other - there are no other
+    # mechanisms to make sure this happens for OOB discovery (except retrying
+    # to execute the data-path request)
+    time.sleep(autils.WAIT_FOR_CLUSTER)
+
+    resp_req_keys = []
+    init_req_keys = []
+    resp_aware_ifs = []
+    init_aware_ifs = []
+
+    # issue N quick requests for identical NDPs - without waiting for result
+    # tests whether pre-setup multiple NDP procedure
+    for i in range(N):
+      # Responder: request network
+      resp_req_keys.append(autils.request_network(
+          resp_dut,
+          resp_dut.droid.wifiAwareCreateNetworkSpecifierOob(
+              resp_ids[i], aconsts.DATA_PATH_RESPONDER, init_mac, None)))
+
+      # Initiator: request network
+      init_req_keys.append(autils.request_network(
+          init_dut,
+          init_dut.droid.wifiAwareCreateNetworkSpecifierOob(
+              init_ids[i], aconsts.DATA_PATH_INITIATOR, resp_mac, None)))
+
+    # remove the first request (hopefully before completed) testing that NDP
+    # is still created
+    resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_keys[0])
+    resp_req_keys.remove(resp_req_keys[0])
+    init_dut.droid.connectivityUnregisterNetworkCallback(init_req_keys[0])
+    init_req_keys.remove(init_req_keys[0])
+
+    # wait for network formation for all initial requests
+    self.wait_for_request_responses(resp_dut, resp_req_keys, resp_aware_ifs)
+    self.wait_for_request_responses(init_dut, init_req_keys, init_aware_ifs)
+
+    # issue N more requests for the same NDPs - tests post-setup multiple NDP
+    for i in range(M):
+      # Responder: request network
+      resp_req_keys.append(autils.request_network(
+          resp_dut,
+          resp_dut.droid.wifiAwareCreateNetworkSpecifierOob(
+              resp_ids[N + i], aconsts.DATA_PATH_RESPONDER, init_mac, None)))
+
+      # Initiator: request network
+      init_req_keys.append(autils.request_network(
+          init_dut,
+          init_dut.droid.wifiAwareCreateNetworkSpecifierOob(
+              init_ids[N + i], aconsts.DATA_PATH_INITIATOR, resp_mac, None)))
+
+    # wait for network formation for all subsequent requests
+    self.wait_for_request_responses(resp_dut, resp_req_keys[N - 1:],
+                                    resp_aware_ifs)
+    self.wait_for_request_responses(init_dut, init_req_keys[N - 1:],
+                                    init_aware_ifs)
+
+    # determine whether all interfaces are identical (single NDP) - can't really
+    # test the IPv6 address since it is not part of the callback event - it is
+    # simply obtained from the system (so we'll always get the same for the same
+    # interface)
+    init_aware_ifs = list(set(init_aware_ifs))
+    resp_aware_ifs = list(set(resp_aware_ifs))
+
+    self.log.info("Interface names: I=%s, R=%s", init_aware_ifs, resp_aware_ifs)
+    self.log.info("Initiator requests: %s", init_req_keys)
+    self.log.info("Responder requests: %s", resp_req_keys)
+
+    asserts.assert_equal(
+        len(init_aware_ifs), 1, "Multiple initiator interfaces")
+    asserts.assert_equal(
+        len(resp_aware_ifs), 1, "Multiple responder interfaces")
+
+    self.log.info("Interface IPv6 (using ifconfig): I=%s, R=%s",
+                  autils.get_ipv6_addr(init_dut, init_aware_ifs[0]),
+                  autils.get_ipv6_addr(resp_dut, resp_aware_ifs[0]))
+
+    for i in range(init_dut.aware_capabilities[aconsts.CAP_MAX_NDI_INTERFACES]):
+      if_name = "%s%d" % (aconsts.AWARE_NDI_PREFIX, i)
+      init_ipv6 = autils.get_ipv6_addr(init_dut, if_name)
+      resp_ipv6 = autils.get_ipv6_addr(resp_dut, if_name)
+
+      asserts.assert_equal(
+          init_ipv6 is None, if_name not in init_aware_ifs,
+          "Initiator interface %s in unexpected state" % if_name)
+      asserts.assert_equal(
+          resp_ipv6 is None, if_name not in resp_aware_ifs,
+          "Responder interface %s in unexpected state" % if_name)
+
+    # release requests
+    for resp_req_key in resp_req_keys:
+      resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_key)
+    for init_req_key in init_req_keys:
+      init_dut.droid.connectivityUnregisterNetworkCallback(init_req_key)
+
+  ########################################################################
+
+  def run_multiple_ndi(self, sec_configs):
+    """Validate that the device can create and use multiple NDIs.
+
+    The security configuration can be:
+    - None: open
+    - String: passphrase
+    - otherwise: PMK (byte array)
+
+    Args:
+      sec_configs: list of security configurations
+    """
+    init_dut = self.android_devices[0]
+    init_dut.pretty_name = "Initiator"
+    resp_dut = self.android_devices[1]
+    resp_dut.pretty_name = "Responder"
+
+    asserts.skip_if(init_dut.aware_capabilities[aconsts.CAP_MAX_NDI_INTERFACES]
+                    < len(sec_configs) or
+                    resp_dut.aware_capabilities[aconsts.CAP_MAX_NDI_INTERFACES]
+                    < len(sec_configs),
+                    "Initiator or Responder do not support multiple NDIs")
+
+    init_id, init_mac = autils.attach_with_identity(init_dut)
+    resp_id, resp_mac = autils.attach_with_identity(resp_dut)
+
+    # wait for for devices to synchronize with each other - there are no other
+    # mechanisms to make sure this happens for OOB discovery (except retrying
+    # to execute the data-path request)
+    time.sleep(autils.WAIT_FOR_CLUSTER)
+
+    resp_req_keys = []
+    init_req_keys = []
+    resp_aware_ifs = []
+    init_aware_ifs = []
+
+    for sec in sec_configs:
+      # Responder: request network
+      resp_req_key = autils.request_network(resp_dut,
+                                            autils.get_network_specifier(
+                                                resp_dut, resp_id,
+                                                aconsts.DATA_PATH_RESPONDER,
+                                                init_mac, sec))
+      resp_req_keys.append(resp_req_key)
+
+      # Initiator: request network
+      init_req_key = autils.request_network(init_dut,
+                                            autils.get_network_specifier(
+                                                init_dut, init_id,
+                                                aconsts.DATA_PATH_INITIATOR,
+                                                resp_mac, sec))
+      init_req_keys.append(init_req_key)
+
+      # Wait for network
+      init_net_event = autils.wait_for_event_with_keys(
+          init_dut, cconsts.EVENT_NETWORK_CALLBACK, autils.EVENT_TIMEOUT,
+          (cconsts.NETWORK_CB_KEY_EVENT,
+           cconsts.NETWORK_CB_LINK_PROPERTIES_CHANGED),
+          (cconsts.NETWORK_CB_KEY_ID, init_req_key))
+      resp_net_event = autils.wait_for_event_with_keys(
+          resp_dut, cconsts.EVENT_NETWORK_CALLBACK, autils.EVENT_TIMEOUT,
+          (cconsts.NETWORK_CB_KEY_EVENT,
+           cconsts.NETWORK_CB_LINK_PROPERTIES_CHANGED),
+          (cconsts.NETWORK_CB_KEY_ID, resp_req_key))
+
+      resp_aware_ifs.append(
+          resp_net_event["data"][cconsts.NETWORK_CB_KEY_INTERFACE_NAME])
+      init_aware_ifs.append(
+          init_net_event["data"][cconsts.NETWORK_CB_KEY_INTERFACE_NAME])
+
+    # check that we are using 2 NDIs
+    init_aware_ifs = list(set(init_aware_ifs))
+    resp_aware_ifs = list(set(resp_aware_ifs))
+
+    self.log.info("Interface names: I=%s, R=%s", init_aware_ifs, resp_aware_ifs)
+    self.log.info("Initiator requests: %s", init_req_keys)
+    self.log.info("Responder requests: %s", resp_req_keys)
+
+    asserts.assert_equal(
+        len(init_aware_ifs), len(sec_configs), "Multiple initiator interfaces")
+    asserts.assert_equal(
+        len(resp_aware_ifs), len(sec_configs), "Multiple responder interfaces")
+
+    for i in range(len(sec_configs)):
+      if_name = "%s%d" % (aconsts.AWARE_NDI_PREFIX, i)
+      init_ipv6 = autils.get_ipv6_addr(init_dut, if_name)
+      resp_ipv6 = autils.get_ipv6_addr(resp_dut, if_name)
+
+      asserts.assert_equal(
+          init_ipv6 is None, if_name not in init_aware_ifs,
+          "Initiator interface %s in unexpected state" % if_name)
+      asserts.assert_equal(
+          resp_ipv6 is None, if_name not in resp_aware_ifs,
+          "Responder interface %s in unexpected state" % if_name)
+
+    # release requests
+    for resp_req_key in resp_req_keys:
+      resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_key)
+    for init_req_key in init_req_keys:
+      init_dut.droid.connectivityUnregisterNetworkCallback(init_req_key)
+
+  @test_tracker_info(uuid="2d728163-11cc-46ba-a973-c8e1e71397fc")
+  def test_multiple_ndi_open_passphrase(self):
+    """Verify that can between 2 DUTs can create 2 NDPs with different security
+    configuration (one open, one using passphrase). The result should use two
+    different NDIs"""
+    self.run_multiple_ndi([None, self.PASSPHRASE])
+
+  @test_tracker_info(uuid="5f2c32aa-20b2-41f0-8b1e-d0b68df73ada")
+  def test_multiple_ndi_open_pmk(self):
+    """Verify that can between 2 DUTs can create 2 NDPs with different security
+    configuration (one open, one using pmk). The result should use two
+    different NDIs"""
+    self.run_multiple_ndi([None, self.PMK])
+
+  @test_tracker_info(uuid="34467659-bcfb-40cd-ba25-7e50560fca63")
+  def test_multiple_ndi_passphrase_pmk(self):
+    """Verify that can between 2 DUTs can create 2 NDPs with different security
+    configuration (one using passphrase, one using pmk). The result should use
+    two different NDIs"""
+    self.run_multiple_ndi([self.PASSPHRASE, self.PMK])
+
+  @test_tracker_info(uuid="d9194ce6-45b6-41b1-9cc8-ada79968966d")
+  def test_multiple_ndi_passphrases(self):
+    """Verify that can between 2 DUTs can create 2 NDPs with different security
+    configuration (using different passphrases). The result should use two
+    different NDIs"""
+    self.run_multiple_ndi([self.PASSPHRASE, self.PASSPHRASE2])
+
+  @test_tracker_info(uuid="879df795-62d2-40d4-a862-bd46d8f7e67f")
+  def test_multiple_ndi_pmks(self):
+    """Verify that can between 2 DUTs can create 2 NDPs with different security
+    configuration (using different PMKS). The result should use two different
+    NDIs"""
+    self.run_multiple_ndi([self.PMK, self.PMK2])
diff --git a/acts/tests/google/wifi/aware/functional/DiscoveryTest.py b/acts/tests/google/wifi/aware/functional/DiscoveryTest.py
index a1ace3e..8732dbb 100644
--- a/acts/tests/google/wifi/aware/functional/DiscoveryTest.py
+++ b/acts/tests/google/wifi/aware/functional/DiscoveryTest.py
@@ -18,6 +18,7 @@
 import time
 
 from acts import asserts
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.wifi.aware import aware_const as aconsts
 from acts.test_utils.wifi.aware import aware_test_utils as autils
 from acts.test_utils.wifi.aware.AwareBaseTest import AwareBaseTest
@@ -540,6 +541,7 @@
   # filter: typical, max, or min.
   #######################################
 
+  @test_tracker_info(uuid="954ebbde-ed2b-4f04-9e68-88239187d69d")
   def test_positive_unsolicited_passive_typical(self):
     """Functional test case / Discovery test cases / positive test case:
     - Solicited publish + passive subscribe
@@ -552,6 +554,7 @@
         stype=aconsts.SUBSCRIBE_TYPE_PASSIVE,
         payload_size=self.PAYLOAD_SIZE_TYPICAL)
 
+  @test_tracker_info(uuid="67fb22bb-6985-4345-95a4-90b76681a58b")
   def test_positive_unsolicited_passive_min(self):
     """Functional test case / Discovery test cases / positive test case:
     - Solicited publish + passive subscribe
@@ -564,6 +567,7 @@
         stype=aconsts.SUBSCRIBE_TYPE_PASSIVE,
         payload_size=self.PAYLOAD_SIZE_MIN)
 
+  @test_tracker_info(uuid="a02a47b9-41bb-47bb-883b-921024a2c30d")
   def test_positive_unsolicited_passive_max(self):
     """Functional test case / Discovery test cases / positive test case:
     - Solicited publish + passive subscribe
@@ -576,7 +580,7 @@
         stype=aconsts.SUBSCRIBE_TYPE_PASSIVE,
         payload_size=self.PAYLOAD_SIZE_MAX)
 
-
+  @test_tracker_info(uuid="586c657f-2388-4e7a-baee-9bce2f3d1a16")
   def test_positive_solicited_active_typical(self):
     """Functional test case / Discovery test cases / positive test case:
     - Unsolicited publish + active subscribe
@@ -589,6 +593,7 @@
         stype=aconsts.SUBSCRIBE_TYPE_ACTIVE,
         payload_size=self.PAYLOAD_SIZE_TYPICAL)
 
+  @test_tracker_info(uuid="5369e4ff-f406-48c5-b41a-df38ec340146")
   def test_positive_solicited_active_min(self):
     """Functional test case / Discovery test cases / positive test case:
     - Unsolicited publish + active subscribe
@@ -601,6 +606,7 @@
         stype=aconsts.SUBSCRIBE_TYPE_ACTIVE,
         payload_size=self.PAYLOAD_SIZE_MIN)
 
+  @test_tracker_info(uuid="634c6eb8-2c4f-42bd-9bbb-d874d0ec22f3")
   def test_positive_solicited_active_max(self):
     """Functional test case / Discovery test cases / positive test case:
     - Unsolicited publish + active subscribe
@@ -624,6 +630,7 @@
   # term_ind: ind_on or ind_off
   #######################################
 
+  @test_tracker_info(uuid="9d7e758e-e0e2-4550-bcee-bfb6a2bff63e")
   def test_ttl_unsolicited_ind_on(self):
     """Functional test case / Discovery test cases / TTL test case:
     - Unsolicited publish
@@ -635,6 +642,7 @@
         stype=None,
         term_ind_on=True)
 
+  @test_tracker_info(uuid="48fd69bc-cc2a-4f65-a0a1-63d7c1720702")
   def test_ttl_unsolicited_ind_off(self):
     """Functional test case / Discovery test cases / TTL test case:
     - Unsolicited publish
@@ -646,6 +654,7 @@
         stype=None,
         term_ind_on=False)
 
+  @test_tracker_info(uuid="afb75fc1-9ba7-446a-b5ed-7cd37ab51b1c")
   def test_ttl_solicited_ind_on(self):
     """Functional test case / Discovery test cases / TTL test case:
     - Solicited publish
@@ -657,6 +666,7 @@
         stype=None,
         term_ind_on=True)
 
+  @test_tracker_info(uuid="703311a6-e444-4055-94ee-ea9b9b71799e")
   def test_ttl_solicited_ind_off(self):
     """Functional test case / Discovery test cases / TTL test case:
     - Solicited publish
@@ -668,6 +678,7 @@
         stype=None,
         term_ind_on=False)
 
+  @test_tracker_info(uuid="38a541c4-ff55-4387-87b7-4d940489da9d")
   def test_ttl_passive_ind_on(self):
     """Functional test case / Discovery test cases / TTL test case:
     - Passive subscribe
@@ -679,6 +690,7 @@
         stype=aconsts.SUBSCRIBE_TYPE_PASSIVE,
         term_ind_on=True)
 
+  @test_tracker_info(uuid="ba971e12-b0ca-417c-a1b5-9451598de47d")
   def test_ttl_passive_ind_off(self):
     """Functional test case / Discovery test cases / TTL test case:
     - Passive subscribe
@@ -690,6 +702,7 @@
         stype=aconsts.SUBSCRIBE_TYPE_PASSIVE,
         term_ind_on=False)
 
+  @test_tracker_info(uuid="7b5d96f2-2415-4b98-9a51-32957f0679a0")
   def test_ttl_active_ind_on(self):
     """Functional test case / Discovery test cases / TTL test case:
     - Active subscribe
@@ -701,6 +714,7 @@
         stype=aconsts.SUBSCRIBE_TYPE_ACTIVE,
         term_ind_on=True)
 
+  @test_tracker_info(uuid="c9268eca-0a30-42dd-8e6c-b8b0b84697fb")
   def test_ttl_active_ind_off(self):
     """Functional test case / Discovery test cases / TTL test case:
     - Active subscribe
@@ -722,6 +736,7 @@
   # sub_type: Type of subscribe discovery session: passive or active.
   #######################################
 
+  @test_tracker_info(uuid="175415e9-7d07-40d0-95f0-3a5f91ea4711")
   def test_mismatch_service_name_unsolicited_passive(self):
     """Functional test case / Discovery test cases / Mismatch service name
     - Unsolicited publish
@@ -734,6 +749,7 @@
         p_service_name="GoogleTestServiceXXX",
         s_service_name="GoogleTestServiceYYY")
 
+  @test_tracker_info(uuid="c22a54ce-9e46-47a5-ac44-831faf93d317")
   def test_mismatch_service_name_solicited_active(self):
     """Functional test case / Discovery test cases / Mismatch service name
     - Solicited publish
@@ -756,6 +772,7 @@
   # sub_type: Type of subscribe discovery session: passive or active.
   #######################################
 
+  @test_tracker_info(uuid="4806f631-d9eb-45fd-9e75-24674962770f")
   def test_mismatch_service_type_unsolicited_active(self):
     """Functional test case / Discovery test cases / Mismatch service name
     - Unsolicited publish
@@ -766,6 +783,7 @@
         p_type=aconsts.PUBLISH_TYPE_UNSOLICITED,
         s_type=aconsts.SUBSCRIBE_TYPE_ACTIVE)
 
+  @test_tracker_info(uuid="12d648fd-b8fa-4c0f-9467-95e2366047de")
   def test_mismatch_service_type_solicited_passive(self):
     """Functional test case / Discovery test cases / Mismatch service name
     - Unsolicited publish
@@ -786,6 +804,7 @@
   # sub_type: Type of subscribe discovery session: passive or active.
   #######################################
 
+  @test_tracker_info(uuid="d98454cb-64af-4266-8fed-f0b545a2d7c4")
   def test_mismatch_match_filter_unsolicited_passive(self):
     """Functional test case / Discovery test cases / Mismatch match filter
     - Unsolicited publish
@@ -798,6 +817,7 @@
         p_mf_1="hello there string",
         s_mf_1="goodbye there string")
 
+  @test_tracker_info(uuid="663c1008-ae11-4e1a-87c7-c311d83f481c")
   def test_mismatch_match_filter_solicited_active(self):
     """Functional test case / Discovery test cases / Mismatch match filter
     - Solicited publish
diff --git a/acts/tests/google/wifi/aware/functional/MacRandomTest.py b/acts/tests/google/wifi/aware/functional/MacRandomTest.py
index 8dabb04..329ead4 100644
--- a/acts/tests/google/wifi/aware/functional/MacRandomTest.py
+++ b/acts/tests/google/wifi/aware/functional/MacRandomTest.py
@@ -17,6 +17,7 @@
 import time
 
 from acts import asserts
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.net import connectivity_const as cconsts
 from acts.test_utils.wifi.aware import aware_const as aconsts
 from acts.test_utils.wifi.aware import aware_test_utils as autils
@@ -50,6 +51,7 @@
 
   ##########################################################################
 
+  @test_tracker_info(uuid="09964368-146a-48e4-9f33-6a319f9eeadc")
   def test_nmi_ndi_randomization_on_enable(self):
     """Validate randomization of the NMI (NAN management interface) and all NDIs
     (NAN data-interface) on each enable/disable cycle"""
@@ -97,6 +99,7 @@
         "Infrastructure MAC address (%s) is used for Aware NMI (all=%s)" %
         (infra_mac, mac_addresses))
 
+  @test_tracker_info(uuid="0fb0b5d8-d9cb-4e37-b9af-51811be5670d")
   def test_nmi_randomization_on_interval(self):
     """Validate randomization of the NMI (NAN management interface) on a set
     interval. Default value is 30 minutes - change to a small value to allow
diff --git a/acts/tests/google/wifi/aware/functional/MatchFilterTest.py b/acts/tests/google/wifi/aware/functional/MatchFilterTest.py
index e01bfff..170b31b 100644
--- a/acts/tests/google/wifi/aware/functional/MatchFilterTest.py
+++ b/acts/tests/google/wifi/aware/functional/MatchFilterTest.py
@@ -19,6 +19,7 @@
 import queue
 
 from acts import asserts
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.wifi.aware import aware_const as aconsts
 from acts.test_utils.wifi.aware import aware_test_utils as autils
 from acts.test_utils.wifi.aware.AwareBaseTest import AwareBaseTest
@@ -178,12 +179,14 @@
 
   ###############################################################
 
+  @test_tracker_info(uuid="bd734f8c-895a-4cf9-820f-ec5060517fe9")
   def test_match_filters_per_spec_unsolicited_passive(self):
     """Validate all the match filter combinations in the Wi-Fi Aware spec,
     Appendix H for Unsolicited Publish (tx filter) Passive Subscribe (rx
     filter)"""
     self.run_match_filters_per_spec(do_unsolicited_passive=True)
 
+  @test_tracker_info(uuid="6560124d-69e5-49ff-a7e5-3cb305983723")
   def test_match_filters_per_spec_solicited_active(self):
     """Validate all the match filter combinations in the Wi-Fi Aware spec,
     Appendix H for Solicited Publish (rx filter) Active Subscribe (tx
diff --git a/acts/tests/google/wifi/aware/functional/MessageTest.py b/acts/tests/google/wifi/aware/functional/MessageTest.py
index 1abca68..194ed6d 100644
--- a/acts/tests/google/wifi/aware/functional/MessageTest.py
+++ b/acts/tests/google/wifi/aware/functional/MessageTest.py
@@ -18,6 +18,7 @@
 import time
 
 from acts import asserts
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.wifi.aware import aware_const as aconsts
 from acts.test_utils.wifi.aware import aware_test_utils as autils
 from acts.test_utils.wifi.aware.AwareBaseTest import AwareBaseTest
@@ -391,42 +392,49 @@
 
   ############################################################################
 
+  @test_tracker_info(uuid="a8cd0512-b279-425f-93cf-949ddba22c7a")
   def test_message_no_queue_min(self):
     """Functional / Message / No queue
     - Minimal payload size (None or "")
     """
     self.run_message_no_queue(self.PAYLOAD_SIZE_MIN)
 
+  @test_tracker_info(uuid="2c26170a-5d0a-4cf4-b0b9-56ef03f5dcf4")
   def test_message_no_queue_typical(self):
     """Functional / Message / No queue
     - Typical payload size
     """
     self.run_message_no_queue(self.PAYLOAD_SIZE_TYPICAL)
 
+  @test_tracker_info(uuid="c984860c-b62d-4d9b-8bce-4d894ea3bfbe")
   def test_message_no_queue_max(self):
     """Functional / Message / No queue
     - Max payload size (based on device capabilities)
     """
     self.run_message_no_queue(self.PAYLOAD_SIZE_MAX)
 
+  @test_tracker_info(uuid="3f06de73-31ab-4e0c-bc6f-59abdaf87f4f")
   def test_message_with_queue_min(self):
     """Functional / Message / With queue
     - Minimal payload size (none or "")
     """
     self.run_message_with_queue(self.PAYLOAD_SIZE_MIN)
 
+  @test_tracker_info(uuid="9b7f5bd8-b0b1-479e-8e4b-9db0bb56767b")
   def test_message_with_queue_typical(self):
     """Functional / Message / With queue
     - Typical payload size
     """
     self.run_message_with_queue(self.PAYLOAD_SIZE_TYPICAL)
 
+  @test_tracker_info(uuid="4f9a6dce-3050-4e6a-a143-53592c6c7c28")
   def test_message_with_queue_max(self):
     """Functional / Message / With queue
     - Max payload size (based on device capabilities)
     """
     self.run_message_with_queue(self.PAYLOAD_SIZE_MAX)
 
+  @test_tracker_info(uuid="4cece232-0983-4d6b-90a9-1bb9314b64f0")
   def test_message_with_multiple_discovery_sessions_typical(self):
     """Functional / Message / Multiple sessions
 
diff --git a/acts/tests/google/wifi/aware/functional/ProtocolsTest.py b/acts/tests/google/wifi/aware/functional/ProtocolsTest.py
index bf4a561..97a61b6 100644
--- a/acts/tests/google/wifi/aware/functional/ProtocolsTest.py
+++ b/acts/tests/google/wifi/aware/functional/ProtocolsTest.py
@@ -15,6 +15,7 @@
 #   limitations under the License.
 
 from acts import asserts
+from acts.test_decorators import test_tracker_info
 from acts.test_utils.net import nsd_const as nconsts
 from acts.test_utils.wifi.aware import aware_const as aconsts
 from acts.test_utils.wifi.aware import aware_test_utils as autils
@@ -46,6 +47,7 @@
 
   ########################################################################
 
+  @test_tracker_info(uuid="ce103067-7fdd-4379-9a2b-d238959f1d53")
   def test_ping6_oob(self):
     """Validate that ping6 works correctly on an NDP created using OOB (out-of
     band) discovery"""
@@ -67,6 +69,7 @@
     resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_key)
     init_dut.droid.connectivityUnregisterNetworkCallback(init_req_key)
 
+  @test_tracker_info(uuid="fef86a48-0e05-464b-8c66-64316275c5ba")
   def test_ping6_ib_unsolicited_passive(self):
     """Validate that ping6 works correctly on an NDP created using Aware
     discovery with UNSOLICITED/PASSIVE sessions."""
@@ -94,6 +97,7 @@
     p_dut.droid.connectivityUnregisterNetworkCallback(p_req_key)
     s_dut.droid.connectivityUnregisterNetworkCallback(s_req_key)
 
+  @test_tracker_info(uuid="5bbd68a9-994b-4c26-88cd-43388cec280b")
   def test_ping6_ib_solicited_active(self):
     """Validate that ping6 works correctly on an NDP created using Aware
     discovery with SOLICITED/ACTIVE sessions."""
@@ -121,6 +125,64 @@
     p_dut.droid.connectivityUnregisterNetworkCallback(p_req_key)
     s_dut.droid.connectivityUnregisterNetworkCallback(s_req_key)
 
+  def test_ping6_oob_max_ndp(self):
+    """Validate that ping6 works correctly on multiple NDPs brought up
+    concurrently. Uses the capability of the device to determine the max
+    number of NDPs to set up.
+
+    Note: the test requires MAX_NDP + 1 devices to be validated. If these are
+    not available the test will fail."""
+    dut = self.android_devices[0]
+
+    # get max NDP: using first available device (assumes all devices are the
+    # same)
+    max_ndp = dut.aware_capabilities[aconsts.CAP_MAX_NDP_SESSIONS]
+    asserts.assert_true(len(self.android_devices) > max_ndp,
+                        'Needed %d devices to run the test, have %d' %
+                        (max_ndp + 1, len(self.android_devices)))
+
+    # create all NDPs
+    dut_aware_if = None
+    dut_ipv6 = None
+    peers_aware_ifs = []
+    peers_ipv6s = []
+    dut_requests = []
+    peers_requests = []
+    for i in range(max_ndp):
+      (init_req_key, resp_req_key, init_aware_if, resp_aware_if, init_ipv6,
+       resp_ipv6) = autils.create_oob_ndp(dut, self.android_devices[i + 1])
+      self.log.info("Interface names: I=%s, R=%s", init_aware_if, resp_aware_if)
+      self.log.info("Interface addresses (IPv6): I=%s, R=%s", init_ipv6,
+                    resp_ipv6)
+
+      dut_requests.append(init_req_key)
+      peers_requests.append(resp_req_key)
+      if dut_aware_if is None:
+        dut_aware_if = init_aware_if
+      else:
+        asserts.assert_equal(
+            dut_aware_if, init_aware_if,
+            "DUT (Initiator) interface changed on subsequent NDPs!?")
+      if dut_ipv6 is None:
+        dut_ipv6 = init_ipv6
+      else:
+        asserts.assert_equal(
+            dut_ipv6, init_ipv6,
+            "DUT (Initiator) IPv6 changed on subsequent NDPs!?")
+      peers_aware_ifs.append(resp_aware_if)
+      peers_ipv6s.append(resp_ipv6)
+
+    # run ping6
+    for i in range(max_ndp):
+      self.run_ping6(dut, peers_ipv6s[i], dut_aware_if)
+      self.run_ping6(self.android_devices[i + 1], dut_ipv6, peers_aware_ifs[i])
+
+    # cleanup
+    for i in range(max_ndp):
+      dut.droid.connectivityUnregisterNetworkCallback(dut_requests[i])
+      self.android_devices[i + 1].droid.connectivityUnregisterNetworkCallback(
+          peers_requests[i])
+
   def test_nsd_oob(self):
     """Validate that NSD (mDNS) works correctly on an NDP created using OOB
     (out-of band) discovery"""
diff --git a/acts/tests/google/wifi/aware/performance/LatencyTest.py b/acts/tests/google/wifi/aware/performance/LatencyTest.py
index 20176c1..bde9ff4 100644
--- a/acts/tests/google/wifi/aware/performance/LatencyTest.py
+++ b/acts/tests/google/wifi/aware/performance/LatencyTest.py
@@ -407,8 +407,9 @@
       init_dut.droid.connectivityUnregisterNetworkCallback(init_req_key)
       resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_key)
 
-      # wait before trying another iteration (need to let CM clean-up)
-      time.sleep(10)
+      # wait to make sure previous NDP terminated, otherwise its termination
+      # time will be counted in the setup latency!
+      time.sleep(2)
 
     autils.extract_stats(
         init_dut,
@@ -537,7 +538,7 @@
         results=results,
         dw_24ghz=aconsts.DW_24_INTERACTIVE,
         dw_5ghz=aconsts.DW_5_INTERACTIVE,
-        num_iterations=10)
+        num_iterations=100)
     asserts.explicit_pass(
         "test_ndp_setup_latency_default_dws finished", extras=results)
 
@@ -550,6 +551,6 @@
         results=results,
         dw_24ghz=aconsts.DW_24_NON_INTERACTIVE,
         dw_5ghz=aconsts.DW_5_NON_INTERACTIVE,
-        num_iterations=10)
+        num_iterations=100)
     asserts.explicit_pass(
         "test_ndp_setup_latency_non_interactive_dws finished", extras=results)
diff --git a/acts/tests/google/wifi/aware/performance/ThroughputTest.py b/acts/tests/google/wifi/aware/performance/ThroughputTest.py
index fe2fd0c..6cf1046 100644
--- a/acts/tests/google/wifi/aware/performance/ThroughputTest.py
+++ b/acts/tests/google/wifi/aware/performance/ThroughputTest.py
@@ -16,6 +16,8 @@
 
 import json
 import pprint
+import queue
+import threading
 import time
 
 from acts import asserts
@@ -30,6 +32,9 @@
 
   SERVICE_NAME = "GoogleTestServiceXYZ"
 
+  PASSPHRASE = "This is some random passphrase - very very secure!!"
+  PASSPHRASE2 = "This is some random passphrase - very very secure - but diff!!"
+
   def __init__(self, controllers):
     AwareBaseTest.__init__(self, controllers)
 
@@ -97,6 +102,116 @@
     self.log.info("iPerf3: Sent = %d bps Received = %d bps", results["tx_rate"],
                   results["rx_rate"])
 
+  def run_iperf(self, q, dut, peer_dut, peer_aware_if, dut_ipv6, port):
+    """Runs iperf and places results in the queue.
+
+    Args:
+      q: The queue into which to place the results
+      dut: The DUT on which to run the iperf server command.
+      peer_dut: The DUT on which to run the iperf client command.
+      peer_aware_if: The interface on the DUT.
+      dut_ipv6: The IPv6 address of the server.
+      port: The port to use for the server and client.
+    """
+    result, data = dut.run_iperf_server("-D -p %d" % port)
+    asserts.assert_true(result, "Can't start iperf3 server")
+
+    result, data = peer_dut.run_iperf_client(
+        "%s%%%s" % (dut_ipv6, peer_aware_if), "-6 -J -p %d" % port)
+    self.log.debug(data)
+    q.put((result, data))
+
+  def run_iperf_max_ndp_aware_only(self, results):
+    """Measure iperf performance on the max number of concurrent OOB NDPs, with
+    Aware enabled and no infrastructure connection - i.e. device is not
+    associated to an AP.
+
+    Note: the test requires MAX_NDP + 1 devices to be validated. If these are
+    not available the test will fail.
+
+    Args:
+      results: Dictionary into which to place test results.
+    """
+    dut = self.android_devices[0]
+
+    # get max NDP: using first available device (assumes all devices are the
+    # same)
+    max_ndp = dut.aware_capabilities[aconsts.CAP_MAX_NDP_SESSIONS]
+    asserts.assert_true(len(self.android_devices) > max_ndp,
+                        'Needed %d devices to run the test, have %d' %
+                        (max_ndp + 1, len(self.android_devices)))
+
+    # create all NDPs
+    dut_aware_if = None
+    dut_ipv6 = None
+    peers_aware_ifs = []
+    peers_ipv6s = []
+    dut_requests = []
+    peers_requests = []
+    for i in range(max_ndp):
+      (init_req_key, resp_req_key, init_aware_if, resp_aware_if, init_ipv6,
+       resp_ipv6) = autils.create_oob_ndp(dut, self.android_devices[i + 1])
+      self.log.info("Interface names: I=%s, R=%s", init_aware_if, resp_aware_if)
+      self.log.info("Interface addresses (IPv6): I=%s, R=%s", init_ipv6,
+                    resp_ipv6)
+
+      dut_requests.append(init_req_key)
+      peers_requests.append(resp_req_key)
+      if dut_aware_if is None:
+        dut_aware_if = init_aware_if
+      else:
+        asserts.assert_equal(
+            dut_aware_if, init_aware_if,
+            "DUT (Initiator) interface changed on subsequent NDPs!?")
+      if dut_ipv6 is None:
+        dut_ipv6 = init_ipv6
+      else:
+        asserts.assert_equal(
+            dut_ipv6, init_ipv6,
+            "DUT (Initiator) IPv6 changed on subsequent NDPs!?")
+      peers_aware_ifs.append(resp_aware_if)
+      peers_ipv6s.append(resp_ipv6)
+
+    # create threads, start them, and wait for all to finish
+    base_port = 5000
+    q = queue.Queue()
+    threads = []
+    for i in range(max_ndp):
+      threads.append(
+          threading.Thread(
+              target=self.run_iperf,
+              args=(q, dut, self.android_devices[i + 1], peers_aware_ifs[i],
+                    dut_ipv6, base_port + i)))
+
+    for thread in threads:
+      thread.start()
+
+    for thread in threads:
+      thread.join()
+
+    # cleanup
+    for i in range(max_ndp):
+      dut.droid.connectivityUnregisterNetworkCallback(dut_requests[i])
+      self.android_devices[i + 1].droid.connectivityUnregisterNetworkCallback(
+          peers_requests[i])
+
+    # collect data
+    for i in range(max_ndp):
+      results[i] = {}
+      result, data = q.get()
+      asserts.assert_true(result,
+                          "Failure starting/running iperf3 in client mode")
+      self.log.debug(pprint.pformat(data))
+      data_json = json.loads("".join(data))
+      if "error" in data_json:
+        asserts.fail(
+            "iperf run failed: %s" % data_json["error"], extras=data_json)
+      results[i]["tx_rate"] = data_json["end"]["sum_sent"]["bits_per_second"]
+      results[i]["rx_rate"] = data_json["end"]["sum_received"][
+          "bits_per_second"]
+      self.log.info("iPerf3: Sent = %d bps Received = %d bps",
+                    results[i]["tx_rate"], results[i]["rx_rate"])
+
   ########################################################################
 
   def test_iperf_single_ndp_aware_only_ib(self):
@@ -113,4 +228,151 @@
     results = {}
     self.run_iperf_single_ndp_aware_only(use_ib=False, results=results)
     asserts.explicit_pass(
-        "test_iperf_single_ndp_aware_only_ib passes", extras=results)
+        "test_iperf_single_ndp_aware_only_oob passes", extras=results)
+
+  def test_iperf_max_ndp_aware_only_oob(self):
+    """Measure throughput using iperf on all possible concurrent NDPs, with
+    Aware enabled and no infrastructure connection. Use out-of-band discovery.
+    """
+    results = {}
+    self.run_iperf_max_ndp_aware_only(results=results)
+    asserts.explicit_pass(
+        "test_iperf_max_ndp_aware_only_oob passes", extras=results)
+
+  ########################################################################
+
+  def run_iperf_max_ndi_aware_only(self, sec_configs, results):
+    """Measure iperf performance on multiple NDPs between 2 devices using
+    different security configurations (and hence different NDIs). Test with
+    Aware enabled and no infrastructure connection - i.e. device is not
+    associated to an AP.
+
+    The security configuration can be:
+    - None: open
+    - String: passphrase
+    - otherwise: PMK (byte array)
+
+    Args:
+      sec_configs: list of security configurations
+      results: Dictionary into which to place test results.
+    """
+    init_dut = self.android_devices[0]
+    init_dut.pretty_name = "Initiator"
+    resp_dut = self.android_devices[1]
+    resp_dut.pretty_name = "Responder"
+
+    asserts.skip_if(init_dut.aware_capabilities[aconsts.CAP_MAX_NDI_INTERFACES]
+                    < len(sec_configs) or
+                    resp_dut.aware_capabilities[aconsts.CAP_MAX_NDI_INTERFACES]
+                    < len(sec_configs),
+                    "Initiator or Responder do not support multiple NDIs")
+
+
+    init_id, init_mac = autils.attach_with_identity(init_dut)
+    resp_id, resp_mac = autils.attach_with_identity(resp_dut)
+
+    # wait for for devices to synchronize with each other - there are no other
+    # mechanisms to make sure this happens for OOB discovery (except retrying
+    # to execute the data-path request)
+    time.sleep(autils.WAIT_FOR_CLUSTER)
+
+    resp_req_keys = []
+    init_req_keys = []
+    resp_aware_ifs = []
+    init_aware_ifs = []
+    resp_aware_ipv6s = []
+    init_aware_ipv6s = []
+
+    for sec in sec_configs:
+      # Responder: request network
+      resp_req_key = autils.request_network(resp_dut,
+                                            autils.get_network_specifier(
+                                                resp_dut, resp_id,
+                                                aconsts.DATA_PATH_RESPONDER,
+                                                init_mac, sec))
+      resp_req_keys.append(resp_req_key)
+
+      # Initiator: request network
+      init_req_key = autils.request_network(init_dut,
+                                            autils.get_network_specifier(
+                                                init_dut, init_id,
+                                                aconsts.DATA_PATH_INITIATOR,
+                                                resp_mac, sec))
+      init_req_keys.append(init_req_key)
+
+      # Wait for network
+      init_net_event = autils.wait_for_event_with_keys(
+          init_dut, cconsts.EVENT_NETWORK_CALLBACK, autils.EVENT_TIMEOUT,
+          (cconsts.NETWORK_CB_KEY_EVENT,
+           cconsts.NETWORK_CB_LINK_PROPERTIES_CHANGED),
+          (cconsts.NETWORK_CB_KEY_ID, init_req_key))
+      resp_net_event = autils.wait_for_event_with_keys(
+          resp_dut, cconsts.EVENT_NETWORK_CALLBACK, autils.EVENT_TIMEOUT,
+          (cconsts.NETWORK_CB_KEY_EVENT,
+           cconsts.NETWORK_CB_LINK_PROPERTIES_CHANGED),
+          (cconsts.NETWORK_CB_KEY_ID, resp_req_key))
+
+      resp_aware_ifs.append(
+          resp_net_event["data"][cconsts.NETWORK_CB_KEY_INTERFACE_NAME])
+      init_aware_ifs.append(
+          init_net_event["data"][cconsts.NETWORK_CB_KEY_INTERFACE_NAME])
+
+      resp_aware_ipv6s.append(
+          autils.get_ipv6_addr(resp_dut, resp_aware_ifs[-1]))
+      init_aware_ipv6s.append(
+          autils.get_ipv6_addr(init_dut, init_aware_ifs[-1]))
+
+    self.log.info("Initiator interfaces/ipv6: %s / %s", init_aware_ifs,
+                  init_aware_ipv6s)
+    self.log.info("Responder interfaces/ipv6: %s / %s", resp_aware_ifs,
+                  resp_aware_ipv6s)
+
+    # create threads, start them, and wait for all to finish
+    base_port = 5000
+    q = queue.Queue()
+    threads = []
+    for i in range(len(sec_configs)):
+      threads.append(
+          threading.Thread(
+              target=self.run_iperf,
+              args=(q, init_dut, resp_dut, resp_aware_ifs[i], init_aware_ipv6s[
+                  i], base_port + i)))
+
+    for thread in threads:
+      thread.start()
+
+    for thread in threads:
+      thread.join()
+
+    # release requests
+    for resp_req_key in resp_req_keys:
+      resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_key)
+    for init_req_key in init_req_keys:
+      init_dut.droid.connectivityUnregisterNetworkCallback(init_req_key)
+
+
+    # collect data
+    for i in range(len(sec_configs)):
+      results[i] = {}
+      result, data = q.get()
+      asserts.assert_true(result,
+                          "Failure starting/running iperf3 in client mode")
+      self.log.debug(pprint.pformat(data))
+      data_json = json.loads("".join(data))
+      if "error" in data_json:
+        asserts.fail(
+            "iperf run failed: %s" % data_json["error"], extras=data_json)
+      results[i]["tx_rate"] = data_json["end"]["sum_sent"]["bits_per_second"]
+      results[i]["rx_rate"] = data_json["end"]["sum_received"][
+        "bits_per_second"]
+      self.log.info("iPerf3: Sent = %d bps Received = %d bps",
+                    results[i]["tx_rate"], results[i]["rx_rate"])
+
+  def test_iperf_max_ndi_aware_only_passphrases(self):
+    """Test throughput for multiple NDIs configured with different passphrases.
+    """
+    results = {}
+    self.run_iperf_max_ndi_aware_only(
+        [self.PASSPHRASE, self.PASSPHRASE2], results=results)
+    asserts.explicit_pass(
+        "test_iperf_max_ndi_aware_only_passphrases passes", extras=results)
diff --git a/acts/tests/google/wifi/aware/stress/DataPathStressTest.py b/acts/tests/google/wifi/aware/stress/DataPathStressTest.py
index ab204b6..9a862cb 100644
--- a/acts/tests/google/wifi/aware/stress/DataPathStressTest.py
+++ b/acts/tests/google/wifi/aware/stress/DataPathStressTest.py
@@ -30,7 +30,7 @@
   ATTACH_ITERATIONS = 2
 
   # Number of iterations on create/destroy NDP in each discovery session.
-  NDP_ITERATIONS = 5
+  NDP_ITERATIONS = 20
 
   def __init__(self, controllers):
     AwareBaseTest.__init__(self, controllers)
@@ -129,9 +129,6 @@
         init_dut.droid.connectivityUnregisterNetworkCallback(init_req_key)
         resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_key)
 
-        # wait before trying another iteration (need to let CM clean-up)
-        time.sleep(10)
-
       # clean-up at end of iteration
       init_dut.droid.wifiAwareDestroy(init_id)
       resp_dut.droid.wifiAwareDestroy(resp_id)
diff --git a/acts/tests/google/wifi/aware/stress/InfraAssociationStressTest.py b/acts/tests/google/wifi/aware/stress/InfraAssociationStressTest.py
new file mode 100644
index 0000000..917a7d9
--- /dev/null
+++ b/acts/tests/google/wifi/aware/stress/InfraAssociationStressTest.py
@@ -0,0 +1,161 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 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.
+
+import queue
+import threading
+
+from acts import asserts
+from acts.test_utils.wifi import wifi_constants as wconsts
+from acts.test_utils.wifi.aware import aware_const as aconsts
+from acts.test_utils.wifi.aware import aware_test_utils as autils
+from acts.test_utils.wifi.aware.AwareBaseTest import AwareBaseTest
+
+
+class InfraAssociationStressTest(AwareBaseTest):
+
+  def __init__(self, controllers):
+    AwareBaseTest.__init__(self, controllers)
+
+  # Length of test in seconds
+  TEST_DURATION_SECONDS = 300
+
+  # Service name
+  SERVICE_NAME = "GoogleTestServiceXYXYXY"
+
+  def is_associated(self, dut):
+    """Checks whether the device is associated (to any AP).
+
+    Args:
+      dut: Device under test.
+
+    Returns: True if associated (to any AP), False otherwise.
+    """
+    info = dut.droid.wifiGetConnectionInfo()
+    return info is not None and info["supplicant_state"] != "disconnected"
+
+  def wait_for_disassociation(self, q, dut):
+    """Waits for a disassociation event on the specified DUT for the given
+    timeout. Place a result into the queue (False) only if disassociation
+    observed.
+
+    Args:
+      q: The synchronization queue into which to place the results.
+      dut: The device to track.
+    """
+    try:
+      dut.ed.pop_event(wconsts.WIFI_DISCONNECTED, self.TEST_DURATION_SECONDS)
+      q.put(True)
+    except queue.Empty:
+      pass
+
+  def run_infra_assoc_oob_ndp_stress(self, with_ndp_traffic):
+    """Validates that Wi-Fi Aware NDP does not interfere with infrastructure
+    (AP) association.
+
+    Test assumes (and verifies) that device is already associated to an AP.
+
+    Args:
+      with_ndp_traffic: True to run traffic over the NDP.
+    """
+    init_dut = self.android_devices[0]
+    resp_dut = self.android_devices[1]
+
+    # check that associated and start tracking
+    init_dut.droid.wifiStartTrackingStateChange()
+    resp_dut.droid.wifiStartTrackingStateChange()
+    asserts.assert_true(
+        self.is_associated(init_dut), "DUT is not associated to an AP!")
+    asserts.assert_true(
+        self.is_associated(resp_dut), "DUT is not associated to an AP!")
+
+    # set up NDP
+    (init_req_key, resp_req_key, init_aware_if, resp_aware_if, init_ipv6,
+     resp_ipv6) = autils.create_oob_ndp(init_dut, resp_dut)
+    self.log.info("Interface names: I=%s, R=%s", init_aware_if, resp_aware_if)
+    self.log.info("Interface addresses (IPv6): I=%s, R=%s", init_ipv6,
+                  resp_ipv6)
+
+    # wait for any disassociation change events
+    q = queue.Queue()
+    init_thread = threading.Thread(
+        target=self.wait_for_disassociation, args=(q, init_dut))
+    resp_thread = threading.Thread(
+        target=self.wait_for_disassociation, args=(q, resp_dut))
+
+    init_thread.start()
+    resp_thread.start()
+
+    any_disassociations = False
+    try:
+      q.get(True, self.TEST_DURATION_SECONDS)
+      any_disassociations = True  # only happens on any disassociation
+    except queue.Empty:
+      pass
+    finally:
+      # TODO: no way to terminate thread (so even if we fast fail we still have
+      # to wait for the full timeout.
+      init_dut.droid.wifiStopTrackingStateChange()
+      resp_dut.droid.wifiStopTrackingStateChange()
+
+    asserts.assert_false(any_disassociations,
+                         "Wi-Fi disassociated during test run")
+
+  ################################################################
+
+  def test_infra_assoc_discovery_stress(self):
+    """Validates that Wi-Fi Aware discovery does not interfere with
+    infrastructure (AP) association.
+
+    Test assumes (and verifies) that device is already associated to an AP.
+    """
+    dut = self.android_devices[0]
+
+    # check that associated and start tracking
+    dut.droid.wifiStartTrackingStateChange()
+    asserts.assert_true(
+        self.is_associated(dut), "DUT is not associated to an AP!")
+
+    # attach
+    session_id = dut.droid.wifiAwareAttach(True)
+    autils.wait_for_event(dut, aconsts.EVENT_CB_ON_ATTACHED)
+
+    # publish
+    p_disc_id = dut.droid.wifiAwarePublish(
+        session_id,
+        autils.create_discovery_config(self.SERVICE_NAME,
+                                       aconsts.PUBLISH_TYPE_UNSOLICITED))
+    autils.wait_for_event(dut, aconsts.SESSION_CB_ON_PUBLISH_STARTED)
+
+    # wait for any disassociation change events
+    any_disassociations = False
+    try:
+      dut.ed.pop_event(wconsts.WIFI_DISCONNECTED, self.TEST_DURATION_SECONDS)
+      any_disassociations = True
+    except queue.Empty:
+      pass
+    finally:
+      dut.droid.wifiStopTrackingStateChange()
+
+    asserts.assert_false(any_disassociations,
+                         "Wi-Fi disassociated during test run")
+
+  def test_infra_assoc_ndp_no_traffic_stress(self):
+    """Validates that Wi-Fi Aware NDP (with no traffic) does not interfere with
+    infrastructure (AP) association.
+
+    Test assumes (and verifies) that devices are already associated to an AP.
+    """
+    self.run_infra_assoc_oob_ndp_stress(with_ndp_traffic=False)
diff --git a/acts/tests/google/wifi/aware/stress/stress b/acts/tests/google/wifi/aware/stress/stress
index 0860507..f79b158 100644
--- a/acts/tests/google/wifi/aware/stress/stress
+++ b/acts/tests/google/wifi/aware/stress/stress
@@ -1,3 +1,4 @@
 MessagesStressTest
 DataPathStressTest
-DiscoveryStressTest
\ No newline at end of file
+DiscoveryStressTest
+InfraAssociationStressTest
\ No newline at end of file
diff --git a/acts/tests/sample/SampleTest.py b/acts/tests/sample/SampleTest.py
index f3fc501..d21e304 100755
--- a/acts/tests/sample/SampleTest.py
+++ b/acts/tests/sample/SampleTest.py
@@ -16,16 +16,15 @@
 
 from acts.base_test import BaseTestClass
 
-class SampleTest(BaseTestClass):
 
+class SampleTest(BaseTestClass):
     def __init__(self, controllers):
         BaseTestClass.__init__(self, controllers)
-        self.tests = (
-            "test_make_toast",
-        )
+        self.tests = ("test_make_toast", )
 
     """Tests"""
+
     def test_make_toast(self):
         for ad in self.android_devices:
             ad.droid.makeToast("Hello World.")
-        return True
\ No newline at end of file
+        return True
diff --git a/tools/commit_message_check.py b/tools/commit_message_check.py
index 092c18a..9304ca5 100755
--- a/tools/commit_message_check.py
+++ b/tools/commit_message_check.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3.4
 #
 #   Copyright 2017 - The Android Open Source Project
 #
diff --git a/tools/lab/README.md b/tools/lab/README.md
index 0b2ebbb..5c5eee4 100644
--- a/tools/lab/README.md
+++ b/tools/lab/README.md
@@ -1 +1,63 @@
 This folder contains tools to be used in the testing lab.
+
+NOTE: The config.json file in this folder contains random values - for the full
+config file, look at the corresponding folder in google3
+
+Python dependencies:
+  - subprocess32
+  - shellescape
+  - psutil
+  - IPy
+
+Metrics that can be gathered, listed by name of file and then key to response
+dict:
+
+* adb_hash:
+    env: whether $ADB_VENDOR_KEYS is set (bool)
+    hash: hash of keys in $ADB_VENDOR_KEYS (string)
+* cpu:
+    cpu: list of CPU core percents (float)
+* disk:
+    total: total space in 1k blocks (int)
+    used: total used in 1k blocks (int)
+    avail: total available in 1k blocks (int)
+    percent_used: percentage space used (0-100) (int)
+* name:
+    name: system's hostname(string)
+* network:
+    connected: whether the network is connected (list of bools)
+* num_users:
+    num_users: number of users (int)
+* process_time:
+    pid_times: a list of (time, PID) tuples where time is a string
+              representing time elapsed in D-HR:MM:SS format and PID is a string
+              representing the pid (string, string)
+* ram:
+    total: total physical RAM available in KB (int)
+    used: total RAM used by system in KB (int)
+    free: total RAM free for new process in KB (int)
+    buffers: total RAM buffered by different applications in KB
+    cached: total RAM for caching of data in KB
+* read:
+    cached_read_rate: cached reads in MB/sec (float)
+    buffered_read_rate: buffered disk reads in MB/sec (float)
+* system_load:
+    load_avg_1_min: system load average for past 1 min (float)
+    load_avg_5_min: system load average for past 5 min (float)
+    load_avg_15_min: system load average for past 15 min (float)
+* uptime:
+    time_seconds: uptime in seconds (float)
+* usb:
+    devices: a list of Device objects, each with name of device, number of bytes
+    transferred, and the usb bus number/device id.
+* verify:
+    device serial number as key: device status as value
+* version:
+    fastboot_version: which version of fastboot (string)
+    adb_version: which version of adb (string)
+    python_version: which version of python (string)
+    kernel_version: which version of kernel (string)
+* zombie:
+    adb_zombies: list of adb zombie processes (PID, state, name)
+    fastboot_zombies: list of fastboot zombie processes (PID, state, name)
+    other_zombies: list of other zombie processes (PID, state, name)
diff --git a/tools/lab/__init__.py b/tools/lab/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/lab/__init__.py
diff --git a/tools/lab/config.json b/tools/lab/config.json
new file mode 100644
index 0000000..5d9bf76
--- /dev/null
+++ b/tools/lab/config.json
@@ -0,0 +1,55 @@
+{
+  "disk": {
+    "percent_used": {
+      "constant": 70,
+      "compare": "LESS_THAN"
+    },
+    "avail": {
+      "constant": 200,
+      "compare": "GREATER_THAN"
+    }
+  },
+  "ram": {
+    "free": {
+      "constant": 100,
+      "compare": "GREATER_THAN"
+    }
+  },
+  "name": {
+    "name": {
+      "constant": "None",
+      "compare": "IP_ADDR"
+    }
+  },
+  "time_sync": {
+      "is_synchronized": {
+          "constant": true,
+          "compare":"EQUALS"
+      }
+  },
+  "network": {
+      "connected": {
+          "constant": true,
+          "compare": "EQUALS_DICT"
+      }
+  },
+  "process_time": {
+    "num_adb_processes": {
+      "constant": 0,
+      "compare": "EQUALS"
+    }
+  },
+  "verify": {
+    "devices": {
+      "constant": "device",
+      "compare": "EQUALS_DICT"
+    }
+  },
+  "zombie": {
+    "num_adb_zombies": {
+      "constant": 0,
+      "compare": "EQUALS"
+    }
+  }
+
+}
diff --git a/tools/lab/health/__init__.py b/tools/lab/health/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/lab/health/__init__.py
diff --git a/tools/lab/health/constant_health_analyzer.py b/tools/lab/health/constant_health_analyzer.py
new file mode 100644
index 0000000..97e92a9
--- /dev/null
+++ b/tools/lab/health/constant_health_analyzer.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from health import health_analyzer
+
+
+class ConstantHealthAnalyzer(health_analyzer.HealthAnalyzer):
+    """Extends HealthAnalyzer for all HealthAnalyzers that compare to a constant
+
+    Attributes:
+        key: a string representing a key to a response dictionary
+        _constant: a constant value to compare metric_results[key] to
+    """
+
+    def __init__(self, key, constant):
+        self.key = key
+        self._constant = constant
+
+    def __eq__(self, other):
+        """ Overwrite comparator so tests can check for correct instance
+
+        Returns True if two of same child class instances were intialized
+          with the same key and constant
+        """
+        return self.key == other.key and self._constant == other._constant\
+            and self.__class__.__name__ == other.__class__.__name__
+
+
+class HealthyIfGreaterThanConstantNumber(ConstantHealthAnalyzer):
+    def is_healthy(self, metric_results):
+        """Returns whether numeric result is greater than numeric constant
+        Args:
+          metric_results: a dictionary of metric results.
+
+        Returns:
+          True if numeric result is greater than numeric constant
+        """
+
+        return metric_results[self.key] > self._constant
+
+
+class HealthyIfLessThanConstantNumber(ConstantHealthAnalyzer):
+    def is_healthy(self, metric_results):
+        """Returns whether numeric result is less than numeric constant
+        Args:
+          metric_results: a dictionary of metric results.
+
+        Returns:
+          True if numeric result is less than numeric constant
+        """
+
+        return metric_results[self.key] < self._constant
+
+
+class HealthyIfEquals(ConstantHealthAnalyzer):
+    def is_healthy(self, metric_results):
+        """Returns whether result is equal to constant
+        Args:
+          metric_results: a dictionary of metric results.
+
+        Returns:
+          True if result is equal to constant
+        """
+        return metric_results[self.key] == self._constant
diff --git a/tools/lab/health/constant_health_analyzer_wrapper.py b/tools/lab/health/constant_health_analyzer_wrapper.py
new file mode 100644
index 0000000..d56438e
--- /dev/null
+++ b/tools/lab/health/constant_health_analyzer_wrapper.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from health import constant_health_analyzer
+
+
+class ConstantHealthAnalyzerForDict(
+        constant_health_analyzer.ConstantHealthAnalyzer):
+    """Extends ConstantHealthAnalyzer to check for every key in a dictionary
+
+    Attributes:
+        key: a string representing a key to a response dictionary, which is
+          also a dictionary
+        _constant: constant value to compare every value in metric_results[key] to
+        analyzer: a ConstantHealthAnalyzer class
+    """
+
+    def is_healthy(self, metric_results):
+        """Returns if analyzer().is_healthy() returned true for all values in dict
+
+        Args:
+          metric_results: a dictionary containing a dictionary of metric_results
+        """
+        for key in metric_results[self.key]:
+            if not self.analyzer(
+                    key, self._constant).is_healthy(metric_results[self.key]):
+                return False
+        return True
+
+
+class HealthyIfValsEqual(ConstantHealthAnalyzerForDict):
+
+    analyzer = constant_health_analyzer.HealthyIfEquals
diff --git a/tools/lab/health/custom_health_analyzer.py b/tools/lab/health/custom_health_analyzer.py
new file mode 100644
index 0000000..c274de0
--- /dev/null
+++ b/tools/lab/health/custom_health_analyzer.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from health import health_analyzer
+from IPy import IP
+
+
+class HealthyIfNotIpAddress(health_analyzer.HealthAnalyzer):
+    def __init__(self, key):
+        self.key = key
+
+    def is_healthy(self, metric_result):
+        """Returns whether numeric result is an non-IP Address.
+        Args:
+            metric_result: a dictionary of metric results.
+
+        Returns:
+            True if the metric is not an IP Address.
+        """
+
+        try:
+            IP(metric_result[self.key])
+        except (ValueError, TypeError):
+            return True
+        return False
diff --git a/tools/lab/health/health_analyzer.py b/tools/lab/health/health_analyzer.py
new file mode 100644
index 0000000..d02b7a1
--- /dev/null
+++ b/tools/lab/health/health_analyzer.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+
+class HealthAnalyzer(object):
+    """Interface class for determining whether metrics are healthy or unhealthy.
+
+    """
+
+    def is_healthy(self, metric_results):
+        """Returns whether a metric is considered healthy
+
+        Function compares the metric mapped to by key it was initialized with
+
+        Args:
+          metric_results: a dictionary of metric results with
+            key = metric field to compare
+        Returns:
+          True if metric is healthy, false otherwise
+        """
+        raise NotImplementedError()
diff --git a/tools/lab/health_checker.py b/tools/lab/health_checker.py
new file mode 100644
index 0000000..4f9a87b
--- /dev/null
+++ b/tools/lab/health_checker.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from health.constant_health_analyzer import HealthyIfGreaterThanConstantNumber
+from health.constant_health_analyzer import HealthyIfLessThanConstantNumber
+from health.constant_health_analyzer import HealthyIfEquals
+from health.custom_health_analyzer import HealthyIfNotIpAddress
+from health.constant_health_analyzer_wrapper import HealthyIfValsEqual
+
+
+class HealthChecker(object):
+    """Takes a dictionary of metrics and returns whether each is healthy
+    Attributes:
+        _analyzers: a list of metric, analyzer tuples where metric is a string
+          representing a metric name ('DiskMetric') and analyzer is a
+          constant_health_analyzer object
+        _comparer_constructor: a dict that maps strings to comparer objects
+        config:
+        a dict formatted as follows:
+        {
+            metric_name: {
+                field_to_compare: {
+                    constant : a constant to compare to
+                    compare : a string specifying a way to compare
+                }
+            }
+        }
+
+
+    """
+
+    COMPARER_CONSTRUCTOR = {
+        'GREATER_THAN': lambda k, c: HealthyIfGreaterThanConstantNumber(k, c),
+        'LESS_THAN': lambda k, c: HealthyIfLessThanConstantNumber(k, c),
+        'EQUALS': lambda k, c: HealthyIfEquals(k, c),
+        'IP_ADDR': lambda k, c: HealthyIfNotIpAddress(k),
+        'EQUALS_DICT': lambda k, c: HealthyIfValsEqual(k, c)
+    }
+
+    def __init__(self, config):
+        self._config = config
+        self._analyzers = []
+        # Create comparators from config object
+        for metric_name, metric_configs in self._config.items():
+            # creates a constant_health_analyzer object for each field
+            for field_name in metric_configs:
+                compare_type = metric_configs[field_name]['compare']
+                constant = metric_configs[field_name]['constant']
+                comparer = self.COMPARER_CONSTRUCTOR[compare_type](field_name,
+                                                                   constant)
+                self._analyzers.append((metric_name, comparer))
+
+    def get_unhealthy(self, response_dict):
+        """Calls comparators to check if metrics are healthy
+
+        Attributes:
+            response_dict: a dict mapping metric names (as strings) to the
+              responses returned from gather_metric()
+
+        Returns:
+            a list of keys, where keys are strings representing
+            the name of a metric (ex: 'DiskMetric')
+        """
+        # loop through and check if healthy
+        unhealthy_metrics = []
+        for (metric, analyzer) in self._analyzers:
+            # if not healthy, add to list so value can be reported
+            if not analyzer.is_healthy(response_dict[metric]):
+                unhealthy_metrics.append(metric)
+        return unhealthy_metrics
diff --git a/tools/lab/main.py b/tools/lab/main.py
index 873f682..6e26e1a 100755
--- a/tools/lab/main.py
+++ b/tools/lab/main.py
@@ -19,25 +19,70 @@
 from __future__ import print_function
 
 import argparse
+import json
+import os
 import sys
 
-from runner import InstantRunner
+import health_checker
+from metrics.adb_hash_metric import AdbHashMetric
+from metrics.cpu_metric import CpuMetric
+from metrics.disk_metric import DiskMetric
+from metrics.name_metric import NameMetric
+from metrics.network_metric import NetworkMetric
+from metrics.num_users_metric import NumUsersMetric
+from metrics.process_time_metric import ProcessTimeMetric
+from metrics.ram_metric import RamMetric
+from metrics.read_metric import ReadMetric
+from metrics.system_load_metric import SystemLoadMetric
+from metrics.time_sync_metric import TimeSyncMetric
+from metrics.uptime_metric import UptimeMetric
 from metrics.usb_metric import UsbMetric
-from reporter import LoggerReporter
+from metrics.verify_metric import VerifyMetric
+from metrics.version_metric import AdbVersionMetric
+from metrics.version_metric import FastbootVersionMetric
+from metrics.version_metric import KernelVersionMetric
+from metrics.version_metric import PythonVersionMetric
+from metrics.zombie_metric import ZombieMetric
+from reporters.json_reporter import JsonReporter
+from reporters.logger_reporter import LoggerReporter
+from runner import InstantRunner
 
 
 class RunnerFactory(object):
     _reporter_constructor = {
-        'logger': lambda: LoggerReporter(),
+        'logger': lambda param: [LoggerReporter(param)],
+        'json': lambda param: [JsonReporter(param)]
     }
 
     _metric_constructor = {
-        'usb_io': lambda param: UsbMetric(),
-        'disk': lambda param: DiskMetric(),
-        'uptime': lambda param: UptimeMetric(),
-        'verify_devices': lambda param: VerifyMetric(),
-        'ram': lambda param: RamMetric(),
-        'cpu': lambda param: CpuMetric(),
+        'usb_io': lambda param: [UsbMetric()],
+        'disk': lambda param: [DiskMetric()],
+        'uptime': lambda param: [UptimeMetric()],
+        'verify_devices':
+            lambda param: [VerifyMetric(), AdbHashMetric()],
+        'ram': lambda param: [RamMetric()],
+        'cpu': lambda param: [CpuMetric()],
+        'network': lambda param: [NetworkMetric(param)],
+        'hostname': lambda param: [NameMetric()],
+        'all': lambda param: [AdbHashMetric(),
+                              AdbVersionMetric(),
+                              CpuMetric(),
+                              DiskMetric(),
+                              FastbootVersionMetric(),
+                              KernelVersionMetric(),
+                              NameMetric(),
+                              NetworkMetric(),
+                              NumUsersMetric(),
+                              ProcessTimeMetric(),
+                              PythonVersionMetric(),
+                              RamMetric(),
+                              ReadMetric(),
+                              SystemLoadMetric(),
+                              TimeSyncMetric(),
+                              UptimeMetric(),
+                              UsbMetric(),
+                              VerifyMetric(),
+                              ZombieMetric()]
     }
 
     @classmethod
@@ -54,17 +99,37 @@
         """
         arg_dict = arguments
         metrics = []
+        reporters = []
 
-        # If no reporter specified, default to logger.
-        reporters = arg_dict.pop('reporter')
-        if reporters is None:
-            reporters = ['logger']
+        # Get health config file, if specified
+        config_file = arg_dict.pop('config', None)
+        # If not specified, default to 'config.json'
+        if not config_file:
+            config_file = os.path.join(sys.path[0], 'config.json')
+        else:
+            config_file = config_file[0]
+        try:
+            with open(config_file) as json_data:
+                health_config = json.load(json_data)
+        except IOError:
+            sys.exit('Config file does not exist')
+        # Create health checker
+        checker = health_checker.HealthChecker(health_config)
+
+        # Get reporters
+        rep_list = arg_dict.pop('reporter')
+        if rep_list is not None:
+            for rep_type in rep_list:
+                reporters += cls._reporter_constructor[rep_type](checker)
+        else:
+            # If no reporter specified, default to logger.
+            reporters += [LoggerReporter(checker)]
 
         # Check keys and values to see what metrics to include.
         for key in arg_dict:
             val = arg_dict[key]
             if val is not None:
-                metrics.append(cls._metric_constructor[key](val))
+                metrics += cls._metric_constructor[key](val)
 
         return InstantRunner(metrics, reporters)
 
@@ -105,7 +170,7 @@
         default=None,
         help='display the current RAM usage')
     parser.add_argument(
-        '-c',
+        '-cp',
         '--cpu',
         action='count',
         default=None,
@@ -115,11 +180,13 @@
         '--verify-devices',
         action='store_true',
         default=None,
-        help='verify all devices connected are in \'device\' mode')
+        help=('verify all devices connected are in \'device\' mode, '
+              'environment variables set properly, '
+              'and hash of directory is correct'))
     parser.add_argument(
         '-r',
         '--reporter',
-        choices=['logger', 'proto', 'json'],
+        choices=['logger', 'json'],
         nargs='+',
         help='choose the reporting method needed')
     parser.add_argument(
@@ -128,6 +195,31 @@
         choices=['python', 'adb', 'fastboot', 'os', 'kernel'],
         nargs='*',
         help='display the versions of chosen programs (default = all)')
+    parser.add_argument(
+        '-n',
+        '--network',
+        nargs='*',
+        default=None,
+        help='retrieve status of network')
+    parser.add_argument(
+        '-a',
+        '--all',
+        action='store_true',
+        default=None,
+        help='Display every metric available')
+    parser.add_argument(
+        '-hn',
+        '--hostname',
+        action='store_true',
+        default=None,
+        help='Display the hostname of the current system')
+    parser.add_argument(
+        '-c',
+        '--config',
+        nargs=1,
+        default=None,
+        metavar="<PATH>",
+        help='Path to health configuration file, defaults to `config.json`')
 
     return parser
 
@@ -139,7 +231,8 @@
         parser.print_help()
         sys.exit(1)
 
-    RunnerFactory().create(vars(parser.parse_args()))
+    r = RunnerFactory().create(vars(parser.parse_args()))
+    r.run()
 
 
 if __name__ == '__main__':
diff --git a/tools/lab/main_test.py b/tools/lab/main_test.py
deleted file mode 100644
index 366767f..0000000
--- a/tools/lab/main_test.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python
-#
-#   Copyright 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.
-
-import unittest
-
-from main import RunnerFactory
-from metrics.usb_metric import UsbMetric
-
-
-class RunnerFactoryTestCase(unittest.TestCase):
-    def test_create_with_reporter(self):
-        self.assertEqual(
-            RunnerFactory.create({
-                'reporter': ['proto']
-            }).reporter_list, ['proto'])
-
-    def test_create_without_reporter(self):
-        self.assertEqual(
-            RunnerFactory.create({
-                'reporter': None
-            }).reporter_list, ['logger'])
-
-    def test_metric_none(self):
-        self.assertEqual(
-            RunnerFactory.create({
-                'disk': None,
-                'reporter': None
-            }).metric_list, [])
-
-    def test_metric_true(self):
-        self.assertIsInstance(
-            RunnerFactory.create({
-                'usb_io': True,
-                'reporter': None
-            }).metric_list[0], UsbMetric)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tools/lab/metrics/adb_hash_metric.py b/tools/lab/metrics/adb_hash_metric.py
new file mode 100644
index 0000000..8ca896a
--- /dev/null
+++ b/tools/lab/metrics/adb_hash_metric.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import os
+
+from metrics.metric import Metric
+
+
+class AdbHashMetric(Metric):
+    """Gathers metrics on environment variable and a hash of that directory.
+
+    This class will verify that $ADB_VENDOR_KEYS is in the environment variables,
+    return True or False, and then verify that the hash of that directory
+    matches the 'golden' directory.
+    """
+    _ADV_ENV_VAR = 'ADB_VENDOR_KEYS'
+    _MD5_COMMAND = ('find $ADB_VENDOR_KEYS -not -path \'*/\.*\' -type f '
+                    '-exec md5sum {} + | awk \'{print $1}\' | sort | md5sum')
+    KEYS_PATH = 'keys_path'
+    KEYS_HASH = 'hash'
+
+    def _verify_env(self):
+        """Determines the path of ADB_VENDOR_KEYS.
+
+        Returns:
+            The path to $ADB_VENDOR_KEYS location, or None if env variable isn't
+            set.
+        """
+        try:
+            return os.environ[self._ADV_ENV_VAR]
+        except KeyError:
+            return None
+
+    def _find_hash(self):
+        """Determines the hash of keys in $ADB_VENDOR_KEYS folder.
+
+        As of now, it just gets the hash, and returns it.
+
+        Returns:
+            The hash of the $ADB_VENDOR_KEYS directory excluding hidden files.
+        """
+        return self._shell.run(self._MD5_COMMAND).stdout.split(' ')[0]
+
+    def gather_metric(self):
+        """Gathers data on adb keys environment variable, and the hash of the
+        directory.
+
+        Returns:
+            A dictionary with 'env' set to the location of adb_vendor_keys, and
+            key 'hash' with an md5sum as value.
+        """
+        adb_keys_path = self._verify_env()
+        if adb_keys_path is not None:
+            adb_keys_hash = self._find_hash()
+        else:
+            adb_keys_hash = None
+
+        return {self.KEYS_PATH: adb_keys_path, self.KEYS_HASH: adb_keys_hash}
diff --git a/tools/lab/metrics/cpu_metric.py b/tools/lab/metrics/cpu_metric.py
new file mode 100644
index 0000000..4daf657
--- /dev/null
+++ b/tools/lab/metrics/cpu_metric.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+import psutil
+
+
+class CpuMetric(Metric):
+    # Fields for response dictionary
+    USAGE_PER_CORE = 'usage_per_core'
+
+    def gather_metric(self):
+        """Finds CPU usage in percentage per core
+
+        Blocks processes for 0.1 seconds for an accurate CPU usage percentage
+
+        Returns:
+            A dict with the following fields:
+                usage_per_core: a list of floats corresponding to CPU usage
+                per core
+        """
+        # Create response dictionary
+        response = {
+            self.USAGE_PER_CORE: psutil.cpu_percent(interval=0.1, percpu=True)
+        }
+        return response
diff --git a/tools/lab/metrics/disk_metric.py b/tools/lab/metrics/disk_metric.py
index 8357b42..e2ec445 100644
--- a/tools/lab/metrics/disk_metric.py
+++ b/tools/lab/metrics/disk_metric.py
@@ -14,12 +14,12 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-import metric
+from metrics.metric import Metric
 
 
-class DiskMetric(metric.Metric):
-
-    COMMAND = "df /var/tmp"
+class DiskMetric(Metric):
+    # This command calls df /var/tmp and ignores line 1.
+    COMMAND = "df /var/tmp | tail -n +2"
     # Fields for response dictionary
     TOTAL = 'total'
     USED = 'used'
@@ -38,14 +38,10 @@
         """
         # Run shell command
         result = self._shell.run(self.COMMAND)
-        """Example stdout:
-        Filesystem     1K-blocks     Used Available Use% Mounted on
-        /dev/dm-1       57542652 18358676  36237928  34% /
-        """
-        # Get only second line
-        output = result.stdout.splitlines()[1]
-        # Split by space
-        fields = output.split()
+
+        # Example stdout:
+        # /dev/sda1 57542652 18358676  36237928  34% /
+        fields = result.stdout.split()
         # Create response dictionary
         response = {
             self.TOTAL: int(fields[1]),
@@ -54,4 +50,4 @@
             # Strip the percentage symbol
             self.PERCENT_USED: int(fields[4][:-1])
         }
-        return (response)
+        return response
diff --git a/tools/lab/metrics/name_metric.py b/tools/lab/metrics/name_metric.py
new file mode 100644
index 0000000..fa43501
--- /dev/null
+++ b/tools/lab/metrics/name_metric.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+
+
+class NameMetric(Metric):
+    COMMAND = 'hostname'
+    # Fields for response dictionary
+    NAME = 'name'
+
+    def gather_metric(self):
+        """Returns the name of system
+
+        Returns:
+            A dict with the following fields:
+              name: a string representing the system's hostname
+
+        """
+        # Run shell command
+        result = self._shell.run(self.COMMAND).stdout
+        # Example stdout:
+        # android1759-test-server-14
+        response = {
+            self.NAME: result,
+        }
+        return response
diff --git a/tools/lab/metrics/network_metric.py b/tools/lab/metrics/network_metric.py
new file mode 100644
index 0000000..ae737a0
--- /dev/null
+++ b/tools/lab/metrics/network_metric.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+from utils import job
+from utils import shell
+
+
+class NetworkMetric(Metric):
+    """Determines if test server is connected to network by passed in ip's.
+
+    The dev servers pinged is determined by the cli arguments.
+    """
+    DEFAULT_IPS = ['8.8.8.8', '8.8.4.4']
+    HOSTNAME_COMMAND = 'hostname | cut -d- -f1'
+    PING_COMMAND = 'ping -c 1 -W 1 {}'
+    CONNECTED = 'connected'
+
+    def __init__(self, ip_list=None, shell=shell.ShellCommand(job)):
+        Metric.__init__(self, shell=shell)
+        self.ip_list = ip_list
+
+    def get_prefix_hostname(self):
+        """Gets the hostname prefix of the test station.
+
+        Example, on android-test-server-14, it would return, android
+
+        Returns:
+            The prefix of the hostname.
+        """
+        return self._shell.run('hostname | cut -d- -f1').stdout
+
+    def check_connected(self, ips=None):
+        """Determines if a network connection can be established to a dev server
+
+        Args:
+            ips: The list of ip's to ping.
+        Returns:
+            A dictionary of ip addresses as keys, and whether they're connected
+            as values.
+        """
+        if not ips:
+            ips = self.DEFAULT_IPS
+
+        ip_dict = {}
+        for ip in ips:
+            # -c 1, ping once, -W 1, set timeout 1 second.
+            stat = self._shell.run(
+                self.PING_COMMAND.format(ip), ignore_status=True).exit_status
+            ip_dict[ip] = stat == 0
+        return ip_dict
+
+    def gather_metric(self):
+        is_connected = self.check_connected(self.ip_list)
+        return {self.CONNECTED: is_connected}
diff --git a/tools/lab/metrics/num_users_metric.py b/tools/lab/metrics/num_users_metric.py
new file mode 100644
index 0000000..507d152
--- /dev/null
+++ b/tools/lab/metrics/num_users_metric.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+
+
+class NumUsersMetric(Metric):
+
+    COMMAND = 'users | wc -w'
+    # Fields for response dictionary
+    NUM_USERS = 'num_users'
+
+    def gather_metric(self):
+        """Returns total (nonunique) users currently logged in to current host
+
+        Returns:
+            A dict with the following fields:
+              num_users : an int representing the number of users
+
+        """
+        # Example stdout:
+        # 2
+        result = self._shell.run(self.COMMAND).stdout
+        response = {self.NUM_USERS: int(result)}
+        return response
diff --git a/tools/lab/metrics/process_time_metric.py b/tools/lab/metrics/process_time_metric.py
new file mode 100644
index 0000000..8071f72
--- /dev/null
+++ b/tools/lab/metrics/process_time_metric.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import itertools
+from metrics.metric import Metric
+
+
+class ProcessTimeMetric(Metric):
+    TIME_COMMAND = 'ps -p %s -o etimes,command | sed 1d'
+    # Number of seconds in 24 hours
+    MIN_TIME = 86400
+    # Fields for response dictionary
+    ADB_PROCESSES = 'adb_processes'
+    NUM_ADB_PROCESSES = 'num_adb_processes'
+    FASTBOOT_PROCESSES = 'fastboot_processes'
+    NUM_FASTBOOT_PROCESSES = 'num_fastboot_processes'
+
+    def gather_metric(self):
+        """Returns ADB and Fastboot processes and their time elapsed
+
+        Returns:
+            A dict with the following fields:
+              adb_processes, fastboot_processes: a list of (PID, serialnumber)
+                tuples where PID is a string representing the pid and
+                serialnumber is serial number as a string, or NONE if number
+                wasn't in command
+              num_adb_processes, num_fastboot_processes: the number of tuples
+                in the previous lists
+        """
+        # Get the process ids
+        pids = self.get_adb_fastboot_pids()
+
+        # Get elapsed time for selected pids
+        adb_processes, fastboot_processes = [], []
+        for pid in pids:
+            # Sample output:
+            # 232893 fastboot -s FA6BM0305019 -w
+
+            output = self._shell.run(self.TIME_COMMAND % pid).stdout
+            spl_ln = output.split()
+
+            # There is a potential race condition between getting pids, and the
+            # pid then dying, so we must check that there is output.
+            if spl_ln:
+                # pull out time in seconds
+                time = int(spl_ln[0])
+            else:
+                continue
+
+            # We only care about processes older than the min time
+            if time > self.MIN_TIME:
+                # ignore fork-server command, because it's not a problematic process
+                if 'fork-server' not in output:
+                    # get serial number, which defaults to none
+                    serial_number = None
+                    if '-s' in spl_ln:
+                        sn_index = spl_ln.index('-s')
+                        # avoid indexing out of range
+                        if sn_index + 1 < len(spl_ln):
+                            serial_number = spl_ln[sn_index + 1]
+                    # append to proper list
+                    if 'fastboot' in output:
+                        fastboot_processes.append((str(pid), serial_number))
+                    elif 'adb' in output:
+                        adb_processes.append((str(pid), serial_number))
+
+        # Create response dictionary
+        response = {
+            self.ADB_PROCESSES: adb_processes,
+            self.NUM_ADB_PROCESSES: len(adb_processes),
+            self.FASTBOOT_PROCESSES: fastboot_processes,
+            self.NUM_FASTBOOT_PROCESSES: len(fastboot_processes)
+        }
+        return response
+
+    def get_adb_fastboot_pids(self):
+        """Finds a list of ADB and Fastboot process ids.
+
+        Returns:
+          A list of PID strings
+        """
+        # Get ids of processes with 'adb' or 'fastboot' in name
+        adb_result = self._shell.get_pids('adb')
+        fastboot_result = self._shell.get_pids('fastboot')
+        # concatenate two generator objects, return as list
+        return list(itertools.chain(adb_result, fastboot_result))
diff --git a/tools/lab/metrics/ram_metric.py b/tools/lab/metrics/ram_metric.py
new file mode 100644
index 0000000..6b6557d
--- /dev/null
+++ b/tools/lab/metrics/ram_metric.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+
+
+class RamMetric(Metric):
+
+    COMMAND = "free -m"
+    # Fields for response dictionary
+    TOTAL = 'total'
+    USED = 'used'
+    FREE = 'free'
+    BUFFERS = 'buffers'
+    CACHED = 'cached'
+
+    def gather_metric(self):
+        """Finds RAM statistics in MB
+
+        Returns:
+            A dict with the following fields:
+                total: int representing total physical RAM available in MB
+                used: int representing total RAM used by system in MB
+                free: int representing total RAM free for new process in MB
+                buffers: total RAM buffered by different applications in MB
+                cached: total RAM for caching of data in MB
+        """
+        # Run shell command
+        result = self._shell.run(self.COMMAND)
+        # Example stdout:
+        #           total       used       free     shared    buffers     cached
+        # Mem:      64350      34633      29717        556       1744      24692
+        # -/+ buffers/cache:     8196      56153
+        # Swap:     65459          0      65459
+
+        # Get only second line
+        output = result.stdout.splitlines()[1]
+        # Split by space
+        fields = output.split()
+        # Create response dictionary
+        response = {
+            self.TOTAL: int(fields[1]),
+            self.USED: int(fields[2]),
+            self.FREE: int(fields[3]),
+            # Skip shared column, since obsolete
+            self.BUFFERS: int(fields[5]),
+            self.CACHED: int(fields[6]),
+        }
+        return (response)
diff --git a/tools/lab/metrics/read_metric.py b/tools/lab/metrics/read_metric.py
new file mode 100644
index 0000000..e2669da
--- /dev/null
+++ b/tools/lab/metrics/read_metric.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+import os
+
+from metrics.metric import Metric
+
+
+class ReadMetric(Metric):
+    """Find read speed of /dev/sda using hdparm
+
+    Attributes:
+      NUM_RUNS: number of times hdparm is run
+    """
+    NUM_RUNS = 3
+    COMMAND = 'for i in {1..%s}; do hdparm -Tt /dev/sda; done'
+    # Fields for response dictionary
+    CACHED_READ_RATE = 'cached_read_rate'
+    BUFFERED_READ_RATE = 'buffered_read_rate'
+
+    def is_privileged(self):
+        """Checks if this module is being ran as the necessary root user.
+
+        Returns:
+            T if being run as root, F if not.
+        """
+
+        return os.getuid() == 0
+
+    def gather_metric(self):
+        """Finds read speed of /dev/sda
+
+        Takes approx 50 seconds to return, runs hdparm 3 times at
+        18 seconds each time to get an average. Should be performed on an
+        inactive system, with no other active processes.
+
+        Returns:
+            A dict with the following fields:
+              cached_read_rate: cached reads in MB/sec
+              buffered_read_rate: buffered disk reads in MB/sec
+        """
+        # Run shell command
+        # Example stdout:
+
+        # /dev/sda:
+        # Timing cached reads:   18192 MB in  2.00 seconds = 9117.49 MB/sec
+        # Timing buffered disk reads: 414 MB in  3.07 seconds = 134.80 MB/sec
+
+        # /dev/sda:
+        # Timing cached reads:   18100 MB in  2.00 seconds = 9071.00 MB/sec
+        # Timing buffered disk reads: 380 MB in  3.01 seconds = 126.35 MB/sec
+
+        # /dev/sda:
+        # Timing cached reads:   18092 MB in  2.00 seconds = 9067.15 MB/sec
+        # Timing buffered disk reads: 416 MB in  3.01 seconds = 138.39 MB/sec
+
+        if not self.is_privileged():
+            return {self.CACHED_READ_RATE: None, self.BUFFERED_READ_RATE: None}
+
+        result = self._shell.run(self.COMMAND % self.NUM_RUNS).stdout
+
+        cached_reads = 0.0
+        buffered_reads = 0.0
+        # Calculate averages
+        for ln in result.splitlines():
+            if ln.startswith(' Timing cached'):
+                cached_reads += float(ln.split()[-2])
+            elif ln.startswith(' Timing buffered'):
+                buffered_reads += float(ln.split()[-2])
+        # Create response dictionary
+        response = {
+            self.CACHED_READ_RATE: cached_reads / self.NUM_RUNS,
+            self.BUFFERED_READ_RATE: buffered_reads / self.NUM_RUNS
+        }
+        return response
diff --git a/tools/lab/metrics/system_load_metric.py b/tools/lab/metrics/system_load_metric.py
new file mode 100644
index 0000000..3de18a5
--- /dev/null
+++ b/tools/lab/metrics/system_load_metric.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+import os
+
+
+class SystemLoadMetric(Metric):
+
+    # Fields for response dictionary
+    LOAD_AVG_1_MIN = 'load_avg_1_min'
+    LOAD_AVG_5_MIN = 'load_avg_5_min'
+    LOAD_AVG_15_MIN = 'load_avg_15_min'
+
+    def gather_metric(self):
+        """Tells average system load
+
+        Returns:
+            A dict with the following fields:
+              load_avg_1_min, load_avg_5_min,load_avg_15_min:
+                float representing system load averages for the
+                past 1, 5, and 15 min
+
+        """
+        result = os.getloadavg()
+        response = {
+            self.LOAD_AVG_1_MIN: result[0],
+            self.LOAD_AVG_5_MIN: result[1],
+            self.LOAD_AVG_15_MIN: result[2]
+        }
+        return response
diff --git a/tools/lab/metrics/time_sync_metric.py b/tools/lab/metrics/time_sync_metric.py
new file mode 100644
index 0000000..450f300
--- /dev/null
+++ b/tools/lab/metrics/time_sync_metric.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+
+
+class TimeSyncMetric(Metric):
+
+    COMMAND = 'timedatectl status | grep synchronized'
+    # Fields for response dictionary
+    IS_SYNCHRONIZED = 'is_synchronized'
+
+    def gather_metric(self):
+        """Tells whether NTP synchronized
+
+        Returns:
+            A dict with the following fields:
+              is_synchronized: True if synchronized, fales otherwise
+
+        """
+        # Run shell command
+        result = self._shell.run(self.COMMAND).stdout
+        # Example stdout:
+        # NTP synchronized: yes
+
+        status = 'yes' in result
+        response = {
+            self.IS_SYNCHRONIZED: status,
+        }
+        return response
diff --git a/tools/lab/metrics/uptime_metric.py b/tools/lab/metrics/uptime_metric.py
new file mode 100644
index 0000000..5683e8d
--- /dev/null
+++ b/tools/lab/metrics/uptime_metric.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+
+
+class UptimeMetric(Metric):
+
+    COMMAND = "cat /proc/uptime"
+    # Fields for response dictionary
+    TIME_SECONDS = 'time_seconds'
+
+    def gather_metric(self):
+        """Tells how long system has been running
+
+        Returns:
+            A dict with the following fields:
+              time_seconds: float uptime in total seconds
+
+        """
+        # Run shell command
+        result = self._shell.run(self.COMMAND).stdout
+        # Example stdout:
+        # 358350.70 14241538.06
+
+        # Get only first number (total time)
+        seconds = float(result.split()[0])
+        response = {
+            self.TIME_SECONDS: seconds,
+        }
+        return response
diff --git a/tools/lab/metrics/usb_metric.py b/tools/lab/metrics/usb_metric.py
index 2bead7a..2de56fb 100644
--- a/tools/lab/metrics/usb_metric.py
+++ b/tools/lab/metrics/usb_metric.py
@@ -14,19 +14,178 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import io
+import os
+import subprocess
+
+import sys
+
 from metrics.metric import Metric
-import job
+from utils import job
+from utils import time_limit
+
+
+def _get_output(stdout):
+    if sys.version_info[0] == 2:
+        return iter(stdout.readline, '')
+    else:
+        return io.TextIOWrapper(stdout, encoding="utf-8")
 
 
 class UsbMetric(Metric):
+    """Class to determine all USB Device traffic over a timeframe."""
+    USB_IO_COMMAND = 'cat /sys/kernel/debug/usb/usbmon/0u | grep -v \'S Ci\''
+    USBMON_CHECK_COMMAND = 'grep usbmon /proc/modules'
+    USBMON_INSTALL_COMMAND = 'modprobe usbmon'
+    DEVICES = 'devices'
+
+    def is_privileged(self):
+        """Checks if this module is being ran as the necessary root user.
+
+        Returns:
+            T if being run as root, F if not.
+        """
+
+        return os.getuid() == 0
+
     def check_usbmon(self):
+        """Checks if the kernel module 'usbmon' is installed.
+
+        Runs the command using shell.py.
+
+        Raises:
+            job.Error: When the module could not be loaded.
+        """
         try:
-            job.run('grep usbmon /proc/modules')
+            self._shell.run(self.USBMON_CHECK_COMMAND)
         except job.Error:
             print('Kernel module not loaded, attempting to load usbmon')
-            result = job.run('modprobe usbmon', ignore_status=True)
-            if result.exit_status != 0:
-                print result.stderr
+            try:
+                self._shell.run(self.USBMON_INSTALL_COMMAND)
+            except job.Error as error:
+                raise job.Error('Cannot load usbmon: %s' % error.result.stderr)
+
+    def get_bytes(self, time=5):
+        """Gathers data about USB Busses in a given timeframe.
+
+        When ran, must have super user privileges as well as having the module
+        'usbmon' installed. Since .../0u is a stream-file, we must read it in
+        as a stream in the off chance of reading in too much data to buffer.
+
+        Args:
+            time: The amount of time data will be gathered in seconds.
+
+        Returns:
+            A dictionary where the key is the device's bus and device number,
+            and value is the amount of bytes transferred in the timeframe.
+        """
+        bytes_sent = {}
+        with time_limit.TimeLimit(time):
+            # Lines matching 'S Ci' do not match output, and only 4 bytes/sec
+            process = subprocess.Popen(
+                self.USB_IO_COMMAND,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                shell=True)
+
+            for line in _get_output(process.stdout):
+                spl_line = line.split(' ')
+                # Example line                  spl_line[3]   " "[5]
+                # ffff88080bb00780 2452973093 C Ii:2:003:1 0:8 8 = 00000000
+
+                # Splits from whole line, into Ii:2:003:1, and then cuts it
+                # down to 2:003, this is for consistency as keys in dicts.
+                dev_id = ':'.join(spl_line[3].split(':')[1:3])
+                if dev_id in bytes_sent:
+                    # spl_line[5] is the number of bytes transferred from a
+                    # device, in the example line, spl_line[5] == 8
+                    bytes_sent[dev_id] += int(spl_line[5])
+                else:
+                    bytes_sent[dev_id] = int(spl_line[5])
+        return bytes_sent
+
+    def match_device_id(self):
+        """ Matches a device's id with its name according to lsusb.
+
+        Returns:
+            A dictionary with the devices 'bus:device' as key, and name of the
+            device as a string. 'bus:device', the bus number is stripped of
+            leading 0's because that is how 'usbmon' formats it.
+        """
+        devices = {}
+        result = self._shell.run('lsusb').stdout
+
+        if result:
+            # Example line
+            # Bus 003 Device 048: ID 18d1:4ee7 Device Name
+            for line in result.split('\n'):
+                line_list = line.split(' ')
+                # Gets bus number, strips leading 0's, adds a ':', and then adds
+                # the device, without its ':'. Example line output: 3:048
+                dev_id = line_list[1].lstrip('0') + ':' + line_list[3].strip(
+                    ':')
+                # Parses the device name, example line output: 'Device Name'
+                dev_name = ' '.join(line_list[6:])
+                devices[dev_id] = dev_name
+        return devices
+
+    def gen_output(self, dev_name_dict, dev_byte_dict):
+        """ Combines all information about device for returning.
+
+        Args:
+            dev_name_dict: A dictionary with the key as 'bus:device', leading
+            0's stripped from bus, and value as the device's name.
+            dev_byte_dict: A dictionary with the key as 'bus:device', leading
+            0's stripped from bus, and value as the number of bytes transferred.
+        Returns:
+            List of populated Device objects.
+        """
+        devices = []
+        for dev in dev_name_dict:
+            if dev in dev_byte_dict:
+                devices.append(
+                    Device(dev, dev_byte_dict[dev], dev_name_dict[dev]))
+            else:
+                devices.append(Device(dev, 0, dev_name_dict[dev]))
+        return devices
 
     def gather_metric(self):
-        self.check_usbmon()
+        """ Gathers the usb bus metric
+
+        Returns:
+            A dictionary, with a single entry, 'devices', and the value of a
+            list of Device objects. This is to fit with the formatting of other
+            metrics.
+        """
+        if self.is_privileged():
+            self.check_usbmon()
+            dev_byte_dict = self.get_bytes()
+            dev_name_dict = self.match_device_id()
+            return {
+                self.DEVICES: self.gen_output(dev_name_dict, dev_byte_dict)
+            }
+        else:
+            return {self.DEVICES: None}
+
+
+class Device:
+    """USB Device Information
+
+    Contains information about bytes transferred in timeframe for a device.
+
+    Attributes:
+        dev_id: The device id, usuall in form BUS:DEVICE
+        trans_bytes: The number of bytes transferred in timeframe.
+        name: The device's name according to lsusb.
+    """
+
+    def __init__(self, dev_id, trans_bytes, name):
+        self.dev_id = dev_id
+        self.trans_bytes = trans_bytes
+        self.name = name
+
+    def __eq__(self, other):
+        return isinstance(other, Device) and \
+               self.dev_id == other.dev_id and \
+               self.trans_bytes == other.trans_bytes and \
+               self.name == other.name
diff --git a/tools/lab/metrics/verify_metric.py b/tools/lab/metrics/verify_metric.py
new file mode 100644
index 0000000..e4ad572
--- /dev/null
+++ b/tools/lab/metrics/verify_metric.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+
+
+class VerifyMetric(Metric):
+    """Gathers the information of connected devices via ADB"""
+    COMMAND = r"adb devices | sed '1d;$d'"
+    DEVICES = 'devices'
+
+    def gather_metric(self):
+        """ Gathers device info based on adb output.
+
+        Returns:
+            A dictionary with the field:
+            devices: a dict with device serial number as key and device status as
+            value.
+        """
+        device_dict = {}
+        # Delete first and last line of output of adb.
+        output = self._shell.run(self.COMMAND).stdout
+
+        # Example Line, Device Serial Num TAB Phone Status
+        # 00bd977c7f504caf	offline
+        if output:
+            for line in output.split('\n'):
+                spl_line = line.split('\t')
+                # spl_line[0] is serial, [1] is status. See example line.
+                device_dict[spl_line[0]] = spl_line[1]
+
+        return {self.DEVICES: device_dict}
diff --git a/tools/lab/metrics/version_metric.py b/tools/lab/metrics/version_metric.py
new file mode 100644
index 0000000..32c7910
--- /dev/null
+++ b/tools/lab/metrics/version_metric.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+
+
+class FastbootVersionMetric(Metric):
+
+    FASTBOOT_COMMAND = 'fastboot --version'
+    FASTBOOT_VERSION = 'fastboot_version'
+
+    # String to return if Fastboot version is too old
+    FASTBOOT_ERROR_MESSAGE = ('this version is older than versions that'
+                              'return versions properly')
+
+    def gather_metric(self):
+        """Tells which versions of fastboot
+
+        Returns:
+            A dict with the following fields:
+              fastboot_version: string representing version of fastboot
+                  Older versions of fastboot do not have a version flag. On an
+                  older version, this metric will print 'this version is older
+                  than versions that return veresions properly'
+
+        """
+        result = self._shell.run(self.FASTBOOT_COMMAND)
+        # If '--version' flag isn't recognized, will print to stderr
+        if result.stderr:
+            version = self.FASTBOOT_ERROR_MESSAGE
+        else:
+            # The version is the last token on the first line
+            version = result.stdout.splitlines()[0].split()[-1]
+
+        response = {self.FASTBOOT_VERSION: version}
+        return response
+
+
+class AdbVersionMetric(Metric):
+
+    ADB_COMMAND = 'adb version'
+    ADB_VERSION = 'adb_version'
+    ADB_REVISION = 'adb_revision'
+
+    def gather_metric(self):
+        """Tells which versions of adb
+
+        Returns:
+            A dict with the following fields:
+              adb_version: string representing version of adb
+              adb_revision: string representing revision of adb
+
+        """
+        result = self._shell.run(self.ADB_COMMAND)
+        stdout = result.stdout.splitlines()
+        adb_version = stdout[0].split()[-1]
+        # Revision information will always be in next line
+        adb_revision = stdout[1].split()[1]
+
+        response = {
+            self.ADB_VERSION: adb_version,
+            self.ADB_REVISION: adb_revision
+        }
+        return response
+
+
+class PythonVersionMetric(Metric):
+
+    PYTHON_COMMAND = 'python -V 2>&1'
+    PYTHON_VERSION = 'python_version'
+
+    def gather_metric(self):
+        """Tells which versions of python
+
+        Returns:
+            A dict with the following fields:
+              python_version: string representing version of python
+
+        """
+        result = self._shell.run(self.PYTHON_COMMAND)
+        # Python prints this to stderr
+        version = result.stdout.split()[-1]
+
+        response = {self.PYTHON_VERSION: version}
+        return response
+
+
+class KernelVersionMetric(Metric):
+
+    KERNEL_COMMAND = 'uname -r'
+    KERNEL_RELEASE = 'kernel_release'
+
+    def gather_metric(self):
+        """Tells which versions of kernel
+
+        Returns:
+            A dict with the following fields:
+              kernel_release: string representing kernel release
+
+        """
+        result = self._shell.run(self.KERNEL_COMMAND).stdout
+        response = {self.KERNEL_RELEASE: result}
+        return response
diff --git a/tools/lab/metrics/zombie_metric.py b/tools/lab/metrics/zombie_metric.py
new file mode 100644
index 0000000..7711333
--- /dev/null
+++ b/tools/lab/metrics/zombie_metric.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from metrics.metric import Metric
+
+
+class ZombieMetric(Metric):
+    COMMAND = 'ps -eo pid,stat,comm,args | awk \'$2~/^Z/ { print }\''
+    ADB_ZOMBIES = 'adb_zombies'
+    NUM_ADB_ZOMBIES = 'num_adb_zombies'
+    FASTBOOT_ZOMBIES = 'fastboot_zombies'
+    NUM_FASTBOOT_ZOMBIES = 'num_fastboot_zombies'
+    OTHER_ZOMBIES = 'other_zombies'
+    NUM_OTHER_ZOMBIES = 'num_other_zombies'
+
+    def gather_metric(self):
+        """Gathers the pids, process names, and serial numbers of processes.
+
+        If process does not have serial, None is returned instead.
+
+        Returns:
+            A dict with the following fields:
+              adb_zombies, fastboot_zombies, other_zombies: lists of
+                (PID, serial number) tuples
+              num_adb_zombies, num_fastboot_zombies, num_other_zombies: int
+                representing the number of tuples in the respective list
+        """
+        adb_zombies, fastboot_zombies, other_zombies = [], [], []
+        result = self._shell.run(self.COMMAND).stdout
+        # Example stdout:
+        # 30797 Z+   adb <defunct> adb -s AHDLSERIAL0001
+        # 30798 Z+   adb <defunct> /usr/bin/adb
+
+        output = result.splitlines()
+        for ln in output:
+            spl_ln = ln.split()
+            # spl_ln looks like ['1xx', 'Z+', 'adb', '<defunct'>, ...]
+            pid, state, name = spl_ln[:3]
+
+            if '-s' in spl_ln:
+                # Finds the '-s' flag, the index after that is the serial.
+                sn_idx = spl_ln.index('-s')
+                if sn_idx + 1 >= len(spl_ln):
+                    sn = None
+                else:
+                    sn = spl_ln[sn_idx + 1]
+                zombie = (pid, sn)
+            else:
+                zombie = (pid, None)
+            if 'adb' in ln:
+                adb_zombies.append(zombie)
+            elif 'fastboot' in ln:
+                fastboot_zombies.append(zombie)
+            else:
+                other_zombies.append(zombie)
+
+        return {
+            self.ADB_ZOMBIES: adb_zombies,
+            self.NUM_ADB_ZOMBIES: len(adb_zombies),
+            self.FASTBOOT_ZOMBIES: fastboot_zombies,
+            self.NUM_FASTBOOT_ZOMBIES: len(fastboot_zombies),
+            self.OTHER_ZOMBIES: other_zombies,
+            self.NUM_OTHER_ZOMBIES: len(other_zombies)
+        }
diff --git a/tools/lab/reporters/__init__.py b/tools/lab/reporters/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/lab/reporters/__init__.py
diff --git a/tools/lab/reporters/file_reporter.py b/tools/lab/reporters/file_reporter.py
new file mode 100644
index 0000000..dd282a7
--- /dev/null
+++ b/tools/lab/reporters/file_reporter.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from reporters import reporter
+
+
+class FileReporter(Reporter):
+    pass
diff --git a/tools/lab/reporters/json_reporter.py b/tools/lab/reporters/json_reporter.py
new file mode 100644
index 0000000..7aa779f
--- /dev/null
+++ b/tools/lab/reporters/json_reporter.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import json
+
+import health_checker
+from metrics.usb_metric import Device
+from reporters.reporter import Reporter
+
+
+class JsonReporter(Reporter):
+    def report(self, metric_responses):
+        unhealthy_metrics = self.health_checker.get_unhealthy(metric_responses)
+        for metric_name in metric_responses:
+            if metric_name not in unhealthy_metrics:
+                metric_responses[metric_name]['is_healthy'] = True
+            else:
+                metric_responses[metric_name]['is_healthy'] = False
+        print(json.dumps(metric_responses, indent=4, cls=AutoJsonEncoder))
+
+
+class AutoJsonEncoder(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, Device):
+            return {
+                'name': obj.name,
+                'trans_bytes': obj.trans_bytes,
+                'dev_id': obj.dev_id
+            }
+        else:
+            return json.JSONEncoder.default(self, obj)
diff --git a/tools/lab/reporters/logger_reporter.py b/tools/lab/reporters/logger_reporter.py
new file mode 100644
index 0000000..ff76059
--- /dev/null
+++ b/tools/lab/reporters/logger_reporter.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import logging
+
+from reporters.reporter import Reporter
+
+
+class LoggerReporter(Reporter):
+    def report(self, metric_responses):
+        # Extra formatter options.
+        extra = {
+            'metric_name': None,
+            'response_key': None,
+            'response_val': None
+        }
+
+        logger = logging.getLogger(__name__)
+        logger.setLevel(logging.INFO)
+        # Stop logger from print to stdout.
+        logger.propagate = False
+
+        handler = logging.FileHandler('lab_health.log')
+        handler.setLevel(logging.INFO)
+
+        formatter = logging.Formatter(
+            '%(asctime)s: %(metric_name)s (%(response_key)s %(response_val)s)')
+        handler.setFormatter(formatter)
+        logger.addHandler(handler)
+
+        logger = logging.LoggerAdapter(logger, extra)
+        # add the handlers to the logger
+        for metric in metric_responses:
+            extra['metric_name'] = metric
+            for response in metric_responses[metric]:
+                extra['response_key'] = response
+                extra['response_val'] = metric_responses[metric][response]
+                logger.info(None)
diff --git a/tools/lab/reporter.py b/tools/lab/reporters/reporter.py
similarity index 80%
rename from tools/lab/reporter.py
rename to tools/lab/reporters/reporter.py
index ce11412..5385446 100644
--- a/tools/lab/reporter.py
+++ b/tools/lab/reporters/reporter.py
@@ -16,23 +16,19 @@
 
 
 class Reporter(object):
-    """ Base class for the multiple ways to report the data gathered.
+    """Base class for the multiple ways to report the data gathered.
 
     The method report takes in a dictionary where the key is the class that
     generated the value, and the value is the actual data gathered from
     collecting that metric. For example, an UptimeMetric, would have
     UptimeMetric() as key, and '1-02:22:42' as the value.
+
+    Attributes:
+      health_checker: a HealthChecker object
     """
 
+    def __init__(self, health_checker):
+        self.health_checker = health_checker
+
     def report(self, responses):
         raise NotImplementedError('Must implement this method')
-
-
-class LoggerReporter(Reporter):
-    def report(self, response_dict):
-        for key in response_dict:
-            print(response_dict[key])
-
-
-class FileReporter(Reporter):
-    pass
diff --git a/tools/lab/runner.py b/tools/lab/runner.py
index 7506938..6755999 100644
--- a/tools/lab/runner.py
+++ b/tools/lab/runner.py
@@ -13,6 +13,13 @@
 #   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.
+import re
+
+# Handles edge case of acronyms then another word, eg. CPUMetric -> CPU_Metric
+# or lower camel case, cpuMetric -> cpu_Metric.
+first_cap_re = re.compile('(.)([A-Z][a-z]+)')
+# Handles CpuMetric -> Cpu_Metric
+all_cap_re = re.compile('([a-z0-9])([A-Z])')
 
 
 class Runner:
@@ -34,10 +41,23 @@
 
 
 class InstantRunner(Runner):
+    def convert_to_snake(self, name):
+        """Converts a CamelCaseName to snake_case_name
+
+        Args:
+            name: The string you want to convert.
+        Returns:
+            snake_case_format of name.
+        """
+        temp_str = first_cap_re.sub(r'\1_\2', name)
+        return all_cap_re.sub(r'\1_\2', temp_str).lower()
+
     def run(self):
         """Calls all metrics, passes responses to reporters."""
         responses = {}
         for metric in self.metric_list:
-            responses[metric] = metric.gatherMetric()
+            # [:-7] removes the ending '_metric'.
+            key_name = self.convert_to_snake(metric.__class__.__name__)[:-7]
+            responses[key_name] = metric.gather_metric()
         for reporter in self.reporter_list:
             reporter.report(responses)
diff --git a/tools/lab/setup.py b/tools/lab/setup.py
new file mode 100755
index 0000000..c2c77ba
--- /dev/null
+++ b/tools/lab/setup.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3.4
+#
+# Copyright 2016 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from distutils import cmd
+from distutils import log
+import pip
+import setuptools
+import sys
+import subprocess
+import os
+
+README_FILE = os.path.join(os.path.dirname(__file__), 'README.md')
+
+install_requires = [
+    # Future needs to have a newer version that contains urllib.
+    'future>=0.16.0',
+    'psutil',
+    'shellescape',
+    'IPy',
+]
+
+if sys.version_info < (3, ):
+    # "futures" is needed for py2 compatibility and it only works in 2.7
+    install_requires.append('futures')
+    install_requires.append('subprocess32')
+
+try:
+    # Need to install python-dev to work with Python2.7 installing.
+    subprocess.check_call(['apt-get', 'install', '-y', 'python-dev'])
+except subprocess.CalledProcessError as cpe:
+    print('Could not install python-dev: %s' % cpe.output)
+
+
+class LabHealthInstallDependencies(cmd.Command):
+    """Installs only required packages
+
+    Installs all required packages for acts to work. Rather than using the
+    normal install system which creates links with the python egg, pip is
+    used to install the packages.
+    """
+
+    description = 'Install dependencies needed for lab health.'
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        pip.main(['install', '--upgrade', 'pip'])
+
+        required_packages = self.distribution.install_requires
+        for package in required_packages:
+            self.announce('Installing %s...' % package, log.INFO)
+            pip.main(['install', package])
+
+        self.announce('Dependencies installed.')
+
+
+def main():
+    setuptools.setup(
+        name='LabHealth',
+        version='0.1',
+        description='Android Test Lab Health',
+        license='Apache2.0',
+        cmdclass={
+            'install_deps': LabHealthInstallDependencies,
+        },
+        packages=setuptools.find_packages(),
+        include_package_data=False,
+        install_requires=install_requires,
+        url="http://www.android.com/",
+        long_description=open(README_FILE).read())
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tools/lab/test_main.py b/tools/lab/test_main.py
index 48dc714..5f7b644 100755
--- a/tools/lab/test_main.py
+++ b/tools/lab/test_main.py
@@ -14,8 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import sys
 import unittest
 
 if __name__ == "__main__":
-    suite = unittest.TestLoader().discover('.', pattern="*_test.py")
-    unittest.TextTestRunner().run(suite)
+    suite = unittest.TestLoader().discover(
+        start_dir='./tools/lab', pattern='*_test.py')
+    runner = unittest.TextTestRunner()
+
+    # Return exit code of tests, so preupload hook fails if tests don't pass
+    ret = not runner.run(suite).wasSuccessful()
+    sys.exit(ret)
diff --git a/tools/lab/tests/adb_hash_metric_test.py b/tools/lab/tests/adb_hash_metric_test.py
new file mode 100644
index 0000000..44b1d43
--- /dev/null
+++ b/tools/lab/tests/adb_hash_metric_test.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+#
+#   copyright 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.
+
+import unittest
+
+from metrics import adb_hash_metric
+import mock
+
+from tests import fake
+
+
+class HashMetricTest(unittest.TestCase):
+    @mock.patch('os.environ', {'ADB_VENDOR_KEYS': '/root/adb/'})
+    def test_gather_metric_env_set(self):
+        # Create sample stdout string ShellCommand.run() would return
+        stdout_string = ('12345abcdef_hash')
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = adb_hash_metric.AdbHashMetric(shell=fake_shell)
+
+        expected_result = {
+            'hash': '12345abcdef_hash',
+            'keys_path': '/root/adb/'
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    @mock.patch('os.environ', {})
+    def test_gather_metric_env_not_set(self):
+        # Create sample stdout string ShellCommand.run() would return
+        stdout_string = ('12345abcdef_hash')
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = adb_hash_metric.AdbHashMetric(shell=fake_shell)
+
+        expected_result = {'hash': None, 'keys_path': None}
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    @mock.patch('os.environ', {'ADB_VENDOR_KEYS': '/root/adb/'})
+    def test_verify_env_set(self):
+        self.assertEquals(adb_hash_metric.AdbHashMetric()._verify_env(),
+                          '/root/adb/')
+
+    @mock.patch('os.environ', {})
+    def test_verify_env_not_set(self):
+        self.assertEquals(adb_hash_metric.AdbHashMetric()._verify_env(), None)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/constant_health_analyzer_test.py b/tools/lab/tests/constant_health_analyzer_test.py
new file mode 100644
index 0000000..ebf38b2
--- /dev/null
+++ b/tools/lab/tests/constant_health_analyzer_test.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from health import constant_health_analyzer as ha
+import unittest
+
+
+class HealthyIfGreaterThanConstantNumberTest(unittest.TestCase):
+    def test_is_healthy_correct_inputs_return_true(self):
+        sample_metric = {'a_key': 3}
+        analyzer = ha.HealthyIfGreaterThanConstantNumber(
+            key='a_key', constant=2)
+        self.assertTrue(analyzer.is_healthy(sample_metric))
+
+    def test_is_healthy_correct_inputs_return_false(self):
+        sample_metric = {'a_key': 3}
+        analyzer = ha.HealthyIfGreaterThanConstantNumber(
+            key='a_key', constant=4)
+        self.assertFalse(analyzer.is_healthy(sample_metric))
+
+
+class HealthyIfLessThanConstantNumberTest(unittest.TestCase):
+    def test_is_healthy_correct_inputs_return_true(self):
+        sample_metric = {'a_key': 1}
+        analyzer = ha.HealthyIfLessThanConstantNumber(key='a_key', constant=2)
+        self.assertTrue(analyzer.is_healthy(sample_metric))
+
+    def test_is_healthy_correct_inputs_return_false(self):
+        sample_metric = {'a_key': 3}
+        analyzer = ha.HealthyIfLessThanConstantNumber(key='a_key', constant=2)
+        self.assertFalse(analyzer.is_healthy(sample_metric))
+
+
+class HealthyIfEqualsTest(unittest.TestCase):
+    def test_is_healthy_correct_string_inputs_return_true(self):
+        sample_metric = {'a_key': "hi"}
+        analyzer = ha.HealthyIfEquals(key='a_key', constant="hi")
+        self.assertTrue(analyzer.is_healthy(sample_metric))
+
+    def test_is_healthy_correct_num_inputs_return_true(self):
+        sample_metric = {'a_key': 1}
+        analyzer = ha.HealthyIfEquals(key='a_key', constant=1)
+        self.assertTrue(analyzer.is_healthy(sample_metric))
+
+    def test_is_healthy_correct_inputs_return_false(self):
+        sample_metric = {'a_key': 3}
+        analyzer = ha.HealthyIfEquals(key='a_key', constant=2)
+        self.assertFalse(analyzer.is_healthy(sample_metric))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/constant_health_analyzer_wrapper_test.py b/tools/lab/tests/constant_health_analyzer_wrapper_test.py
new file mode 100644
index 0000000..1be9f40
--- /dev/null
+++ b/tools/lab/tests/constant_health_analyzer_wrapper_test.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from health import constant_health_analyzer_wrapper as cha
+import unittest
+
+
+class HealthyIfEqualsTest(unittest.TestCase):
+    def test_is_healthy_correct_string_inputs_return_true(self):
+        sample_metric = {'verify': {'serial1': 'device', 'serial2': 'device'}}
+        analyzer = cha.HealthyIfValsEqual(key='verify', constant='device')
+        self.assertTrue(analyzer.is_healthy(sample_metric))
+
+    def test_is_healthy_one_incorrect_inputs(self):
+        sample_metric = {
+            'verify': {
+                'serial1': 'disconnected',
+                'serial2': 'device'
+            }
+        }
+        analyzer = cha.HealthyIfValsEqual(key='verify', constant='device')
+        self.assertFalse(analyzer.is_healthy(sample_metric))
+
+    def test_is_healthy_all_incorrect_inputs(self):
+        sample_metric = {
+            'verify': {
+                'serial1': 'disconnected',
+                'serial2': 'unauthorized'
+            }
+        }
+        analyzer = cha.HealthyIfValsEqual(key='verify', constant='device')
+        self.assertFalse(analyzer.is_healthy(sample_metric))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/custom_health_analyzer_test.py b/tools/lab/tests/custom_health_analyzer_test.py
new file mode 100644
index 0000000..e2b19cb
--- /dev/null
+++ b/tools/lab/tests/custom_health_analyzer_test.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from health import custom_health_analyzer as ha
+import unittest
+
+
+class HealthyIfNotIpTest(unittest.TestCase):
+    def test_hostname_not_ip(self):
+        sample_metric = {'hostname': 'random_hostname_of_server'}
+        analyzer = ha.HealthyIfNotIpAddress(key='hostname')
+        self.assertTrue(analyzer.is_healthy(sample_metric))
+
+    def test_hostname_ip4(self):
+        sample_metric = {'hostname': '12.12.12.12'}
+        analyzer = ha.HealthyIfNotIpAddress(key='hostname')
+        self.assertFalse(analyzer.is_healthy(sample_metric))
+
+    def test_hostname_ip6_empty(self):
+        sample_metric = {'hostname': '::1'}
+        analyzer = ha.HealthyIfNotIpAddress(key='hostname')
+        self.assertFalse(analyzer.is_healthy(sample_metric))
+
+    def test_hostname_ip6_full(self):
+        sample_metric = {'hostname': '2001:db8:85a3:0:0:8a2e:370:7334'}
+        analyzer = ha.HealthyIfNotIpAddress(key='hostname')
+        self.assertFalse(analyzer.is_healthy(sample_metric))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/disk_metric_test.py b/tools/lab/tests/disk_metric_test.py
index 339ed49..9183685 100755
--- a/tools/lab/tests/disk_metric_test.py
+++ b/tools/lab/tests/disk_metric_test.py
@@ -1,18 +1,18 @@
 #!/usr/bin/env python
 #
-#   copyright 2017 - the android open source project
+#   Copyright 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
+#   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
+#       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.
+#   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.
 
 import unittest
 
@@ -23,17 +23,12 @@
 class DiskMetricTest(unittest.TestCase):
     """Class for testing DiskMetric."""
 
-    def setUp(self):
-        pass
-
     def test_return_total_used_avail_percent(self):
         # Create sample stdout string ShellCommand.run() would return
-        stdout_string = ('Filesystem     1K-blocks     Used Available Use% '
-                         'mounted on\n/dev/dm-1       57542652 18358676 '
-                         '36237928  34% /')
-        self.FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
-        fake_shell = fake.MockShellCommand(fake_result=self.FAKE_RESULT)
-        self.metric_obj = disk_metric.DiskMetric(shell=fake_shell)
+        stdout_string = '/dev/sda 57542652 18358676 ' '36237928  34% /'
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = disk_metric.DiskMetric(shell=fake_shell)
 
         expected_result = {
             disk_metric.DiskMetric.TOTAL: 57542652,
@@ -41,7 +36,7 @@
             disk_metric.DiskMetric.AVAIL: 36237928,
             disk_metric.DiskMetric.PERCENT_USED: 34
         }
-        self.assertEqual(expected_result, self.metric_obj.gather_metric())
+        self.assertEqual(expected_result, metric_obj.gather_metric())
 
 
 if __name__ == '__main__':
diff --git a/tools/lab/tests/fake.py b/tools/lab/tests/fake.py
index 628a725..07aa4e0 100644
--- a/tools/lab/tests/fake.py
+++ b/tools/lab/tests/fake.py
@@ -1,24 +1,24 @@
 #!/usr/bin/env python
 #
-#   copyright 2017 - the android open source project
+#   Copyright 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
+#   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
+#       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.
+#   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.
 
 
 class FakeResult(object):
     """A fake version of the object returned from ShellCommand.run. """
 
-    def __init__(self, exit_status=1, stdout='', stderr=''):
+    def __init__(self, exit_status=0, stdout='', stderr=''):
         self.exit_status = exit_status
         self.stdout = stdout
         self.stderr = stderr
@@ -28,20 +28,43 @@
     """A fake ShellCommand object.
 
     Attributes:
-        fake_result: a FakeResult object
+        fake_result: a FakeResult object, or a list of FakeResult objects
+        fake_pids: a dictionary that maps string identifier to list of pids
     """
 
-    def __init__(self, fake_result):
+    def __init__(self, fake_result=None, fake_pids=[]):
         self._fake_result = fake_result
+        self._fake_pids = fake_pids
+        self._counter = 0
 
-    """Returns a FakeResult object.
+    def run(self, command, timeout=3600, ignore_status=False):
+        """Returns a FakeResult object.
 
-    Args:
-        Same as ShellCommand.run, but none are used in function
+        Args:
+            Same as ShellCommand.run, but none are used in function
 
-    Returns:
-        The FakeResult object it was initalized with
-    """
+        Returns:
+            The FakeResult object it was initalized with. If it was initialized
+            with a list, returns the next element in list and increments counter
 
-    def run(self, command, timeout=3600):
-        return self._fake_result
+        Raises:
+          IndexError: Function was called more times than num elements in list
+
+        """
+        if isinstance(self._fake_result, list):
+            self._counter += 1
+            return self._fake_result[self._counter - 1]
+        else:
+            return self._fake_result
+
+    def get_pids(self, identifier):
+        """Returns a generator of fake pids
+
+        Args:
+          Same as ShellCommand.get_pids, but none are used in the function
+        Returns:
+          A generator of the fake pids it was initialized with
+        """
+
+        for pid in self._fake_pids[identifier]:
+            yield pid
diff --git a/tools/lab/tests/health_checker_test.py b/tools/lab/tests/health_checker_test.py
new file mode 100644
index 0000000..28d2e64
--- /dev/null
+++ b/tools/lab/tests/health_checker_test.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import io
+import mock
+import unittest
+
+from health_checker import HealthChecker
+from health.constant_health_analyzer import HealthyIfGreaterThanConstantNumber
+from health.constant_health_analyzer import HealthyIfLessThanConstantNumber
+
+
+class HealthCheckerTestCase(unittest.TestCase):
+    def test_correct_comparers(self):
+        fake_config = {
+            'RamMetric': {
+                'free': {
+                    'compare': 'GREATER_THAN',
+                    'constant': 100
+                }
+            }
+        }
+        checker = HealthChecker(fake_config)
+        self.assertEquals(checker._analyzers[0][0], 'RamMetric')
+        self.assertIsInstance(checker._analyzers[0][1],
+                              HealthyIfGreaterThanConstantNumber)
+
+    def test_multiple_compares_one_metric(self):
+        fake_config = {
+            'DiskMetric': {
+                'avail': {
+                    'compare': 'GREATER_THAN',
+                    'constant': 200
+                },
+                'percent_used': {
+                    'compare': 'LESS_THAN',
+                    'constant': 70
+                }
+            }
+        }
+        checker = HealthChecker(fake_config)
+        expected_analyzers = [('DiskMetric',
+                               HealthyIfGreaterThanConstantNumber(
+                                   'avail', 200)),
+                              ('DiskMetric', HealthyIfLessThanConstantNumber(
+                                  'percent_used', 70))]
+        # Have to sort the lists by first element of tuple to ensure equality
+        self.assertEqual(
+            checker._analyzers.sort(key=lambda x: x[0]),
+            expected_analyzers.sort(key=lambda x: x[0]))
+
+    def test_get_unhealthy_return_none(self):
+        # all metrics are healthy
+        fake_config = {
+            'DiskMetric': {
+                'avail': {
+                    'compare': 'GREATER_THAN',
+                    'constant': 50
+                },
+                'percent_used': {
+                    'compare': 'LESS_THAN',
+                    'constant': 70
+                }
+            }
+        }
+        checker = HealthChecker(fake_config)
+        fake_metric_response = {
+            'DiskMetric': {
+                'total': 100,
+                'used': 0,
+                'avail': 100,
+                'percent_used': 0
+            }
+        }
+        # Should return an empty list, which evalutates to false
+        self.assertFalse(checker.get_unhealthy(fake_metric_response))
+
+    def test_get_unhealthy_return_all_unhealthy(self):
+        fake_config = {
+            'DiskMetric': {
+                'avail': {
+                    'compare': 'GREATER_THAN',
+                    'constant': 50
+                },
+                'percent_used': {
+                    'compare': 'LESS_THAN',
+                    'constant': 70
+                }
+            }
+        }
+        checker = HealthChecker(fake_config)
+        fake_metric_response = {
+            'DiskMetric': {
+                'total': 100,
+                'used': 90,
+                'avail': 10,
+                'percent_used': 90
+            }
+        }
+        expected_unhealthy = ['DiskMetric']
+        self.assertEqual(
+            set(checker.get_unhealthy(fake_metric_response)),
+            set(expected_unhealthy))
+
+    def test_get_unhealthy_check_vals_metric(self):
+        fake_config = {
+            "verify": {
+                "devices": {
+                    "constant": "device",
+                    "compare": "EQUALS_DICT"
+                }
+            }
+        }
+        checker = HealthChecker(fake_config)
+        fake_metric_response = {
+            'verify': {
+                'devices': {
+                    'serialnumber': 'unauthorized'
+                }
+            }
+        }
+        expected_unhealthy = ['verify']
+        self.assertEqual(
+            set(checker.get_unhealthy(fake_metric_response)),
+            set(expected_unhealthy))
+
+    def test_get_healthy_check_vals_metric(self):
+        fake_config = {
+            "verify": {
+                "devices": {
+                    "constant": "device",
+                    "compare": "EQUALS_DICT"
+                }
+            }
+        }
+        checker = HealthChecker(fake_config)
+        fake_metric_response = {
+            'verify': {
+                'devices': {
+                    'serialnumber': 'device'
+                }
+            }
+        }
+        expected_unhealthy = []
+        self.assertEqual(
+            set(checker.get_unhealthy(fake_metric_response)),
+            set(expected_unhealthy))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/main_test.py b/tools/lab/tests/main_test.py
new file mode 100644
index 0000000..b3cdc44
--- /dev/null
+++ b/tools/lab/tests/main_test.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import io
+import mock
+import unittest
+
+from main import RunnerFactory
+from health_checker import HealthChecker
+from metrics.usb_metric import UsbMetric
+from metrics.verify_metric import VerifyMetric
+from metrics.adb_hash_metric import AdbHashMetric
+from reporters.logger_reporter import LoggerReporter
+
+
+class RunnerFactoryTestCase(unittest.TestCase):
+    def test_create_with_reporter(self):
+        self.assertIsInstance(
+            RunnerFactory.create({
+                'reporter': ['logger']
+            }).reporter_list[0], LoggerReporter)
+
+    def test_create_without_reporter(self):
+        self.assertIsInstance(
+            RunnerFactory.create({
+                'reporter': None
+            }).reporter_list[0], LoggerReporter)
+
+    def test_metric_none(self):
+        self.assertEqual(
+            RunnerFactory.create({
+                'disk': None,
+                'reporter': None
+            }).metric_list, [])
+
+    def test_metric_true(self):
+        self.assertIsInstance(
+            RunnerFactory.create({
+                'usb_io': True,
+                'reporter': None
+            }).metric_list[0], UsbMetric)
+
+    def test_verify(self):
+        run = RunnerFactory.create({'reporter': None, 'verify_devices': True})
+        self.assertIsInstance(run.metric_list[0], VerifyMetric)
+        self.assertIsInstance(run.metric_list[1], AdbHashMetric)
+        self.assertEquals(len(run.metric_list), 2)
+
+    def test_invalid_config_file(self):
+        with self.assertRaises(SystemExit):
+            RunnerFactory.create({
+                'disk': None,
+                'reporter': None,
+                'config': 'not_a_valid_file.json'
+            })
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/name_metric_test.py b/tools/lab/tests/name_metric_test.py
new file mode 100755
index 0000000..894426d
--- /dev/null
+++ b/tools/lab/tests/name_metric_test.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+#
+#   copyright 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.
+
+import unittest
+
+from metrics import name_metric
+from tests import fake
+
+
+class NameMetricTest(unittest.TestCase):
+    """Class for testing NameMetric."""
+
+    def test_correct_name(self):
+        stdout_string = "android1759-test-server-14"
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = name_metric.NameMetric(shell=fake_shell)
+        expected_result = {
+            name_metric.NameMetric.NAME: stdout_string,
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/network_metric_test.py b/tools/lab/tests/network_metric_test.py
new file mode 100644
index 0000000..f7c0f19
--- /dev/null
+++ b/tools/lab/tests/network_metric_test.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+from metrics.network_metric import NetworkMetric
+from tests import fake
+
+
+class NetworkMetricTest(unittest.TestCase):
+    def test_hostname_prefix(self):
+        mock_stdout = 'android'
+        FAKE_RESULT = fake.FakeResult(stdout=mock_stdout)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = NetworkMetric(['8.8.8.8'], shell=fake_shell)
+        expected_result = 'android'
+        self.assertEqual(metric_obj.get_prefix_hostname(), expected_result)
+
+    def test_connected_empty(self):
+        mock_result = fake.FakeResult(exit_status=0, stdout="connected")
+        metric_obj = NetworkMetric(shell=fake.MockShellCommand(
+            fake_result=mock_result))
+        exp_out = {'8.8.8.8': True, '8.8.4.4': True}
+        self.assertEquals(metric_obj.check_connected(), exp_out)
+
+    def test_connected_false(self):
+        mock_result = fake.FakeResult(exit_status=1, stdout="not connected")
+        metric_obj = NetworkMetric(shell=fake.MockShellCommand(
+            fake_result=mock_result))
+        exp_out = {'8.8.8.8': False, '8.8.4.4': False}
+        self.assertEquals(metric_obj.check_connected(), exp_out)
+
+    def test_connected_true_passed_in(self):
+        mock_result = fake.FakeResult(exit_status=0, stdout="connected")
+        metric_obj = NetworkMetric(
+            ['8.8.8.8'], shell=fake.MockShellCommand(fake_result=mock_result))
+        self.assertEquals(
+            metric_obj.check_connected(metric_obj.ip_list), {'8.8.8.8': True})
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/num_users_test.py b/tools/lab/tests/num_users_test.py
new file mode 100755
index 0000000..05b7d79
--- /dev/null
+++ b/tools/lab/tests/num_users_test.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+from metrics import num_users_metric
+from tests import fake
+
+
+class NumUsersMetricTest(unittest.TestCase):
+    """Class for testing NumUsersMetric."""
+
+    def test_num_users(self):
+        stdout_string = '3'
+        fake_result = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=fake_result)
+        metric_obj = num_users_metric.NumUsersMetric(shell=fake_shell)
+
+        expected_result = {
+            num_users_metric.NumUsersMetric.NUM_USERS: 3,
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/process_time_metric_test.py b/tools/lab/tests/process_time_metric_test.py
new file mode 100644
index 0000000..f3e4c8f
--- /dev/null
+++ b/tools/lab/tests/process_time_metric_test.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+from metrics import process_time_metric
+from tests import fake
+import mock
+
+
+class ProcessTimeMetricTest(unittest.TestCase):
+    def test_get_adb_fastboot_pids_only_adb(self):
+        fake_pids = {'adb': ['123', '456', '789'], 'fastboot': []}
+        fake_shell = fake.MockShellCommand(fake_pids=fake_pids)
+        metric_obj = process_time_metric.ProcessTimeMetric(shell=fake_shell)
+        self.assertEqual(metric_obj.get_adb_fastboot_pids(), fake_pids['adb'])
+
+    def test_get_adb_fastboot_pids_only_fastboot(self):
+        fake_pids = {'adb': [], 'fastboot': ['123', '456', '789']}
+        fake_shell = fake.MockShellCommand(fake_pids=fake_pids)
+
+        metric_obj = process_time_metric.ProcessTimeMetric(shell=fake_shell)
+        self.assertEqual(metric_obj.get_adb_fastboot_pids(),
+                         fake_pids['fastboot'])
+
+    def test_get_adb_fastboot_pids_both(self):
+        fake_pids = {
+            'adb': ['987', '654', '321'],
+            'fastboot': ['123', '456', '789']
+        }
+        fake_shell = fake.MockShellCommand(fake_pids=fake_pids)
+        metric_obj = process_time_metric.ProcessTimeMetric(shell=fake_shell)
+        self.assertEqual(metric_obj.get_adb_fastboot_pids(),
+                         fake_pids['adb'] + fake_pids['fastboot'])
+
+    def test_gather_metric_returns_only_older_times(self):
+        fake_result = [
+            fake.FakeResult(stdout='1234 other command'),
+            fake.FakeResult(stdout='232893 fastboot -s FA6BM0305019 -w')
+        ]
+        fake_pids = {'adb': ['123'], 'fastboot': ['456']}
+        fake_shell = fake.MockShellCommand(
+            fake_pids=fake_pids, fake_result=fake_result)
+        metric_obj = process_time_metric.ProcessTimeMetric(shell=fake_shell)
+        expected_result = {
+            process_time_metric.ProcessTimeMetric.ADB_PROCESSES: [],
+            process_time_metric.ProcessTimeMetric.NUM_ADB_PROCESSES:
+            0,
+            process_time_metric.ProcessTimeMetric.FASTBOOT_PROCESSES:
+            [("456", 'FA6BM0305019')],
+            process_time_metric.ProcessTimeMetric.NUM_FASTBOOT_PROCESSES:
+            1
+        }
+
+        self.assertEqual(metric_obj.gather_metric(), expected_result)
+
+    def test_gather_metric_returns_times_no_forkserver(self):
+        fake_result = [
+            fake.FakeResult(
+                stdout='198797 /usr/bin/adb -s FAKESN wait-for-device'),
+            fake.FakeResult(stdout='9999999 adb -s FAKESN2'),
+            fake.FakeResult(stdout='9999998 fork-server adb')
+        ]
+        fake_pids = {'adb': ['123', '456', '789'], 'fastboot': []}
+        fake_shell = fake.MockShellCommand(
+            fake_pids=fake_pids, fake_result=fake_result)
+        metric_obj = process_time_metric.ProcessTimeMetric(shell=fake_shell)
+        expected_result = {
+            process_time_metric.ProcessTimeMetric.ADB_PROCESSES:
+            [('123', 'FAKESN'), ('456', 'FAKESN2')],
+            process_time_metric.ProcessTimeMetric.NUM_ADB_PROCESSES:
+            2,
+            process_time_metric.ProcessTimeMetric.FASTBOOT_PROCESSES: [],
+            process_time_metric.ProcessTimeMetric.NUM_FASTBOOT_PROCESSES:
+            0
+        }
+
+        self.assertEqual(metric_obj.gather_metric(), expected_result)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/ram_metric_test.py b/tools/lab/tests/ram_metric_test.py
new file mode 100755
index 0000000..940184d
--- /dev/null
+++ b/tools/lab/tests/ram_metric_test.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+from metrics import ram_metric
+from tests import fake
+
+
+class RamMetricTest(unittest.TestCase):
+    """Class for testing RamMetric."""
+
+    def test_correct_ram_output(self):
+        # Create sample stdout string ShellCommand.run() would return
+        stdout_string = ('             total       used       free     shared'
+                         'buffers     cached\nMem:      64350   34633   '
+                         '29717     309024    1744   24692\n-/+ '
+                         'buffers/cache:    9116   56777\nSwap:     '
+                         '67031          0   67031')
+
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = ram_metric.RamMetric(shell=fake_shell)
+
+        expected_result = {
+            ram_metric.RamMetric.TOTAL: 64350,
+            ram_metric.RamMetric.USED: 34633,
+            ram_metric.RamMetric.FREE: 29717,
+            ram_metric.RamMetric.BUFFERS: 1744,
+            ram_metric.RamMetric.CACHED: 24692
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/read_metric_test.py b/tools/lab/tests/read_metric_test.py
new file mode 100755
index 0000000..a2feda6
--- /dev/null
+++ b/tools/lab/tests/read_metric_test.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+import mock
+from metrics import read_metric
+from tests import fake
+
+
+class ReadMetricTest(unittest.TestCase):
+    """Class for testing ReadMetric."""
+
+    @mock.patch('os.getuid')
+    def test_return_total_used_avail_percent_with_permiss(self, getuid_func):
+        getuid_func.return_value = 0
+        # Create sample stdout string ShellCommand.run() would return
+        stdout_string = ('/dev/sda:\n'
+                         ' Timing cached reads: '
+                         '18192 MB in  2.00 seconds = 9117.49 MB/sec\n'
+                         ' Timing buffered disk reads: '
+                         '414 MB in 3.07 seconds = 134.80 MB/sec\n'
+                         '\n/dev/sda:\n'
+                         ' Timing cached reads:   '
+                         '18100 MB in  2.00 seconds = 9071.00 MB/sec\n'
+                         ' Timing buffered disk reads: '
+                         '380 MB in  3.01 seconds = 126.35 MB/sec\n'
+                         '\n/dev/sda:\n'
+                         ' Timing cached reads:   '
+                         '18092 MB in  2.00 seconds = 9067.15 MB/sec\n'
+                         ' Timing buffered disk reads: '
+                         '416 MB in  3.01 seconds = 138.39 MB/sec')
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = read_metric.ReadMetric(shell=fake_shell)
+
+        exp_res = {
+            read_metric.ReadMetric.CACHED_READ_RATE:
+            27255.64 / read_metric.ReadMetric.NUM_RUNS,
+            read_metric.ReadMetric.BUFFERED_READ_RATE:
+            399.54 / read_metric.ReadMetric.NUM_RUNS
+        }
+        result = metric_obj.gather_metric()
+        self.assertAlmostEqual(
+            exp_res[read_metric.ReadMetric.CACHED_READ_RATE],
+            result[metric_obj.CACHED_READ_RATE])
+        self.assertAlmostEqual(
+            exp_res[read_metric.ReadMetric.BUFFERED_READ_RATE],
+            result[metric_obj.BUFFERED_READ_RATE])
+
+    @mock.patch('os.getuid')
+    def test_return_total_used_avail_percent_with_no_permiss(
+            self, getuid_func):
+        getuid_func.return_value = 1
+        exp_res = {
+            read_metric.ReadMetric.CACHED_READ_RATE: None,
+            read_metric.ReadMetric.BUFFERED_READ_RATE: None
+        }
+        result = read_metric.ReadMetric().gather_metric()
+        self.assertEquals(result, exp_res)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/time_sync_metric_test.py b/tools/lab/tests/time_sync_metric_test.py
new file mode 100755
index 0000000..e7e5911
--- /dev/null
+++ b/tools/lab/tests/time_sync_metric_test.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+from metrics import time_sync_metric
+from tests import fake
+
+
+class TimeSyncMetricTest(unittest.TestCase):
+    """Class for testing TimeSyncMetric."""
+
+    def test_yes_time_sync(self):
+        stdout_string = "NTP synchronized: yes"
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = time_sync_metric.TimeSyncMetric(shell=fake_shell)
+
+        expected_result = {
+            time_sync_metric.TimeSyncMetric.IS_SYNCHRONIZED: True,
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    def test_no_time_sync(self):
+        stdout_string = 'NTP synchronized: no'
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = time_sync_metric.TimeSyncMetric(shell=fake_shell)
+
+        expected_result = {
+            time_sync_metric.TimeSyncMetric.IS_SYNCHRONIZED: False,
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/uptime_metric_test.py b/tools/lab/tests/uptime_metric_test.py
new file mode 100755
index 0000000..5c73523
--- /dev/null
+++ b/tools/lab/tests/uptime_metric_test.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+from metrics import uptime_metric
+from tests import fake
+
+
+class UptimeMetricTest(unittest.TestCase):
+    """Class for testing UptimeMetric."""
+
+    def test_correct_uptime(self):
+        # Create sample stdout string ShellCommand.run() would return
+        stdout_string = "358350.70 14241538.06"
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = uptime_metric.UptimeMetric(shell=fake_shell)
+
+        expected_result = {
+            uptime_metric.UptimeMetric.TIME_SECONDS: 358350.70,
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/usb_metric_test.py b/tools/lab/tests/usb_metric_test.py
new file mode 100644
index 0000000..1a7162a
--- /dev/null
+++ b/tools/lab/tests/usb_metric_test.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from io import BytesIO
+import unittest
+
+from tests import fake
+from metrics.usb_metric import Device
+from metrics.usb_metric import UsbMetric
+import mock
+
+
+class UsbMetricTest(unittest.TestCase):
+    def test_check_usbmon_install_t(self):
+        pass
+
+    def test_check_usbmon_install_f(self):
+        pass
+
+    def test_check_get_bytes_2(self):
+        with mock.patch('subprocess.Popen') as mock_popen:
+            mock_popen.return_value.stdout = BytesIO(
+                b'x x C Ii:2:003:1 0:8 8 = x x\nx x S Ii:2:004:1 -115:8 8 <')
+
+            self.assertEquals(UsbMetric().get_bytes(0),
+                              {'2:003': 8,
+                               '2:004': 8})
+
+    def test_check_get_bytes_empty(self):
+        with mock.patch('subprocess.Popen') as mock_popen:
+            mock_popen.return_value.stdout = BytesIO(b'')
+            self.assertEquals(UsbMetric().get_bytes(0), {})
+
+    def test_match_device_id(self):
+        mock_lsusb = ('Bus 003 Device 047: ID 18d1:d00d Device 0\n'
+                      'Bus 003 Device 001: ID 1d6b:0002 Device 1')
+        exp_res = {'3:047': 'Device 0', '3:001': 'Device 1'}
+        fake_result = fake.FakeResult(stdout=mock_lsusb)
+        fake_shell = fake.MockShellCommand(fake_result=fake_result)
+        m = UsbMetric(shell=fake_shell)
+        self.assertEquals(m.match_device_id(), exp_res)
+
+    def test_match_device_id_empty(self):
+        mock_lsusb = ''
+        exp_res = {}
+        fake_result = fake.FakeResult(stdout=mock_lsusb)
+        fake_shell = fake.MockShellCommand(fake_result=fake_result)
+        m = UsbMetric(shell=fake_shell)
+        self.assertEquals(m.match_device_id(), exp_res)
+
+    def test_gen_output(self):
+        dev_name_dict = {
+            '1:001': 'Device 1',
+            '1:002': 'Device 2',
+            '1:003': 'Device 3'
+        }
+        dev_byte_dict = {'1:001': 256, '1:002': 200}
+
+        dev_1 = Device('1:002', 200, 'Device 2')
+        dev_2 = Device('1:001', 256, 'Device 1')
+        dev_3 = Device('1:003', 0, 'Device 3')
+
+        act_out = UsbMetric().gen_output(dev_name_dict, dev_byte_dict)
+
+        self.assertTrue(dev_1 in act_out and dev_2 in act_out and
+                        dev_3 in act_out)
diff --git a/tools/lab/tests/verify_metric_test.py b/tools/lab/tests/verify_metric_test.py
new file mode 100644
index 0000000..67a08bc
--- /dev/null
+++ b/tools/lab/tests/verify_metric_test.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+from metrics import verify_metric
+from tests import fake
+
+
+class VerifyMetricTest(unittest.TestCase):
+    def test_gather_device_empty(self):
+        mock_output = ''
+        FAKE_RESULT = fake.FakeResult(stdout=mock_output)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = verify_metric.VerifyMetric(shell=fake_shell)
+
+        expected_result = {verify_metric.VerifyMetric.DEVICES: {}}
+        self.assertEquals(metric_obj.gather_metric(), expected_result)
+
+    def test_gather_device_two(self):
+        mock_output = '00serial01\toffline\n' \
+                      '01serial00\tdevice'
+        FAKE_RESULT = fake.FakeResult(stdout=mock_output)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = verify_metric.VerifyMetric(shell=fake_shell)
+
+        expected_result = {
+            verify_metric.VerifyMetric.DEVICES: {
+                '00serial01': 'offline',
+                '01serial00': 'device',
+            }
+        }
+        self.assertEquals(metric_obj.gather_metric(), expected_result)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/version_metric_test.py b/tools/lab/tests/version_metric_test.py
new file mode 100755
index 0000000..777d9bd
--- /dev/null
+++ b/tools/lab/tests/version_metric_test.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+from metrics import version_metric
+from tests import fake
+
+
+class VersionMetricTest(unittest.TestCase):
+    """Class for testing VersionMetric."""
+
+    def test_get_fastboot_version_error_message(self):
+        stderr_str = version_metric.FastbootVersionMetric.FASTBOOT_ERROR_MESSAGE,
+        FAKE_RESULT = fake.FakeResult(stderr=stderr_str)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = version_metric.FastbootVersionMetric(shell=fake_shell)
+
+        expected_result = {
+            version_metric.FastbootVersionMetric.FASTBOOT_VERSION:
+            version_metric.FastbootVersionMetric.FASTBOOT_ERROR_MESSAGE
+        }
+
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    def test_get_fastboot_version_one_line_input(self):
+        stdout_str = 'fastboot version 5cf8bbd5b29d-android\n'
+
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_str)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = version_metric.FastbootVersionMetric(shell=fake_shell)
+
+        expected_result = {
+            version_metric.FastbootVersionMetric.FASTBOOT_VERSION:
+            '5cf8bbd5b29d-android'
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    def test_get_fastboot_version_two_line_input(self):
+        stdout_str = ('fastboot version 5cf8bbd5b29d-android\n'
+                      'Installed as /usr/bin/fastboot\n')
+
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_str)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = version_metric.FastbootVersionMetric(shell=fake_shell)
+
+        expected_result = {
+            version_metric.FastbootVersionMetric.FASTBOOT_VERSION:
+            '5cf8bbd5b29d-android'
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    def test_get_adb_version(self):
+        stdout_str = ('Android Debug Bridge version 1.0.39\n'
+                      'Revision 3db08f2c6889-android\n')
+
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_str)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = version_metric.AdbVersionMetric(shell=fake_shell)
+
+        expected_result = {
+            version_metric.AdbVersionMetric.ADB_VERSION: '1.0.39',
+            version_metric.AdbVersionMetric.ADB_REVISION:
+            '3db08f2c6889-android'
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    def test_get_python_version(self):
+        stdout_str = 'Python 2.7.6'
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_str)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = version_metric.PythonVersionMetric(shell=fake_shell)
+
+        expected_result = {
+            version_metric.PythonVersionMetric.PYTHON_VERSION: '2.7.6'
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    def test_get_kernel_release(self):
+        stdout_str = '4.4.0-78-generic'
+
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_str)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = version_metric.KernelVersionMetric(shell=fake_shell)
+
+        expected_result = {
+            version_metric.KernelVersionMetric.KERNEL_RELEASE:
+            '4.4.0-78-generic'
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/tests/zombie_metric_test.py b/tools/lab/tests/zombie_metric_test.py
new file mode 100755
index 0000000..0392f4c
--- /dev/null
+++ b/tools/lab/tests/zombie_metric_test.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+import unittest
+
+from metrics import zombie_metric
+from tests import fake
+
+
+class ZombieMetricTest(unittest.TestCase):
+    """Class for testing ZombieMetric."""
+
+    def test_gather_metric_oob(self):
+        stdout_string = '30888 Z+ adb -s'
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = zombie_metric.ZombieMetric(shell=fake_shell)
+
+        expected_result = {
+            zombie_metric.ZombieMetric.ADB_ZOMBIES: [('30888', None)],
+            zombie_metric.ZombieMetric.NUM_ADB_ZOMBIES: 1,
+            zombie_metric.ZombieMetric.FASTBOOT_ZOMBIES: [],
+            zombie_metric.ZombieMetric.NUM_FASTBOOT_ZOMBIES: 0,
+            zombie_metric.ZombieMetric.OTHER_ZOMBIES: [],
+            zombie_metric.ZombieMetric.NUM_OTHER_ZOMBIES: 0
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    def test_gather_metric_no_serial(self):
+        stdout_string = '30888 Z+ adb <defunct>'
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = zombie_metric.ZombieMetric(shell=fake_shell)
+
+        expected_result = {
+            zombie_metric.ZombieMetric.ADB_ZOMBIES: [('30888', None)],
+            zombie_metric.ZombieMetric.NUM_ADB_ZOMBIES: 1,
+            zombie_metric.ZombieMetric.FASTBOOT_ZOMBIES: [],
+            zombie_metric.ZombieMetric.NUM_FASTBOOT_ZOMBIES: 0,
+            zombie_metric.ZombieMetric.OTHER_ZOMBIES: [],
+            zombie_metric.ZombieMetric.NUM_OTHER_ZOMBIES: 0
+        }
+        self.assertEqual(expected_result, metric_obj.gather_metric())
+
+    def test_gather_metric_with_serial(self):
+        stdout_string = ('12345 Z+ fastboot -s M4RKY_M4RK\n'
+                         '99999 Z+ adb -s OR3G4N0\n')
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = zombie_metric.ZombieMetric(shell=fake_shell)
+
+        expected_result = {
+            zombie_metric.ZombieMetric.ADB_ZOMBIES: [('99999', 'OR3G4N0')],
+            zombie_metric.ZombieMetric.NUM_ADB_ZOMBIES:
+            1,
+            zombie_metric.ZombieMetric.FASTBOOT_ZOMBIES: [('12345',
+                                                           'M4RKY_M4RK')],
+            zombie_metric.ZombieMetric.NUM_FASTBOOT_ZOMBIES:
+            1,
+            zombie_metric.ZombieMetric.OTHER_ZOMBIES: [],
+            zombie_metric.ZombieMetric.NUM_OTHER_ZOMBIES:
+            0
+        }
+        self.assertEquals(metric_obj.gather_metric(), expected_result)
+
+    def test_gather_metric_adb_fastboot_no_s(self):
+        stdout_string = ('12345 Z+ fastboot\n' '99999 Z+ adb\n')
+        FAKE_RESULT = fake.FakeResult(stdout=stdout_string)
+        fake_shell = fake.MockShellCommand(fake_result=FAKE_RESULT)
+        metric_obj = zombie_metric.ZombieMetric(shell=fake_shell)
+
+        expected_result = {
+            zombie_metric.ZombieMetric.ADB_ZOMBIES: [('99999', None)],
+            zombie_metric.ZombieMetric.NUM_ADB_ZOMBIES: 1,
+            zombie_metric.ZombieMetric.FASTBOOT_ZOMBIES: [('12345', None)],
+            zombie_metric.ZombieMetric.NUM_FASTBOOT_ZOMBIES: 1,
+            zombie_metric.ZombieMetric.OTHER_ZOMBIES: [],
+            zombie_metric.ZombieMetric.NUM_OTHER_ZOMBIES: 0
+        }
+        self.assertEquals(metric_obj.gather_metric(), expected_result)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tools/lab/utils/shell.py b/tools/lab/utils/shell.py
index 5655dd4..d1e8c48 100644
--- a/tools/lab/utils/shell.py
+++ b/tools/lab/utils/shell.py
@@ -40,7 +40,7 @@
         self._runner = runner
         self._working_dir = working_dir
 
-    def run(self, command, timeout=3600):
+    def run(self, command, timeout=3600, ignore_status=False):
         """Runs a generic command through the runner.
 
         Takes the command and prepares it to be run in the target shell using
@@ -49,7 +49,7 @@
         Args:
             command: The command to run.
             timeout: How long to wait for the command (in seconds).
-
+            ignore_status: Whether or not to throw exception based upon status.
         Returns:
             A CmdResult object containing the results of the shell command.
 
@@ -61,7 +61,8 @@
         else:
             command_str = command
 
-        return self._runner.run(command_str, timeout=timeout)
+        return self._runner.run(
+            command_str, timeout=timeout, ignore_status=ignore_status)
 
     def is_alive(self, identifier):
         """Checks to see if a program is alive.
diff --git a/tools/lab/utils/time_limit.py b/tools/lab/utils/time_limit.py
new file mode 100644
index 0000000..dc453a6
--- /dev/null
+++ b/tools/lab/utils/time_limit.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+#
+#   Copyright 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.
+
+from utils import job
+import signal
+
+
+class TimeLimit:
+    """A class that limits the execution time of functions.
+
+    Attributes:
+        seconds: Number of seconds to execute task.
+        error_message: What error message provides upon raising exception.
+
+    Raises:
+        job.TimeoutError: When raised, gets handled in class, and will not
+        affect execution.
+    """
+    DEF_ERR_MSG = 'Time limit has been reached.'
+
+    def __init__(self, seconds=5, error_message=DEF_ERR_MSG):
+        self.seconds = seconds
+        self.error_message = error_message
+        self.timeout = False
+
+    def handle_timeout(self, signum, frame):
+        self.timeout = True
+        raise TimeLimitError
+
+    def __enter__(self):
+        signal.signal(signal.SIGALRM, self.handle_timeout)
+        signal.alarm(self.seconds)
+
+    def __exit__(self, type, value, traceback):
+        return self.timeout
+
+
+class TimeLimitError(Exception):
+    pass
diff --git a/wts-acts/README b/wts-acts/README
new file mode 100644
index 0000000..77924b2
--- /dev/null
+++ b/wts-acts/README
@@ -0,0 +1,4 @@
+Note that this directory contains additional files to be packaged specifically
+with the wts-acts distribution, for use when running the Wear Test Suite.
+
+See ../Android.mk for details.
diff --git a/wts-acts/__init__.py b/wts-acts/__init__.py
new file mode 100644
index 0000000..345b574
--- /dev/null
+++ b/wts-acts/__init__.py
@@ -0,0 +1,13 @@
+#   Copyright 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.
diff --git a/wts-acts/__main__.py b/wts-acts/__main__.py
new file mode 100644
index 0000000..d805838
--- /dev/null
+++ b/wts-acts/__main__.py
@@ -0,0 +1,7 @@
+#!/usr/bin/python
+__requires__ = 'acts==0.9'
+import sys
+
+from acts.bin.act import main
+main(sys.argv[1:])
+