Merge "Add haydennix@ to OWNERS in controllers"
diff --git a/acts/.gitignore b/acts/.gitignore
index 5cd10e7..3f46a3b 100644
--- a/acts/.gitignore
+++ b/acts/.gitignore
@@ -68,4 +68,6 @@
 
 # PyCharm
 .idea/
+
+# IntelliJ
 *.iml
diff --git a/acts/framework/acts/controllers/android_lib/tel/__init__.py b/acts/framework/acts/controllers/android_lib/tel/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/controllers/android_lib/tel/__init__.py
diff --git a/acts/framework/acts/controllers/android_lib/tel/tel_utils.py b/acts/framework/acts/controllers/android_lib/tel/tel_utils.py
new file mode 100644
index 0000000..53a59ea
--- /dev/null
+++ b/acts/framework/acts/controllers/android_lib/tel/tel_utils.py
@@ -0,0 +1,667 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2020 - 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.
+
+"""Generic telephony utility functions. Cloned from test_utils.tel."""
+
+import re
+import struct
+import time
+from queue import Empty
+
+from acts.logger import epoch_to_log_line_timestamp
+
+
+INCALL_UI_DISPLAY_FOREGROUND = "foreground"
+INCALL_UI_DISPLAY_BACKGROUND = "background"
+INCALL_UI_DISPLAY_DEFAULT = "default"
+
+# Max time to wait after caller make a call and before
+# callee start ringing
+MAX_WAIT_TIME_ACCEPT_CALL_TO_OFFHOOK_EVENT = 30
+
+# Max time to wait after toggle airplane mode and before
+# get expected event
+MAX_WAIT_TIME_AIRPLANEMODE_EVENT = 90
+
+# Wait time between state check retry
+WAIT_TIME_BETWEEN_STATE_CHECK = 5
+
+# Constant for Data Roaming State
+DATA_ROAMING_ENABLE = 1
+DATA_ROAMING_DISABLE = 0
+
+# Constant for Telephony Manager Call State
+TELEPHONY_STATE_RINGING = "RINGING"
+TELEPHONY_STATE_IDLE = "IDLE"
+TELEPHONY_STATE_OFFHOOK = "OFFHOOK"
+TELEPHONY_STATE_UNKNOWN = "UNKNOWN"
+
+# Constant for Service State
+SERVICE_STATE_EMERGENCY_ONLY = "EMERGENCY_ONLY"
+SERVICE_STATE_IN_SERVICE = "IN_SERVICE"
+SERVICE_STATE_OUT_OF_SERVICE = "OUT_OF_SERVICE"
+SERVICE_STATE_POWER_OFF = "POWER_OFF"
+SERVICE_STATE_UNKNOWN = "UNKNOWN"
+
+# Constant for Network Mode
+NETWORK_MODE_GSM_ONLY = "NETWORK_MODE_GSM_ONLY"
+NETWORK_MODE_WCDMA_ONLY = "NETWORK_MODE_WCDMA_ONLY"
+NETWORK_MODE_LTE_ONLY = "NETWORK_MODE_LTE_ONLY"
+
+# Constant for Events
+EVENT_CALL_STATE_CHANGED = "CallStateChanged"
+EVENT_SERVICE_STATE_CHANGED = "ServiceStateChanged"
+
+
+class CallStateContainer:
+    INCOMING_NUMBER = "incomingNumber"
+    SUBSCRIPTION_ID = "subscriptionId"
+    CALL_STATE = "callState"
+
+
+class ServiceStateContainer:
+    VOICE_REG_STATE = "voiceRegState"
+    VOICE_NETWORK_TYPE = "voiceNetworkType"
+    DATA_REG_STATE = "dataRegState"
+    DATA_NETWORK_TYPE = "dataNetworkType"
+    OPERATOR_NAME = "operatorName"
+    OPERATOR_ID = "operatorId"
+    IS_MANUAL_NW_SELECTION = "isManualNwSelection"
+    ROAMING = "roaming"
+    IS_EMERGENCY_ONLY = "isEmergencyOnly"
+    NETWORK_ID = "networkId"
+    SYSTEM_ID = "systemId"
+    SUBSCRIPTION_ID = "subscriptionId"
+    SERVICE_STATE = "serviceState"
+
+
+def dumpsys_last_call_info(ad):
+    """ Get call information by dumpsys telecom. """
+    num = dumpsys_last_call_number(ad)
+    output = ad.adb.shell("dumpsys telecom")
+    result = re.search(r"Call TC@%s: {(.*?)}" % num, output, re.DOTALL)
+    call_info = {"TC": num}
+    if result:
+        result = result.group(1)
+        for attr in ("startTime", "endTime", "direction", "isInterrupted",
+                     "callTechnologies", "callTerminationsReason",
+                     "isVideoCall", "callProperties"):
+            match = re.search(r"%s: (.*)" % attr, result)
+            if match:
+                if attr in ("startTime", "endTime"):
+                    call_info[attr] = epoch_to_log_line_timestamp(
+                        int(match.group(1)))
+                else:
+                    call_info[attr] = match.group(1)
+    ad.log.debug("call_info = %s", call_info)
+    return call_info
+
+
+def dumpsys_last_call_number(ad):
+    output = ad.adb.shell("dumpsys telecom")
+    call_nums = re.findall("Call TC@(\d+):", output)
+    if not call_nums:
+        return 0
+    else:
+        return int(call_nums[-1])
+
+
+def get_device_epoch_time(ad):
+    return int(1000 * float(ad.adb.shell("date +%s.%N")))
+
+
+def get_outgoing_voice_sub_id(ad):
+    """ Get outgoing voice subscription id
+    """
+    if hasattr(ad, "outgoing_voice_sub_id"):
+        return ad.outgoing_voice_sub_id
+    else:
+        return ad.droid.subscriptionGetDefaultVoiceSubId()
+
+
+def get_rx_tx_power_levels(log, ad):
+    """ Obtains Rx and Tx power levels from the MDS application.
+
+    The method requires the MDS app to be installed in the DUT.
+
+    Args:
+        log: logger object
+        ad: an android device
+
+    Return:
+        A tuple where the first element is an array array with the RSRP value
+        in Rx chain, and the second element is the transmitted power in dBm.
+        Values for invalid Rx / Tx chains are set to None.
+    """
+    cmd = ('am instrument -w -e request "80 00 e8 03 00 08 00 00 00" -e '
+           'response wait "com.google.mdstest/com.google.mdstest.instrument.'
+           'ModemCommandInstrumentation"')
+    output = ad.adb.shell(cmd)
+
+    if 'result=SUCCESS' not in output:
+        raise RuntimeError('Could not obtain Tx/Rx power levels from MDS. Is '
+                           'the MDS app installed?')
+
+    response = re.search(r"(?<=response=).+", output)
+
+    if not response:
+        raise RuntimeError('Invalid response from the MDS app:\n' + output)
+
+    # Obtain a list of bytes in hex format from the response string
+    response_hex = response.group(0).split(' ')
+
+    def get_bool(pos):
+        """ Obtain a boolean variable from the byte array. """
+        return response_hex[pos] == '01'
+
+    def get_int32(pos):
+        """ Obtain an int from the byte array. Bytes are printed in
+        little endian format."""
+        return struct.unpack(
+            '<i', bytearray.fromhex(''.join(response_hex[pos:pos + 4])))[0]
+
+    rx_power = []
+    RX_CHAINS = 4
+
+    for i in range(RX_CHAINS):
+        # Calculate starting position for the Rx chain data structure
+        start = 12 + i * 22
+
+        # The first byte in the data structure indicates if the rx chain is
+        # valid.
+        if get_bool(start):
+            rx_power.append(get_int32(start + 2) / 10)
+        else:
+            rx_power.append(None)
+
+    # Calculate the position for the tx chain data structure
+    tx_pos = 12 + RX_CHAINS * 22
+
+    tx_valid = get_bool(tx_pos)
+    if tx_valid:
+        tx_power = get_int32(tx_pos + 2) / -10
+    else:
+        tx_power = None
+
+    return rx_power, tx_power
+
+
+def get_telephony_signal_strength(ad):
+    #{'evdoEcio': -1, 'asuLevel': 28, 'lteSignalStrength': 14, 'gsmLevel': 0,
+    # 'cdmaAsuLevel': 99, 'evdoDbm': -120, 'gsmDbm': -1, 'cdmaEcio': -160,
+    # 'level': 2, 'lteLevel': 2, 'cdmaDbm': -120, 'dbm': -112, 'cdmaLevel': 0,
+    # 'lteAsuLevel': 28, 'gsmAsuLevel': 99, 'gsmBitErrorRate': 0,
+    # 'lteDbm': -112, 'gsmSignalStrength': 99}
+    try:
+        signal_strength = ad.droid.telephonyGetSignalStrength()
+        if not signal_strength:
+            signal_strength = {}
+    except Exception as e:
+        ad.log.error(e)
+        signal_strength = {}
+    return signal_strength
+
+
+def initiate_call(log,
+                  ad,
+                  callee_number,
+                  emergency=False,
+                  incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND,
+                  video=False):
+    """Make phone call from caller to callee.
+
+    Args:
+        log: log object.
+        ad: Caller android device object.
+        callee_number: Callee phone number.
+        emergency : specify the call is emergency.
+            Optional. Default value is False.
+        incall_ui_display: show the dialer UI foreground or background
+        video: whether to initiate as video call
+
+    Returns:
+        result: if phone call is placed successfully.
+    """
+    ad.ed.clear_events(EVENT_CALL_STATE_CHANGED)
+    sub_id = get_outgoing_voice_sub_id(ad)
+    begin_time = get_device_epoch_time(ad)
+    ad.droid.telephonyStartTrackingCallStateForSubscription(sub_id)
+    try:
+        # Make a Call
+        ad.log.info("Make a phone call to %s", callee_number)
+        if emergency:
+            ad.droid.telecomCallEmergencyNumber(callee_number)
+        else:
+            ad.droid.telecomCallNumber(callee_number, video)
+
+        # Verify OFFHOOK state
+        if not wait_for_call_offhook_for_subscription(
+                log, ad, sub_id, event_tracking_started=True):
+            ad.log.info("sub_id %s not in call offhook state", sub_id)
+            last_call_drop_reason(ad, begin_time=begin_time)
+            return False
+        else:
+            return True
+    finally:
+        if hasattr(ad, "sdm_log") and getattr(ad, "sdm_log"):
+            ad.adb.shell("i2cset -fy 3 64 6 1 b", ignore_status=True)
+            ad.adb.shell("i2cset -fy 3 65 6 1 b", ignore_status=True)
+        ad.droid.telephonyStopTrackingCallStateChangeForSubscription(sub_id)
+        if incall_ui_display == INCALL_UI_DISPLAY_FOREGROUND:
+            ad.droid.telecomShowInCallScreen()
+        elif incall_ui_display == INCALL_UI_DISPLAY_BACKGROUND:
+            ad.droid.showHomeScreen()
+
+
+def is_event_match(event, field, value):
+    """Return if <field> in "event" match <value> or not.
+
+    Args:
+        event: event to test. This event need to have <field>.
+        field: field to match.
+        value: value to match.
+
+    Returns:
+        True if <field> in "event" match <value>.
+        False otherwise.
+    """
+    return is_event_match_for_list(event, field, [value])
+
+
+def is_event_match_for_list(event, field, value_list):
+    """Return if <field> in "event" match any one of the value
+        in "value_list" or not.
+
+    Args:
+        event: event to test. This event need to have <field>.
+        field: field to match.
+        value_list: a list of value to match.
+
+    Returns:
+        True if <field> in "event" match one of the value in "value_list".
+        False otherwise.
+    """
+    try:
+        value_in_event = event['data'][field]
+    except KeyError:
+        return False
+    for value in value_list:
+        if value_in_event == value:
+            return True
+    return False
+
+
+def is_phone_in_call(log, ad):
+    """Return True if phone in call.
+
+    Args:
+        log: log object.
+        ad:  android device.
+    """
+    try:
+        return ad.droid.telecomIsInCall()
+    except:
+        return "mCallState=2" in ad.adb.shell(
+            "dumpsys telephony.registry | grep mCallState")
+
+
+def last_call_drop_reason(ad, begin_time=None):
+    reasons = ad.search_logcat(
+        "qcril_qmi_voice_map_qmi_to_ril_last_call_failure_cause", begin_time)
+    reason_string = ""
+    if reasons:
+        log_msg = "Logcat call drop reasons:"
+        for reason in reasons:
+            log_msg = "%s\n\t%s" % (log_msg, reason["log_message"])
+            if "ril reason str" in reason["log_message"]:
+                reason_string = reason["log_message"].split(":")[-1].strip()
+        ad.log.info(log_msg)
+    reasons = ad.search_logcat("ACTION_FORBIDDEN_NO_SERVICE_AUTHORIZATION",
+                               begin_time)
+    if reasons:
+        ad.log.warning("ACTION_FORBIDDEN_NO_SERVICE_AUTHORIZATION is seen")
+    ad.log.info("last call dumpsys: %s",
+                sorted(dumpsys_last_call_info(ad).items()))
+    return reason_string
+
+
+def toggle_airplane_mode(log, ad, new_state=None, strict_checking=True):
+    """ Toggle the state of airplane mode.
+
+    Args:
+        log: log handler.
+        ad: android_device object.
+        new_state: Airplane mode state to set to.
+            If None, opposite of the current state.
+        strict_checking: Whether to turn on strict checking that checks all features.
+
+    Returns:
+        result: True if operation succeed. False if error happens.
+    """
+    if ad.skip_sl4a:
+        return toggle_airplane_mode_by_adb(log, ad, new_state)
+    else:
+        return toggle_airplane_mode_msim(
+            log, ad, new_state, strict_checking=strict_checking)
+
+
+def toggle_airplane_mode_by_adb(log, ad, new_state=None):
+    """ Toggle the state of airplane mode.
+
+    Args:
+        log: log handler.
+        ad: android_device object.
+        new_state: Airplane mode state to set to.
+            If None, opposite of the current state.
+
+    Returns:
+        result: True if operation succeed. False if error happens.
+    """
+    cur_state = bool(int(ad.adb.shell("settings get global airplane_mode_on")))
+    if new_state == cur_state:
+        ad.log.info("Airplane mode already in %s", new_state)
+        return True
+    elif new_state is None:
+        new_state = not cur_state
+    ad.log.info("Change airplane mode from %s to %s", cur_state, new_state)
+    try:
+        ad.adb.shell("settings put global airplane_mode_on %s" % int(new_state))
+        ad.adb.shell("am broadcast -a android.intent.action.AIRPLANE_MODE")
+    except Exception as e:
+        ad.log.error(e)
+        return False
+    changed_state = bool(int(ad.adb.shell("settings get global airplane_mode_on")))
+    return changed_state == new_state
+
+
+def toggle_airplane_mode_msim(log, ad, new_state=None, strict_checking=True):
+    """ Toggle the state of airplane mode.
+
+    Args:
+        log: log handler.
+        ad: android_device object.
+        new_state: Airplane mode state to set to.
+            If None, opposite of the current state.
+        strict_checking: Whether to turn on strict checking that checks all features.
+
+    Returns:
+        result: True if operation succeed. False if error happens.
+    """
+
+    cur_state = ad.droid.connectivityCheckAirplaneMode()
+    if cur_state == new_state:
+        ad.log.info("Airplane mode already in %s", new_state)
+        return True
+    elif new_state is None:
+        new_state = not cur_state
+        ad.log.info("Toggle APM mode, from current tate %s to %s", cur_state,
+                    new_state)
+    sub_id_list = []
+    active_sub_info = ad.droid.subscriptionGetAllSubInfoList()
+    if active_sub_info:
+        for info in active_sub_info:
+            sub_id_list.append(info['subscriptionId'])
+
+    ad.ed.clear_all_events()
+    time.sleep(0.1)
+    service_state_list = []
+    if new_state:
+        service_state_list.append(SERVICE_STATE_POWER_OFF)
+        ad.log.info("Turn on airplane mode")
+
+    else:
+        # If either one of these 3 events show up, it should be OK.
+        # Normal SIM, phone in service
+        service_state_list.append(SERVICE_STATE_IN_SERVICE)
+        # NO SIM, or Dead SIM, or no Roaming coverage.
+        service_state_list.append(SERVICE_STATE_OUT_OF_SERVICE)
+        service_state_list.append(SERVICE_STATE_EMERGENCY_ONLY)
+        ad.log.info("Turn off airplane mode")
+
+    for sub_id in sub_id_list:
+        ad.droid.telephonyStartTrackingServiceStateChangeForSubscription(
+            sub_id)
+
+    timeout_time = time.time() + MAX_WAIT_TIME_AIRPLANEMODE_EVENT
+    ad.droid.connectivityToggleAirplaneMode(new_state)
+
+    try:
+        try:
+            event = ad.ed.wait_for_event(
+                EVENT_SERVICE_STATE_CHANGED,
+                is_event_match_for_list,
+                timeout=MAX_WAIT_TIME_AIRPLANEMODE_EVENT,
+                field=ServiceStateContainer.SERVICE_STATE,
+                value_list=service_state_list)
+            ad.log.info("Got event %s", event)
+        except Empty:
+            ad.log.warning("Did not get expected service state change to %s",
+                           service_state_list)
+        finally:
+            for sub_id in sub_id_list:
+                ad.droid.telephonyStopTrackingServiceStateChangeForSubscription(
+                    sub_id)
+    except Exception as e:
+        ad.log.error(e)
+
+    # APM on (new_state=True) will turn off bluetooth but may not turn it on
+    try:
+        if new_state and not _wait_for_bluetooth_in_state(
+                log, ad, False, timeout_time - time.time()):
+            ad.log.error(
+                "Failed waiting for bluetooth during airplane mode toggle")
+            if strict_checking: return False
+    except Exception as e:
+        ad.log.error("Failed to check bluetooth state due to %s", e)
+        if strict_checking:
+            raise
+
+    # APM on (new_state=True) will turn off wifi but may not turn it on
+    if new_state and not _wait_for_wifi_in_state(log, ad, False,
+                                                 timeout_time - time.time()):
+        ad.log.error("Failed waiting for wifi during airplane mode toggle on")
+        if strict_checking: return False
+
+    if ad.droid.connectivityCheckAirplaneMode() != new_state:
+        ad.log.error("Set airplane mode to %s failed", new_state)
+        return False
+    return True
+
+
+def toggle_cell_data_roaming(ad, state):
+    """Enable cell data roaming for default data subscription.
+
+    Wait for the data roaming status to be DATA_STATE_CONNECTED
+        or DATA_STATE_DISCONNECTED.
+
+    Args:
+        ad: Android Device Object.
+        state: True or False for enable or disable cell data roaming.
+
+    Returns:
+        True if success.
+        False if failed.
+    """
+    state_int = {True: DATA_ROAMING_ENABLE, False: DATA_ROAMING_DISABLE}[state]
+    action_str = {True: "Enable", False: "Disable"}[state]
+    if ad.droid.connectivityCheckDataRoamingMode() == state:
+        ad.log.info("Data roaming is already in state %s", state)
+        return True
+    if not ad.droid.connectivitySetDataRoaming(state_int):
+        ad.error.info("Fail to config data roaming into state %s", state)
+        return False
+    if ad.droid.connectivityCheckDataRoamingMode() == state:
+        ad.log.info("Data roaming is configured into state %s", state)
+        return True
+    else:
+        ad.log.error("Data roaming is not configured into state %s", state)
+        return False
+
+
+def wait_for_call_offhook_event(
+        log,
+        ad,
+        sub_id,
+        event_tracking_started=False,
+        timeout=MAX_WAIT_TIME_ACCEPT_CALL_TO_OFFHOOK_EVENT):
+    """Wait for an incoming call on specified subscription.
+
+    Args:
+        log: log object.
+        ad: android device object.
+        event_tracking_started: True if event tracking already state outside
+        timeout: time to wait for event
+
+    Returns:
+        True: if call offhook event is received.
+        False: if call offhook event is not received.
+    """
+    if not event_tracking_started:
+        ad.ed.clear_events(EVENT_CALL_STATE_CHANGED)
+        ad.droid.telephonyStartTrackingCallStateForSubscription(sub_id)
+    try:
+        ad.ed.wait_for_event(
+            EVENT_CALL_STATE_CHANGED,
+            is_event_match,
+            timeout=timeout,
+            field=CallStateContainer.CALL_STATE,
+            value=TELEPHONY_STATE_OFFHOOK)
+        ad.log.info("Got event %s", TELEPHONY_STATE_OFFHOOK)
+    except Empty:
+        ad.log.info("No event for call state change to OFFHOOK")
+        return False
+    finally:
+        if not event_tracking_started:
+            ad.droid.telephonyStopTrackingCallStateChangeForSubscription(
+                sub_id)
+    return True
+
+
+def wait_for_call_offhook_for_subscription(
+        log,
+        ad,
+        sub_id,
+        event_tracking_started=False,
+        timeout=MAX_WAIT_TIME_ACCEPT_CALL_TO_OFFHOOK_EVENT,
+        interval=WAIT_TIME_BETWEEN_STATE_CHECK):
+    """Wait for an incoming call on specified subscription.
+
+    Args:
+        log: log object.
+        ad: android device object.
+        sub_id: subscription ID
+        timeout: time to wait for ring
+        interval: checking interval
+
+    Returns:
+        True: if incoming call is received and answered successfully.
+        False: for errors
+    """
+    if not event_tracking_started:
+        ad.ed.clear_events(EVENT_CALL_STATE_CHANGED)
+        ad.droid.telephonyStartTrackingCallStateForSubscription(sub_id)
+    offhook_event_received = False
+    end_time = time.time() + timeout
+    try:
+        while time.time() < end_time:
+            if not offhook_event_received:
+                if wait_for_call_offhook_event(log, ad, sub_id, True,
+                                               interval):
+                    offhook_event_received = True
+            telephony_state = ad.droid.telephonyGetCallStateForSubscription(
+                sub_id)
+            telecom_state = ad.droid.telecomGetCallState()
+            if telephony_state == TELEPHONY_STATE_OFFHOOK and (
+                    telecom_state == TELEPHONY_STATE_OFFHOOK):
+                ad.log.info("telephony and telecom are in OFFHOOK state")
+                return True
+            else:
+                ad.log.info(
+                    "telephony in %s, telecom in %s, expecting OFFHOOK state",
+                    telephony_state, telecom_state)
+            if offhook_event_received:
+                time.sleep(interval)
+    finally:
+        if not event_tracking_started:
+            ad.droid.telephonyStopTrackingCallStateChangeForSubscription(
+                sub_id)
+
+
+def _wait_for_bluetooth_in_state(log, ad, state, max_wait):
+    # FIXME: These event names should be defined in a common location
+    _BLUETOOTH_STATE_ON_EVENT = 'BluetoothStateChangedOn'
+    _BLUETOOTH_STATE_OFF_EVENT = 'BluetoothStateChangedOff'
+    ad.ed.clear_events(_BLUETOOTH_STATE_ON_EVENT)
+    ad.ed.clear_events(_BLUETOOTH_STATE_OFF_EVENT)
+
+    ad.droid.bluetoothStartListeningForAdapterStateChange()
+    try:
+        bt_state = ad.droid.bluetoothCheckState()
+        if bt_state == state:
+            return True
+        if max_wait <= 0:
+            ad.log.error("Time out: bluetooth state still %s, expecting %s",
+                         bt_state, state)
+            return False
+
+        event = {
+            False: _BLUETOOTH_STATE_OFF_EVENT,
+            True: _BLUETOOTH_STATE_ON_EVENT
+        }[state]
+        event = ad.ed.pop_event(event, max_wait)
+        ad.log.info("Got event %s", event['name'])
+        return True
+    except Empty:
+        ad.log.error("Time out: bluetooth state still in %s, expecting %s",
+                     bt_state, state)
+        return False
+    finally:
+        ad.droid.bluetoothStopListeningForAdapterStateChange()
+
+
+def wait_for_droid_in_call(log, ad, max_time):
+    """Wait for android to be in call state.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        If phone become in call state within max_time, return True.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time, is_phone_in_call)
+
+
+def _wait_for_droid_in_state(log, ad, max_time, state_check_func, *args,
+                             **kwargs):
+    while max_time >= 0:
+        if state_check_func(log, ad, *args, **kwargs):
+            return True
+
+        time.sleep(WAIT_TIME_BETWEEN_STATE_CHECK)
+        max_time -= WAIT_TIME_BETWEEN_STATE_CHECK
+
+    return False
+
+
+# TODO: replace this with an event-based function
+def _wait_for_wifi_in_state(log, ad, state, max_wait):
+    return _wait_for_droid_in_state(log, ad, max_wait,
+        lambda log, ad, state: \
+                (True if ad.droid.wifiCheckState() == state else False),
+                state)
diff --git a/acts/framework/acts/controllers/buds_lib/test_actions/apollo_acts.py b/acts/framework/acts/controllers/buds_lib/test_actions/apollo_acts.py
index 4b2160b..67937bd 100644
--- a/acts/framework/acts/controllers/buds_lib/test_actions/apollo_acts.py
+++ b/acts/framework/acts/controllers/buds_lib/test_actions/apollo_acts.py
@@ -19,14 +19,14 @@
 
 import time
 
+from acts.controllers.android_lib.tel.tel_utils import initiate_call
+from acts.controllers.android_lib.tel.tel_utils import wait_for_droid_in_call
 from acts.controllers.buds_lib.apollo_lib import DeviceError
 from acts.controllers.buds_lib.test_actions.agsa_acts import AgsaOTAError
 from acts.controllers.buds_lib.test_actions.base_test_actions import BaseTestAction
 from acts.controllers.buds_lib.test_actions.base_test_actions import timed_action
 from acts.controllers.buds_lib.test_actions.bt_utils import BTUtils
 from acts.libs.utils.timer import TimeRecorder
-from acts.test_utils.tel.tel_test_utils import initiate_call
-from acts.test_utils.tel.tel_test_utils import wait_for_droid_in_call
 from acts.utils import wait_until
 
 PACKAGE_NAME_AGSA = 'com.google.android.googlequicksearchbox'
diff --git a/acts/framework/acts/controllers/cellular_lib/AndroidCellularDut.py b/acts/framework/acts/controllers/cellular_lib/AndroidCellularDut.py
index aaef6f5..94bc4df 100644
--- a/acts/framework/acts/controllers/cellular_lib/AndroidCellularDut.py
+++ b/acts/framework/acts/controllers/cellular_lib/AndroidCellularDut.py
@@ -14,9 +14,8 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+from acts.controllers.android_lib.tel import tel_utils
 from acts.controllers.cellular_lib import BaseCellularDut
-from acts.test_utils.tel import tel_test_utils as tel_utils
-from acts.test_utils.tel import tel_defines
 
 
 class AndroidCellularDut(BaseCellularDut.BaseCellularDut):
@@ -74,11 +73,11 @@
           type: an instance of class PreferredNetworkType
         """
         if type == BaseCellularDut.PreferredNetworkType.LTE_ONLY:
