Merge "update_device engine is now called by python2.7" am: ca03f968df am: 1b048033c8
am: b26eec0695

Change-Id: I7a8cf34372dc22ab6840722cf12fb218c64b6596
diff --git a/Android.mk b/Android.mk
index 6e7225f..4040a1c 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/framework))
@@ -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/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/controllers/adb.py b/acts/framework/acts/controllers/adb.py
index 0a652a3..0c753cb 100644
--- a/acts/framework/acts/controllers/adb.py
+++ b/acts/framework/acts/controllers/adb.py
@@ -126,10 +126,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
 
diff --git a/acts/framework/acts/controllers/android_device.py b/acts/framework/acts/controllers/android_device.py
index 40dadbc..8e5ff43 100755
--- a/acts/framework/acts/controllers/android_device.py
+++ b/acts/framework/acts/controllers/android_device.py
@@ -893,6 +893,9 @@
         """Get files names with provided directory."""
         file_names = []
         out = self.adb.shell("ls %s" % directory, ignore_status=True)
+        if "Permission denied" in out:
+            self.adb.root_adb()
+            out = self.adb.shell("ls %s" % directory, ignore_status=True)
         if out and "No such" not in out:
             return out.split('\n')
         else:
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/test_runner.py b/acts/framework/acts/test_runner.py
index 8324650..f27f1f3 100644
--- a/acts/framework/acts/test_runner.py
+++ b/acts/framework/acts/test_runner.py
@@ -533,6 +533,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
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/bt_test_utils.py b/acts/framework/acts/test_utils/bt/bt_test_utils.py
index 73d4dba..8b79621 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
@@ -816,7 +817,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(
@@ -908,7 +909,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(
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 f76a880..c778466 100644
--- a/acts/framework/acts/test_utils/tel/tel_test_utils.py
+++ b/acts/framework/acts/test_utils/tel/tel_test_utils.py
@@ -3886,12 +3886,10 @@
     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:
         if ad.droid.telecomIsInCall():
             ad.droid.telecomEndCall()
diff --git a/acts/framework/acts/test_utils/wifi/aware/AwareBaseTest.py b/acts/framework/acts/test_utils/wifi/aware/AwareBaseTest.py
index ec60bf1..0b9192d 100644
--- a/acts/framework/acts/test_utils/wifi/aware/AwareBaseTest.py
+++ b/acts/framework/acts/test_utils/wifi/aware/AwareBaseTest.py
@@ -34,24 +34,30 @@
   device_startup_offset = 2
 
   def setup_test(self):
-    required_params = ("default_power_mode", )
+    required_params = ("aware_default_power_mode", )
     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)
@@ -78,13 +84,13 @@
   def set_power_mode_parameters(self, ad):
     """Set the power configuration DW parameters for the device based on any
     configuration overrides (if provided)"""
-    if self.default_power_mode == "INTERACTIVE":
+    if self.aware_default_power_mode == "INTERACTIVE":
       autils.config_dw_high_power(ad)
-    elif self.default_power_mode == "NON_INTERACTIVE":
+    elif self.aware_default_power_mode == "NON_INTERACTIVE":
       autils.config_dw_low_power(ad)
     else:
       asserts.assert_false(
-          "The 'default_power_mode' configuration must be INTERACTIVE or "
+          "The 'aware_default_power_mode' configuration must be INTERACTIVE or "
           "NON_INTERACTIVE"
       )
 
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/tests/google/ble/scan/BleBackgroundScanTest.py b/acts/tests/google/ble/scan/BleBackgroundScanTest.py
index 115b960..12009b5 100644
--- a/acts/tests/google/ble/scan/BleBackgroundScanTest.py
+++ b/acts/tests/google/ble/scan/BleBackgroundScanTest.py
@@ -226,8 +226,7 @@
         """
         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.adb.shell("settings put global ble_scan_always_enabled 1")
 
         self.scn_ad.droid.bluetoothToggleState(False)
         try:
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/net/CoreNetworkingTest.py b/acts/tests/google/net/CoreNetworkingTest.py
index d87be69..d0d393f 100644
--- a/acts/tests/google/net/CoreNetworkingTest.py
+++ b/acts/tests/google/net/CoreNetworkingTest.py
@@ -13,21 +13,15 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-import logging
-import time
-import socket
-
 from acts import asserts
 from acts import base_test
-from acts import test_runner
-from acts import utils
 from acts.controllers import adb
-from acts.test_utils.tel import tel_data_utils
-from acts.test_utils.tel import tel_test_utils
-from acts.test_utils.tel import tel_defines
-from acts.test_utils.wifi import wifi_test_utils
+from acts.test_decorators import test_tracker_info
+from acts.test_utils.tel.tel_data_utils import wait_for_cell_data_connection
+from acts.test_utils.tel.tel_test_utils import verify_http_connection
+from acts.test_utils.wifi import wifi_test_utils as wutils
 
-dum_class = "com.android.uid.DummyActivity"
+dum_class = "com.android.tests.connectivity.uid.DummyActivity"
 
 
 class CoreNetworkingTest(base_test.BaseTestClass):
@@ -36,71 +30,20 @@
     def setup_class(self):
         """ Setup devices for tests and unpack params """
         self.dut = self.android_devices[0]
-        wifi_test_utils.wifi_toggle_state(self.dut, False)
+        wutils.wifi_toggle_state(self.dut, False)
         self.dut.droid.telephonyToggleDataConnection(True)
-        tel_data_utils.wait_for_cell_data_connection(self.log, self.dut, True)
+        wait_for_cell_data_connection(self.log, self.dut, True)
         asserts.assert_true(
-            tel_test_utils.verify_http_connection(self.log, self.dut),
+            verify_http_connection(self.log, self.dut),
             "HTTP verification failed on cell data connection")
 
     def teardown_class(self):
         """ Reset devices """
-        wifi_test_utils.wifi_toggle_state(self.dut, True)
+        wutils.wifi_toggle_state(self.dut, True)
 
     """ Test Cases """
 
-    def test_uid_derace_doze_mode(self):
-        """ Verify UID de-race doze mode
-
-        Steps:
-            1. Connect to DUT to data network and verify internet
-            2. Enable doze mode
-            3. Launch app and verify internet connectiviy
-            4. Disable doze mode
-        """
-        # Enable doze mode
-        self.log.info("Enable Doze mode")
-        asserts.assert_true(utils.enable_doze(self.dut),
-                            "Could not enable doze mode")
-
-        # Launch app, check internet connectivity and close app
-        res = self.dut.droid.launchForResult(dum_class)
-        self.log.info("Internet connectivity status after app launch: %s "
-                      % res['extras']['result'])
-
-        # Disable doze mode
-        self.log.info("Disable Doze mode")
-        asserts.assert_true(utils.disable_doze(self.dut),
-                            "Could not disable doze mode")
-
-        return res['extras']['result']
-
-    def test_uid_derace_doze_light_mode(self):
-        """ Verify UID de-race doze light mode
-
-        Steps:
-            1. Connect DUT to data network and verify internet
-            2. Enable doze light mode
-            3. Launch app and verify internet connectivity
-            4. Disable doze light mode
-        """
-        # Enable doze light mode
-        self.log.info("Enable doze light mode")
-        asserts.assert_true(utils.enable_doze_light(self.dut),
-                            "Could not enable doze light mode")
-
-        # Launch app, check internet connectivity and close app
-        res = self.dut.droid.launchForResult(dum_class)
-        self.log.info("Internet connectivity status after app launch: %s "
-                      % res['extras']['result'])
-
-        # Disable doze light mode
-        self.log.info("Disable doze light mode")
-        asserts.assert_true(utils.disable_doze_light(self.dut),
-                            "Could not disable doze light mode")
-
-        return res['extras']['result']
-
+    @test_tracker_info(uuid="0c89d632-aafe-4bbd-a812-7b0eca6aafc7")
     def test_uid_derace_data_saver_mode(self):
         """ Verify UID de-race data saver mode
 
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/wifi/aware/README.md b/acts/tests/google/wifi/aware/README.md
index 3762049..11a8296 100644
--- a/acts/tests/google/wifi/aware/README.md
+++ b/acts/tests/google/wifi/aware/README.md
@@ -42,7 +42,7 @@
 single device while others use 2 devices. In addition, the configurations
 define the following key to configure the test:
 
-* **default_power_mode**: The power mode in which to run all tests. Options
+* **aware_default_power_mode**: The power mode in which to run all tests. Options
 are `INTERACTIVE` and `NON_INTERACTIVE`.
 
 The following configurations are provided:
diff --git a/acts/tests/google/wifi/aware/config/wifi_aware.json b/acts/tests/google/wifi/aware/config/wifi_aware.json
index 7f384ef..97323fc 100644
--- a/acts/tests/google/wifi/aware/config/wifi_aware.json
+++ b/acts/tests/google/wifi/aware/config/wifi_aware.json
@@ -11,5 +11,5 @@
     "logpath": "~/logs",
     "testpaths": ["./tools/test/connectivity/acts/tests/google/wifi"],
     "adb_logcat_param": "-b all",
-    "default_power_mode": "INTERACTIVE"
+    "aware_default_power_mode": "INTERACTIVE"
 }
diff --git a/acts/tests/google/wifi/aware/config/wifi_aware_non_interactive.json b/acts/tests/google/wifi/aware/config/wifi_aware_non_interactive.json
index 16e26b9..f2e6e79 100644
--- a/acts/tests/google/wifi/aware/config/wifi_aware_non_interactive.json
+++ b/acts/tests/google/wifi/aware/config/wifi_aware_non_interactive.json
@@ -11,5 +11,5 @@
     "logpath": "~/logs",
     "testpaths": ["./tools/test/connectivity/acts/tests/google/wifi"],
     "adb_logcat_param": "-b all",
-    "default_power_mode": "NON_INTERACTIVE"
+    "aware_default_power_mode": "NON_INTERACTIVE"
 }
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/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:])
+