-            formatted_type = tel_defines.NETWORK_MODE_LTE_ONLY
+            formatted_type = tel_utils.NETWORK_MODE_LTE_ONLY
         elif type == BaseCellularDut.PreferredNetworkType.WCDMA_ONLY:
-            formatted_type = tel_defines.NETWORK_MODE_WCDMA_ONLY
+            formatted_type = tel_utils.NETWORK_MODE_WCDMA_ONLY
         elif type == BaseCellularDut.PreferredNetworkType.GSM_ONLY:
-            formatted_type = tel_defines.NETWORK_MODE_GSM_ONLY
+            formatted_type = tel_utils.NETWORK_MODE_GSM_ONLY
         else:
             raise ValueError('Invalid RAT type.')
 
diff --git a/acts/framework/acts/test_utils/abstract_devices/wmm_transceiver.py b/acts/framework/acts/test_utils/abstract_devices/wmm_transceiver.py
new file mode 100644
index 0000000..f1aad98
--- /dev/null
+++ b/acts/framework/acts/test_utils/abstract_devices/wmm_transceiver.py
@@ -0,0 +1,665 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2020 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import logging
+import multiprocessing
+import time
+
+from datetime import datetime
+from uuid import uuid4
+
+from acts import signals
+from acts import tracelogger
+from acts import utils
+from acts.controllers import iperf_client
+from acts.controllers import iperf_server
+
+AC_VO = 'AC_VO'
+AC_VI = 'AC_VI'
+AC_BE = 'AC_BE'
+AC_BK = 'AC_BK'
+
+# TODO(fxb/61421): Add tests to check all DSCP classes are mapped to the correct
+# AC (there are many that aren't included here). Requires implementation of
+# sniffer.
+DEFAULT_AC_TO_TOS_TAG_MAP = {
+    AC_VO: '0xC0',
+    AC_VI: '0x80',
+    AC_BE: '0x0',
+    AC_BK: '0x20'
+}
+UDP = 'udp'
+TCP = 'tcp'
+DEFAULT_IPERF_PORT = 5201
+DEFAULT_STREAM_TIME = 10
+DEFAULT_IP_ADDR_TIMEOUT = 15
+PROCESS_JOIN_TIMEOUT = 60
+AVAILABLE = True
+UNAVAILABLE = False
+
+
+class WmmTransceiverError(signals.ControllerError):
+    pass
+
+
+def create(config, identifier=None, wlan_devices=None, access_points=None):
+    """Creates a WmmTransceiver from a config.
+
+    Args:
+        config: dict, config parameters for the transceiver. Contains:
+            - iperf_config: dict, the config to use for creating IPerfClients
+                and IPerfServers (excluding port).
+            - port_range_start: int, the lower bound of the port range to use
+                for creating IPerfServers. Defaults to 5201.
+            - wlan_device: string, the identifier of the wlan_device used for
+                this WmmTransceiver (optional)
+
+        identifier: string, identifier for the WmmTransceiver. Must be provided
+            either as arg or in the config.
+        wlan_devices: list of WlanDevice objects from which to get the
+            wlan_device, if any, used as this transceiver
+        access_points: list of AccessPoint objects from which to get the
+            access_point, if any, used as this transceiver
+    """
+    try:
+        # If identifier is not provided as func arg, it must be provided via
+        # config file.
+        if not identifier:
+            identifier = config['identifier']
+        iperf_config = config['iperf_config']
+
+    except KeyError as err:
+        raise WmmTransceiverError(
+            'Parameter not provided as func arg, nor found in config: %s' %
+            err)
+
+    if wlan_devices is None:
+        wlan_devices = []
+
+    if access_points is None:
+        access_points = []
+
+    port_range_start = config.get('port_range_start', DEFAULT_IPERF_PORT)
+
+    wd = None
+    ap = None
+    if 'wlan_device' in config:
+        wd = _find_wlan_device(config['wlan_device'], wlan_devices)
+    elif 'access_point' in config:
+        ap = _find_access_point(config['access_point'], access_points)
+
+    return WmmTransceiver(iperf_config,
+                          identifier,
+                          wlan_device=wd,
+                          access_point=ap,
+                          port_range_start=port_range_start)
+
+
+def _find_wlan_device(wlan_device_identifier, wlan_devices):
+    """Returns WlanDevice based on string identifier (e.g. ip, serial, etc.)
+
+    Args:
+        wlan_device_identifier: string, identifier for the desired WlanDevice
+        wlan_devices: list, WlanDevices to search through
+
+    Returns:
+        WlanDevice, with identifier matching wlan_device_identifier
+
+    Raises:
+        WmmTransceiverError, if no WlanDevice matches identifier
+    """
+    for wd in wlan_devices:
+        if wlan_device_identifier == wd.identifier:
+            return wd
+    raise WmmTransceiverError('No WlanDevice with identifier: %s' %
+                              wlan_device_identifier)
+
+
+def _find_access_point(access_point_ip, access_points):
+    """Returns AccessPoint based on string ip address
+
+    Args:
+        access_point_ip: string, control plane ip addr of the desired AP,
+        access_points: list, AccessPoints to search through
+
+    Returns:
+        AccessPoint, with hostname matching access_point_ip
+
+    Raises:
+        WmmTransceiverError, if no AccessPoint matches ip"""
+    for ap in access_points:
+        if ap.ssh_settings.hostname == access_point_ip:
+            return ap
+    raise WmmTransceiverError('No AccessPoint with ip: %s' % access_point_ip)
+
+
+class WmmTransceiver(object):
+    """Object for handling WMM tagged streams between devices"""
+    def __init__(self,
+                 iperf_config,
+                 identifier,
+                 wlan_device=None,
+                 access_point=None,
+                 port_range_start=5201):
+
+        self.identifier = identifier
+        self.log = tracelogger.TraceLogger(
+            WmmTransceiverLoggerAdapter(logging.getLogger(),
+                                        {'identifier': self.identifier}))
+        # WlanDevice or AccessPoint, that is used as the transceiver. Only one
+        # will be set. This helps consolodate association, setup, teardown, etc.
+        self.wlan_device = wlan_device
+        self.access_point = access_point
+
+        # Parameters used to create IPerfClient and IPerfServer objects on
+        # device
+        self._iperf_config = iperf_config
+        self._test_interface = self._iperf_config.get('test_interface')
+        self._port_range_start = port_range_start
+        self._next_server_port = port_range_start
+
+        # Maps IPerfClients, used for streams from this device, to True if
+        # available, False if reserved
+        self._iperf_clients = {}
+
+        # Maps IPerfServers, used to receive streams from other devices, to True
+        # if available, False if reserved
+        self._iperf_servers = {}
+
+        # Maps ports of servers, which are provided to other transceivers, to
+        # the actual IPerfServer objects
+        self._iperf_server_ports = {}
+
+        # Maps stream UUIDs to IPerfClients reserved for that streams use
+        self._reserved_clients = {}
+
+        # Maps stream UUIDs to (WmmTransceiver, IPerfServer) tuples, where the
+        # server is reserved on the transceiver for that streams use
+        self._reserved_servers = {}
+
+        # Maps with shared memory functionality to be used across the parallel
+        # streams. active_streams holds UUIDs of streams that are currently
+        # running on this device (mapped to True, since there is no
+        # multiprocessing set). stream_results maps UUIDs of streams completed
+        # on this device to IPerfResult results for that stream.
+        self._manager = multiprocessing.Manager()
+        self._active_streams = self._manager.dict()
+        self._stream_results = self._manager.dict()
+
+        # Holds parameters for streams that are prepared to run asynchronously
+        # (i.e. resources have been allocated). Maps UUIDs of the future streams
+        # to a dict, containing the stream parameters.
+        self._pending_async_streams = {}
+
+        # Set of UUIDs of asynchronous streams that have at least started, but
+        # have not had their resources reclaimed yet
+        self._ran_async_streams = set()
+
+        # Set of stream parallel process, which can be joined if completed
+        # successfully, or  terminated and joined in the event of an error
+        self._running_processes = set()
+
+    def run_synchronous_traffic_stream(self, stream_parameters, subnet):
+        """Runs a traffic stream with IPerf3 between two WmmTransceivers and
+        saves the results.
+
+        Args:
+            stream_parameters: dict, containing parameters to used for the
+                stream. See _parse_stream_parameters for details.
+            subnet: string, the subnet of the network to use for the stream
+
+        Returns:
+            uuid: UUID object, identifier of the stream
+        """
+        (receiver, access_category, bandwidth,
+         stream_time) = self._parse_stream_parameters(stream_parameters)
+        uuid = uuid4()
+
+        (client, server_ip,
+         server_port) = self._get_stream_resources(uuid, receiver, subnet)
+
+        self._validate_server_address(server_ip, uuid)
+
+        self.log.info('Running synchronous stream to %s WmmTransceiver' %
+                      receiver.identifier)
+        self._run_traffic(uuid,
+                          client,
+                          server_ip,
+                          server_port,
+                          self._active_streams,
+                          self._stream_results,
+                          access_category=access_category,
+                          bandwidth=bandwidth,
+                          stream_time=stream_time)
+
+        self._return_stream_resources(uuid)
+        return uuid
+
+    def prepare_asynchronous_stream(self, stream_parameters, subnet):
+        """Reserves resources and saves configs for upcoming asynchronous
+        traffic streams, so they can be started more simultaneously.
+
+        Args:
+            stream_parameters: dict, containing parameters to used for the
+                stream. See _parse_stream_parameters for details.
+            subnet: string, the subnet of the network to use for the stream
+
+        Returns:
+            uuid: UUID object, identifier of the stream
+        """
+        (receiver, access_category, bandwidth,
+         time) = self._parse_stream_parameters(stream_parameters)
+        uuid = uuid4()
+
+        (client, server_ip,
+         server_port) = self._get_stream_resources(uuid, receiver, subnet)
+
+        self._validate_server_address(server_ip, uuid)
+
+        pending_stream_config = {
+            'client': client,
+            'server_ip': server_ip,
+            'server_port': server_port,
+            'access_category': access_category,
+            'bandwidth': bandwidth,
+            'time': time
+        }
+
+        self._pending_async_streams[uuid] = pending_stream_config
+        self.log.info('Stream to %s WmmTransceiver prepared.' %
+                      receiver.identifier)
+        return uuid
+
+    def start_asynchronous_streams(self, start_time=None):
+        """Starts pending asynchronous streams between two WmmTransceivers as
+        parallel processes.
+
+        Args:
+            start_time: float, time, seconds since epoch, at which to start the
+                stream (for better synchronicity). If None, start immediately.
+        """
+        for uuid in self._pending_async_streams:
+            pending_stream_config = self._pending_async_streams[uuid]
+            client = pending_stream_config['client']
+            server_ip = pending_stream_config['server_ip']
+            server_port = pending_stream_config['server_port']
+            access_category = pending_stream_config['access_category']
+            bandwidth = pending_stream_config['bandwidth']
+            time = pending_stream_config['time']
+
+            process = multiprocessing.Process(target=self._run_traffic,
+                                              args=[
+                                                  uuid, client, server_ip,
+                                                  server_port,
+                                                  self._active_streams,
+                                                  self._stream_results
+                                              ],
+                                              kwargs={
+                                                  'access_category':
+                                                  access_category,
+                                                  'bandwidth': bandwidth,
+                                                  'stream_time': time,
+                                                  'start_time': start_time
+                                              })
+
+            # This needs to be set here to ensure its marked active before
+            # it even starts.
+            self._active_streams[uuid] = True
+            process.start()
+            self._ran_async_streams.add(uuid)
+            self._running_processes.add(process)
+
+        self._pending_async_streams.clear()
+
+    def cleanup_asynchronous_streams(self, timeout=PROCESS_JOIN_TIMEOUT):
+        """Releases reservations on resources (IPerfClients and IPerfServers)
+        that were held for asynchronous streams, both pending and finished.
+        Attempts to join any running processes, logging an error if timeout is
+        exceeded.
+
+        Args:
+            timeout: time, in seconds, to wait for each running process, if any,
+                to join
+        """
+        self.log.info('Cleaning up any asynchronous streams.')
+
+        # Releases resources for any streams that were prepared, but no run
+        for uuid in self._pending_async_streams:
+            self.log.error(
+                'Pending asynchronous stream %s never ran. Cleaning.' % uuid)
+            self._return_stream_resources(uuid)
+        self._pending_async_streams.clear()
+
+        # Attempts to join any running streams, terminating them after timeout
+        # if necessary.
+        while self._running_processes:
+            process = self._running_processes.pop()
+            process.join(timeout)
+            if process.is_alive():
+                self.log.error(
+                    'Stream process failed to join in %s seconds. Terminating.'
+                    % timeout)
+                process.terminate()
+                process.join()
+        self._active_streams.clear()
+
+        # Release resources for any finished streams
+        while self._ran_async_streams:
+            uuid = self._ran_async_streams.pop()
+            self._return_stream_resources(uuid)
+
+    def get_results(self, uuid):
+        """Retrieves a streams IPerfResults from stream_results
+
+        Args:
+            uuid: UUID object, identifier of the stream
+        """
+        return self._stream_results.get(uuid, None)
+
+    def destroy_resources(self):
+        for server in self._iperf_servers:
+            server.stop()
+        self._iperf_servers.clear()
+        self._iperf_server_ports.clear()
+        self._iperf_clients.clear()
+        self._next_server_port = self._port_range_start
+        self._stream_results.clear()
+
+    @property
+    def has_active_streams(self):
+        return bool(self._active_streams)
+
+    # Helper Functions
+
+    def _run_traffic(self,
+                     uuid,
+                     client,
+                     server_ip,
+                     server_port,
+                     active_streams,
+                     stream_results,
+                     access_category=None,
+                     bandwidth=None,
+                     stream_time=DEFAULT_STREAM_TIME,
+                     start_time=None):
+        """Runs an iperf3 stream.
+
+        1. Adds stream UUID to active_streams
+        2. Runs stream
+        3. Saves results to stream_results
+        4. Removes stream UUID from active_streams
+
+        Args:
+            uuid: UUID object, identifier for stream
+            client: IPerfClient object on device
+            server_ip: string, ip address of IPerfServer for stream
+            server_port: int, port of the IPerfServer for stream
+            active_streams: multiprocessing.Manager.dict, which holds stream
+                UUIDs of active streams on the device
+            stream_results: multiprocessing.Manager.dict, which maps stream
+                UUIDs of streams to IPerfResult objects
+            access_category: string, WMM access category to use with iperf
+                (AC_BK, AC_BE, AC_VI, AC_VO). Unset if None.
+            bandwidth: int, bandwidth in mbps to use with iperf. Implies UDP.
+                Unlimited if None.
+            stream_time: int, time in seconds, to run iperf stream
+            start_time: float, time, seconds since epoch, at which to start the
+                stream (for better synchronicity). If None, start immediately.
+        """
+        active_streams[uuid] = True
+        # SSH sessions must be started within the process that is going to
+        # use it.
+        if type(client) == iperf_client.IPerfClientOverSsh:
+            with utils.SuppressLogOutput():
+                client.start_ssh()
+
+        ac_flag = ''
+        bandwidth_flag = ''
+        time_flag = '-t %s' % stream_time
+
+        if access_category:
+            ac_flag = ' -S %s' % DEFAULT_AC_TO_TOS_TAG_MAP[access_category]
+
+        if bandwidth:
+            bandwidth_flag = ' -u -b %sM' % bandwidth
+
+        iperf_flags = '-p %s -i 1 %s%s%s -J' % (server_port, time_flag,
+                                                ac_flag, bandwidth_flag)
+        if not start_time:
+            start_time = time.time()
+        time_str = datetime.fromtimestamp(start_time).strftime('%H:%M:%S.%f')
+        self.log.info(
+            'At %s, starting %s second stream to %s:%s with (AC: %s, Bandwidth: %s)'
+            % (time_str, stream_time, server_ip, server_port, access_category,
+               bandwidth if bandwidth else 'Unlimited'))
+
+        # If present, wait for stream start time
+        if start_time:
+            current_time = time.time()
+            while current_time < start_time:
+                current_time = time.time()
+        path = client.start(server_ip, iperf_flags, '%s' % uuid)
+        stream_results[uuid] = iperf_server.IPerfResult(
+            path, reporting_speed_units='mbps')
+
+        if type(client) == iperf_client.IPerfClientOverSsh:
+            client.close_ssh()
+        active_streams.pop(uuid)
+
+    def _get_stream_resources(self, uuid, receiver, subnet):
+        """Reserves an IPerfClient and IPerfServer for a stream.
+
+        Args:
+            uuid: UUID object, identifier of the stream
+            receiver: WmmTransceiver object, which will be the streams receiver
+            subnet: string, subnet of test network, to retrieve the appropriate
+                server address
+
+        Returns:
+            (IPerfClient, string, int) representing the client, server address,
+            and server port to use for the stream
+        """
+        client = self._get_client(uuid)
+        server_ip, server_port = self._get_server(receiver, uuid, subnet)
+        return (client, server_ip, server_port)
+
+    def _return_stream_resources(self, uuid):
+        """Releases reservations on a streams IPerfClient and IPerfServer, so
+        they can be used by a future stream.
+
+        Args:
+            uuid: UUID object, identifier of the stream
+        """
+        if uuid in self._active_streams:
+            raise EnvironmentError('Resource still being used by stream %s' %
+                                   uuid)
+        (receiver, server_port) = self._reserved_servers.pop(uuid)
+        receiver._release_server(server_port)
+        client = self._reserved_clients.pop(uuid)
+        self._iperf_clients[client] = AVAILABLE
+
+    def _get_client(self, uuid):
+        """Retrieves and reserves IPerfClient for use in a stream. If none are
+        available, a new one is created.
+
+        Args:
+            uuid: UUID object, identifier for stream, used to link client to
+                stream for teardown
+
+        Returns:
+            IPerfClient on device
+        """
+        reserved_client = None
+        for client in self._iperf_clients:
+            if self._iperf_clients[client] == AVAILABLE:
+                reserved_client = client
+                break
+        else:
+            reserved_client = iperf_client.create([self._iperf_config])[0]
+            # Due to the nature of multiprocessing, ssh connections must
+            # be started inside the parallel processes, so it must be closed
+            # here.
+            if type(reserved_client) == iperf_client.IPerfClientOverSsh:
+                reserved_client.close_ssh()
+
+        self._iperf_clients[reserved_client] = UNAVAILABLE
+        self._reserved_clients[uuid] = reserved_client
+        return reserved_client
+
+    def _get_server(self, receiver, uuid, subnet):
+        """Retrieves the address and port of a reserved IPerfServer object from
+        the receiver object for use in a stream.
+
+        Args:
+            receiver: WmmTransceiver, to get an IPerfServer from
+            uuid: UUID, identifier for stream, used to link server to stream
+                for teardown
+            subnet: string, subnet of test network, to retrieve the appropriate
+                server address
+
+        Returns:
+            (string, int) representing the IPerfServer address and port
+        """
+        (server_ip, server_port) = receiver._reserve_server(subnet)
+        self._reserved_servers[uuid] = (receiver, server_port)
+        return (server_ip, server_port)
+
+    def _reserve_server(self, subnet):
+        """Reserves an available IPerfServer for use in a stream from another
+        WmmTransceiver. If none are available, a new one is created.
+
+        Args:
+            subnet: string, subnet of test network, to retrieve the appropriate
+                server address
+
+        Returns:
+            (string, int) representing the IPerfServer address and port
+        """
+        reserved_server = None
+        for server in self._iperf_servers:
+            if self._iperf_servers[server] == AVAILABLE:
+                reserved_server = server
+                break
+        else:
+            iperf_server_config = self._iperf_config
+            iperf_server_config.update({'port': self._next_server_port})
+            self._next_server_port += 1
+            reserved_server = iperf_server.create([iperf_server_config])[0]
+            self._iperf_server_ports[reserved_server.port] = reserved_server
+
+        self._iperf_servers[reserved_server] = UNAVAILABLE
+        reserved_server.start()
+        end_time = time.time() + DEFAULT_IP_ADDR_TIMEOUT
+        while time.time() < end_time:
+            if self.wlan_device:
+                addresses = utils.get_interface_ip_addresses(
+                    self.wlan_device.device, self._test_interface)
+            else:
+                addresses = reserved_server.get_interface_ip_addresses(
+                    self._test_interface)
+            for addr in addresses['ipv4_private']:
+                if utils.ip_in_subnet(addr, subnet):
+                    return (addr, reserved_server.port)
+        raise AttributeError(
+            'Reserved server has no ipv4 address in the %s subnet' % subnet)
+
+    def _release_server(self, server_port):
+        """Releases reservation on IPerfServer, which was held for a stream
+        from another WmmTransceiver.
+
+        Args:
+            server_port: int, the port of the IPerfServer being returned (since)
+                it is the identifying characteristic
+        """
+        server = self._iperf_server_ports[server_port]
+        server.stop()
+        self._iperf_servers[server] = AVAILABLE
+
+    def _validate_server_address(self, server_ip, uuid, timeout=60):
+        """ Verifies server address can be pinged before attempting to run
+        traffic, since iperf is unforgiving when the server is unreachable.
+
+        Args:
+            server_ip: string, ip address of the iperf server
+            uuid: string, uuid of the stream to use this server
+            timeout: int, time in seconds to wait for server to respond to pings
+
+        Raises:
+            WmmTransceiverError, if, after timeout, server ip is unreachable.
+        """
+        self.log.info('Verifying server address (%s) is reachable.' %
+                      server_ip)
+        end_time = time.time() + timeout
+        while time.time() < end_time:
+            if self.can_ping(server_ip):
+                break
+            else:
+                self.log.debug(
+                    'Could not ping server address (%s). Retrying in 1 second.'
+                    % (server_ip))
+                time.sleep(1)
+        else:
+            self._return_stream_resources(uuid)
+            raise WmmTransceiverError('IPerfServer address (%s) unreachable.' %
+                                      server_ip)
+
+    def can_ping(self, dest_ip):
+        """ Utilizes can_ping function in wlan_device or access_point device to
+        ping dest_ip
+
+        Args:
+            dest_ip: string, ip address to ping
+
+        Returns:
+            True, if dest address is reachable
+            False, otherwise
+        """
+        if self.wlan_device:
+            return self.wlan_device.can_ping(dest_ip)
+        else:
+            return self.access_point.can_ping(dest_ip)
+
+    def _parse_stream_parameters(self, stream_parameters):
+        """Parses stream_parameters from dictionary.
+
+        Args:
+            stream_parameters: dict of stream parameters
+                'receiver': WmmTransceiver, the receiver for the stream
+                'access_category': String, the access category to use for the
+                    stream. Unset if None.
+                'bandwidth': int, bandwidth in mbps for the stream. If set,
+                    implies UDP. If unset, implies TCP and unlimited bandwidth.
+                'time': int, time in seconds to run stream.
+
+        Returns:
+            (receiver, access_category, bandwidth, time) as
+            (WmmTransceiver, String, int, int)
+        """
+        receiver = stream_parameters['receiver']
+        access_category = stream_parameters.get('access_category', None)
+        bandwidth = stream_parameters.get('bandwidth', None)
+        time = stream_parameters.get('time', DEFAULT_STREAM_TIME)
+        return (receiver, access_category, bandwidth, time)
+
+
+class WmmTransceiverLoggerAdapter(logging.LoggerAdapter):
+    def process(self, msg, kwargs):
+        if self.extra['identifier']:
+            log_identifier = ' | %s' % self.extra['identifier']
+        else:
+            log_identifier = ''
+        msg = "[WmmTransceiver%s] %s" % (log_identifier, msg)
+        return (msg, kwargs)
diff --git a/acts_tests/.gitignore b/acts_tests/.gitignore
index 5cd10e7..3f46a3b 100644
--- a/acts_tests/.gitignore
+++ b/acts_tests/.gitignore
@@ -68,4 +68,6 @@
 
 # PyCharm
 .idea/
+
+# IntelliJ
 *.iml