Merge changes I5dc62758,I74cf261e

* changes:
  BassClient: Fix using Metadata cache
  BassClient: Fix not sending connection change intent
diff --git a/README.md b/README.md
index 74b9376..60daa85 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,8 @@
   libprotobuf-dev ninja-build generate-ninja protobuf-compiler \
   libre2-9 debmake \
   llvm libc++abi-dev \
-  libre2-dev libdouble-conversion-dev
+  libre2-dev libdouble-conversion-dev \
+  libgtest-dev libgmock-dev libabsl-dev
 ```
 
 You will also need a recent-ish version of Rust and Cargo. Please follow the
@@ -89,26 +90,6 @@
 sudo dpkg -i outdir/libchrome/*.deb
 ```
 
-The googletest packages provided by Debian/Ubuntu (libgmock-dev and
-libgtest-dev) do not provide pkg-config files, so you can build your own
-googletest using the steps below:
-
-```sh
-git clone https://github.com/google/googletest.git -b release-1.10.0
-cd googletest        # Main directory of the cloned repository.
-mkdir build          # Create a directory to hold the build output.
-cd build
-cmake ..             # Generate native build scripts for GoogleTest.
-sudo make install -DCMAKE_INSTALL_PREFIX=/usr
-
-# Optional steps if pkgconfig isn't installed to desired location
-# Modify the source (/usr/lib/x86_64-linux-gnu) and target (/usr/lib) based on
-# your local installation.
-for f in $(ls /usr/lib/x86_64-linux-gnu/pkgconfig/{gtest,gmock}*); do \
-  ln -sf $f /usr/lib/pkgconfig/$(basename $f);
-done
-```
-
 ### Rust dependencies
 
 **Note**: Handled by `--run-bootstrap` option.
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
index 704f55a..2a281ab 100644
--- a/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
+++ b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
@@ -181,6 +181,9 @@
             mTbsGatt.clearSilentModeFlag();
         }
 
+        // Android supports inband ringtone
+        mTbsGatt.setInbandRingtoneFlag();
+
         mReceiver = new Receiver();
         mTbsGatt.getContext().registerReceiver(mReceiver,
                 new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION));
diff --git a/system/audio_hal_interface/aidl/le_audio_software_aidl.cc b/system/audio_hal_interface/aidl/le_audio_software_aidl.cc
index ee8969b..40f991e 100644
--- a/system/audio_hal_interface/aidl/le_audio_software_aidl.cc
+++ b/system/audio_hal_interface/aidl/le_audio_software_aidl.cc
@@ -360,6 +360,7 @@
     {30, ::le_audio::codec_spec_conf::kLeAudioCodecLC3FrameLen30},
     {40, ::le_audio::codec_spec_conf::kLeAudioCodecLC3FrameLen40},
     {60, ::le_audio::codec_spec_conf::kLeAudioCodecLC3FrameLen60},
+    {80, ::le_audio::codec_spec_conf::kLeAudioCodecLC3FrameLen80},
     {120, ::le_audio::codec_spec_conf::kLeAudioCodecLC3FrameLen120}};
 
 std::unordered_map<AudioLocation, uint32_t> audio_location_map{
diff --git a/system/blueberry/tests/gd_sl4a/lib/bt_constants.py b/system/blueberry/tests/gd_sl4a/lib/bt_constants.py
index 2dab54d..3ea669d 100644
--- a/system/blueberry/tests/gd_sl4a/lib/bt_constants.py
+++ b/system/blueberry/tests/gd_sl4a/lib/bt_constants.py
@@ -60,7 +60,6 @@
 bluetooth_off = "BluetoothStateChangedOff"
 bluetooth_on = "BluetoothStateChangedOn"
 mtu_changed = "GattConnect{}onMtuChanged"
-gatt_connection_state_change = "GattConnect{}onConnectionStateChange"
 advertising_set_started = "AdvertisingSet{}onAdvertisingSetStarted"
 advertising_set_stopped = "AdvertisingSet{}onAdvertisingSetStopped"
 advertising_set_on_own_address_read = "AdvertisingSet{}onOwnAddressRead"
@@ -261,274 +260,6 @@
 
 ### Bluetooth Low Energy Constants End ###
 
-### Bluetooth GATT Constants Begin ###
-
-# Gatt Callback error messages
-gatt_cb_err = {
-    "char_write_req_err": "Characteristic Write Request event not found. Expected {}",
-    "char_write_err": "Characteristic Write event not found. Expected {}",
-    "desc_write_req_err": "Descriptor Write Request event not found. Expected {}",
-    "desc_write_err": "Descriptor Write event not found. Expected {}",
-    "char_read_err": "Characteristic Read event not found. Expected {}",
-    "char_read_req_err": "Characteristic Read Request not found. Expected {}",
-    "desc_read_err": "Descriptor Read event not found. Expected {}",
-    "desc_read_req_err": "Descriptor Read Request event not found. Expected {}",
-    "rd_remote_rssi_err": "Read Remote RSSI event not found. Expected {}",
-    "gatt_serv_disc_err": "GATT Services Discovered event not found. Expected {}",
-    "serv_added_err": "Service Added event not found. Expected {}",
-    "mtu_changed_err": "MTU Changed event not found. Expected {}",
-    "mtu_serv_changed_err": "MTU Server Changed event not found. Expected {}",
-    "gatt_conn_changed_err": "GATT Connection Changed event not found. Expected {}",
-    "char_change_err": "GATT Characteristic Changed event not fond. Expected {}",
-    "phy_read_err": "Phy Read event not fond. Expected {}",
-    "phy_update_err": "Phy Update event not fond. Expected {}",
-    "exec_write_err": "GATT Execute Write event not found. Expected {}"
-}
-
-# GATT callback strings as defined in GattClientFacade.java and
-# GattServerFacade.java implemented callbacks.
-gatt_cb_strings = {
-    "char_write_req": "GattServer{}onCharacteristicWriteRequest",
-    "exec_write": "GattServer{}onExecuteWrite",
-    "char_write": "GattConnect{}onCharacteristicWrite",
-    "desc_write_req": "GattServer{}onDescriptorWriteRequest",
-    "desc_write": "GattConnect{}onDescriptorWrite",
-    "char_read": "GattConnect{}onCharacteristicRead",
-    "char_read_req": "GattServer{}onCharacteristicReadRequest",
-    "desc_read": "GattConnect{}onDescriptorRead",
-    "desc_read_req": "GattServer{}onDescriptorReadRequest",
-    "rd_remote_rssi": "GattConnect{}onReadRemoteRssi",
-    "gatt_serv_disc": "GattConnect{}onServicesDiscovered",
-    "serv_added": "GattServer{}onServiceAdded",
-    "mtu_changed": "GattConnect{}onMtuChanged",
-    "mtu_serv_changed": "GattServer{}onMtuChanged",
-    "gatt_conn_change": "GattConnect{}onConnectionStateChange",
-    "char_change": "GattConnect{}onCharacteristicChanged",
-    "phy_read": "GattConnect{}onPhyRead",
-    "phy_update": "GattConnect{}onPhyUpdate",
-    "serv_phy_read": "GattServer{}onPhyRead",
-    "serv_phy_update": "GattServer{}onPhyUpdate",
-}
-
-# GATT event dictionary of expected callbacks and errors.
-gatt_event = {
-    "char_write_req": {
-        "evt": gatt_cb_strings["char_write_req"],
-        "err": gatt_cb_err["char_write_req_err"]
-    },
-    "exec_write": {
-        "evt": gatt_cb_strings["exec_write"],
-        "err": gatt_cb_err["exec_write_err"]
-    },
-    "char_write": {
-        "evt": gatt_cb_strings["char_write"],
-        "err": gatt_cb_err["char_write_err"]
-    },
-    "desc_write_req": {
-        "evt": gatt_cb_strings["desc_write_req"],
-        "err": gatt_cb_err["desc_write_req_err"]
-    },
-    "desc_write": {
-        "evt": gatt_cb_strings["desc_write"],
-        "err": gatt_cb_err["desc_write_err"]
-    },
-    "char_read": {
-        "evt": gatt_cb_strings["char_read"],
-        "err": gatt_cb_err["char_read_err"]
-    },
-    "char_read_req": {
-        "evt": gatt_cb_strings["char_read_req"],
-        "err": gatt_cb_err["char_read_req_err"]
-    },
-    "desc_read": {
-        "evt": gatt_cb_strings["desc_read"],
-        "err": gatt_cb_err["desc_read_err"]
-    },
-    "desc_read_req": {
-        "evt": gatt_cb_strings["desc_read_req"],
-        "err": gatt_cb_err["desc_read_req_err"]
-    },
-    "rd_remote_rssi": {
-        "evt": gatt_cb_strings["rd_remote_rssi"],
-        "err": gatt_cb_err["rd_remote_rssi_err"]
-    },
-    "gatt_serv_disc": {
-        "evt": gatt_cb_strings["gatt_serv_disc"],
-        "err": gatt_cb_err["gatt_serv_disc_err"]
-    },
-    "serv_added": {
-        "evt": gatt_cb_strings["serv_added"],
-        "err": gatt_cb_err["serv_added_err"]
-    },
-    "mtu_changed": {
-        "evt": gatt_cb_strings["mtu_changed"],
-        "err": gatt_cb_err["mtu_changed_err"]
-    },
-    "gatt_conn_change": {
-        "evt": gatt_cb_strings["gatt_conn_change"],
-        "err": gatt_cb_err["gatt_conn_changed_err"]
-    },
-    "char_change": {
-        "evt": gatt_cb_strings["char_change"],
-        "err": gatt_cb_err["char_change_err"]
-    },
-    "phy_read": {
-        "evt": gatt_cb_strings["phy_read"],
-        "err": gatt_cb_err["phy_read_err"]
-    },
-    "phy_update": {
-        "evt": gatt_cb_strings["phy_update"],
-        "err": gatt_cb_err["phy_update_err"]
-    },
-    "serv_phy_read": {
-        "evt": gatt_cb_strings["serv_phy_read"],
-        "err": gatt_cb_err["phy_read_err"]
-    },
-    "serv_phy_update": {
-        "evt": gatt_cb_strings["serv_phy_update"],
-        "err": gatt_cb_err["phy_update_err"]
-    }
-}
-
-# Matches constants of connection states defined in BluetoothGatt.java
-gatt_connection_state = {"disconnected": 0, "connecting": 1, "connected": 2, "disconnecting": 3, "closed": 4}
-
-# Matches constants of Bluetooth GATT Characteristic values as defined
-# in BluetoothGattCharacteristic.java
-gatt_characteristic = {
-    "property_broadcast": 0x01,
-    "property_read": 0x02,
-    "property_write_no_response": 0x04,
-    "property_write": 0x08,
-    "property_notify": 0x10,
-    "property_indicate": 0x20,
-    "property_signed_write": 0x40,
-    "property_extended_props": 0x80,
-    "permission_read": 0x01,
-    "permission_read_encrypted": 0x02,
-    "permission_read_encrypted_mitm": 0x04,
-    "permission_write": 0x10,
-    "permission_write_encrypted": 0x20,
-    "permission_write_encrypted_mitm": 0x40,
-    "permission_write_signed": 0x80,
-    "permission_write_signed_mitm": 0x100,
-    "write_type_default": 0x02,
-    "write_type_no_response": 0x01,
-    "write_type_signed": 0x04,
-}
-
-# Matches constants of Bluetooth GATT Characteristic values as defined
-# in BluetoothGattDescriptor.java
-gatt_descriptor = {
-    "enable_notification_value": [0x01, 0x00],
-    "enable_indication_value": [0x02, 0x00],
-    "disable_notification_value": [0x00, 0x00],
-    "permission_read": 0x01,
-    "permission_read_encrypted": 0x02,
-    "permission_read_encrypted_mitm": 0x04,
-    "permission_write": 0x10,
-    "permission_write_encrypted": 0x20,
-    "permission_write_encrypted_mitm": 0x40,
-    "permission_write_signed": 0x80,
-    "permission_write_signed_mitm": 0x100
-}
-
-# https://www.bluetooth.com/specifications/gatt/descriptors
-gatt_char_desc_uuids = {
-    "char_ext_props": '00002900-0000-1000-8000-00805f9b34fb',
-    "char_user_desc": '00002901-0000-1000-8000-00805f9b34fb',
-    "client_char_cfg": '00002902-0000-1000-8000-00805f9b34fb',
-    "server_char_cfg": '00002903-0000-1000-8000-00805f9b34fb',
-    "char_fmt_uuid": '00002904-0000-1000-8000-00805f9b34fb',
-    "char_agreg_fmt": '00002905-0000-1000-8000-00805f9b34fb',
-    "char_valid_range": '00002906-0000-1000-8000-00805f9b34fb',
-    "external_report_reference": '00002907-0000-1000-8000-00805f9b34fb',
-    "report_reference": '00002908-0000-1000-8000-00805f9b34fb'
-}
-
-# https://www.bluetooth.com/specifications/gatt/characteristics
-gatt_char_types = {
-    "device_name": '00002a00-0000-1000-8000-00805f9b34fb',
-    "appearance": '00002a01-0000-1000-8000-00805f9b34fb',
-    "peripheral_priv_flag": '00002a02-0000-1000-8000-00805f9b34fb',
-    "reconnection_address": '00002a03-0000-1000-8000-00805f9b34fb',
-    "peripheral_pref_conn": '00002a04-0000-1000-8000-00805f9b34fb',
-    "service_changed": '00002a05-0000-1000-8000-00805f9b34fb',
-    "system_id": '00002a23-0000-1000-8000-00805f9b34fb',
-    "model_number_string": '00002a24-0000-1000-8000-00805f9b34fb',
-    "serial_number_string": '00002a25-0000-1000-8000-00805f9b34fb',
-    "firmware_revision_string": '00002a26-0000-1000-8000-00805f9b34fb',
-    "hardware_revision_string": '00002a27-0000-1000-8000-00805f9b34fb',
-    "software_revision_string": '00002a28-0000-1000-8000-00805f9b34fb',
-    "manufacturer_name_string": '00002a29-0000-1000-8000-00805f9b34fb',
-    "pnp_id": '00002a50-0000-1000-8000-00805f9b34fb',
-}
-
-# Matches constants of Bluetooth GATT Characteristic values as defined
-# in BluetoothGattCharacteristic.java
-gatt_characteristic_value_format = {
-    "string": 0x1,
-    "byte": 0x2,
-    "sint8": 0x21,
-    "uint8": 0x11,
-    "sint16": 0x22,
-    "unit16": 0x12,
-    "sint32": 0x24,
-    "uint32": 0x14
-}
-
-# Matches constants of Bluetooth Gatt Service types as defined in
-# BluetoothGattService.java
-gatt_service_types = {"primary": 0, "secondary": 1}
-
-# Matches constants of Bluetooth Gatt Connection Priority values as defined in
-# BluetoothGatt.java
-gatt_connection_priority = {"balanced": 0, "high": 1, "low_power": 2}
-
-# Min and max MTU values
-gatt_mtu_size = {"min": 23, "max": 217}
-
-# Gatt Characteristic attribute lengths
-gatt_characteristic_attr_length = {"attr_1": 1, "attr_2": 3, "attr_3": 15}
-
-# Matches constants of Bluetooth Gatt operations status as defined in
-# BluetoothGatt.java
-gatt_status = {"success": 0, "failure": 0x101}
-
-# Matches constants of Bluetooth transport values as defined in
-# BluetoothDevice.java
-gatt_transport = {"auto": 0x00, "bredr": 0x01, "le": 0x02}
-
-# Matches constants of Bluetooth physical channeling values as defined in
-# BluetoothDevice.java
-gatt_phy = {"1m": 1, "2m": 2, "le_coded": 3}
-
-# Matches constants of Bluetooth physical channeling bitmask values as defined
-# in BluetoothDevice.java
-gatt_phy_mask = {"1m_mask": 1, "2m_mask": 2, "coded_mask": 4}
-
-# Values as defiend in the Bluetooth GATT specification
-gatt_server_responses = {
-    "GATT_SUCCESS": 0x0,
-    "GATT_FAILURE": 0x1,
-    "GATT_READ_NOT_PERMITTED": 0x2,
-    "GATT_WRITE_NOT_PERMITTED": 0x3,
-    "GATT_INVALID_PDU": 0x4,
-    "GATT_INSUFFICIENT_AUTHENTICATION": 0x5,
-    "GATT_REQUEST_NOT_SUPPORTED": 0x6,
-    "GATT_INVALID_OFFSET": 0x7,
-    "GATT_INSUFFICIENT_AUTHORIZATION": 0x8,
-    "GATT_INVALID_ATTRIBUTE_LENGTH": 0xd,
-    "GATT_INSUFFICIENT_ENCRYPTION": 0xf,
-    "GATT_CONNECTION_CONGESTED": 0x8f,
-    "GATT_13_ERR": 0x13,
-    "GATT_12_ERR": 0x12,
-    "GATT_0C_ERR": 0x0C,
-    "GATT_16": 0x16
-}
-
-### Bluetooth GATT Constants End ###
-
 ### Chameleon Constants Begin ###
 
 # Chameleon audio bits per sample.
diff --git a/system/blueberry/tests/gd_sl4a/lib/gd_sl4a_base_test.py b/system/blueberry/tests/gd_sl4a/lib/gd_sl4a_base_test.py
index a5ae587..899df00 100644
--- a/system/blueberry/tests/gd_sl4a/lib/gd_sl4a_base_test.py
+++ b/system/blueberry/tests/gd_sl4a/lib/gd_sl4a_base_test.py
@@ -26,11 +26,9 @@
 from mobly.controllers.android_device_lib.adb import AdbError
 from mobly.controllers import android_device
 from mobly.controllers.android_device import MOBLY_CONTROLLER_CONFIG_NAME as ANDROID_DEVICE_COFNIG_NAME
-from mobly.controllers.android_device_lib.jsonrpc_client_base import \
-    AppRestoreConnectionError
-from mobly.controllers.android_device_lib.services.sl4a_service import Sl4aService
-import mobly.controllers.android_device_lib.sl4a_client as sl4a_client
 
+from blueberry.utils.mobly_sl4a_utils import setup_sl4a
+from blueberry.utils.mobly_sl4a_utils import teardown_sl4a
 from blueberry.tests.gd.cert.context import get_current_context
 from blueberry.tests.gd.cert.gd_device import MOBLY_CONTROLLER_CONFIG_NAME as GD_DEVICE_CONFIG_NAME
 from blueberry.tests.gd_sl4a.lib.ble_lib import enable_bluetooth, disable_bluetooth, BleLib
@@ -54,25 +52,14 @@
         self.cert = self.gd_devices[0]
 
         # Parse and construct Android device objects
+        self.android_devices = self.register_controller(android_device, required=True)
         server_port = int(self.controller_configs[ANDROID_DEVICE_COFNIG_NAME][0]['server_port'])
         forwarded_port = int(self.controller_configs[ANDROID_DEVICE_COFNIG_NAME][0]['forwarded_port'])
-        sl4a_client._DEVICE_SIDE_PORT = server_port
-        sl4a_client._APP_START_WAIT_TIME = 0.5
-        self.android_devices = self.register_controller(android_device, required=True)
         self.dut = self.android_devices[0]
-        self.dut.services.register('sl4a', Sl4aService, start_service=False)
-        try:
-            self.dut.sl4a.start()
-        except AppRestoreConnectionError:
-            pass
-        try:
-            self.dut.sl4a.clear_host_port()
-        except AdbError:
-            pass
-        sl4a_client._APP_START_WAIT_TIME = 2 * 60
-        self.dut.sl4a.restore_app_connection(port=forwarded_port)
+        setup_sl4a(self.dut, server_port, forwarded_port)
 
         # Enable full btsnoop log
+        self.dut.adb.root()
         self.dut.adb.shell("setprop persist.bluetooth.btsnooplogmode full")
         getprop_result = self.dut.adb.shell("getprop persist.bluetooth.btsnooplogmode") == "full"
         if not getprop_result:
@@ -81,7 +68,7 @@
         self.ble = BleLib(dut=self.dut)
 
     def teardown_class(self):
-        pass
+        teardown_sl4a(self.dut)
 
     def setup_test(self):
         self.cert.rootservice.StartStack(
diff --git a/system/blueberry/tests/sl4a_sl4a/gatt/gatt_connect_test.py b/system/blueberry/tests/sl4a_sl4a/gatt/gatt_connect_test.py
index 1d21bc6..a2dfea1 100644
--- a/system/blueberry/tests/sl4a_sl4a/gatt/gatt_connect_test.py
+++ b/system/blueberry/tests/sl4a_sl4a/gatt/gatt_connect_test.py
@@ -1,141 +1,1006 @@
 #!/usr/bin/env python3
 #
-#   Copyright 2021 - The Android Open Source Project
+# Copyright (C) 2016 The Android Open Source Project
 #
-#   Licensed under the Apache License, Version 2.0 (the "License");
-#   you may not use this file except in compliance with the License.
-#   You may obtain a copy of the License at
+# 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.
+"""
+This test script exercises different GATT connection tests.
 
-import io
-import os
-import queue
+Original location:
+  tools/test/connectivity/acts_tests/tests/google/ble/gatt/GattConnectTest.py
+"""
+
 import logging
+import time
+from queue import Empty
 
-from blueberry.tests.gd.cert.context import get_current_context
-from blueberry.tests.gd.cert.truth import assertThat
-from blueberry.tests.gd_sl4a.lib.bt_constants import adv_succ, ble_advertise_settings_modes, ble_scan_settings_modes, ble_address_types, ble_scan_settings_phys, gatt_connection_state_change, gatt_transport, scan_result
-from blueberry.tests.gd_sl4a.lib.ble_lib import generate_ble_scan_objects, generate_ble_advertise_objects
+from blueberry.tests.gd.cert.test_decorators import test_tracker_info
+from blueberry.tests.gd_sl4a.lib.bt_constants import scan_result
 from blueberry.tests.sl4a_sl4a.lib import sl4a_sl4a_base_test
-from blueberry.facade import common_pb2 as common
-
+from blueberry.utils.ble_scan_adv_constants import BleAdvertiseSettingsMode
+from blueberry.utils.ble_scan_adv_constants import BleScanSettingsMatchNums
+from blueberry.utils.ble_scan_adv_constants import BleScanSettingsModes
+from blueberry.utils.bt_constants import BluetoothProfile
+from blueberry.utils.bt_gatt_constants import GattCallbackError
+from blueberry.utils.bt_gatt_constants import GattCallbackString
+from blueberry.utils.bt_gatt_constants import GattCharacteristic
+from blueberry.utils.bt_gatt_constants import GattConnectionState
+from blueberry.utils.bt_gatt_constants import GattMtuSize
+from blueberry.utils.bt_gatt_constants import GattPhyMask
+from blueberry.utils.bt_gatt_constants import GattServiceType
+from blueberry.utils.bt_gatt_constants import GattTransport
+from blueberry.utils.bt_gatt_utils import GattTestUtilsError
+from blueberry.utils.bt_gatt_utils import close_gatt_client
+from blueberry.utils.bt_gatt_utils import disconnect_gatt_connection
+from blueberry.utils.bt_gatt_utils import get_mac_address_of_generic_advertisement
+from blueberry.utils.bt_gatt_utils import log_gatt_server_uuids
+from blueberry.utils.bt_gatt_utils import orchestrate_gatt_connection
+from blueberry.utils.bt_gatt_utils import setup_gatt_connection
+from blueberry.utils.bt_gatt_utils import setup_multiple_services
+from blueberry.utils.bt_gatt_utils import wait_for_gatt_disconnect_event
+from blueberry.utils.bt_test_utils import clear_bonded_devices
 from mobly import test_runner
-from mobly.controllers.android_device_lib.adb import AdbError
+
+PHYSICAL_DISCONNECT_TIMEOUT = 5
 
 
 class GattConnectTest(sl4a_sl4a_base_test.Sl4aSl4aBaseTestClass):
+    adv_instances = []
+    bluetooth_gatt_list = []
+    gatt_server_list = []
+    default_timeout = 10
+    default_discovery_timeout = 3
 
     def setup_class(self):
         super().setup_class()
-        self.default_timeout = 10  # seconds
+        self.central = self.dut
+        self.peripheral = self.cert
 
     def setup_test(self):
         super().setup_test()
+        bluetooth_gatt_list = []
+        self.gatt_server_list = []
+        self.adv_instances = []
+        # Ensure there is ample time for a physical disconnect in between
+        # testcases.
+        logging.info("Waiting for {} seconds for physical GATT disconnections".format(PHYSICAL_DISCONNECT_TIMEOUT))
+        time.sleep(PHYSICAL_DISCONNECT_TIMEOUT)
 
     def teardown_test(self):
+        for bluetooth_gatt in self.bluetooth_gatt_list:
+            self.central.sl4a.gattClientClose(bluetooth_gatt)
+        for gatt_server in self.gatt_server_list:
+            self.peripheral.sl4a.gattServerClose(gatt_server)
+        for adv in self.adv_instances:
+            self.peripheral.sl4a.bleStopBleAdvertising(adv)
         super().teardown_test()
+        return True
 
-    def _wait_for_event(self, expected_event_name, device):
+    def _orchestrate_gatt_disconnection(self, bluetooth_gatt, gatt_callback):
+        logging.info("Disconnecting from peripheral device.")
         try:
-            event_info = device.ed.pop_event(expected_event_name, self.default_timeout)
-            logging.info(event_info)
-        except queue.Empty as error:
-            logging.error("Failed to find event: %s", expected_event_name)
+            disconnect_gatt_connection(self.central, bluetooth_gatt, gatt_callback)
+            close_gatt_client(self.central, bluetooth_gatt)
+            if bluetooth_gatt in self.bluetooth_gatt_list:
+                self.bluetooth_gatt_list.remove(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
             return False
         return True
 
-    def _wait_for_scan_result_event(self, expected_event_name, device):
+    def _find_service_added_event(self, gatt_server_cb, uuid):
+        expected_event = GattCallbackString.SERV_ADDED.format(gatt_server_cb)
         try:
-            event_info = device.ed.pop_event(expected_event_name, self.default_timeout)
-        except queue.Empty as error:
-            logging.error("Could not find scan result event: %s", expected_event_name)
-            return None
-        return event_info['data']['Result']['deviceInfo']['address']
-
-    def _get_cert_public_address_and_irk_from_bt_config(self):
-        # Pull IRK from SL4A cert side to pass in from SL4A DUT side when scanning
-        bt_config_file_path = os.path.join(get_current_context().get_full_output_path(),
-                                           "DUT_%s_bt_config.conf" % self.cert.serial)
-        try:
-            self.cert.adb.pull(["/data/misc/bluedroid/bt_config.conf", bt_config_file_path])
-        except AdbError as error:
-            logging.error("Failed to pull SL4A cert BT config")
+            event = self.peripheral.ed.pop_event(expected_event, self.default_timeout)
+        except Empty:
+            logging.error(GattCallbackError.SERV_ADDED_ERR.format(expected_event))
             return False
-        logging.debug("Reading SL4A cert BT config")
-        with io.open(bt_config_file_path) as f:
-            for line in f.readlines():
-                stripped_line = line.strip()
-                if (stripped_line.startswith("Address")):
-                    address_fields = stripped_line.split(' ')
-                    # API currently requires public address to be capitalized
-                    address = address_fields[2].upper()
-                    logging.debug("Found cert address: %s" % address)
-                    continue
-                if (stripped_line.startswith("LE_LOCAL_KEY_IRK")):
-                    irk_fields = stripped_line.split(' ')
-                    irk = irk_fields[2]
-                    logging.debug("Found cert IRK: %s" % irk)
-                    continue
+        if event['data']['serviceUuid'].lower() != uuid.lower():
+            logging.error("Uuid mismatch. Found: {}, Expected {}.".format(event['data']['serviceUuid'], uuid))
+            return False
+        return True
 
-        return address, irk
+    def _verify_mtu_changed_on_client_and_server(self, expected_mtu, gatt_callback, gatt_server_callback):
+        expected_event = GattCallbackString.MTU_CHANGED.format(gatt_callback)
+        try:
+            mtu_event = self.central.ed.pop_event(expected_event, self.default_timeout)
+            mtu_size_found = mtu_event['data']['MTU']
+            if mtu_size_found != expected_mtu:
+                logging.error("MTU size found: {}, expected: {}".format(mtu_size_found, expected_mtu))
+                return False
+        except Empty:
+            logging.error(GattCallbackError.MTU_CHANGED_ERR.format(expected_event))
+            return False
 
-    def test_scan_connect_unbonded_device_public_address_with_irk(self):
-        # Set up SL4A cert side to advertise
-        logging.info("Starting advertising")
-        self.cert.sl4a.bleSetAdvertiseSettingsIsConnectable(True)
-        self.cert.sl4a.bleSetAdvertiseDataIncludeDeviceName(True)
-        self.cert.sl4a.bleSetAdvertiseSettingsAdvertiseMode(ble_advertise_settings_modes['low_latency'])
-        self.cert.sl4a.bleSetAdvertiseSettingsOwnAddressType(common.RANDOM_DEVICE_ADDRESS)
-        advertise_callback, advertise_data, advertise_settings = generate_ble_advertise_objects(self.cert.sl4a)
-        self.cert.sl4a.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings)
+        expected_event = GattCallbackString.MTU_SERV_CHANGED.format(gatt_server_callback)
+        try:
+            mtu_event = self.peripheral.ed.pop_event(expected_event, self.default_timeout)
+            mtu_size_found = mtu_event['data']['MTU']
+            if mtu_size_found != expected_mtu:
+                logging.error("MTU size found: {}, expected: {}".format(mtu_size_found, expected_mtu))
+                return False
+        except Empty:
+            logging.error(GattCallbackError.MTU_SERV_CHANGED_ERR.format(expected_event))
+            return False
+        return True
 
-        # Wait for SL4A cert to start advertising
-        assertThat(self._wait_for_event(adv_succ.format(advertise_callback), self.cert)).isTrue()
-        logging.info("Advertising started")
+    @test_tracker_info(uuid='8a3530a3-c8bb-466b-9710-99e694c38618')
+    def test_gatt_connect(self):
+        """Test GATT connection over LE.
 
-        # Pull IRK from SL4A cert side to pass in from SL4A DUT side when scanning
-        cert_public_address, irk = self._get_cert_public_address_and_irk_from_bt_config()
+          Test establishing a gatt connection between a GATT server and GATT
+          client.
 
-        # Set up SL4A DUT side to scan
-        addr_type = ble_address_types["public"]
-        logging.info("Start scanning for PUBLIC_ADDRESS %s with address type %d and IRK %s" % (cert_public_address,
-                                                                                               addr_type, irk))
-        self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
-        self.dut.sl4a.bleSetScanSettingsLegacy(False)
-        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+          Steps:
+            1. Start a generic advertisement.
+            2. Start a generic scanner.
+            3. Find the advertisement and extract the mac address.
+            4. Stop the first scanner.
+            5. Create a GATT connection between the scanner and advertiser.
+            6. Disconnect the GATT connection.
+
+          Expected Result:
+            Verify that a connection was established and then disconnected
+            successfully.
+
+          Returns:
+            Pass if True
+            Fail if False
+
+          TAGS: LE, Advertising, Filtering, Scanning, GATT
+          Priority: 0
+          """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.adv_instances.append(adv_callback)
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='a839b505-03ac-4783-be7e-1d43129a1948')
+    def test_gatt_connect_stop_advertising(self):
+        """Test GATT connection over LE then stop advertising
+
+          A test case that verifies the GATT connection doesn't
+          disconnect when LE advertisement is stopped.
+
+          Steps:
+            1. Start a generic advertisement.
+            2. Start a generic scanner.
+            3. Find the advertisement and extract the mac address.
+            4. Stop the first scanner.
+            5. Create a GATT connection between the scanner and advertiser.
+            6. Stop the advertiser.
+            7. Verify no connection state changed happened.
+            8. Disconnect the GATT connection.
+
+          Expected Result:
+            Verify that a connection was established and not disconnected
+            when advertisement stops.
+
+          Returns:
+            Pass if True
+            Fail if False
+
+          TAGS: LE, Advertising, Filtering, Scanning, GATT
+          Priority: 0
+          """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.peripheral.sl4a.bleStopBleAdvertising(adv_callback)
+        try:
+            event = self.central.ed.pop_event(
+                GattCallbackString.GATT_CONN_CHANGE.format(gatt_callback, self.default_timeout))
+            logging.error("Connection event found when not expected: {}".format(event))
+            return False
+        except Empty:
+            logging.info("No connection state change as expected")
+        try:
+            self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+        except Exception as err:
+            logging.info("Failed to orchestrate disconnect: {}".format(err))
+            return False
+        return True
+
+    @test_tracker_info(uuid='b82f91a8-54bb-4779-a117-73dc7fdb28cc')
+    def test_gatt_connect_autoconnect(self):
+        """Test GATT connection over LE.
+
+          Test re-establishing a gatt connection using autoconnect
+          set to True in order to test connection allowlist.
+
+          Steps:
+            1. Start a generic advertisement.
+            2. Start a generic scanner.
+            3. Find the advertisement and extract the mac address.
+            4. Stop the first scanner.
+            5. Create a GATT connection between the scanner and advertiser.
+            6. Disconnect the GATT connection.
+            7. Create a GATT connection with autoconnect set to True
+            8. Disconnect the GATT connection.
+
+          Expected Result:
+            Verify that a connection was re-established and then disconnected
+            successfully.
+
+          Returns:
+            Pass if True
+            Fail if False
+
+          TAGS: LE, Advertising, Filtering, Scanning, GATT
+          Priority: 0
+          """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        autoconnect = False
+        mac_address, adv_callback, scan_callback = (get_mac_address_of_generic_advertisement(
+            self.central, self.peripheral))
+        self.adv_instances.append(adv_callback)
+        try:
+            bluetooth_gatt, gatt_callback = setup_gatt_connection(self.central, mac_address, autoconnect)
+            self.central.sl4a.bleStopBleScan(scan_callback)
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        try:
+            disconnect_gatt_connection(self.central, bluetooth_gatt, gatt_callback)
+            close_gatt_client(self.central, bluetooth_gatt)
+            if bluetooth_gatt in self.bluetooth_gatt_list:
+                self.bluetooth_gatt_list.remove(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        autoconnect = True
+        bluetooth_gatt = self.central.sl4a.gattClientConnectGatt(
+            gatt_callback, mac_address, autoconnect, GattTransport.TRANSPORT_AUTO, False, GattPhyMask.PHY_LE_1M_MASK)
+        self.bluetooth_gatt_list.append(bluetooth_gatt)
+        expected_event = GattCallbackString.GATT_CONN_CHANGE.format(gatt_callback)
+        try:
+            event = self.central.ed.pop_event(expected_event, self.default_timeout)
+        except Empty:
+            logging.error(GattCallbackError.GATT_CONN_CHANGE_ERR.format(expected_event))
+            test_result = False
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='e506fa50-7cd9-4bd8-938a-6b85dcfe6bc6')
+    def test_gatt_connect_opportunistic(self):
+        """Test opportunistic GATT connection over LE.
+
+          Test establishing a gatt connection between a GATT server and GATT
+          client in opportunistic mode.
+
+          Steps:
+            1. Start a generic advertisement.
+            2. Start a generic scanner.
+            3. Find the advertisement and extract the mac address.
+            4. Stop the first scanner.
+            5. Create GATT connection 1 between the scanner and advertiser normally
+            6. Create GATT connection 2 between the scanner and advertiser using
+               opportunistic mode
+            7. Disconnect GATT connection 1
+
+          Expected Result:
+            Verify GATT connection 2 automatically disconnects when GATT connection
+            1 disconnect
+
+          Returns:
+            Pass if True
+            Fail if False
+
+          TAGS: LE, Advertising, Filtering, Scanning, GATT
+          Priority: 0
+          """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        mac_address, adv_callback, scan_callback = (get_mac_address_of_generic_advertisement(
+            self.central, self.peripheral))
+        # Make GATT connection 1
+        try:
+            bluetooth_gatt_1, gatt_callback_1 = setup_gatt_connection(
+                self.central, mac_address, False, transport=GattTransport.TRANSPORT_AUTO, opportunistic=False)
+            self.central.sl4a.bleStopBleScan(scan_callback)
+            self.adv_instances.append(adv_callback)
+            self.bluetooth_gatt_list.append(bluetooth_gatt_1)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        # Make GATT connection 2
+        try:
+            bluetooth_gatt_2, gatt_callback_2 = setup_gatt_connection(
+                self.central, mac_address, False, transport=GattTransport.TRANSPORT_AUTO, opportunistic=True)
+            self.bluetooth_gatt_list.append(bluetooth_gatt_2)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        # Disconnect GATT connection 1
+        try:
+            disconnect_gatt_connection(self.central, bluetooth_gatt_1, gatt_callback_1)
+            close_gatt_client(self.central, bluetooth_gatt_1)
+            if bluetooth_gatt_1 in self.bluetooth_gatt_list:
+                self.bluetooth_gatt_list.remove(bluetooth_gatt_1)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        # Confirm that GATT connection 2 also disconnects
+        wait_for_gatt_disconnect_event(self.central, gatt_callback_2)
+        close_gatt_client(self.central, bluetooth_gatt_2)
+        if bluetooth_gatt_2 in self.bluetooth_gatt_list:
+            self.bluetooth_gatt_list.remove(bluetooth_gatt_2)
+        return True
+
+    @test_tracker_info(uuid='1e01838e-c4de-4720-9adf-9e0419378226')
+    def test_gatt_request_min_mtu(self):
+        """Test GATT connection over LE and exercise MTU sizes.
+
+          Test establishing a gatt connection between a GATT server and GATT
+          client. Request an MTU size that matches the correct minimum size.
+
+          Steps:
+            1. Start a generic advertisement.
+            2. Start a generic scanner.
+            3. Find the advertisement and extract the mac address.
+            4. Stop the first scanner.
+            5. Create a GATT connection between the scanner and advertiser.
+            6. From the scanner (client) request MTU size change to the
+            minimum value.
+            7. Find the MTU changed event on the client.
+            8. Disconnect the GATT connection.
+
+          Expected Result:
+            Verify that a connection was established and the MTU value found
+            matches the expected MTU value.
+
+          Returns:
+            Pass if True
+            Fail if False
+
+          TAGS: LE, Advertising, Filtering, Scanning, GATT, MTU
+          Priority: 0
+          """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.adv_instances.append(adv_callback)
+        expected_mtu = GattMtuSize.MIN
+        self.central.sl4a.gattClientRequestMtu(bluetooth_gatt, expected_mtu)
+        if not self._verify_mtu_changed_on_client_and_server(expected_mtu, gatt_callback, gatt_server_cb):
+            return False
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='c1fa3a2d-fb47-47db-bdd1-458928cd6a5f')
+    def test_gatt_request_max_mtu(self):
+        """Test GATT connection over LE and exercise MTU sizes.
+
+          Test establishing a gatt connection between a GATT server and GATT
+          client. Request an MTU size that matches the correct maximum size.
+
+          Steps:
+            1. Start a generic advertisement.
+            2. Start a generic scanner.
+            3. Find the advertisement and extract the mac address.
+            4. Stop the first scanner.
+            5. Create a GATT connection between the scanner and advertiser.
+            6. From the scanner (client) request MTU size change to the
+            maximum value.
+            7. Find the MTU changed event on the client.
+            8. Disconnect the GATT connection.
+
+          Expected Result:
+            Verify that a connection was established and the MTU value found
+            matches the expected MTU value.
+
+          Returns:
+            Pass if True
+            Fail if False
+
+          TAGS: LE, Advertising, Filtering, Scanning, GATT, MTU
+          Priority: 0
+          """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.adv_instances.append(adv_callback)
+        expected_mtu = GattMtuSize.MAX
+        self.central.sl4a.gattClientRequestMtu(bluetooth_gatt, expected_mtu)
+        if not self._verify_mtu_changed_on_client_and_server(expected_mtu, gatt_callback, gatt_server_cb):
+            return False
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='4416d483-dec3-46cb-8038-4d82620f873a')
+    def test_gatt_request_out_of_bounds_mtu(self):
+        """Test GATT connection over LE and exercise an out of bound MTU size.
+
+          Test establishing a gatt connection between a GATT server and GATT
+          client. Request an MTU size that is the MIN value minus 1.
+
+          Steps:
+            1. Start a generic advertisement.
+            2. Start a generic scanner.
+            3. Find the advertisement and extract the mac address.
+            4. Stop the first scanner.
+            5. Create a GATT connection between the scanner and advertiser.
+            6. From the scanner (client) request MTU size change to the
+            minimum value minus one.
+            7. Find the MTU changed event on the client.
+            8. Disconnect the GATT connection.
+
+          Expected Result:
+            Verify that an MTU changed event was not discovered and that
+            it didn't cause an exception when requesting an out of bounds
+            MTU.
+
+          Returns:
+            Pass if True
+            Fail if False
+
+          TAGS: LE, Advertising, Filtering, Scanning, GATT, MTU
+          Priority: 0
+          """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.adv_instances.append(adv_callback)
+        unexpected_mtu = GattMtuSize.MIN - 1
+        self.central.sl4a.gattClientRequestMtu(bluetooth_gatt, unexpected_mtu)
+        if self._verify_mtu_changed_on_client_and_server(unexpected_mtu, gatt_callback, gatt_server_cb):
+            return False
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='31ffb9ca-cc75-43fb-9802-c19f1c5856b6')
+    def test_gatt_connect_trigger_on_read_rssi(self):
+        """Test GATT connection over LE read RSSI.
+
+        Test establishing a gatt connection between a GATT server and GATT
+        client then read the RSSI.
+
+        Steps:
+          1. Start a generic advertisement.
+          2. Start a generic scanner.
+          3. Find the advertisement and extract the mac address.
+          4. Stop the first scanner.
+          5. Create a GATT connection between the scanner and advertiser.
+          6. From the scanner, request to read the RSSI of the advertiser.
+          7. Disconnect the GATT connection.
+
+        Expected Result:
+          Verify that a connection was established and then disconnected
+          successfully. Verify that the RSSI was ready correctly.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, GATT, RSSI
+        Priority: 1
+        """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.adv_instances.append(adv_callback)
+        expected_event = GattCallbackString.RD_REMOTE_RSSI.format(gatt_callback)
+        if self.central.sl4a.gattClientReadRSSI(bluetooth_gatt):
+            try:
+                self.central.ed.pop_event(expected_event, self.default_timeout)
+            except Empty:
+                logging.error(GattCallbackError.RD_REMOTE_RSSI_ERR.format(expected_event))
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='dee9ef28-b872-428a-821b-cc62f27ba936')
+    def test_gatt_connect_trigger_on_services_discovered(self):
+        """Test GATT connection and discover services of peripheral.
+
+          Test establishing a gatt connection between a GATT server and GATT
+          client the discover all services from the connected device.
+
+          Steps:
+            1. Start a generic advertisement.
+            2. Start a generic scanner.
+            3. Find the advertisement and extract the mac address.
+            4. Stop the first scanner.
+            5. Create a GATT connection between the scanner and advertiser.
+            6. From the scanner (central device), discover services.
+            7. Disconnect the GATT connection.
+
+          Expected Result:
+            Verify that a connection was established and then disconnected
+            successfully. Verify that the service were discovered.
+
+          Returns:
+            Pass if True
+            Fail if False
+
+          TAGS: LE, Advertising, Filtering, Scanning, GATT, Services
+          Priority: 1
+          """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.adv_instances.append(adv_callback)
+        if self.central.sl4a.gattClientDiscoverServices(bluetooth_gatt):
+            expected_event = GattCallbackString.GATT_SERV_DISC.format(gatt_callback)
+            try:
+                event = self.central.ed.pop_event(expected_event, self.default_timeout)
+            except Empty:
+                logging.error(GattCallbackError.GATT_SERV_DISC_ERR.format(expected_event))
+                return False
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='01883bdd-0cf8-48fb-bf15-467bbd4f065b')
+    def test_gatt_connect_trigger_on_services_discovered_iterate_attributes(self):
+        """Test GATT connection and iterate peripherals attributes.
+
+        Test establishing a gatt connection between a GATT server and GATT
+        client and iterate over all the characteristics and descriptors of the
+        discovered services.
+
+        Steps:
+          1. Start a generic advertisement.
+          2. Start a generic scanner.
+          3. Find the advertisement and extract the mac address.
+          4. Stop the first scanner.
+          5. Create a GATT connection between the scanner and advertiser.
+          6. From the scanner (central device), discover services.
+          7. Iterate over all the characteristics and descriptors of the
+          discovered features.
+          8. Disconnect the GATT connection.
+
+        Expected Result:
+          Verify that a connection was established and then disconnected
+          successfully. Verify that the services, characteristics, and descriptors
+          were discovered.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, GATT, Services
+        Characteristics, Descriptors
+        Priority: 1
+        """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.adv_instances.append(adv_callback)
+        if self.central.sl4a.gattClientDiscoverServices(bluetooth_gatt):
+            expected_event = GattCallbackString.GATT_SERV_DISC.format(gatt_callback)
+            try:
+                event = self.central.ed.pop_event(expected_event, self.default_timeout)
+                discovered_services_index = event['data']['ServicesIndex']
+            except Empty:
+                logging.error(GattCallbackError.GATT_SERV_DISC_ERR.format(expected_event))
+                return False
+            log_gatt_server_uuids(self.central, discovered_services_index)
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='d4277bee-da99-4f48-8a4d-f81b5389da18')
+    def test_gatt_connect_with_service_uuid_variations(self):
+        """Test GATT connection with multiple service uuids.
+
+        Test establishing a gatt connection between a GATT server and GATT
+        client with multiple service uuid variations.
+
+        Steps:
+          1. Start a generic advertisement.
+          2. Start a generic scanner.
+          3. Find the advertisement and extract the mac address.
+          4. Stop the first scanner.
+          5. Create a GATT connection between the scanner and advertiser.
+          6. From the scanner (central device), discover services.
+          7. Verify that all the service uuid variations are found.
+          8. Disconnect the GATT connection.
+
+        Expected Result:
+          Verify that a connection was established and then disconnected
+          successfully. Verify that the service uuid variations are found.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, GATT, Services
+        Priority: 2
+        """
+        try:
+            gatt_server_cb, gatt_server = setup_multiple_services(self.peripheral)
+            self.gatt_server_list.append(gatt_server)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.adv_instances.append(adv_callback)
+        if self.central.sl4a.gattClientDiscoverServices(bluetooth_gatt):
+            expected_event = GattCallbackString.GATT_SERV_DISC.format(gatt_callback)
+            try:
+                event = self.central.ed.pop_event(expected_event, self.default_timeout)
+            except Empty:
+                logging.error(GattCallbackError.GATT_SERV_DISC_ERR.format(expected_event))
+                return False
+            discovered_services_index = event['data']['ServicesIndex']
+            log_gatt_server_uuids(self.central, discovered_services_index)
+
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='7d3442c5-f71f-44ae-bd35-f2569f01b3b8')
+    def test_gatt_connect_in_quick_succession(self):
+        """Test GATT connections multiple times.
+
+        Test establishing a gatt connection between a GATT server and GATT
+        client with multiple iterations.
+
+        Steps:
+          1. Start a generic advertisement.
+          2. Start a generic scanner.
+          3. Find the advertisement and extract the mac address.
+          4. Stop the first scanner.
+          5. Create a GATT connection between the scanner and advertiser.
+          6. Disconnect the GATT connection.
+          7. Repeat steps 5 and 6 twenty times.
+
+        Expected Result:
+          Verify that a connection was established and then disconnected
+          successfully twenty times.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, GATT, Stress
+        Priority: 1
+        """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        mac_address, adv_callback, scan_callback = get_mac_address_of_generic_advertisement(
+            self.central, self.peripheral)
+        autoconnect = False
+        for i in range(100):
+            logging.info("Starting connection iteration {}".format(i + 1))
+            try:
+                bluetooth_gatt, gatt_callback = setup_gatt_connection(self.central, mac_address, autoconnect)
+                self.central.sl4a.bleStopBleScan(scan_callback)
+            except GattTestUtilsError as err:
+                logging.error(err)
+                return False
+            test_result = self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+            if not test_result:
+                logging.info("Failed to disconnect from peripheral device.")
+                return False
+        self.adv_instances.append(adv_callback)
+        return True
+
+    @test_tracker_info(uuid='148469d9-7ab0-4c08-b2e9-7e49e88da1fc')
+    def test_gatt_connect_on_path_attack(self):
+        """Test GATT connection with permission write encrypted with on-path attacker prevention
+
+        Test establishing a gatt connection between a GATT server and GATT
+        client while the GATT server's characteristic includes the property
+        write value and the permission write encrypted on-path attacker prevention
+        value. This will prompt LE pairing and then the devices will create a bond.
+
+        Steps:
+          1. Create a GATT server and server callback on the peripheral device.
+          2. Create a unique service and characteristic uuid on the peripheral.
+          3. Create a characteristic on the peripheral with these properties:
+              GattCharacteristic.PROPERTY_WRITE,
+              GattCharacteristic.PERMISSION_WRITE_ENCRYPTED_MITM
+          4. Create a GATT service on the peripheral.
+          5. Add the characteristic to the GATT service.
+          6. Create a GATT connection between your central and peripheral device.
+          7. From the central device, discover the peripheral's services.
+          8. Iterate the services found until you find the unique characteristic
+              created in step 3.
+          9. Once found, write a random but valid value to the characteristic.
+          10. Start pairing helpers on both devices immediately after attempting
+              to write to the characteristic.
+          11. Within 10 seconds of writing the characteristic, there should be
+              a prompt to bond the device from the peripheral. The helpers will
+              handle the UI interaction automatically. (see
+              BluetoothConnectionFacade.java bluetoothStartPairingHelper).
+          12. Verify that the two devices are bonded.
+
+        Expected Result:
+          Verify that a connection was established and the devices are bonded.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Filtering, Scanning, GATT, Characteristic, OnPathAttacker
+        Priority: 1
+        """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        service_uuid = "3846D7A0-69C8-11E4-BA00-0002A5D5C51B"
+        test_uuid = "aa7edd5a-4d1d-4f0e-883a-d145616a1630"
+        bonded = False
+        characteristic = self.peripheral.sl4a.gattServerCreateBluetoothGattCharacteristic(
+            test_uuid, GattCharacteristic.PROPERTY_WRITE, GattCharacteristic.PERMISSION_WRITE_ENCRYPTED_MITM)
+        gatt_service = self.peripheral.sl4a.gattServerCreateService(service_uuid, GattServiceType.SERVICE_TYPE_PRIMARY)
+        self.peripheral.sl4a.gattServerAddCharacteristicToService(gatt_service, characteristic)
+        self.peripheral.sl4a.gattServerAddService(gatt_server, gatt_service)
+        result = self._find_service_added_event(gatt_server_cb, service_uuid)
+        if not result:
+            return False
+        bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+        self.bluetooth_gatt_list.append(bluetooth_gatt)
+        self.adv_instances.append(adv_callback)
+        if self.central.sl4a.gattClientDiscoverServices(bluetooth_gatt):
+            expected_event = GattCallbackString.GATT_SERV_DISC.format(gatt_callback)
+            try:
+                event = self.central.ed.pop_event(expected_event, self.default_timeout)
+            except Empty:
+                logging.error(GattCallbackError.GATT_SERV_DISC_ERR.format(expected_event))
+                return False
+            discovered_services_index = event['data']['ServicesIndex']
+        else:
+            logging.info("Failed to discover services.")
+            return False
+        test_value = [1, 2, 3, 4, 5, 6, 7]
+        services_count = self.central.sl4a.gattClientGetDiscoveredServicesCount(discovered_services_index)
+        for i in range(services_count):
+            characteristic_uuids = (self.central.sl4a.gattClientGetDiscoveredCharacteristicUuids(
+                discovered_services_index, i))
+            for characteristic_uuid in characteristic_uuids:
+                if characteristic_uuid == test_uuid:
+                    self.central.sl4a.bluetoothStartPairingHelper()
+                    self.peripheral.sl4a.bluetoothStartPairingHelper()
+                    self.central.sl4a.gattClientCharacteristicSetValue(bluetooth_gatt, discovered_services_index, i,
+                                                                       characteristic_uuid, test_value)
+                    self.central.sl4a.gattClientWriteCharacteristic(bluetooth_gatt, discovered_services_index, i,
+                                                                    characteristic_uuid)
+                    start_time = time.time() + self.default_timeout
+                    target_name = self.peripheral.sl4a.bluetoothGetLocalName()
+                    while time.time() < start_time and bonded == False:
+                        bonded_devices = \
+                            self.central.sl4a.bluetoothGetBondedDevices()
+                        for device in bonded_devices:
+                            if ('name' in device.keys() and device['name'] == target_name):
+                                bonded = True
+                                break
+                    bonded = False
+                    target_name = self.central.sl4a.bluetoothGetLocalName()
+                    while time.time() < start_time and bonded == False:
+                        bonded_devices = \
+                            self.peripheral.sl4a.bluetoothGetBondedDevices()
+                        for device in bonded_devices:
+                            if ('name' in device.keys() and device['name'] == target_name):
+                                bonded = True
+                                break
+
+        # Dual mode devices will establish connection over the classic transport,
+        # in order to establish bond over both transports, and do SDP. Starting
+        # disconnection before all this is finished is not safe, might lead to
+        # race conditions, i.e. bond over classic tranport shows up after LE
+        # bond is already removed.
+        time.sleep(4)
+
+        for ad in [self.central, self.peripheral]:
+            if not clear_bonded_devices(ad):
+                return False
+
+        # Necessary sleep time for entries to update unbonded state
+        time.sleep(2)
+
+        for ad in [self.central, self.peripheral]:
+            bonded_devices = ad.sl4a.bluetoothGetBondedDevices()
+            if len(bonded_devices) > 0:
+                logging.error("Failed to unbond devices: {}".format(bonded_devices))
+                return False
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='cc3fc361-7bf1-4ee2-9e46-4a27c88ce6a8')
+    def test_gatt_connect_get_connected_devices(self):
+        """Test GATT connections show up in getConnectedDevices
+
+        Test establishing a gatt connection between a GATT server and GATT
+        client. Verify that active connections show up using
+        BluetoothManager.getConnectedDevices API.
+
+        Steps:
+          1. Start a generic advertisement.
+          2. Start a generic scanner.
+          3. Find the advertisement and extract the mac address.
+          4. Stop the first scanner.
+          5. Create a GATT connection between the scanner and advertiser.
+          7. Verify the GATT Client has an open connection to the GATT Server.
+          8. Verify the GATT Server has an open connection to the GATT Client.
+          9. Disconnect the GATT connection.
+
+        Expected Result:
+          Verify that a connection was established, connected devices are found
+          on both the central and peripheral devices, and then disconnected
+          successfully.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Scanning, GATT
+        Priority: 2
+        """
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+        self.gatt_server_list.append(gatt_server)
+        try:
+            bluetooth_gatt, gatt_callback, adv_callback = (orchestrate_gatt_connection(self.central, self.peripheral))
+            self.bluetooth_gatt_list.append(bluetooth_gatt)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        conn_cen_devices = self.central.sl4a.bluetoothGetConnectedLeDevices(BluetoothProfile.GATT)
+        conn_per_devices = self.peripheral.sl4a.bluetoothGetConnectedLeDevices(BluetoothProfile.GATT_SERVER)
+        target_name = self.peripheral.sl4a.bluetoothGetLocalName()
+        error_message = ("Connected device {} not found in list of connected " "devices {}")
+        if not any(d['name'] == target_name for d in conn_cen_devices):
+            logging.error(error_message.format(target_name, conn_cen_devices))
+            return False
+        # For the GATT server only check the size of the list since
+        # it may or may not include the device name.
+        target_name = self.central.sl4a.bluetoothGetLocalName()
+        if not conn_per_devices:
+            logging.error(error_message.format(target_name, conn_per_devices))
+            return False
+        self.adv_instances.append(adv_callback)
+        return self._orchestrate_gatt_disconnection(bluetooth_gatt, gatt_callback)
+
+    @test_tracker_info(uuid='a0a37ca6-9fa8-4d35-9fdb-0e25b4b8a363')
+    def test_gatt_connect_second_adv_after_canceling_first_adv(self):
+        """Test GATT connection to peripherals second advertising address.
+
+        Test the ability of cancelling GATT connections and trying to reconnect
+        to the same device via a different address.
+
+        Steps:
+          1. A starts advertising
+          2. B starts scanning and finds A's mac address
+          3. Stop advertisement from step 1. Start a new advertisement on A and
+            find the new new mac address, B knows of both old and new address.
+          4. B1 sends connect request to old address of A
+          5. B1 cancel connect attempt after 10 seconds
+          6. B1 sends connect request to new address of A
+          7. Verify B1 establish connection to A in less than 10 seconds
+
+        Expected Result:
+          Verify that a connection was established only on the second
+          advertisement's mac address.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, Advertising, Scanning, GATT
+        Priority: 3
+        """
+        autoconnect = False
+        transport = GattTransport.TRANSPORT_AUTO
+        opportunistic = False
+        # Setup a basic Gatt server on the peripheral
+        gatt_server_cb = self.peripheral.sl4a.gattServerCreateGattServerCallback()
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_cb)
+
+        # Set advertisement settings to include local name in advertisement
+        # and set the advertising mode to low_latency.
+        self.peripheral.sl4a.bleSetAdvertiseSettingsIsConnectable(True)
+        self.peripheral.sl4a.bleSetAdvertiseDataIncludeDeviceName(True)
+        self.peripheral.sl4a.bleSetAdvertiseSettingsAdvertiseMode(BleAdvertiseSettingsMode.LOW_LATENCY)
+
+        # Setup necessary advertisement objects.
+        advertise_data = self.peripheral.sl4a.bleBuildAdvertiseData()
+        advertise_settings = self.peripheral.sl4a.bleBuildAdvertiseSettings()
+        advertise_callback = self.peripheral.sl4a.bleGenBleAdvertiseCallback()
+
+        # Step 1: Start advertisement
+        self.peripheral.sl4a.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings)
+
+        # Setup scan settings for low_latency scanning and to include the local name
+        # of the advertisement started in step 1.
+        filter_list = self.central.sl4a.bleGenFilterList()
+        self.central.sl4a.bleSetScanSettingsNumOfMatches(BleScanSettingsMatchNums.ONE)
+        self.central.sl4a.bleSetScanFilterDeviceName(self.peripheral.sl4a.bluetoothGetLocalName())
+        self.central.sl4a.bleBuildScanFilter(filter_list)
+        self.central.sl4a.bleSetScanSettingsScanMode(BleScanSettingsModes.LOW_LATENCY)
+
+        # Setup necessary scan objects.
+        scan_settings = self.central.sl4a.bleBuildScanSetting()
+        scan_callback = self.central.sl4a.bleGenScanCallback()
+
+        # Step 2: Start scanning on central Android device and find peripheral
+        # address.
+        self.central.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
         expected_event_name = scan_result.format(scan_callback)
+        try:
+            mac_address_pre_restart = self.central.ed.pop_event(
+                expected_event_name, self.default_timeout)['data']['Result']['deviceInfo']['address']
+            logging.info("Peripheral advertisement found with mac address: {}".format(mac_address_pre_restart))
+        except Empty:
+            logging.info("Peripheral advertisement not found")
+            test_result = False
 
-        # Start scanning on SL4A DUT
-        self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrkHexString(cert_public_address, int(addr_type), irk)
-        self.dut.sl4a.bleBuildScanFilter(filter_list)
-        self.dut.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
-        logging.info("Started scanning")
+        # Step 3: Restart peripheral advertising such that a new mac address is
+        # created.
+        self.peripheral.sl4a.bleStopBleAdvertising(advertise_callback)
+        self.peripheral.sl4a.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings)
 
-        # Verify that scan result is received on SL4A DUT
-        mac_address = self._wait_for_scan_result_event(expected_event_name, self.dut)
-        assertThat(mac_address).isNotNone()
-        logging.info("Filter advertisement with address {}".format(mac_address))
+        mac_address_post_restart = mac_address_pre_restart
 
-        # Stop scanning and try to connect GATT
-        self.dut.sl4a.bleStopBleScan(scan_callback)
-        gatt_callback = self.dut.sl4a.gattCreateGattCallback()
-        bluetooth_gatt = self.dut.sl4a.gattClientConnectGatt(gatt_callback, mac_address, False, gatt_transport['le'],
-                                                             False, None)
-        assertThat(bluetooth_gatt).isNotNone()
+        while True:
+            try:
+                mac_address_post_restart = self.central.ed.pop_event(
+                    expected_event_name, self.default_timeout)['data']['Result']['deviceInfo']['address']
+                logging.info("Peripheral advertisement found with mac address: {}".format(mac_address_post_restart))
+            except Empty:
+                logging.info("Peripheral advertisement not found")
+                test_result = False
 
-        # Verify that GATT connect event occurs on SL4A DUT
-        expected_event_name = gatt_connection_state_change.format(gatt_callback)
-        assertThat(self._wait_for_event(expected_event_name, self.dut)).isTrue()
-
-        # Test over
-        self.cert.sl4a.bleStopBleAdvertising(advertise_callback)
+            if mac_address_pre_restart != mac_address_post_restart:
+                break
 
 
 if __name__ == '__main__':
diff --git a/system/blueberry/tests/sl4a_sl4a/gatt/gatt_connect_with_irk_test.py b/system/blueberry/tests/sl4a_sl4a/gatt/gatt_connect_with_irk_test.py
new file mode 100644
index 0000000..eab98fe
--- /dev/null
+++ b/system/blueberry/tests/sl4a_sl4a/gatt/gatt_connect_with_irk_test.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2021 - 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 logging
+import os
+import queue
+
+from blueberry.facade import common_pb2 as common
+from blueberry.tests.gd.cert.context import get_current_context
+from blueberry.tests.gd.cert.truth import assertThat
+from blueberry.tests.gd_sl4a.lib.ble_lib import generate_ble_advertise_objects
+from blueberry.tests.gd_sl4a.lib.ble_lib import generate_ble_scan_objects
+from blueberry.tests.gd_sl4a.lib.bt_constants import adv_succ
+from blueberry.tests.gd_sl4a.lib.bt_constants import ble_address_types
+from blueberry.tests.gd_sl4a.lib.bt_constants import ble_advertise_settings_modes
+from blueberry.tests.gd_sl4a.lib.bt_constants import ble_scan_settings_modes
+from blueberry.tests.gd_sl4a.lib.bt_constants import scan_result
+from blueberry.tests.sl4a_sl4a.lib import sl4a_sl4a_base_test
+from blueberry.utils.bt_gatt_constants import GattCallbackString
+from blueberry.utils.bt_gatt_constants import GattTransport
+from mobly import test_runner
+from mobly.controllers.android_device_lib.adb import AdbError
+
+
+class GattConnectWithIrkTest(sl4a_sl4a_base_test.Sl4aSl4aBaseTestClass):
+
+    def setup_class(self):
+        super().setup_class()
+        self.default_timeout = 10  # seconds
+
+    def setup_test(self):
+        super().setup_test()
+
+    def teardown_test(self):
+        super().teardown_test()
+
+    def _wait_for_event(self, expected_event_name, device):
+        try:
+            event_info = device.ed.pop_event(expected_event_name, self.default_timeout)
+            logging.info(event_info)
+        except queue.Empty as error:
+            logging.error("Failed to find event: %s", expected_event_name)
+            return False
+        return True
+
+    def _wait_for_scan_result_event(self, expected_event_name, device):
+        try:
+            event_info = device.ed.pop_event(expected_event_name, self.default_timeout)
+        except queue.Empty as error:
+            logging.error("Could not find scan result event: %s", expected_event_name)
+            return None
+        return event_info['data']['Result']['deviceInfo']['address']
+
+    def _get_cert_public_address_and_irk_from_bt_config(self):
+        # Pull IRK from SL4A cert side to pass in from SL4A DUT side when scanning
+        bt_config_file_path = os.path.join(get_current_context().get_full_output_path(),
+                                           "DUT_%s_bt_config.conf" % self.cert.serial)
+        try:
+            self.cert.adb.pull(["/data/misc/bluedroid/bt_config.conf", bt_config_file_path])
+        except AdbError as error:
+            logging.error("Failed to pull SL4A cert BT config")
+            return False
+        logging.debug("Reading SL4A cert BT config")
+        with io.open(bt_config_file_path) as f:
+            for line in f.readlines():
+                stripped_line = line.strip()
+                if (stripped_line.startswith("Address")):
+                    address_fields = stripped_line.split(' ')
+                    # API currently requires public address to be capitalized
+                    address = address_fields[2].upper()
+                    logging.debug("Found cert address: %s" % address)
+                    continue
+                if (stripped_line.startswith("LE_LOCAL_KEY_IRK")):
+                    irk_fields = stripped_line.split(' ')
+                    irk = irk_fields[2]
+                    logging.debug("Found cert IRK: %s" % irk)
+                    continue
+
+        return address, irk
+
+    def test_scan_connect_unbonded_device_public_address_with_irk(self):
+        # Set up SL4A cert side to advertise
+        logging.info("Starting advertising")
+        self.cert.sl4a.bleSetAdvertiseSettingsIsConnectable(True)
+        self.cert.sl4a.bleSetAdvertiseDataIncludeDeviceName(True)
+        self.cert.sl4a.bleSetAdvertiseSettingsAdvertiseMode(ble_advertise_settings_modes['low_latency'])
+        self.cert.sl4a.bleSetAdvertiseSettingsOwnAddressType(common.RANDOM_DEVICE_ADDRESS)
+        advertise_callback, advertise_data, advertise_settings = generate_ble_advertise_objects(self.cert.sl4a)
+        self.cert.sl4a.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings)
+
+        # Wait for SL4A cert to start advertising
+        assertThat(self._wait_for_event(adv_succ.format(advertise_callback), self.cert)).isTrue()
+        logging.info("Advertising started")
+
+        # Pull IRK from SL4A cert side to pass in from SL4A DUT side when scanning
+        cert_public_address, irk = self._get_cert_public_address_and_irk_from_bt_config()
+
+        # Set up SL4A DUT side to scan
+        addr_type = ble_address_types["public"]
+        logging.info("Start scanning for PUBLIC_ADDRESS %s with address type %d and IRK %s" % (cert_public_address,
+                                                                                               addr_type, irk))
+        self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
+        self.dut.sl4a.bleSetScanSettingsLegacy(False)
+        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+        expected_event_name = scan_result.format(scan_callback)
+
+        # Start scanning on SL4A DUT
+        self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrkHexString(cert_public_address, int(addr_type), irk)
+        self.dut.sl4a.bleBuildScanFilter(filter_list)
+        self.dut.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
+        logging.info("Started scanning")
+
+        # Verify that scan result is received on SL4A DUT
+        mac_address = self._wait_for_scan_result_event(expected_event_name, self.dut)
+        assertThat(mac_address).isNotNone()
+        logging.info("Filter advertisement with address {}".format(mac_address))
+
+        # Stop scanning and try to connect GATT
+        self.dut.sl4a.bleStopBleScan(scan_callback)
+        gatt_callback = self.dut.sl4a.gattCreateGattCallback()
+        bluetooth_gatt = self.dut.sl4a.gattClientConnectGatt(gatt_callback, mac_address, False,
+                                                             GattTransport.TRANSPORT_LE, False, None)
+        assertThat(bluetooth_gatt).isNotNone()
+
+        # Verify that GATT connect event occurs on SL4A DUT
+        expected_event_name = GattCallbackString.GATT_CONN_CHANGE.format(gatt_callback)
+        assertThat(self._wait_for_event(expected_event_name, self.dut)).isTrue()
+
+        # Test over
+        self.cert.sl4a.bleStopBleAdvertising(advertise_callback)
+
+
+if __name__ == '__main__':
+    test_runner.main()
diff --git a/system/blueberry/tests/sl4a_sl4a/gatt/gatt_notify_test.py b/system/blueberry/tests/sl4a_sl4a/gatt/gatt_notify_test.py
new file mode 100644
index 0000000..5e02675
--- /dev/null
+++ b/system/blueberry/tests/sl4a_sl4a/gatt/gatt_notify_test.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""
+This test script exercises GATT notify/indicate procedures.
+
+Original location:
+  tools/test/connectivity/acts_tests/tests/google/ble/gatt/GattNotifyTest.py
+"""
+
+from blueberry.tests.gd.cert.test_decorators import test_tracker_info
+
+from blueberry.tests.gd.cert.truth import assertThat
+from blueberry.tests.sl4a_sl4a.lib import gatt_connected_base_test
+from blueberry.utils.bt_gatt_constants import GattCharDesc
+from blueberry.utils.bt_gatt_constants import GattDescriptor
+from blueberry.utils.bt_gatt_constants import GattEvent
+from mobly import test_runner
+
+
+class GattNotifyTest(gatt_connected_base_test.GattConnectedBaseTest):
+
+    @test_tracker_info(uuid='e0ba60af-c1f2-4516-a5d5-89e2def0c757')
+    def test_notify_char(self):
+        """Test notify characteristic value.
+
+        Test GATT notify characteristic value.
+
+        Steps:
+        1. Central: write CCC - register for notifications.
+        2. Peripheral: receive CCC modification.
+        3. Peripheral: send characteristic notification.
+        4. Central: receive notification, verify it's conent matches what was
+           sent
+
+        Expected Result:
+        Verify that notification registration and delivery works.
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: LE, GATT, Characteristic
+        Priority: 0
+        """
+        # write CCC descriptor to enable notifications
+        self.central.sl4a.gattClientDescriptorSetValue(
+            self.bluetooth_gatt, self.discovered_services_index, self.test_service_index, self.NOTIFIABLE_CHAR_UUID,
+            GattCharDesc.GATT_CLIENT_CHARAC_CFG_UUID, GattDescriptor.ENABLE_NOTIFICATION_VALUE)
+
+        self.central.sl4a.gattClientWriteDescriptor(self.bluetooth_gatt, self.discovered_services_index,
+                                                    self.test_service_index, self.NOTIFIABLE_CHAR_UUID,
+                                                    GattCharDesc.GATT_CLIENT_CHARAC_CFG_UUID)
+
+        # enable notifications in app
+        self.central.sl4a.gattClientSetCharacteristicNotification(self.bluetooth_gatt, self.discovered_services_index,
+                                                                  self.test_service_index, self.NOTIFIABLE_CHAR_UUID,
+                                                                  True)
+
+        event = self._server_wait(GattEvent.DESC_WRITE_REQ)
+
+        request_id = event['data']['requestId']
+        bt_device_id = 0
+        status = 0
+        # confirm notification registration was successful
+        self.peripheral.sl4a.gattServerSendResponse(self.gatt_server, bt_device_id, request_id, status, 0, [])
+        # wait for client to get response
+        event = self._client_wait(GattEvent.DESC_WRITE)
+
+        # set notified value
+        notified_value = [1, 5, 9, 7, 5, 3, 6, 4, 8, 2]
+        self.peripheral.sl4a.gattServerCharacteristicSetValue(self.notifiable_char_index, notified_value)
+
+        # send notification
+        self.peripheral.sl4a.gattServerNotifyCharacteristicChanged(self.gatt_server, bt_device_id,
+                                                                   self.notifiable_char_index, False)
+
+        # wait for client to receive the notification
+        event = self._client_wait(GattEvent.CHAR_CHANGE)
+        assertThat(event["data"]["CharacteristicValue"]).isEqualTo(notified_value)
+
+        return True
+
+
+if __name__ == '__main__':
+    test_runner.main()
diff --git a/system/blueberry/tests/sl4a_sl4a/lib/gatt_connected_base_test.py b/system/blueberry/tests/sl4a_sl4a/lib/gatt_connected_base_test.py
new file mode 100644
index 0000000..9a4e606
--- /dev/null
+++ b/system/blueberry/tests/sl4a_sl4a/lib/gatt_connected_base_test.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""
+This is base class for tests that exercises different GATT procedures between two connected devices.
+Setup/Teardown methods take care of establishing connection, and doing GATT DB initialization/discovery.
+
+Original file:
+    tools/test/connectivity/acts_tests/acts_contrib/test_utils/bt/GattConnectedBaseTest.py
+"""
+
+import logging
+from queue import Empty
+
+from blueberry.tests.gd_sl4a.lib.bt_constants import bt_default_timeout
+from blueberry.tests.sl4a_sl4a.lib.sl4a_sl4a_base_test import Sl4aSl4aBaseTestClass
+from blueberry.utils.bt_gatt_constants import GattCallbackError
+from blueberry.utils.bt_gatt_constants import GattCallbackString
+from blueberry.utils.bt_gatt_constants import GattCharDesc
+from blueberry.utils.bt_gatt_constants import GattCharacteristic
+from blueberry.utils.bt_gatt_constants import GattDescriptor
+from blueberry.utils.bt_gatt_constants import GattEvent
+from blueberry.utils.bt_gatt_constants import GattMtuSize
+from blueberry.utils.bt_gatt_constants import GattServiceType
+from blueberry.utils.bt_gatt_utils import GattTestUtilsError
+from blueberry.utils.bt_gatt_utils import disconnect_gatt_connection
+from blueberry.utils.bt_gatt_utils import orchestrate_gatt_connection
+from blueberry.utils.bt_gatt_utils import setup_gatt_characteristics
+from blueberry.utils.bt_gatt_utils import setup_gatt_descriptors
+
+
+class GattConnectedBaseTest(Sl4aSl4aBaseTestClass):
+
+    TEST_SERVICE_UUID = "3846D7A0-69C8-11E4-BA00-0002A5D5C51B"
+    READABLE_CHAR_UUID = "21c0a0bf-ad51-4a2d-8124-b74003e4e8c8"
+    READABLE_DESC_UUID = "aa7edd5a-4d1d-4f0e-883a-d145616a1630"
+    WRITABLE_CHAR_UUID = "aa7edd5a-4d1d-4f0e-883a-d145616a1630"
+    WRITABLE_DESC_UUID = "76d5ed92-ca81-4edb-bb6b-9f019665fb32"
+    NOTIFIABLE_CHAR_UUID = "b2c83efa-34ca-11e6-ac61-9e71128cae77"
+
+    def setup_class(self):
+        super().setup_class()
+        self.central = self.dut
+        self.peripheral = self.cert
+
+    def setup_test(self):
+        super(GattConnectedBaseTest, self).setup_test()
+
+        self.gatt_server_callback, self.gatt_server = \
+            self._setup_multiple_services()
+        if not self.gatt_server_callback or not self.gatt_server:
+            raise AssertionError('Service setup failed')
+
+        self.bluetooth_gatt, self.gatt_callback, self.adv_callback = (orchestrate_gatt_connection(
+            self.central, self.peripheral))
+        self.peripheral.sl4a.bleStopBleAdvertising(self.adv_callback)
+
+        self.mtu = GattMtuSize.MIN
+
+        if self.central.sl4a.gattClientDiscoverServices(self.bluetooth_gatt):
+            event = self._client_wait(GattEvent.GATT_SERV_DISC)
+            self.discovered_services_index = event['data']['ServicesIndex']
+        services_count = self.central.sl4a.gattClientGetDiscoveredServicesCount(self.discovered_services_index)
+        self.test_service_index = None
+        for i in range(services_count):
+            disc_service_uuid = (self.central.sl4a.gattClientGetDiscoveredServiceUuid(
+                self.discovered_services_index, i).upper())
+            if disc_service_uuid == self.TEST_SERVICE_UUID:
+                self.test_service_index = i
+                break
+
+        if not self.test_service_index:
+            print("Service not found")
+            return False
+
+        connected_device_list = self.peripheral.sl4a.gattServerGetConnectedDevices(self.gatt_server)
+        if len(connected_device_list) == 0:
+            logging.info("No devices connected from peripheral.")
+            return False
+
+        return True
+
+    def teardown_test(self):
+        self.peripheral.sl4a.gattServerClearServices(self.gatt_server)
+        self.peripheral.sl4a.gattServerClose(self.gatt_server)
+
+        del self.gatt_server_callback
+        del self.gatt_server
+
+        self._orchestrate_gatt_disconnection(self.bluetooth_gatt, self.gatt_callback)
+
+        return super(GattConnectedBaseTest, self).teardown_test()
+
+    def _server_wait(self, gatt_event):
+        return self._timed_pop(gatt_event, self.peripheral, self.gatt_server_callback)
+
+    def _client_wait(self, gatt_event: GattEvent):
+        return self._timed_pop(gatt_event, self.central, self.gatt_callback)
+
+    def _timed_pop(self, gatt_event: GattEvent, sl4a, gatt_callback):
+        expected_event = gatt_event["evt"].format(gatt_callback)
+        try:
+            return sl4a.ed.pop_event(expected_event, bt_default_timeout)
+        except Empty as emp:
+            raise AssertionError(gatt_event["err"].format(expected_event))
+
+    def _setup_characteristics_and_descriptors(self, droid):
+        characteristic_input = [
+            {
+                'uuid': self.WRITABLE_CHAR_UUID,
+                'property': GattCharacteristic.PROPERTY_WRITE | GattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
+                'permission': GattCharacteristic.PERMISSION_WRITE
+            },
+            {
+                'uuid': self.READABLE_CHAR_UUID,
+                'property': GattCharacteristic.PROPERTY_READ,
+                'permission': GattCharacteristic.PROPERTY_READ
+            },
+            {
+                'uuid': self.NOTIFIABLE_CHAR_UUID,
+                'property': GattCharacteristic.PROPERTY_NOTIFY | GattCharacteristic.PROPERTY_INDICATE,
+                'permission': GattCharacteristic.PERMISSION_READ
+            },
+        ]
+        descriptor_input = [{
+            'uuid': self.WRITABLE_DESC_UUID,
+            'property': GattDescriptor.PERMISSION_READ[0] | GattDescriptor.PERMISSION_WRITE[0]
+        }, {
+            'uuid': self.READABLE_DESC_UUID,
+            'property': GattDescriptor.PERMISSION_READ[0] | GattDescriptor.PERMISSION_WRITE[0],
+        }, {
+            'uuid': GattCharDesc.GATT_CLIENT_CHARAC_CFG_UUID,
+            'property': GattDescriptor.PERMISSION_READ[0] | GattDescriptor.PERMISSION_WRITE[0],
+        }]
+        characteristic_list = setup_gatt_characteristics(droid, characteristic_input)
+        self.notifiable_char_index = characteristic_list[2]
+        descriptor_list = setup_gatt_descriptors(droid, descriptor_input)
+        return characteristic_list, descriptor_list
+
+    def _orchestrate_gatt_disconnection(self, bluetooth_gatt, gatt_callback):
+        logging.info("Disconnecting from peripheral device.")
+        try:
+            disconnect_gatt_connection(self.central, bluetooth_gatt, gatt_callback)
+        except GattTestUtilsError as err:
+            logging.error(err)
+            return False
+        self.central.sl4a.gattClientClose(bluetooth_gatt)
+        return True
+
+    def _find_service_added_event(self, gatt_server_callback, uuid):
+        expected_event = GattCallbackString.SERV_ADDED.format(gatt_server_callback)
+        try:
+            event = self.peripheral.sl4a.ed.pop_event(expected_event, bt_default_timeout)
+        except Empty:
+            logging.error(GattCallbackError.SERV_ADDED_ERR.format(expected_event))
+            return False
+        if event['data']['serviceUuid'].lower() != uuid.lower():
+            logging.error("Uuid mismatch. Found: {}, Expected {}.".format(event['data']['serviceUuid'], uuid))
+            return False
+        return True
+
+    def _setup_multiple_services(self):
+        gatt_server_callback = (self.peripheral.sl4a.gattServerCreateGattServerCallback())
+        gatt_server = self.peripheral.sl4a.gattServerOpenGattServer(gatt_server_callback)
+        characteristic_list, descriptor_list = (self._setup_characteristics_and_descriptors(self.peripheral.sl4a))
+        self.peripheral.sl4a.gattServerCharacteristicAddDescriptor(characteristic_list[0], descriptor_list[0])
+        self.peripheral.sl4a.gattServerCharacteristicAddDescriptor(characteristic_list[1], descriptor_list[1])
+        self.peripheral.sl4a.gattServerCharacteristicAddDescriptor(characteristic_list[2], descriptor_list[2])
+        gatt_service3 = self.peripheral.sl4a.gattServerCreateService(self.TEST_SERVICE_UUID,
+                                                                     GattServiceType.SERVICE_TYPE_PRIMARY)
+        for characteristic in characteristic_list:
+            self.peripheral.sl4a.gattServerAddCharacteristicToService(gatt_service3, characteristic)
+        self.peripheral.sl4a.gattServerAddService(gatt_server, gatt_service3)
+        result = self._find_service_added_event(gatt_server_callback, self.TEST_SERVICE_UUID)
+        if not result:
+            return False, False
+        return gatt_server_callback, gatt_server
diff --git a/system/blueberry/tests/sl4a_sl4a/lib/sl4a_sl4a_base_test.py b/system/blueberry/tests/sl4a_sl4a/lib/sl4a_sl4a_base_test.py
index 14eeaf4..86efbd2 100644
--- a/system/blueberry/tests/sl4a_sl4a/lib/sl4a_sl4a_base_test.py
+++ b/system/blueberry/tests/sl4a_sl4a/lib/sl4a_sl4a_base_test.py
@@ -14,79 +14,68 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-import traceback
-import os
 import logging
-
+import os
+import traceback
 from functools import wraps
-from grpc import RpcError
-
-from mobly import signals
-from mobly.base_test import BaseTestClass
-from mobly.controllers.android_device_lib.adb import AdbError
-from mobly.controllers import android_device
-from mobly.controllers.android_device import MOBLY_CONTROLLER_CONFIG_NAME as ANDROID_DEVICE_CONFIG_NAME
-from mobly.controllers.android_device_lib.jsonrpc_client_base import \
-  AppRestoreConnectionError
-from mobly.controllers.android_device_lib.services.sl4a_service import Sl4aService
-import mobly.controllers.android_device_lib.sl4a_client as sl4a_client
 
 from blueberry.tests.gd.cert.context import get_current_context
-from blueberry.tests.gd_sl4a.lib.ble_lib import enable_bluetooth, disable_bluetooth, BleLib
+from blueberry.tests.gd_sl4a.lib.ble_lib import BleLib
+from blueberry.tests.gd_sl4a.lib.ble_lib import disable_bluetooth
+from blueberry.tests.gd_sl4a.lib.ble_lib import enable_bluetooth
+from blueberry.utils.mobly_sl4a_utils import setup_sl4a
+from blueberry.utils.mobly_sl4a_utils import teardown_sl4a
+from grpc import RpcError
+from mobly import signals
+from mobly.base_test import BaseTestClass
+from mobly.controllers import android_device
+from mobly.controllers.android_device import MOBLY_CONTROLLER_CONFIG_NAME as ANDROID_DEVICE_CONFIG_NAME
+from mobly.controllers.android_device_lib.adb import AdbError
 
 
 class Sl4aSl4aBaseTestClass(BaseTestClass):
 
     SUBPROCESS_WAIT_TIMEOUT_SECONDS = 10
 
-    def setup_sl4a(self, device, server_port, forwarded_port):
-        device.services.register('sl4a', Sl4aService, start_service=False)
-        sl4a_client._DEVICE_SIDE_PORT = server_port
-        sl4a_client._APP_START_WAIT_TIME = 0.5
-        try:
-            device.sl4a.start()
-        except AppRestoreConnectionError:
-            pass
-        try:
-            device.sl4a.clear_host_port()
-        except AdbError:
-            pass
-        sl4a_client._APP_START_WAIT_TIME = 2 * 60
-        device.sl4a.restore_app_connection(port=forwarded_port)
-
     def setup_class(self):
         self.log_path_base = get_current_context().get_full_output_path()
         self.verbose_mode = bool(self.user_params.get('verbose_mode', False))
 
         # Parse and construct Android device objects
         self.android_devices = self.register_controller(android_device, required=True)
-        self.cert = self.android_devices[1]
-        server_port = int(self.controller_configs[ANDROID_DEVICE_CONFIG_NAME][1]['server_port'])
-        forwarded_port = int(self.controller_configs[ANDROID_DEVICE_CONFIG_NAME][1]['forwarded_port'])
-        self.setup_sl4a(self.cert, server_port, forwarded_port)
 
+        # Setup SL4A for dut, overriding default mobly port settings
         self.dut = self.android_devices[0]
         server_port = int(self.controller_configs[ANDROID_DEVICE_CONFIG_NAME][0]['server_port'])
         forwarded_port = int(self.controller_configs[ANDROID_DEVICE_CONFIG_NAME][0]['forwarded_port'])
+        setup_sl4a(self.dut, server_port, forwarded_port)
 
-        sl4a_client._DEVICE_SIDE_PORT = server_port
-        sl4a_client._APP_START_WAIT_TIME = 0.5
-        self.setup_sl4a(self.dut, server_port, forwarded_port)
+        # Setup SL4A for cert, overriding default mobly port settings
+        self.cert = self.android_devices[1]
+        server_port = int(self.controller_configs[ANDROID_DEVICE_CONFIG_NAME][1]['server_port'])
+        forwarded_port = int(self.controller_configs[ANDROID_DEVICE_CONFIG_NAME][1]['forwarded_port'])
+        setup_sl4a(self.cert, server_port, forwarded_port)
 
         # Enable full btsnoop log
+        self.dut.adb.root()
         self.dut.adb.shell("setprop persist.bluetooth.btsnooplogmode full")
-        getprop_result = self.dut.adb.shell("getprop persist.bluetooth.btsnooplogmode") == "full"
-        if not getprop_result:
-            self.dut.log.warning("Failed to enable Bluetooth Hci Snoop Logging.")
+        getprop_result = self.dut.adb.getprop("persist.bluetooth.btsnooplogmode")
+        if getprop_result is None or ("full" not in getprop_result.lower()):
+            self.dut.log.warning("Failed to enable Bluetooth HCI Snoop Logging on DUT, mode is {}"
+                                 .format(getprop_result))
+        self.cert.adb.root()
         self.cert.adb.shell("setprop persist.bluetooth.btsnooplogmode full")
-        getprop_result = self.cert.adb.shell("getprop persist.bluetooth.btsnooplogmode") == "full"
-        if not getprop_result:
-            self.cert.log.warning("Failed to enable Bluetooth Hci Snoop Logging.")
+        getprop_result = self.cert.adb.getprop("persist.bluetooth.btsnooplogmode")
+        if getprop_result is None or ("full" not in getprop_result.lower()):
+            self.cert.log.warning("Failed to enable Bluetooth HCI Snoop Logging on CERT, mode is {}"
+                                  .format(getprop_result))
 
         self.ble = BleLib(dut=self.dut)
 
     def teardown_class(self):
-        pass
+        teardown_sl4a(self.cert)
+        teardown_sl4a(self.dut)
+        super().teardown_class()
 
     def setup_device_for_test(self, device):
         device.ed.clear_all_events()
diff --git a/system/blueberry/tests/sl4a_sl4a/sl4a_sl4a_device_config.yaml b/system/blueberry/tests/sl4a_sl4a/sl4a_sl4a_device_config.yaml
index 0c14066..e96a272 100644
--- a/system/blueberry/tests/sl4a_sl4a/sl4a_sl4a_device_config.yaml
+++ b/system/blueberry/tests/sl4a_sl4a/sl4a_sl4a_device_config.yaml
@@ -4,8 +4,6 @@
   Controllers:
     AndroidDevice:
     - label: dut
-      # Preferred client port number on the PC host side for SL4A
-      client_port: '8893'
       # Preferred server port number forwarded from Android to the host PC
       # via adb for SL4A connections
       forwarded_port: '8897'
@@ -13,8 +11,6 @@
       server_port: '8897'
       serial: 'DUT'
     - label: cert
-      # Preferred client port number on the PC host side for SL4A
-      client_port: '8895'
       # Preferred server port number forwarded from Android to the host PC
       # via adb for SL4A connections
       forwarded_port: '8899'
diff --git a/system/blueberry/tests/sl4a_sl4a/sl4a_sl4a_test_runner.py b/system/blueberry/tests/sl4a_sl4a/sl4a_sl4a_test_runner.py
index 96c4736..07641ff 100644
--- a/system/blueberry/tests/sl4a_sl4a/sl4a_sl4a_test_runner.py
+++ b/system/blueberry/tests/sl4a_sl4a/sl4a_sl4a_test_runner.py
@@ -15,11 +15,17 @@
 #   limitations under the License.
 
 from blueberry.tests.sl4a_sl4a.gatt.gatt_connect_test import GattConnectTest
+from blueberry.tests.sl4a_sl4a.gatt.gatt_connect_with_irk_test import GattConnectWithIrkTest
+from blueberry.tests.sl4a_sl4a.gatt.gatt_notify_test import GattNotifyTest
 
 from mobly import suite_runner
 import argparse
 
-ALL_TESTS = [GattConnectTest]
+ALL_TESTS = [
+    GattConnectTest,
+    GattConnectWithIrk,
+    GattNotifyTest,
+]
 
 
 def main():
diff --git a/system/blueberry/utils/android_bluetooth_decorator.py b/system/blueberry/utils/android_bluetooth_decorator.py
index b258d6b..e3c18d3 100644
--- a/system/blueberry/utils/android_bluetooth_decorator.py
+++ b/system/blueberry/utils/android_bluetooth_decorator.py
@@ -24,9 +24,9 @@
 
 from blueberry.controllers import derived_bt_device
 from blueberry.utils import bt_constants
+from blueberry.utils import ble_scan_adv_constants
 from blueberry.utils import bt_test_utils
 
-
 # Map for media passthrough commands and the corresponding events.
 MEDIA_CMD_MAP = {
     bt_constants.CMD_MEDIA_PAUSE: bt_constants.EVENT_PAUSE_RECEIVED,
@@ -65,70 +65,64 @@
 
 
 class DeviceBootError(signals.ControllerError):
-  """Exception raised for Android device boot failures."""
-  pass
+    """Exception raised for Android device boot failures."""
+    pass
 
 
 class Error(Exception):
-  """Raised when an operation in this module fails."""
-  pass
+    """Raised when an operation in this module fails."""
+    pass
 
 
 class DiscoveryError(signals.ControllerError):
-  """Exception raised for Bluetooth device discovery failures."""
-  pass
+    """Exception raised for Bluetooth device discovery failures."""
+    pass
 
 
 class AndroidBluetoothDecorator(android_device.AndroidDevice):
-  """Decorates an AndroidDevice with Bluetooth-specific functionality."""
+    """Decorates an AndroidDevice with Bluetooth-specific functionality."""
 
-  def __init__(self, ad: android_device.AndroidDevice):
-    self._ad = ad
-    self._user_params = None
-    if not self._ad or not isinstance(self._ad, android_device.AndroidDevice):
-      raise TypeError('Must apply AndroidBluetoothDecorator to an '
-                      'AndroidDevice')
-    self.ble_advertise_callback = None
-    self.regex_logcat_time = re.compile(
-        r'(?P<datetime>[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}.[\d]{3})'
-        r'[ ]+\d+.*')
-    self._regex_bt_crash = re.compile(
-        r'Bluetooth crashed (?P<num_bt_crashes>\d+) times')
+    def __init__(self, ad: android_device.AndroidDevice):
+        self._ad = ad
+        self._user_params = None
+        if not self._ad or not isinstance(self._ad, android_device.AndroidDevice):
+            raise TypeError('Must apply AndroidBluetoothDecorator to an ' 'AndroidDevice')
+        self.ble_advertise_callback = None
+        self.regex_logcat_time = re.compile(r'(?P<datetime>[\d]{2}-[\d]{2} [\d]{2}:[\d]{2}:[\d]{2}.[\d]{3})'
+                                            r'[ ]+\d+.*')
+        self._regex_bt_crash = re.compile(r'Bluetooth crashed (?P<num_bt_crashes>\d+) times')
 
-  def __getattr__(self, name: Any) -> Any:
-    return getattr(self._ad, name)
+    def __getattr__(self, name: Any) -> Any:
+        return getattr(self._ad, name)
 
-  def _is_device_connected(self, mac_address):
-    """Wrapper method to help with unit testability of this class."""
-    return self._ad.sl4a.bluetoothIsDeviceConnected(mac_address)
+    def _is_device_connected(self, mac_address):
+        """Wrapper method to help with unit testability of this class."""
+        return self._ad.sl4a.bluetoothIsDeviceConnected(mac_address)
 
-  def _is_profile_connected(self, mac_address, profile):
-    """Checks if the profile is connected."""
-    status = None
-    pri_ad = self._ad
-    if profile == bt_constants.BluetoothProfile.HEADSET_CLIENT:
-      status = pri_ad.sl4a.bluetoothHfpClientGetConnectionStatus(mac_address)
-    elif profile == bt_constants.BluetoothProfile.A2DP_SINK:
-      status = pri_ad.sl4a.bluetoothA2dpSinkGetConnectionStatus(mac_address)
-    elif profile == bt_constants.BluetoothProfile.PBAP_CLIENT:
-      status = pri_ad.sl4a.bluetoothPbapClientGetConnectionStatus(mac_address)
-    elif profile == bt_constants.BluetoothProfile.MAP_MCE:
-      connected_devices = self._ad.sl4a.bluetoothMapClientGetConnectedDevices()
-      return any(
-          mac_address in device['address'] for device in connected_devices)
-    else:
-      pri_ad.log.warning(
-          'The connection check for profile %s is not supported '
-          'yet', profile)
-      return False
-    return status == bt_constants.BluetoothConnectionStatus.STATE_CONNECTED
+    def _is_profile_connected(self, mac_address, profile):
+        """Checks if the profile is connected."""
+        status = None
+        pri_ad = self._ad
+        if profile == bt_constants.BluetoothProfile.HEADSET_CLIENT:
+            status = pri_ad.sl4a.bluetoothHfpClientGetConnectionStatus(mac_address)
+        elif profile == bt_constants.BluetoothProfile.A2DP_SINK:
+            status = pri_ad.sl4a.bluetoothA2dpSinkGetConnectionStatus(mac_address)
+        elif profile == bt_constants.BluetoothProfile.PBAP_CLIENT:
+            status = pri_ad.sl4a.bluetoothPbapClientGetConnectionStatus(mac_address)
+        elif profile == bt_constants.BluetoothProfile.MAP_MCE:
+            connected_devices = self._ad.sl4a.bluetoothMapClientGetConnectedDevices()
+            return any(mac_address in device['address'] for device in connected_devices)
+        else:
+            pri_ad.log.warning('The connection check for profile %s is not supported ' 'yet', profile)
+            return False
+        return status == bt_constants.BluetoothConnectionStatus.STATE_CONNECTED
 
-  def _get_bluetooth_le_state(self):
-    """Wrapper method to help with unit testability of this class."""
-    return self._ad.sl4a.bluetoothGetLeState
+    def _get_bluetooth_le_state(self):
+        """Wrapper method to help with unit testability of this class."""
+        return self._ad.sl4a.bluetoothGetLeState
 
-  def _generate_id_by_size(self, size):
-    """Generate string of random ascii letters and digits.
+    def _generate_id_by_size(self, size):
+        """Generate string of random ascii letters and digits.
 
     Args:
       size: required size of string.
@@ -136,15 +130,10 @@
     Returns:
       String of random chars.
     """
-    return ''.join(
-        random.choice(string.ascii_letters + string.digits)
-        for _ in range(size))
+        return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(size))
 
-  def _wait_for_bluetooth_manager_state(self,
-                                        state=None,
-                                        timeout=10,
-                                        threshold=5):
-    """Waits for Bluetooth normalized state or normalized explicit state.
+    def _wait_for_bluetooth_manager_state(self, state=None, timeout=10, threshold=5):
+        """Waits for Bluetooth normalized state or normalized explicit state.
 
     Args:
       state: expected Bluetooth state
@@ -153,76 +142,75 @@
     Returns:
       True if successful, false if unsuccessful.
     """
-    all_states = []
-    start_time = time.time()
-    while time.time() < start_time + timeout:
-      all_states.append(self._get_bluetooth_le_state())
-      if len(all_states) >= threshold:
-        # for any normalized state
-        if state is None:
-          if len(all_states[-threshold:]) == 1:
-            logging.info('State normalized %s', all_states[-threshold:])
-            return True
-        else:
-          # explicit check against normalized state
-          if state in all_states[-threshold:]:
-            return True
-      time.sleep(0.5)
-    logging.error(
-        'Bluetooth state fails to normalize' if state is None else
-        'Failed to match bluetooth state, current state {} expected state {}'
-        .format(self._get_bluetooth_le_state(), state))
-    return False
+        all_states = []
+        start_time = time.time()
+        while time.time() < start_time + timeout:
+            all_states.append(self._get_bluetooth_le_state())
+            if len(all_states) >= threshold:
+                # for any normalized state
+                if state is None:
+                    if len(all_states[-threshold:]) == 1:
+                        logging.info('State normalized %s', all_states[-threshold:])
+                        return True
+                else:
+                    # explicit check against normalized state
+                    if state in all_states[-threshold:]:
+                        return True
+            time.sleep(0.5)
+        logging.error('Bluetooth state fails to normalize'
+                      if state is None else 'Failed to match bluetooth state, current state {} expected state {}'
+                      .format(self._get_bluetooth_le_state(), state))
+        return False
 
-  def init_setup(self) -> None:
-    """Sets up android device for bluetooth tests."""
-    self._ad.services.register('sl4a', sl4a_service.Sl4aService)
-    self._ad.load_snippet('mbs', 'com.google.android.mobly.snippet.bundled')
-    self._ad.adb.shell('setenforce 0')
+    def init_setup(self) -> None:
+        """Sets up android device for bluetooth tests."""
+        self._ad.services.register('sl4a', sl4a_service.Sl4aService)
+        self._ad.load_snippet('mbs', 'com.google.android.mobly.snippet.bundled')
+        self._ad.adb.shell('setenforce 0')
 
-    # Adds 2 seconds waiting time to see it can fix the NullPointerException
-    # when executing the following sl4a.bluetoothStartPairingHelper method.
-    time.sleep(2)
-    self._ad.sl4a.bluetoothStartPairingHelper()
-    self.factory_reset_bluetooth()
+        # Adds 2 seconds waiting time to see it can fix the NullPointerException
+        # when executing the following sl4a.bluetoothStartPairingHelper method.
+        time.sleep(2)
+        self._ad.sl4a.bluetoothStartPairingHelper()
+        self.factory_reset_bluetooth()
 
-  def sl4a_setup(self) -> None:
-    """A common setup routine for android device sl4a function.
+    def sl4a_setup(self) -> None:
+        """A common setup routine for android device sl4a function.
 
     Things this method setup:
     1. Set Bluetooth local name to random string of size 4
     2. Disable BLE background scanning.
     """
 
-    sl4a = self._ad.sl4a
-    sl4a.bluetoothStartConnectionStateChangeMonitor('')
-    setup_result = sl4a.bluetoothSetLocalName(self._generate_id_by_size(4))
-    if not setup_result:
-      self.log.error('Failed to set device name.')
-      return
-    sl4a.bluetoothDisableBLE()
-    bonded_devices = sl4a.bluetoothGetBondedDevices()
-    for b in bonded_devices:
-      self.log.info('Removing bond for device {}'.format(b['address']))
-      sl4a.bluetoothUnbond(b['address'])
+        sl4a = self._ad.sl4a
+        sl4a.bluetoothStartConnectionStateChangeMonitor('')
+        setup_result = sl4a.bluetoothSetLocalName(self._generate_id_by_size(4))
+        if not setup_result:
+            self.log.error('Failed to set device name.')
+            return
+        sl4a.bluetoothDisableBLE()
+        bonded_devices = sl4a.bluetoothGetBondedDevices()
+        for b in bonded_devices:
+            self.log.info('Removing bond for device {}'.format(b['address']))
+            sl4a.bluetoothUnbond(b['address'])
 
-  def set_user_params(self, params: Dict[str, Any]) -> None:
-    self._user_params = params
+    def set_user_params(self, params: Dict[str, Any]) -> None:
+        self._user_params = params
 
-  def get_user_params(self) -> Dict[str, Any]:
-    return self._user_params
+    def get_user_params(self) -> Dict[str, Any]:
+        return self._user_params
 
-  def is_sim_state_loaded(self) -> bool:
-    """Checks if SIM state is loaded.
+    def is_sim_state_loaded(self) -> bool:
+        """Checks if SIM state is loaded.
 
     Returns:
       True if SIM state is loaded else False.
     """
-    state = self._ad.adb.shell('getprop gsm.sim.state').decode().strip()
-    return state == 'LOADED'
+        state = self._ad.adb.shell('getprop gsm.sim.state').decode().strip()
+        return state == 'LOADED'
 
-  def is_package_installed(self, package_name: str) -> bool:
-    """Checks if a package is installed.
+    def is_package_installed(self, package_name: str) -> bool:
+        """Checks if a package is installed.
 
     Args:
       package_name: string, a package to be checked.
@@ -230,13 +218,12 @@
     Returns:
       True if the package is installed else False.
     """
-    # The package is installed if result is 1, not installed if result is 0.
-    result = int(self._ad.adb.shell('pm list packages | grep -i %s$ | wc -l' %
-                                    package_name))
-    return bool(result)
+        # The package is installed if result is 1, not installed if result is 0.
+        result = int(self._ad.adb.shell('pm list packages | grep -i %s$ | wc -l' % package_name))
+        return bool(result)
 
-  def connect_with_rfcomm(self, other_ad: android_device.AndroidDevice) -> bool:
-    """Establishes an RFCOMM connection with other android device.
+    def connect_with_rfcomm(self, other_ad: android_device.AndroidDevice) -> bool:
+        """Establishes an RFCOMM connection with other android device.
 
     Connects this android device (as a client) to the other android device
     (as a server).
@@ -247,22 +234,21 @@
     Returns:
         True if connection was successful, False if unsuccessful.
     """
-    server_address = other_ad.sl4a.bluetoothGetLocalAddress()
-    logging.info('Pairing and connecting devices')
-    if not self._ad.sl4a.bluetoothDiscoverAndBond(server_address):
-      logging.info('Failed to pair and connect devices')
-      return False
+        server_address = other_ad.sl4a.bluetoothGetLocalAddress()
+        logging.info('Pairing and connecting devices')
+        if not self._ad.sl4a.bluetoothDiscoverAndBond(server_address):
+            logging.info('Failed to pair and connect devices')
+            return False
 
-    # Create RFCOMM connection
-    logging.info('establishing RFCOMM connection')
-    return self.orchestrate_rfcomm_connection(other_ad)
+        # Create RFCOMM connection
+        logging.info('establishing RFCOMM connection')
+        return self.orchestrate_rfcomm_connection(other_ad)
 
-  def orchestrate_rfcomm_connection(
-      self,
-      other_ad: android_device.AndroidDevice,
-      accept_timeout_ms: int = bt_constants.DEFAULT_RFCOMM_TIMEOUT_MS,
-      uuid: Optional[Text] = None) -> bool:
-    """Sets up the RFCOMM connection to another android device.
+    def orchestrate_rfcomm_connection(self,
+                                      other_ad: android_device.AndroidDevice,
+                                      accept_timeout_ms: int = bt_constants.DEFAULT_RFCOMM_TIMEOUT_MS,
+                                      uuid: Optional[Text] = None) -> bool:
+        """Sets up the RFCOMM connection to another android device.
 
     It sets up the connection with a Bluetooth Socket connection with other
     device.
@@ -275,39 +261,33 @@
     Returns:
         True if connection was successful, False if unsuccessful.
     """
-    if uuid is None:
-      uuid = bt_constants.BT_RFCOMM_UUIDS['default_uuid']
-    other_ad.sl4a.bluetoothStartPairingHelper()
-    self._ad.sl4a.bluetoothStartPairingHelper()
-    other_ad.sl4a.bluetoothSocketConnBeginAcceptThreadUuid(uuid,
-                                                           accept_timeout_ms)
-    self._ad.sl4a.bluetoothSocketConnBeginConnectThreadUuid(
-        other_ad.sl4a.bluetoothGetLocalAddress(), uuid)
+        if uuid is None:
+            uuid = bt_constants.BT_RFCOMM_UUIDS['default_uuid']
+        other_ad.sl4a.bluetoothStartPairingHelper()
+        self._ad.sl4a.bluetoothStartPairingHelper()
+        other_ad.sl4a.bluetoothSocketConnBeginAcceptThreadUuid(uuid, accept_timeout_ms)
+        self._ad.sl4a.bluetoothSocketConnBeginConnectThreadUuid(other_ad.sl4a.bluetoothGetLocalAddress(), uuid)
 
-    end_time = time.time() + bt_constants.BT_DEFAULT_TIMEOUT_SECONDS
-    test_result = True
-
-    while time.time() < end_time:
-      number_socket_connections = len(
-          other_ad.sl4a.bluetoothSocketConnActiveConnections())
-      connected = number_socket_connections > 0
-      if connected:
+        end_time = time.time() + bt_constants.BT_DEFAULT_TIMEOUT_SECONDS
         test_result = True
-        other_ad.log.info('Bluetooth socket Client Connection Active')
-        break
-      else:
-        test_result = False
-      time.sleep(1)
-    if not test_result:
-      other_ad.log.error('Failed to establish a Bluetooth socket connection')
-      return False
-    return True
 
-  def wait_for_discovery_success(
-      self,
-      mac_address: str,
-      timeout: float = 30) -> float:
-    """Waits for a device to be discovered by AndroidDevice.
+        while time.time() < end_time:
+            number_socket_connections = len(other_ad.sl4a.bluetoothSocketConnActiveConnections())
+            connected = number_socket_connections > 0
+            if connected:
+                test_result = True
+                other_ad.log.info('Bluetooth socket Client Connection Active')
+                break
+            else:
+                test_result = False
+            time.sleep(1)
+        if not test_result:
+            other_ad.log.error('Failed to establish a Bluetooth socket connection')
+            return False
+        return True
+
+    def wait_for_discovery_success(self, mac_address: str, timeout: float = 30) -> float:
+        """Waits for a device to be discovered by AndroidDevice.
 
     Args:
       mac_address: The Bluetooth mac address of the peripheral device.
@@ -319,30 +299,23 @@
     Raises:
       DiscoveryError
     """
-    device_start_time = self.get_device_time()
-    start_time = time.time()
-    event_name = f'Discovery{mac_address}'
-    try:
-      self._ad.ed.wait_for_event(event_name,
-                                 lambda x: x['data']['Status'], timeout)
-      discovery_time = time.time() - start_time
-      return discovery_time
+        device_start_time = self.get_device_time()
+        start_time = time.time()
+        event_name = f'Discovery{mac_address}'
+        try:
+            self._ad.ed.wait_for_event(event_name, lambda x: x['data']['Status'], timeout)
+            discovery_time = time.time() - start_time
+            return discovery_time
 
-    except queue.Empty:
-      # TODO(user): Remove this check when this bug is fixed.
-      if self.logcat_filter(device_start_time, event_name):
-        self._ad.log.info(
-            'Actually the event "%s" was posted within %d seconds.',
-            event_name, timeout)
-        return timeout
-      raise DiscoveryError('Failed to discover device %s after %d seconds' %
-                           (mac_address, timeout))
+        except queue.Empty:
+            # TODO(user): Remove this check when this bug is fixed.
+            if self.logcat_filter(device_start_time, event_name):
+                self._ad.log.info('Actually the event "%s" was posted within %d seconds.', event_name, timeout)
+                return timeout
+            raise DiscoveryError('Failed to discover device %s after %d seconds' % (mac_address, timeout))
 
-  def wait_for_pairing_success(
-      self,
-      mac_address: str,
-      timeout: float = 30) -> float:
-    """Waits for a device to pair with the AndroidDevice.
+    def wait_for_pairing_success(self, mac_address: str, timeout: float = 30) -> float:
+        """Waits for a device to pair with the AndroidDevice.
 
     Args:
       mac_address: The Bluetooth mac address of the peripheral device.
@@ -354,23 +327,17 @@
     Raises:
       ControllerError
     """
-    start_time = time.time()
-    try:
-      self._ad.ed.wait_for_event('Bond%s' % mac_address,
-                                 lambda x: x['data']['Status'], timeout)
-      pairing_time = time.time() - start_time
-      return pairing_time
+        start_time = time.time()
+        try:
+            self._ad.ed.wait_for_event('Bond%s' % mac_address, lambda x: x['data']['Status'], timeout)
+            pairing_time = time.time() - start_time
+            return pairing_time
 
-    except queue.Empty:
-      raise signals.ControllerError(
-          'Failed to bond with device %s after %d seconds' %
-          (mac_address, timeout))
+        except queue.Empty:
+            raise signals.ControllerError('Failed to bond with device %s after %d seconds' % (mac_address, timeout))
 
-  def wait_for_connection_success(
-      self,
-      mac_address: str,
-      timeout: int = 30) -> float:
-    """Waits for a device to connect with the AndroidDevice.
+    def wait_for_connection_success(self, mac_address: str, timeout: int = 30) -> float:
+        """Waits for a device to connect with the AndroidDevice.
 
     Args:
       mac_address: The Bluetooth mac address of the peripheral device.
@@ -382,62 +349,50 @@
     Raises:
       ControllerError
     """
-    start_time = time.time()
-    end_time = start_time + timeout
-    while time.time() < end_time:
-      if self._is_device_connected(mac_address):
-        connection_time = (time.time() - start_time)
-        logging.info('Connected device %s in %d seconds', mac_address,
-                     connection_time)
-        return connection_time
+        start_time = time.time()
+        end_time = start_time + timeout
+        while time.time() < end_time:
+            if self._is_device_connected(mac_address):
+                connection_time = (time.time() - start_time)
+                logging.info('Connected device %s in %d seconds', mac_address, connection_time)
+                return connection_time
 
-    raise signals.ControllerError(
-        'Failed to connect device within %d seconds.' % timeout)
+        raise signals.ControllerError('Failed to connect device within %d seconds.' % timeout)
 
-  def factory_reset_bluetooth(self) -> None:
-    """Factory resets Bluetooth on an AndroidDevice."""
+    def factory_reset_bluetooth(self) -> None:
+        """Factory resets Bluetooth on an AndroidDevice."""
 
-    logging.info('Factory resetting Bluetooth for AndroidDevice.')
-    self._ad.sl4a.bluetoothToggleState(True)
-    paired_devices = self._ad.mbs.btGetPairedDevices()
-    for device in paired_devices:
-      self._ad.sl4a.bluetoothUnbond(device['Address'])
-    self._ad.sl4a.bluetoothFactoryReset()
-    self._wait_for_bluetooth_manager_state()
-    self.wait_for_bluetooth_toggle_state(True)
+        logging.info('Factory resetting Bluetooth for AndroidDevice.')
+        self._ad.sl4a.bluetoothToggleState(True)
+        paired_devices = self._ad.mbs.btGetPairedDevices()
+        for device in paired_devices:
+            self._ad.sl4a.bluetoothUnbond(device['Address'])
+        self._ad.sl4a.bluetoothFactoryReset()
+        self._wait_for_bluetooth_manager_state()
+        self.wait_for_bluetooth_toggle_state(True)
 
-  def get_device_info(self) -> Dict[str, Any]:
-    """Gets the configuration info of an AndroidDevice.
+    def get_device_info(self) -> Dict[str, Any]:
+        """Gets the configuration info of an AndroidDevice.
 
     Returns:
       dict, A dictionary mapping metric keys to their respective values.
     """
 
-    device_info = {
-        'device_class':
-            'AndroidDevice',
-        'device_model':
-            self._ad.device_info['model'],
-        'hardware_version':
-            self._ad.adb.getprop('ro.boot.hardware.revision'),
-        'software_version':
-            self._ad.build_info['build_id'],
-        'android_build_type':
-            self._ad.build_info['build_type'],
-        'android_build_number':
-            self._ad.adb.getprop('ro.build.version.incremental'),
-        'android_release_id':
-            self._ad.build_info['build_id']
-    }
+        device_info = {
+            'device_class': 'AndroidDevice',
+            'device_model': self._ad.device_info['model'],
+            'hardware_version': self._ad.adb.getprop('ro.boot.hardware.revision'),
+            'software_version': self._ad.build_info['build_id'],
+            'android_build_type': self._ad.build_info['build_type'],
+            'android_build_number': self._ad.adb.getprop('ro.build.version.incremental'),
+            'android_release_id': self._ad.build_info['build_id']
+        }
 
-    return device_info
+        return device_info
 
-  def pair_and_connect_bluetooth(
-      self,
-      mac_address: str,
-      attempts: int = 3,
-      enable_pairing_retry: bool = True) -> Tuple[float, float]:
-    """Pairs and connects an AndroidDevice with a peripheral Bluetooth device.
+    def pair_and_connect_bluetooth(self, mac_address: str, attempts: int = 3,
+                                   enable_pairing_retry: bool = True) -> Tuple[float, float]:
+        """Pairs and connects an AndroidDevice with a peripheral Bluetooth device.
 
     Ensures that an AndroidDevice is paired and connected to a peripheral
     device. If the devices are already connected, does nothing. If
@@ -466,50 +421,43 @@
       ControllerError: Raised if failed to bond the peripheral device.
     """
 
-    connected = self._is_device_connected(mac_address)
-    pairing_time = 0
-    connection_time = 0
-    if connected:
-      logging.info('Device %s already paired and connected', mac_address)
-      return pairing_time, connection_time
+        connected = self._is_device_connected(mac_address)
+        pairing_time = 0
+        connection_time = 0
+        if connected:
+            logging.info('Device %s already paired and connected', mac_address)
+            return pairing_time, connection_time
 
-    paired_devices = [device['address'] for device in
-                      self._ad.sl4a.bluetoothGetBondedDevices()]
-    if mac_address in paired_devices:
-      self._ad.sl4a.bluetoothConnectBonded(mac_address)
-      return pairing_time, self.wait_for_connection_success(mac_address)
+        paired_devices = [device['address'] for device in self._ad.sl4a.bluetoothGetBondedDevices()]
+        if mac_address in paired_devices:
+            self._ad.sl4a.bluetoothConnectBonded(mac_address)
+            return pairing_time, self.wait_for_connection_success(mac_address)
 
-    logging.info('Initiate pairing to the device "%s".', mac_address)
-    for i in range(attempts):
-      self._ad.sl4a.bluetoothDiscoverAndBond(mac_address)
-      try:
-        self.wait_for_discovery_success(mac_address)
-        pairing_time = self.wait_for_pairing_success(mac_address)
-        break
-      except DiscoveryError:
-        if i + 1 < attempts:
-          logging.error(
-              'Failed to find the device "%s" on Attempt %d. '
-              'Retrying discovery...', mac_address, i + 1)
-          continue
-        raise DiscoveryError('Failed to find the device "%s".' % mac_address)
-      except signals.ControllerError:
-        if i + 1 < attempts and enable_pairing_retry:
-          logging.error(
-              'Failed to bond the device "%s" on Attempt %d. '
-              'Retrying pairing...', mac_address, i + 1)
-          continue
-        raise signals.ControllerError('Failed to bond the device "%s".' %
-                                      mac_address)
+        logging.info('Initiate pairing to the device "%s".', mac_address)
+        for i in range(attempts):
+            self._ad.sl4a.bluetoothDiscoverAndBond(mac_address)
+            try:
+                self.wait_for_discovery_success(mac_address)
+                pairing_time = self.wait_for_pairing_success(mac_address)
+                break
+            except DiscoveryError:
+                if i + 1 < attempts:
+                    logging.error('Failed to find the device "%s" on Attempt %d. '
+                                  'Retrying discovery...', mac_address, i + 1)
+                    continue
+                raise DiscoveryError('Failed to find the device "%s".' % mac_address)
+            except signals.ControllerError:
+                if i + 1 < attempts and enable_pairing_retry:
+                    logging.error('Failed to bond the device "%s" on Attempt %d. '
+                                  'Retrying pairing...', mac_address, i + 1)
+                    continue
+                raise signals.ControllerError('Failed to bond the device "%s".' % mac_address)
 
-    connection_time = self.wait_for_connection_success(mac_address)
-    return pairing_time, connection_time
+        connection_time = self.wait_for_connection_success(mac_address)
+        return pairing_time, connection_time
 
-  def disconnect_bluetooth(
-      self,
-      mac_address: str,
-      timeout: float = 30) -> float:
-    """Disconnects Bluetooth between an AndroidDevice and peripheral device.
+    def disconnect_bluetooth(self, mac_address: str, timeout: float = 30) -> float:
+        """Disconnects Bluetooth between an AndroidDevice and peripheral device.
 
     Args:
       mac_address: The Bluetooth mac address of the peripheral device.
@@ -523,24 +471,23 @@
     Raises:
       ControllerError: Raised if failed to disconnect the peripheral device.
     """
-    if not self._is_device_connected(mac_address):
-      logging.info('Device %s already disconnected', mac_address)
-      return 0
+        if not self._is_device_connected(mac_address):
+            logging.info('Device %s already disconnected', mac_address)
+            return 0
 
-    self._ad.sl4a.bluetoothDisconnectConnected(mac_address)
-    start_time = time.time()
-    end_time = time.time() + timeout
-    while time.time() < end_time:
-      connected = self._is_device_connected(mac_address)
-      if not connected:
-        logging.info('Device %s disconnected successfully.', mac_address)
-        return time.time() - start_time
+        self._ad.sl4a.bluetoothDisconnectConnected(mac_address)
+        start_time = time.time()
+        end_time = time.time() + timeout
+        while time.time() < end_time:
+            connected = self._is_device_connected(mac_address)
+            if not connected:
+                logging.info('Device %s disconnected successfully.', mac_address)
+                return time.time() - start_time
 
-    raise signals.ControllerError(
-        'Failed to disconnect device within %d seconds.' % timeout)
+        raise signals.ControllerError('Failed to disconnect device within %d seconds.' % timeout)
 
-  def connect_bluetooth(self, mac_address: str, timeout: float = 30) -> float:
-    """Connects Bluetooth between an AndroidDevice and peripheral device.
+    def connect_bluetooth(self, mac_address: str, timeout: float = 30) -> float:
+        """Connects Bluetooth between an AndroidDevice and peripheral device.
 
     Args:
       mac_address: The Bluetooth mac address of the peripheral device.
@@ -554,59 +501,53 @@
     Raises:
       ControllerError: Raised if failed to connect the peripheral device.
     """
-    if self._is_device_connected(mac_address):
-      logging.info('Device %s already connected', mac_address)
-      return 0
+        if self._is_device_connected(mac_address):
+            logging.info('Device %s already connected', mac_address)
+            return 0
 
-    self._ad.sl4a.bluetoothConnectBonded(mac_address)
-    connect_time = self.wait_for_connection_success(mac_address)
+        self._ad.sl4a.bluetoothConnectBonded(mac_address)
+        connect_time = self.wait_for_connection_success(mac_address)
 
-    return connect_time
+        return connect_time
 
-  def activate_pairing_mode(self) -> None:
-    """Activates pairing mode on an AndroidDevice."""
-    logging.info('Activating pairing mode on AndroidDevice.')
-    self._ad.sl4a.bluetoothMakeDiscoverable()
-    self._ad.sl4a.bluetoothStartPairingHelper()
+    def activate_pairing_mode(self) -> None:
+        """Activates pairing mode on an AndroidDevice."""
+        logging.info('Activating pairing mode on AndroidDevice.')
+        self._ad.sl4a.bluetoothMakeDiscoverable()
+        self._ad.sl4a.bluetoothStartPairingHelper()
 
-  def activate_ble_pairing_mode(self) -> None:
-    """Activates BLE pairing mode on an AndroidDevice."""
-    self.ble_advertise_callback = self._ad.sl4a.bleGenBleAdvertiseCallback()
-    self._ad.sl4a.bleSetAdvertiseDataIncludeDeviceName(True)
-    # Sets advertise mode to low latency.
-    self._ad.sl4a.bleSetAdvertiseSettingsAdvertiseMode(
-        bt_constants.BleAdvertiseSettingsMode.LOW_LATENCY)
-    self._ad.sl4a.bleSetAdvertiseSettingsIsConnectable(True)
-    # Sets TX power level to High.
-    self._ad.sl4a.bleSetAdvertiseSettingsTxPowerLevel(
-        bt_constants.BleAdvertiseSettingsTxPower.HIGH)
-    advertise_data = self._ad.sl4a.bleBuildAdvertiseData()
-    advertise_settings = self._ad.sl4a.bleBuildAdvertiseSettings()
-    logging.info('Activating BLE pairing mode on AndroidDevice.')
-    self._ad.sl4a.bleStartBleAdvertising(
-        self.ble_advertise_callback, advertise_data, advertise_settings)
+    def activate_ble_pairing_mode(self) -> None:
+        """Activates BLE pairing mode on an AndroidDevice."""
+        self.ble_advertise_callback = self._ad.sl4a.bleGenBleAdvertiseCallback()
+        self._ad.sl4a.bleSetAdvertiseDataIncludeDeviceName(True)
+        # Sets advertise mode to low latency.
+        self._ad.sl4a.bleSetAdvertiseSettingsAdvertiseMode(ble_scan_adv_constants.BleAdvertiseSettingsMode.LOW_LATENCY)
+        self._ad.sl4a.bleSetAdvertiseSettingsIsConnectable(True)
+        # Sets TX power level to High.
+        self._ad.sl4a.bleSetAdvertiseSettingsTxPowerLevel(ble_scan_adv_constants.BleAdvertiseSettingsTxPower.HIGH)
+        advertise_data = self._ad.sl4a.bleBuildAdvertiseData()
+        advertise_settings = self._ad.sl4a.bleBuildAdvertiseSettings()
+        logging.info('Activating BLE pairing mode on AndroidDevice.')
+        self._ad.sl4a.bleStartBleAdvertising(self.ble_advertise_callback, advertise_data, advertise_settings)
 
-  def deactivate_ble_pairing_mode(self) -> None:
-    """Deactivates BLE pairing mode on an AndroidDevice."""
-    if not self.ble_advertise_callback:
-      self._ad.log.debug('BLE pairing mode is not activated.')
-      return
-    logging.info('Deactivating BLE pairing mode on AndroidDevice.')
-    self._ad.sl4a.bleStopBleAdvertising(self.ble_advertise_callback)
-    self.ble_advertise_callback = None
+    def deactivate_ble_pairing_mode(self) -> None:
+        """Deactivates BLE pairing mode on an AndroidDevice."""
+        if not self.ble_advertise_callback:
+            self._ad.log.debug('BLE pairing mode is not activated.')
+            return
+        logging.info('Deactivating BLE pairing mode on AndroidDevice.')
+        self._ad.sl4a.bleStopBleAdvertising(self.ble_advertise_callback)
+        self.ble_advertise_callback = None
 
-  def get_bluetooth_mac_address(self) -> str:
-    """Gets Bluetooth mac address of an AndroidDevice."""
-    logging.info('Getting Bluetooth mac address for AndroidDevice.')
-    mac_address = self._ad.sl4a.bluetoothGetLocalAddress()
-    logging.info('Bluetooth mac address of AndroidDevice: %s', mac_address)
-    return mac_address
+    def get_bluetooth_mac_address(self) -> str:
+        """Gets Bluetooth mac address of an AndroidDevice."""
+        logging.info('Getting Bluetooth mac address for AndroidDevice.')
+        mac_address = self._ad.sl4a.bluetoothGetLocalAddress()
+        logging.info('Bluetooth mac address of AndroidDevice: %s', mac_address)
+        return mac_address
 
-  def scan_and_get_ble_device_address(
-      self,
-      device_name: str,
-      timeout_sec: float = 30) -> str:
-    """Searchs a BLE device by BLE scanner and returns it's BLE mac address.
+    def scan_and_get_ble_device_address(self, device_name: str, timeout_sec: float = 30) -> str:
+        """Searchs a BLE device by BLE scanner and returns it's BLE mac address.
 
     Args:
       device_name: string, the name of BLE device.
@@ -618,43 +559,38 @@
     Raises:
       ControllerError: Raised if failed to get the BLE device address
     """
-    filter_list = self._ad.sl4a.bleGenFilterList()
-    scan_settings = self._ad.sl4a.bleBuildScanSetting()
-    scan_callback = self._ad.sl4a.bleGenScanCallback()
-    self._ad.sl4a.bleSetScanFilterDeviceName(device_name)
-    self._ad.sl4a.bleBuildScanFilter(filter_list)
-    self._ad.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
-    try:
-      event = self._ad.ed.pop_event(
-          'BleScan%sonScanResults' % scan_callback, timeout_sec)
-    except queue.Empty:
-      raise signals.ControllerError(
-          'Timed out %ds after waiting for phone finding BLE device: %s.' %
-          (timeout_sec, device_name))
-    finally:
-      self._ad.sl4a.bleStopBleScan(scan_callback)
-    return event['data']['Result']['deviceInfo']['address']
+        filter_list = self._ad.sl4a.bleGenFilterList()
+        scan_settings = self._ad.sl4a.bleBuildScanSetting()
+        scan_callback = self._ad.sl4a.bleGenScanCallback()
+        self._ad.sl4a.bleSetScanFilterDeviceName(device_name)
+        self._ad.sl4a.bleBuildScanFilter(filter_list)
+        self._ad.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
+        try:
+            event = self._ad.ed.pop_event('BleScan%sonScanResults' % scan_callback, timeout_sec)
+        except queue.Empty:
+            raise signals.ControllerError(
+                'Timed out %ds after waiting for phone finding BLE device: %s.' % (timeout_sec, device_name))
+        finally:
+            self._ad.sl4a.bleStopBleScan(scan_callback)
+        return event['data']['Result']['deviceInfo']['address']
 
-  def get_device_name(self) -> str:
-    """Gets Bluetooth device name of an AndroidDevice."""
-    logging.info('Getting Bluetooth device name for AndroidDevice.')
-    device_name = self._ad.sl4a.bluetoothGetLocalName()
-    logging.info('Bluetooth device name of AndroidDevice: %s', device_name)
-    return device_name
+    def get_device_name(self) -> str:
+        """Gets Bluetooth device name of an AndroidDevice."""
+        logging.info('Getting Bluetooth device name for AndroidDevice.')
+        device_name = self._ad.sl4a.bluetoothGetLocalName()
+        logging.info('Bluetooth device name of AndroidDevice: %s', device_name)
+        return device_name
 
-  def is_bluetooth_sco_on(self) -> bool:
-    """Checks whether communications use Bluetooth SCO."""
-    cmd = 'dumpsys bluetooth_manager | grep "isBluetoothScoOn"'
-    get_status = self._ad.adb.shell(cmd)
-    if isinstance(get_status, bytes):
-      get_status = get_status.decode()
-    return 'true' in get_status
+    def is_bluetooth_sco_on(self) -> bool:
+        """Checks whether communications use Bluetooth SCO."""
+        cmd = 'dumpsys bluetooth_manager | grep "isBluetoothScoOn"'
+        get_status = self._ad.adb.shell(cmd)
+        if isinstance(get_status, bytes):
+            get_status = get_status.decode()
+        return 'true' in get_status
 
-  def connect_with_profile(
-      self,
-      snd_ad_mac_address: str,
-      profile: bt_constants.BluetoothProfile) -> bool:
-    """Connects with the profile.
+    def connect_with_profile(self, snd_ad_mac_address: str, profile: bt_constants.BluetoothProfile) -> bool:
+        """Connects with the profile.
 
     The connection can only be completed after the bluetooth devices are paired.
     To connected with the profile, the bluetooth connection policy is set to
@@ -669,33 +605,29 @@
     Returns:
       The profile connection succeed/fail
     """
-    if profile == bt_constants.BluetoothProfile.MAP_MCE:
-      self._ad.sl4a.bluetoothMapClientConnect(snd_ad_mac_address)
-    elif profile == bt_constants.BluetoothProfile.PBAP_CLIENT:
-      self.set_profile_policy(
-          snd_ad_mac_address, profile,
-          bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_ALLOWED)
-      self._ad.sl4a.bluetoothPbapClientConnect(snd_ad_mac_address)
-    else:
-      self.set_profile_policy(
-          snd_ad_mac_address, profile,
-          bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_FORBIDDEN)
-      self.set_profile_policy(
-          snd_ad_mac_address, profile,
-          bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_ALLOWED)
-      self._ad.sl4a.bluetoothConnectBonded(snd_ad_mac_address)
-    time.sleep(BT_CONNECTION_WAITING_TIME_SECONDS)
-    is_connected = self._is_profile_connected(snd_ad_mac_address, profile)
-    self.log.info('The connection between %s and %s for profile %s succeed: %s',
-                  self.serial, snd_ad_mac_address, profile, is_connected)
-    return is_connected
+        if profile == bt_constants.BluetoothProfile.MAP_MCE:
+            self._ad.sl4a.bluetoothMapClientConnect(snd_ad_mac_address)
+        elif profile == bt_constants.BluetoothProfile.PBAP_CLIENT:
+            self.set_profile_policy(snd_ad_mac_address, profile,
+                                    bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_ALLOWED)
+            self._ad.sl4a.bluetoothPbapClientConnect(snd_ad_mac_address)
+        else:
+            self.set_profile_policy(snd_ad_mac_address, profile,
+                                    bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_FORBIDDEN)
+            self.set_profile_policy(snd_ad_mac_address, profile,
+                                    bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_ALLOWED)
+            self._ad.sl4a.bluetoothConnectBonded(snd_ad_mac_address)
+        time.sleep(BT_CONNECTION_WAITING_TIME_SECONDS)
+        is_connected = self._is_profile_connected(snd_ad_mac_address, profile)
+        self.log.info('The connection between %s and %s for profile %s succeed: %s', self.serial, snd_ad_mac_address,
+                      profile, is_connected)
+        return is_connected
 
-  def connect_to_snd_with_profile(
-      self,
-      snd_ad: android_device.AndroidDevice,
-      profile: bt_constants.BluetoothProfile,
-      attempts: int = 5) -> bool:
-    """Connects pri android device to snd android device with profile.
+    def connect_to_snd_with_profile(self,
+                                    snd_ad: android_device.AndroidDevice,
+                                    profile: bt_constants.BluetoothProfile,
+                                    attempts: int = 5) -> bool:
+        """Connects pri android device to snd android device with profile.
 
     Args:
         snd_ad: android device accepting connection
@@ -705,27 +637,24 @@
     Returns:
         Boolean of connecting result
     """
-    pri_ad = self._ad
-    curr_attempts = 0
-    snd_ad_mac_address = snd_ad.sl4a.bluetoothGetLocalAddress()
-    if not self.is_bt_paired(snd_ad_mac_address):
-      self.log.error('Devices %s and %s not paired before connecting',
-                     self.serial, snd_ad.serial)
-      return False
-    while curr_attempts < attempts:
-      curr_attempts += 1
-      self.log.info('Connection of profile %s at curr attempt %d (total %d)',
-                    profile, curr_attempts, attempts)
-      if self.connect_with_profile(snd_ad_mac_address, profile):
-        self.log.info('Connection between devices %s and %s succeeds at %d try',
-                      pri_ad.serial, snd_ad.serial, curr_attempts)
-        return True
-    self.log.error('Connection of profile %s failed after %d attempts', profile,
-                   attempts)
-    return False
+        pri_ad = self._ad
+        curr_attempts = 0
+        snd_ad_mac_address = snd_ad.sl4a.bluetoothGetLocalAddress()
+        if not self.is_bt_paired(snd_ad_mac_address):
+            self.log.error('Devices %s and %s not paired before connecting', self.serial, snd_ad.serial)
+            return False
+        while curr_attempts < attempts:
+            curr_attempts += 1
+            self.log.info('Connection of profile %s at curr attempt %d (total %d)', profile, curr_attempts, attempts)
+            if self.connect_with_profile(snd_ad_mac_address, profile):
+                self.log.info('Connection between devices %s and %s succeeds at %d try', pri_ad.serial, snd_ad.serial,
+                              curr_attempts)
+                return True
+        self.log.error('Connection of profile %s failed after %d attempts', profile, attempts)
+        return False
 
-  def is_bt_paired(self, mac_address: str) -> bool:
-    """Check if the bluetooth device with mac_address is paired to ad.
+    def is_bt_paired(self, mac_address: str) -> bool:
+        """Check if the bluetooth device with mac_address is paired to ad.
 
     Args:
       mac_address: the mac address of the bluetooth device for pairing
@@ -733,11 +662,11 @@
     Returns:
       True if they are paired
     """
-    bonded_info = self._ad.sl4a.bluetoothGetBondedDevices()
-    return mac_address in [info['address'] for info in bonded_info]
+        bonded_info = self._ad.sl4a.bluetoothGetBondedDevices()
+        return mac_address in [info['address'] for info in bonded_info]
 
-  def is_a2dp_sink_connected(self, mac_address: str) -> bool:
-    """Checks if the Android device connects to a A2DP sink device.
+    def is_a2dp_sink_connected(self, mac_address: str) -> bool:
+        """Checks if the Android device connects to a A2DP sink device.
 
     Args:
       mac_address: String, Bluetooth MAC address of the A2DP sink device.
@@ -745,11 +674,11 @@
     Returns:
       True if connected else False.
     """
-    connected_devices = self._ad.sl4a.bluetoothA2dpGetConnectedDevices()
-    return mac_address in [d['address'] for d in connected_devices]
+        connected_devices = self._ad.sl4a.bluetoothA2dpGetConnectedDevices()
+        return mac_address in [d['address'] for d in connected_devices]
 
-  def hfp_connect(self, ag_ad: android_device.AndroidDevice) -> bool:
-    """Hfp connecting hf android device to ag android device.
+    def hfp_connect(self, ag_ad: android_device.AndroidDevice) -> bool:
+        """Hfp connecting hf android device to ag android device.
 
     The android device should support the Headset Client profile. For example,
     the android device with git_master-bds-dev build.
@@ -760,11 +689,10 @@
     Returns:
         Boolean of connecting result
     """
-    return self.connect_to_snd_with_profile(
-        ag_ad, bt_constants.BluetoothProfile.HEADSET_CLIENT)
+        return self.connect_to_snd_with_profile(ag_ad, bt_constants.BluetoothProfile.HEADSET_CLIENT)
 
-  def a2dp_sink_connect(self, src_ad: android_device.AndroidDevice) -> bool:
-    """Connects pri android device to secondary android device.
+    def a2dp_sink_connect(self, src_ad: android_device.AndroidDevice) -> bool:
+        """Connects pri android device to secondary android device.
 
     The android device should support the A2dp Sink profile. For example, the
     android device with git_master-bds-dev build.
@@ -775,11 +703,10 @@
     Returns:
       Boolean of connecting result
     """
-    return self.connect_to_snd_with_profile(
-        src_ad, bt_constants.BluetoothProfile.A2DP_SINK)
+        return self.connect_to_snd_with_profile(src_ad, bt_constants.BluetoothProfile.A2DP_SINK)
 
-  def map_connect(self, map_ad: android_device.AndroidDevice) -> bool:
-    """Connects primary device to secondary device via MAP MCE profile.
+    def map_connect(self, map_ad: android_device.AndroidDevice) -> bool:
+        """Connects primary device to secondary device via MAP MCE profile.
 
     The primary device should support the MAP MCE profile. For example,
     the android device with git_master-bds-dev build.
@@ -790,11 +717,10 @@
     Returns:
         Boolean of connecting result
     """
-    return self.connect_to_snd_with_profile(
-        map_ad, bt_constants.BluetoothProfile.MAP_MCE)
+        return self.connect_to_snd_with_profile(map_ad, bt_constants.BluetoothProfile.MAP_MCE)
 
-  def map_disconnect(self, bluetooth_address: str) -> bool:
-    """Disconnects a MAP MSE device with specified Bluetooth MAC address.
+    def map_disconnect(self, bluetooth_address: str) -> bool:
+        """Disconnects a MAP MSE device with specified Bluetooth MAC address.
 
     Args:
       bluetooth_address: a connected device's bluetooth address.
@@ -802,15 +728,15 @@
     Returns:
       True if the device is disconnected else False.
     """
-    self._ad.sl4a.bluetoothMapClientDisconnect(bluetooth_address)
-    return bt_test_utils.wait_until(
-        timeout_sec=COMMON_TIMEOUT_SECONDS,
-        condition_func=self._is_profile_connected,
-        func_args=[bluetooth_address, bt_constants.BluetoothProfile.MAP_MCE],
-        expected_value=False)
+        self._ad.sl4a.bluetoothMapClientDisconnect(bluetooth_address)
+        return bt_test_utils.wait_until(
+            timeout_sec=COMMON_TIMEOUT_SECONDS,
+            condition_func=self._is_profile_connected,
+            func_args=[bluetooth_address, bt_constants.BluetoothProfile.MAP_MCE],
+            expected_value=False)
 
-  def pbap_connect(self, pbap_ad: android_device.AndroidDevice) -> bool:
-    """Connects primary device to secondary device via PBAP client profile.
+    def pbap_connect(self, pbap_ad: android_device.AndroidDevice) -> bool:
+        """Connects primary device to secondary device via PBAP client profile.
 
     The primary device should support the PBAP client profile. For example,
     the android device with git_master-bds-dev build.
@@ -821,40 +747,33 @@
     Returns:
         Boolean of connecting result
     """
-    return self.connect_to_snd_with_profile(
-        pbap_ad, bt_constants.BluetoothProfile.PBAP_CLIENT)
+        return self.connect_to_snd_with_profile(pbap_ad, bt_constants.BluetoothProfile.PBAP_CLIENT)
 
-  def set_bluetooth_tethering(self, status_enabled: bool) -> None:
-    """Sets Bluetooth tethering to be specific status.
+    def set_bluetooth_tethering(self, status_enabled: bool) -> None:
+        """Sets Bluetooth tethering to be specific status.
 
     Args:
       status_enabled: Bool, Bluetooth tethering will be set to enable if True,
           else disable.
     """
-    if self._ad.sl4a.bluetoothPanIsTetheringOn() == status_enabled:
-      self._ad.log.info('Already %s Bluetooth tethering.' %
-                        ('enabled' if status_enabled else 'disabled'))
-      return
+        if self._ad.sl4a.bluetoothPanIsTetheringOn() == status_enabled:
+            self._ad.log.info('Already %s Bluetooth tethering.' % ('enabled' if status_enabled else 'disabled'))
+            return
 
-    self._ad.log.info('%s Bluetooth tethering.' %
-                      ('Enable' if status_enabled else 'Disable'))
-    self._ad.sl4a.bluetoothPanSetBluetoothTethering(status_enabled)
+        self._ad.log.info('%s Bluetooth tethering.' % ('Enable' if status_enabled else 'Disable'))
+        self._ad.sl4a.bluetoothPanSetBluetoothTethering(status_enabled)
 
-    bt_test_utils.wait_until(
-        timeout_sec=COMMON_TIMEOUT_SECONDS,
-        condition_func=self._ad.sl4a.bluetoothPanIsTetheringOn,
-        func_args=[],
-        expected_value=status_enabled,
-        exception=signals.ControllerError(
-            'Failed to %s Bluetooth tethering.' %
-            ('enable' if status_enabled else 'disable')))
+        bt_test_utils.wait_until(
+            timeout_sec=COMMON_TIMEOUT_SECONDS,
+            condition_func=self._ad.sl4a.bluetoothPanIsTetheringOn,
+            func_args=[],
+            expected_value=status_enabled,
+            exception=signals.ControllerError('Failed to %s Bluetooth tethering.' % ('enable'
+                                                                                     if status_enabled else 'disable')))
 
-  def set_profile_policy(
-      self,
-      snd_ad_mac_address: str,
-      profile: bt_constants.BluetoothProfile,
-      policy: bt_constants.BluetoothConnectionPolicy) -> None:
-    """Sets policy of the profile car related profiles to OFF.
+    def set_profile_policy(self, snd_ad_mac_address: str, profile: bt_constants.BluetoothProfile,
+                           policy: bt_constants.BluetoothConnectionPolicy) -> None:
+        """Sets policy of the profile car related profiles to OFF.
 
     This avoids autoconnect being triggered randomly. The use of this function
     is encouraged when you're testing individual profiles in isolation.
@@ -864,47 +783,40 @@
       profile: the profiles to be set
       policy: the policy value to be set
     """
-    pri_ad = self._ad
-    pri_ad_local_name = pri_ad.sl4a.bluetoothGetLocalName()
-    pri_ad.log.info('Sets profile %s on %s for %s to policy %s', profile,
-                    pri_ad_local_name, snd_ad_mac_address, policy)
-    if profile == bt_constants.BluetoothProfile.A2DP:
-      pri_ad.sl4a.bluetoothA2dpSetPriority(snd_ad_mac_address, policy.value)
-    elif profile == bt_constants.BluetoothProfile.A2DP_SINK:
-      pri_ad.sl4a.bluetoothA2dpSinkSetPriority(snd_ad_mac_address, policy.value)
-    elif profile == bt_constants.BluetoothProfile.HEADSET_CLIENT:
-      pri_ad.sl4a.bluetoothHfpClientSetPriority(snd_ad_mac_address,
-                                                policy.value)
-    elif profile == bt_constants.BluetoothProfile.PBAP_CLIENT:
-      pri_ad.sl4a.bluetoothPbapClientSetPriority(snd_ad_mac_address,
-                                                 policy.value)
-    elif profile == bt_constants.BluetoothProfile.HID_HOST:
-      pri_ad.sl4a.bluetoothHidSetPriority(snd_ad_mac_address, policy.value)
-    else:
-      pri_ad.log.error('Profile %s not yet supported for policy settings',
-                       profile)
+        pri_ad = self._ad
+        pri_ad_local_name = pri_ad.sl4a.bluetoothGetLocalName()
+        pri_ad.log.info('Sets profile %s on %s for %s to policy %s', profile, pri_ad_local_name, snd_ad_mac_address,
+                        policy)
+        if profile == bt_constants.BluetoothProfile.A2DP:
+            pri_ad.sl4a.bluetoothA2dpSetPriority(snd_ad_mac_address, policy.value)
+        elif profile == bt_constants.BluetoothProfile.A2DP_SINK:
+            pri_ad.sl4a.bluetoothA2dpSinkSetPriority(snd_ad_mac_address, policy.value)
+        elif profile == bt_constants.BluetoothProfile.HEADSET_CLIENT:
+            pri_ad.sl4a.bluetoothHfpClientSetPriority(snd_ad_mac_address, policy.value)
+        elif profile == bt_constants.BluetoothProfile.PBAP_CLIENT:
+            pri_ad.sl4a.bluetoothPbapClientSetPriority(snd_ad_mac_address, policy.value)
+        elif profile == bt_constants.BluetoothProfile.HID_HOST:
+            pri_ad.sl4a.bluetoothHidSetPriority(snd_ad_mac_address, policy.value)
+        else:
+            pri_ad.log.error('Profile %s not yet supported for policy settings', profile)
 
-  def set_profiles_policy(
-      self,
-      snd_ad: android_device.AndroidDevice,
-      profile_list: Sequence[bt_constants.BluetoothProfile],
-      policy: bt_constants.BluetoothConnectionPolicy) -> None:
-    """Sets the policy of said profile(s) on pri_ad for snd_ad.
+    def set_profiles_policy(self, snd_ad: android_device.AndroidDevice,
+                            profile_list: Sequence[bt_constants.BluetoothProfile],
+                            policy: bt_constants.BluetoothConnectionPolicy) -> None:
+        """Sets the policy of said profile(s) on pri_ad for snd_ad.
 
     Args:
       snd_ad: android device accepting connection
       profile_list: list of the profiles to be set
       policy: the policy to be set
     """
-    mac_address = snd_ad.sl4a.bluetoothGetLocalAddress()
-    for profile in profile_list:
-      self.set_profile_policy(mac_address, profile, policy)
+        mac_address = snd_ad.sl4a.bluetoothGetLocalAddress()
+        for profile in profile_list:
+            self.set_profile_policy(mac_address, profile, policy)
 
-  def set_profiles_policy_off(
-      self,
-      snd_ad: android_device.AndroidDevice,
-      profile_list: Sequence[bt_constants.BluetoothProfile]) -> None:
-    """Sets policy of the profiles to OFF.
+    def set_profiles_policy_off(self, snd_ad: android_device.AndroidDevice,
+                                profile_list: Sequence[bt_constants.BluetoothProfile]) -> None:
+        """Sets policy of the profiles to OFF.
 
     This avoids autoconnect being triggered randomly. The use of this function
     is encouraged when you're testing individual profiles in isolation
@@ -913,16 +825,14 @@
       snd_ad: android device accepting connection
       profile_list: list of the profiles to be turned off
     """
-    self.set_profiles_policy(
-        snd_ad, profile_list,
-        bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_FORBIDDEN)
+        self.set_profiles_policy(snd_ad, profile_list,
+                                 bt_constants.BluetoothConnectionPolicy.CONNECTION_POLICY_FORBIDDEN)
 
-  def wait_for_call_state(
-      self,
-      call_state: Union[int, bt_constants.CallState],
-      timeout_sec: float,
-      wait_interval: int = 3) -> bool:
-    """Waits for call state of the device to be changed.
+    def wait_for_call_state(self,
+                            call_state: Union[int, bt_constants.CallState],
+                            timeout_sec: float,
+                            wait_interval: int = 3) -> bool:
+        """Waits for call state of the device to be changed.
 
     Args:
       call_state: int, the expected call state. Call state values are:
@@ -935,28 +845,23 @@
     Returns:
       True if the call state has been changed else False.
     """
-    # TODO(user): Force external call to use CallState instead of int
-    if isinstance(call_state, bt_constants.CallState):
-      call_state = call_state.value
-    expiration_time = time.time() + timeout_sec
-    which_cycle = 1
-    while time.time() < expiration_time:
-      # Waits for the call state change in every cycle.
-      time.sleep(wait_interval)
-      self._ad.log.info(
-          'in cycle %d of waiting for call state %d', which_cycle, call_state)
-      if call_state == self._ad.mbs.getTelephonyCallState():
-        return True
-    self._ad.log.info('The call state did not change to %d before timeout',
-                      call_state)
-    return False
+        # TODO(user): Force external call to use CallState instead of int
+        if isinstance(call_state, bt_constants.CallState):
+            call_state = call_state.value
+        expiration_time = time.time() + timeout_sec
+        which_cycle = 1
+        while time.time() < expiration_time:
+            # Waits for the call state change in every cycle.
+            time.sleep(wait_interval)
+            self._ad.log.info('in cycle %d of waiting for call state %d', which_cycle, call_state)
+            if call_state == self._ad.mbs.getTelephonyCallState():
+                return True
+        self._ad.log.info('The call state did not change to %d before timeout', call_state)
+        return False
 
-  def add_call_log(
-      self,
-      call_log_type: Union[int, bt_constants.CallLogType],
-      phone_number: str,
-      call_time: int) -> None:
-    """Add call number and time to specified log.
+    def add_call_log(self, call_log_type: Union[int, bt_constants.CallLogType], phone_number: str,
+                     call_time: int) -> None:
+        """Add call number and time to specified log.
 
     Args:
       call_log_type: int, number of call log type. Call log type values are:
@@ -969,17 +874,17 @@
     Returns:
       None
     """
-    # TODO(user): Force external call to use CallLogType instead of int
-    if isinstance(call_log_type, bt_constants.CallLogType):
-      call_log_type = call_log_type.value
-    new_call_log = {}
-    new_call_log['type'] = str(call_log_type)
-    new_call_log['number'] = phone_number
-    new_call_log['time'] = str(call_time)
-    self._ad.sl4a.callLogsPut(new_call_log)
+        # TODO(user): Force external call to use CallLogType instead of int
+        if isinstance(call_log_type, bt_constants.CallLogType):
+            call_log_type = call_log_type.value
+        new_call_log = {}
+        new_call_log['type'] = str(call_log_type)
+        new_call_log['number'] = phone_number
+        new_call_log['time'] = str(call_time)
+        self._ad.sl4a.callLogsPut(new_call_log)
 
-  def get_call_volume(self) -> int:
-    """Gets current call volume of an AndroidDevice when Bluetooth SCO On.
+    def get_call_volume(self) -> int:
+        """Gets current call volume of an AndroidDevice when Bluetooth SCO On.
 
     Returns:
       An integer specifying the number of current call volume level.
@@ -987,19 +892,16 @@
     Raises:
       Error: If the pattern search failed.
     """
-    cmd = 'dumpsys audio | grep "STREAM_BLUETOOTH_SCO" | tail -1'
-    out = self._ad.adb.shell(cmd).decode()
-    pattern = r'(?<=SCO index:)\d+'
-    result = re.search(pattern, out)
-    if result is None:
-      raise Error(f'Pattern "{pattern}" search failed, dump output: {out}')
-    return int(result.group())
+        cmd = 'dumpsys audio | grep "STREAM_BLUETOOTH_SCO" | tail -1'
+        out = self._ad.adb.shell(cmd).decode()
+        pattern = r'(?<=SCO index:)\d+'
+        result = re.search(pattern, out)
+        if result is None:
+            raise Error(f'Pattern "{pattern}" search failed, dump output: {out}')
+        return int(result.group())
 
-  def make_phone_call(
-      self,
-      callee: android_device.AndroidDevice,
-      timeout_sec: float = 30) -> None:
-    """Make a phone call to callee and check if callee is ringing.
+    def make_phone_call(self, callee: android_device.AndroidDevice, timeout_sec: float = 30) -> None:
+        """Make a phone call to callee and check if callee is ringing.
 
     Args:
       callee: AndroidDevice, The callee in the phone call.
@@ -1008,18 +910,13 @@
     Raises:
       TestError
     """
-    self._ad.sl4a.telecomCallNumber(callee.dimensions['phone_number'])
-    is_ringing = callee.wait_for_call_state(bt_constants.CALL_STATE_RINGING,
-                                            timeout_sec)
-    if not is_ringing:
-      raise signals.TestError(
-          'Timed out after %ds waiting for call state: RINGING' % timeout_sec)
+        self._ad.sl4a.telecomCallNumber(callee.dimensions['phone_number'])
+        is_ringing = callee.wait_for_call_state(bt_constants.CALL_STATE_RINGING, timeout_sec)
+        if not is_ringing:
+            raise signals.TestError('Timed out after %ds waiting for call state: RINGING' % timeout_sec)
 
-  def wait_for_disconnection_success(
-      self,
-      mac_address: str,
-      timeout: float = 30) -> float:
-    """Waits for a device to connect with the AndroidDevice.
+    def wait_for_disconnection_success(self, mac_address: str, timeout: float = 30) -> float:
+        """Waits for a device to connect with the AndroidDevice.
 
     Args:
       mac_address: The Bluetooth mac address of the peripheral device.
@@ -1031,21 +928,18 @@
     Raises:
       ControllerError
     """
-    start_time = time.time()
-    end_time = start_time + timeout
-    while time.time() < end_time:
-      if not self._ad.sl4a.bluetoothIsDeviceConnected(mac_address):
-        disconnection_time = (time.time() - start_time)
-        logging.info('Disconnected device %s in %d seconds', mac_address,
-                     disconnection_time)
-        return disconnection_time
+        start_time = time.time()
+        end_time = start_time + timeout
+        while time.time() < end_time:
+            if not self._ad.sl4a.bluetoothIsDeviceConnected(mac_address):
+                disconnection_time = (time.time() - start_time)
+                logging.info('Disconnected device %s in %d seconds', mac_address, disconnection_time)
+                return disconnection_time
 
-    raise signals.ControllerError(
-        'Failed to disconnect device within %d seconds.' % timeout)
+        raise signals.ControllerError('Failed to disconnect device within %d seconds.' % timeout)
 
-  def first_pair_and_connect_bluetooth(
-      self, bt_device: Any) -> None:
-    """Pairs and connects an AndroidDevice with a Bluetooth device.
+    def first_pair_and_connect_bluetooth(self, bt_device: Any) -> None:
+        """Pairs and connects an AndroidDevice with a Bluetooth device.
 
     This method does factory reset bluetooth first and then pairs and connects
     the devices.
@@ -1057,25 +951,21 @@
     Returns:
       None
     """
-    bt_device.factory_reset_bluetooth()
-    mac_address = bt_device.get_bluetooth_mac_address()
-    bt_device.activate_pairing_mode()
-    self.pair_and_connect_bluetooth(mac_address)
+        bt_device.factory_reset_bluetooth()
+        mac_address = bt_device.get_bluetooth_mac_address()
+        bt_device.activate_pairing_mode()
+        self.pair_and_connect_bluetooth(mac_address)
 
-  def get_device_time(self) -> str:
-    """Get device epoch time and transfer to logcat timestamp format.
+    def get_device_time(self) -> str:
+        """Get device epoch time and transfer to logcat timestamp format.
 
     Returns:
       String of the device time.
     """
-    return self._ad.adb.shell(
-        'date +"%m-%d %H:%M:%S.000"').decode().splitlines()[0]
+        return self._ad.adb.shell('date +"%m-%d %H:%M:%S.000"').decode().splitlines()[0]
 
-  def logcat_filter(
-      self,
-      start_time: str,
-      text_filter: str = '') -> str:
-    """Returns logcat after a given time.
+    def logcat_filter(self, start_time: str, text_filter: str = '') -> str:
+        """Returns logcat after a given time.
 
     This method calls from the android_device logcat service file and filters
     all logcat line prior to the start_time.
@@ -1090,30 +980,26 @@
     Raises:
       ValueError Exception if start_time is invalid format.
     """
-    try:
-      start_time_conv = datetime.datetime.strptime(start_time, _DATETIME_FMT)
-    except ValueError as ex:
-      logging.error('Invalid time format!')
-      raise ex
-    logcat_response = ''
-    with open(self._ad.adb_logcat_file_path, 'r', errors='replace') \
-        as logcat_file:
-      post_start_time = False
-      for line in logcat_file:
-        match = self.regex_logcat_time.match(line)
-        if match:
-          if (datetime.datetime.strptime(
-              match.group('datetime'), _DATETIME_FMT) >= start_time_conv):
-            post_start_time = True
-          if post_start_time and line.find(text_filter) >= 0:
-            logcat_response += line
-    return logcat_response
+        try:
+            start_time_conv = datetime.datetime.strptime(start_time, _DATETIME_FMT)
+        except ValueError as ex:
+            logging.error('Invalid time format!')
+            raise ex
+        logcat_response = ''
+        with open(self._ad.adb_logcat_file_path, 'r', errors='replace') \
+            as logcat_file:
+            post_start_time = False
+            for line in logcat_file:
+                match = self.regex_logcat_time.match(line)
+                if match:
+                    if (datetime.datetime.strptime(match.group('datetime'), _DATETIME_FMT) >= start_time_conv):
+                        post_start_time = True
+                    if post_start_time and line.find(text_filter) >= 0:
+                        logcat_response += line
+        return logcat_response
 
-  def logcat_filter_message(
-      self,
-      current_time: str,
-      text: str = '') -> str:
-    """DEPRECATED Builds the logcat command.
+    def logcat_filter_message(self, current_time: str, text: str = '') -> str:
+        """DEPRECATED Builds the logcat command.
 
     This method builds the logcat command to check for a specified log
     message after the specified time. If text=None, the logcat returned will be
@@ -1127,13 +1013,11 @@
     Returns:
       The response of the logcat filter.
     """
-    return self.logcat_filter(current_time, text)
+        return self.logcat_filter(current_time, text)
 
-  def send_media_passthrough_cmd(
-      self,
-      command: str,
-      event_receiver: Optional[android_device.AndroidDevice] = None) -> None:
-    """Sends a media passthrough command.
+    def send_media_passthrough_cmd(self, command: str,
+                                   event_receiver: Optional[android_device.AndroidDevice] = None) -> None:
+        """Sends a media passthrough command.
 
     Args:
       command: string, media passthrough command.
@@ -1143,87 +1027,75 @@
     Raises:
       signals.ControllerError: raised if the event is not received.
     """
-    if event_receiver is None:
-      event_receiver = self._ad
-    self._ad.log.info('Sending Media Passthough: %s' % command)
-    self._ad.sl4a.bluetoothMediaPassthrough(command)
-    if not event_receiver:
-      event_receiver = self._ad
-    try:
-      event_receiver.ed.pop_event(MEDIA_CMD_MAP[command],
-                                  MEDIA_EVENT_TIMEOUT_SEC)
-    except queue.Empty:
-      raise signals.ControllerError(
-          'Device "%s" failed to receive the event "%s" '
-          'when the command "%s" was sent.' %
-          (event_receiver.serial, MEDIA_CMD_MAP[command], command))
+        if event_receiver is None:
+            event_receiver = self._ad
+        self._ad.log.info('Sending Media Passthough: %s' % command)
+        self._ad.sl4a.bluetoothMediaPassthrough(command)
+        if not event_receiver:
+            event_receiver = self._ad
+        try:
+            event_receiver.ed.pop_event(MEDIA_CMD_MAP[command], MEDIA_EVENT_TIMEOUT_SEC)
+        except queue.Empty:
+            raise signals.ControllerError(
+                'Device "%s" failed to receive the event "%s" '
+                'when the command "%s" was sent.' % (event_receiver.serial, MEDIA_CMD_MAP[command], command))
 
-  def pause(self) -> None:
-    """Sends the AVRCP command "pause"."""
-    self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_PAUSE)
+    def pause(self) -> None:
+        """Sends the AVRCP command "pause"."""
+        self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_PAUSE)
 
-  def play(self) -> None:
-    """Sends the AVRCP command "play"."""
-    self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_PLAY)
+    def play(self) -> None:
+        """Sends the AVRCP command "play"."""
+        self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_PLAY)
 
-  def track_previous(self) -> None:
-    """Sends the AVRCP command "skipPrev"."""
-    self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_SKIP_PREV)
+    def track_previous(self) -> None:
+        """Sends the AVRCP command "skipPrev"."""
+        self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_SKIP_PREV)
 
-  def track_next(self) -> None:
-    """Sends the AVRCP command "skipNext"."""
-    self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_SKIP_NEXT)
+    def track_next(self) -> None:
+        """Sends the AVRCP command "skipNext"."""
+        self.send_media_passthrough_cmd(bt_constants.CMD_MEDIA_SKIP_NEXT)
 
-  def get_current_track_info(self) -> Dict[str, Any]:
-    """Returns Dict (Media metadata) representing the current track."""
-    return self._ad.sl4a.bluetoothMediaGetCurrentMediaMetaData()
+    def get_current_track_info(self) -> Dict[str, Any]:
+        """Returns Dict (Media metadata) representing the current track."""
+        return self._ad.sl4a.bluetoothMediaGetCurrentMediaMetaData()
 
-  def get_current_playback_state(self) -> int:
-    """Returns Integer representing the current playback state."""
-    return self._ad.sl4a.bluetoothMediaGetCurrentPlaybackState()['state']
+    def get_current_playback_state(self) -> int:
+        """Returns Integer representing the current playback state."""
+        return self._ad.sl4a.bluetoothMediaGetCurrentPlaybackState()['state']
 
-  def verify_playback_state_changed(
-      self,
-      expected_state: str,
-      exception: Optional[Exception] = None) -> bool:
-    """Verifies the playback state is changed to be the expected state.
+    def verify_playback_state_changed(self, expected_state: str, exception: Optional[Exception] = None) -> bool:
+        """Verifies the playback state is changed to be the expected state.
 
     Args:
       expected_state: string, the changed state as expected.
       exception: Exception, raised when the state is not changed if needed.
     """
-    bt_test_utils.wait_until(
-        timeout_sec=MEDIA_UPDATE_TIMEOUT_SEC,
-        condition_func=self.get_current_playback_state,
-        func_args=[],
-        expected_value=expected_state,
-        exception=exception,
-        interval_sec=1)
+        bt_test_utils.wait_until(
+            timeout_sec=MEDIA_UPDATE_TIMEOUT_SEC,
+            condition_func=self.get_current_playback_state,
+            func_args=[],
+            expected_value=expected_state,
+            exception=exception,
+            interval_sec=1)
 
-  def verify_current_track_changed(
-      self,
-      expected_track: str,
-      exception: Optional[Exception] = None) -> bool:
-    """Verifies the Now playing track is changed to be the expected track.
+    def verify_current_track_changed(self, expected_track: str, exception: Optional[Exception] = None) -> bool:
+        """Verifies the Now playing track is changed to be the expected track.
 
     Args:
       expected_track: string, the changed track as expected.
       exception: Exception, raised when the track is not changed if needed.
     """
-    bt_test_utils.wait_until(
-        timeout_sec=MEDIA_UPDATE_TIMEOUT_SEC,
-        condition_func=self.get_current_track_info,
-        func_args=[],
-        expected_value=expected_track,
-        exception=exception,
-        interval_sec=1)
+        bt_test_utils.wait_until(
+            timeout_sec=MEDIA_UPDATE_TIMEOUT_SEC,
+            condition_func=self.get_current_track_info,
+            func_args=[],
+            expected_value=expected_track,
+            exception=exception,
+            interval_sec=1)
 
-  def verify_avrcp_event(
-      self,
-      event_name: bt_constants.AvrcpEvent,
-      check_time: str,
-      timeout_sec: float = 20) -> bool:
-    """Verifies that an AVRCP event was received by an AndroidDevice.
+    def verify_avrcp_event(self, event_name: bt_constants.AvrcpEvent, check_time: str, timeout_sec: float = 20) -> bool:
+        """Verifies that an AVRCP event was received by an AndroidDevice.
 
     Checks logcat to verify that an AVRCP event was received after a given
     time.
@@ -1244,25 +1116,25 @@
     Returns:
       True if the event was received.
     """
-    avrcp_events = [
-        'State:NOT_PLAYING->PLAYING', 'State:PLAYING->NOT_PLAYING',
-        'sendMediaKeyEvent: keyEvent=76', 'sendMediaKeyEvent: keyEvent=75'
-    ]
-    if event_name.value not in avrcp_events:
-      raise signals.TestError('An unexpected AVRCP event is specified.')
+        avrcp_events = [
+            'State:NOT_PLAYING->PLAYING', 'State:PLAYING->NOT_PLAYING', 'sendMediaKeyEvent: keyEvent=76',
+            'sendMediaKeyEvent: keyEvent=75'
+        ]
+        if event_name.value not in avrcp_events:
+            raise signals.TestError('An unexpected AVRCP event is specified.')
 
-    end_time = time.time() + timeout_sec
-    while time.time() < end_time:
-      if self.logcat_filter_message(check_time, event_name.value):
-        logging.info('%s event received successfully.', event_name)
-        return True
-      time.sleep(1)
-    logging.error('AndroidDevice failed to receive %s event.', event_name)
-    logging.info('Logcat:\n%s', self.logcat_filter_message(check_time))
-    return False
+        end_time = time.time() + timeout_sec
+        while time.time() < end_time:
+            if self.logcat_filter_message(check_time, event_name.value):
+                logging.info('%s event received successfully.', event_name)
+                return True
+            time.sleep(1)
+        logging.error('AndroidDevice failed to receive %s event.', event_name)
+        logging.info('Logcat:\n%s', self.logcat_filter_message(check_time))
+        return False
 
-  def add_google_account(self, retries: int = 5) -> bool:
-    """Login Google account.
+    def add_google_account(self, retries: int = 5) -> bool:
+        """Login Google account.
 
     Args:
         retries: int, the number of retries.
@@ -1273,21 +1145,20 @@
     Raises:
       TestError
     """
-    for _ in range(retries):
-      output = self._ad.adb.shell(
-          'am instrument -w -e account "%s" -e password '
-          '"%s" -e sync true -e wait-for-checkin false '
-          'com.google.android.tradefed.account/.AddAccount' %
-          (self._ad.dimensions['google_account'],
-           self._ad.dimensions['google_account_password'])).decode()
-      if 'result=SUCCESS' in output:
-        logging.info('Google account is added successfully')
-        time.sleep(3)  # Wait for account to steady state
-        return True
-    raise signals.TestError('Failed to add google account: %s' % output)
+        for _ in range(retries):
+            output = self._ad.adb.shell(
+                'am instrument -w -e account "%s" -e password '
+                '"%s" -e sync true -e wait-for-checkin false '
+                'com.google.android.tradefed.account/.AddAccount' %
+                (self._ad.dimensions['google_account'], self._ad.dimensions['google_account_password'])).decode()
+            if 'result=SUCCESS' in output:
+                logging.info('Google account is added successfully')
+                time.sleep(3)  # Wait for account to steady state
+                return True
+        raise signals.TestError('Failed to add google account: %s' % output)
 
-  def remove_google_account(self, retries: int = 5) -> bool:
-    """Remove Google account.
+    def remove_google_account(self, retries: int = 5) -> bool:
+        """Remove Google account.
 
     Args:
         retries: int, the number of retries.
@@ -1298,18 +1169,16 @@
     Raises:
       TestError
     """
-    for _ in range(retries):
-      output = self._ad.adb.shell(
-          'am instrument -w com.google.android.tradefed.account/.RemoveAccounts'
-      ).decode()
-      if 'result=SUCCESS' in output:
-        logging.info('Google account is removed successfully')
-        return True
-      time.sleep(1)  # Buffer between retries.
-    raise signals.TestError('Failed to remove google account: %s' % output)
+        for _ in range(retries):
+            output = self._ad.adb.shell('am instrument -w com.google.android.tradefed.account/.RemoveAccounts').decode()
+            if 'result=SUCCESS' in output:
+                logging.info('Google account is removed successfully')
+                return True
+            time.sleep(1)  # Buffer between retries.
+        raise signals.TestError('Failed to remove google account: %s' % output)
 
-  def detect_and_pull_ssrdump(self, ramdump_type: str = 'ramdump_bt') -> bool:
-    """Detect and pull RAMDUMP log.
+    def detect_and_pull_ssrdump(self, ramdump_type: str = 'ramdump_bt') -> bool:
+        """Detect and pull RAMDUMP log.
 
     Args:
       ramdump_type: str, the partial of file names to search for in ramdump
@@ -1319,47 +1188,45 @@
     Returns:
       True if there is a file with file name matching the ramdump type.
     """
-    files = self._ad.adb.shell('ls %s' % bt_constants.RAMDUMP_PATH).decode()
-    if ramdump_type in files:
-      logging.info('RAMDUMP is found.')
-      log_name_timestamp = mobly_logger.get_log_file_timestamp()
-      destination = os.path.join(self._ad.log_path, 'RamdumpLogs',
-                                 log_name_timestamp)
-      utils.create_dir(destination)
-      self._ad.adb.pull([bt_constants.RAMDUMP_PATH, destination])
-      return True
-    return False
+        files = self._ad.adb.shell('ls %s' % bt_constants.RAMDUMP_PATH).decode()
+        if ramdump_type in files:
+            logging.info('RAMDUMP is found.')
+            log_name_timestamp = mobly_logger.get_log_file_timestamp()
+            destination = os.path.join(self._ad.log_path, 'RamdumpLogs', log_name_timestamp)
+            utils.create_dir(destination)
+            self._ad.adb.pull([bt_constants.RAMDUMP_PATH, destination])
+            return True
+        return False
 
-  def get_bt_num_of_crashes(self) -> int:
-    """Get number of Bluetooth crash times from bluetooth_manager.
+    def get_bt_num_of_crashes(self) -> int:
+        """Get number of Bluetooth crash times from bluetooth_manager.
 
     Returns:
       Number of Bluetooth crashed times.
     """
-    out = self._regex_bt_crash.search(
-        self._ad.adb.shell('dumpsys bluetooth_manager').decode())
-    # TODO(user): Need to consider the case "out=None" when miss in
-    # matching
-    return int(out.group('num_bt_crashes'))
+        out = self._regex_bt_crash.search(self._ad.adb.shell('dumpsys bluetooth_manager').decode())
+        # TODO(user): Need to consider the case "out=None" when miss in
+        # matching
+        return int(out.group('num_bt_crashes'))
 
-  def clean_ssrdump(self) -> None:
-    """Clean RAMDUMP log.
+    def clean_ssrdump(self) -> None:
+        """Clean RAMDUMP log.
 
     Returns:
       None
     """
-    self._ad.adb.shell('rm -rf %s/*' % bt_constants.RAMDUMP_PATH)
+        self._ad.adb.shell('rm -rf %s/*' % bt_constants.RAMDUMP_PATH)
 
-  def set_target(self, bt_device: derived_bt_device.BtDevice) -> None:
-    """Allows for use to get target device object for target interaction."""
-    self._target_device = bt_device
+    def set_target(self, bt_device: derived_bt_device.BtDevice) -> None:
+        """Allows for use to get target device object for target interaction."""
+        self._target_device = bt_device
 
-  def wait_for_hsp_connection_state(self,
-                                    mac_address: str,
-                                    connected: bool,
-                                    raise_error: bool = True,
-                                    timeout_sec: float = 30) -> bool:
-    """Waits for HSP connection to be in a expected state on Android device.
+    def wait_for_hsp_connection_state(self,
+                                      mac_address: str,
+                                      connected: bool,
+                                      raise_error: bool = True,
+                                      timeout_sec: float = 30) -> bool:
+        """Waits for HSP connection to be in a expected state on Android device.
 
     Args:
       mac_address: The Bluetooth mac address of the peripheral device.
@@ -1370,44 +1237,40 @@
     Returns:
       True if HSP connection state is the expected state.
     """
-    expected_state = bt_constants.BluetoothConnectionStatus.STATE_DISCONNECTED
-    if connected:
-      expected_state = bt_constants.BluetoothConnectionStatus.STATE_CONNECTED
-    msg = ('Failed to %s the device "%s" within %d seconds via HSP.' %
-           ('connect' if connected else 'disconnect', mac_address, timeout_sec))
-    return bt_test_utils.wait_until(
-        timeout_sec=timeout_sec,
-        condition_func=self._ad.sl4a.bluetoothHspGetConnectionStatus,
-        func_args=[mac_address],
-        expected_value=expected_state,
-        exception=signals.TestError(msg) if raise_error else None)
+        expected_state = bt_constants.BluetoothConnectionStatus.STATE_DISCONNECTED
+        if connected:
+            expected_state = bt_constants.BluetoothConnectionStatus.STATE_CONNECTED
+        msg = ('Failed to %s the device "%s" within %d seconds via HSP.' % ('connect' if connected else 'disconnect',
+                                                                            mac_address, timeout_sec))
+        return bt_test_utils.wait_until(
+            timeout_sec=timeout_sec,
+            condition_func=self._ad.sl4a.bluetoothHspGetConnectionStatus,
+            func_args=[mac_address],
+            expected_value=expected_state,
+            exception=signals.TestError(msg) if raise_error else None)
 
-  def wait_for_bluetooth_toggle_state(self,
-                                      enabled: bool = True,
-                                      timeout_sec: float = 30) -> bool:
-    """Waits for Bluetooth to be in an expected state.
+    def wait_for_bluetooth_toggle_state(self, enabled: bool = True, timeout_sec: float = 30) -> bool:
+        """Waits for Bluetooth to be in an expected state.
 
     Args:
       enabled: True if Bluetooth status is enabled as expected.
       timeout_sec: Number of seconds to wait for Bluetooth to be in the expected
           state.
     """
-    bt_test_utils.wait_until(
-        timeout_sec=timeout_sec,
-        condition_func=self._ad.mbs.btIsEnabled,
-        func_args=[],
-        expected_value=enabled,
-        exception=signals.TestError(
-            'Bluetooth is not %s within %d seconds on the device "%s".' %
-            ('enabled' if enabled else 'disabled', timeout_sec,
-             self._ad.serial)))
+        bt_test_utils.wait_until(
+            timeout_sec=timeout_sec,
+            condition_func=self._ad.mbs.btIsEnabled,
+            func_args=[],
+            expected_value=enabled,
+            exception=signals.TestError('Bluetooth is not %s within %d seconds on the device "%s".' %
+                                        ('enabled' if enabled else 'disabled', timeout_sec, self._ad.serial)))
 
-  def wait_for_a2dp_connection_state(self,
-                                     mac_address: str,
-                                     connected: bool,
-                                     raise_error: bool = True,
-                                     timeout_sec: float = 30) -> bool:
-    """Waits for A2DP connection to be in a expected state on Android device.
+    def wait_for_a2dp_connection_state(self,
+                                       mac_address: str,
+                                       connected: bool,
+                                       raise_error: bool = True,
+                                       timeout_sec: float = 30) -> bool:
+        """Waits for A2DP connection to be in a expected state on Android device.
 
     Args:
       mac_address: The Bluetooth mac address of the peripheral device.
@@ -1418,21 +1281,18 @@
     Returns:
       True if A2DP connection state is in the expected state.
     """
-    msg = ('Failed to %s the device "%s" within %d seconds via A2DP.' %
-           ('connect' if connected else 'disconnect', mac_address, timeout_sec))
-    return bt_test_utils.wait_until(
-        timeout_sec=timeout_sec,
-        condition_func=self.is_a2dp_sink_connected,
-        func_args=[mac_address],
-        expected_value=connected,
-        exception=signals.TestError(msg) if raise_error else None)
+        msg = ('Failed to %s the device "%s" within %d seconds via A2DP.' % ('connect' if connected else 'disconnect',
+                                                                             mac_address, timeout_sec))
+        return bt_test_utils.wait_until(
+            timeout_sec=timeout_sec,
+            condition_func=self.is_a2dp_sink_connected,
+            func_args=[mac_address],
+            expected_value=connected,
+            exception=signals.TestError(msg) if raise_error else None)
 
-  def wait_for_nap_service_connection(
-      self,
-      connected_mac_addr: str,
-      state_connected: bool,
-      exception: Exception) -> bool:
-    """Waits for NAP service connection to be expected state.
+    def wait_for_nap_service_connection(self, connected_mac_addr: str, state_connected: bool,
+                                        exception: Exception) -> bool:
+        """Waits for NAP service connection to be expected state.
 
     Args:
       connected_mac_addr: String, Bluetooth Mac address is needed to be checked.
@@ -1444,26 +1304,27 @@
     Raises:
       exception: Raised if NAP service connection is not expected state.
     """
-    def is_device_connected():
-      """Returns True if connected else False."""
-      connected_devices = self._ad.sl4a.bluetoothPanGetConnectedDevices()
-      # Check if the Bluetooth mac address is in the connected device list.
-      return connected_mac_addr in [d['address'] for d in connected_devices]
 
-    bt_test_utils.wait_until(
-        timeout_sec=bt_constants.NAP_CONNECTION_TIMEOUT_SECS,
-        condition_func=is_device_connected,
-        func_args=[],
-        expected_value=state_connected,
-        exception=exception)
+        def is_device_connected():
+            """Returns True if connected else False."""
+            connected_devices = self._ad.sl4a.bluetoothPanGetConnectedDevices()
+            # Check if the Bluetooth mac address is in the connected device list.
+            return connected_mac_addr in [d['address'] for d in connected_devices]
 
-  def verify_internet(self,
-                      allow_access: bool,
-                      exception: Exception,
-                      test_url: str = TEST_URL,
-                      interval_sec: int = PING_INTERVAL_TIME_SEC,
-                      timeout_sec: float = PING_TIMEOUT_SEC) -> bool:
-    """Verifies that internet is in expected state.
+        bt_test_utils.wait_until(
+            timeout_sec=bt_constants.NAP_CONNECTION_TIMEOUT_SECS,
+            condition_func=is_device_connected,
+            func_args=[],
+            expected_value=state_connected,
+            exception=exception)
+
+    def verify_internet(self,
+                        allow_access: bool,
+                        exception: Exception,
+                        test_url: str = TEST_URL,
+                        interval_sec: int = PING_INTERVAL_TIME_SEC,
+                        timeout_sec: float = PING_TIMEOUT_SEC) -> bool:
+        """Verifies that internet is in expected state.
 
     Continuously make ping request to a URL for internet verification.
 
@@ -1480,38 +1341,34 @@
     Raises:
       exception: Raised if internet is not in expected state.
     """
-    self._ad.log.info('Verify that internet %s be used.' %
-                      ('can' if allow_access else 'can not'))
+        self._ad.log.info('Verify that internet %s be used.' % ('can' if allow_access else 'can not'))
 
-    def http_ping():
-      """Returns True if http ping success else False."""
-      try:
-        return bool(self._ad.sl4a.httpPing(test_url))
-      except jsonrpc_client_base.ApiError as e:
-        # ApiError is raised by httpPing() when no internet.
-        self._ad.log.debug(str(e))
-      return False
+        def http_ping():
+            """Returns True if http ping success else False."""
+            try:
+                return bool(self._ad.sl4a.httpPing(test_url))
+            except jsonrpc_client_base.ApiError as e:
+                # ApiError is raised by httpPing() when no internet.
+                self._ad.log.debug(str(e))
+            return False
 
-    bt_test_utils.wait_until(
-        timeout_sec=timeout_sec,
-        condition_func=http_ping,
-        func_args=[],
-        expected_value=allow_access,
-        exception=exception,
-        interval_sec=interval_sec)
+        bt_test_utils.wait_until(
+            timeout_sec=timeout_sec,
+            condition_func=http_ping,
+            func_args=[],
+            expected_value=allow_access,
+            exception=exception,
+            interval_sec=interval_sec)
 
-  def allow_extra_permissions(self) -> None:
-    """A method to allow extra permissions.
+    def allow_extra_permissions(self) -> None:
+        """A method to allow extra permissions.
 
     This method has no any logics. It is used to skip the operation when it is
     called if a test is not Wear OS use case.
     """
 
-  def is_service_running(
-      self,
-      mac_address: str,
-      timeout_sec: float) -> bool:
-    """Checks bluetooth profile state.
+    def is_service_running(self, mac_address: str, timeout_sec: float) -> bool:
+        """Checks bluetooth profile state.
 
        Check bluetooth headset/a2dp profile connection
        status from bluetooth manager log.
@@ -1524,52 +1381,41 @@
     Returns:
         True: If pattern match with bluetooth_manager_log.
     """
-    pattern_headset = (r'\sm\w+e:\sC\w+d')
-    pattern_a2dp = (r'StateMachine:.*state=Connected')
-    output_headset = self._ad.adb.shell(
-        'dumpsys bluetooth_manager | egrep -A20 "Profile: HeadsetService"'
-    ).decode()
-    output_a2dp = self._ad.adb.shell(
-        'dumpsys bluetooth_manager | egrep -A30 "Profile: A2dpService"').decode(
-        )
-    service_type = {
-        'a2dp': ((pattern_a2dp), (output_a2dp)),
-        'headset': ((pattern_headset), (output_headset))
-    }
-    start_time = time.time()
-    end_time = start_time + timeout_sec
-    while start_time < end_time:
-      try:
-        match = service_type
-        if match and mac_address in service_type:
-          return True
-      except adb.AdbError as e:
-        logging.exception(e)
-      time.sleep(ADB_WAITING_TIME_SECONDS)
-    return False
+        pattern_headset = (r'\sm\w+e:\sC\w+d')
+        pattern_a2dp = (r'StateMachine:.*state=Connected')
+        output_headset = self._ad.adb.shell('dumpsys bluetooth_manager | egrep -A20 "Profile: HeadsetService"').decode()
+        output_a2dp = self._ad.adb.shell('dumpsys bluetooth_manager | egrep -A30 "Profile: A2dpService"').decode()
+        service_type = {'a2dp': ((pattern_a2dp), (output_a2dp)), 'headset': ((pattern_headset), (output_headset))}
+        start_time = time.time()
+        end_time = start_time + timeout_sec
+        while start_time < end_time:
+            try:
+                match = service_type
+                if match and mac_address in service_type:
+                    return True
+            except adb.AdbError as e:
+                logging.exception(e)
+            time.sleep(ADB_WAITING_TIME_SECONDS)
+        return False
 
-  def connect_wifi_from_other_device_hotspot(
-      self, wifi_hotspot_device: android_device.AndroidDevice) -> None:
-    """Turns on 2.4G Wifi hotspot from the other android device and connect on the android device.
+    def connect_wifi_from_other_device_hotspot(self, wifi_hotspot_device: android_device.AndroidDevice) -> None:
+        """Turns on 2.4G Wifi hotspot from the other android device and connect on the android device.
 
     Args:
       wifi_hotspot_device: Android device, turn on 2.4G Wifi hotspot.
     """
-    wifi_hotspot_2_4g_config = bt_constants.WIFI_HOTSPOT_2_4G.copy()
-    if int(wifi_hotspot_device.build_info['build_version_sdk']) > 29:
-      wifi_hotspot_2_4g_config['apBand'] = 1
-    # Turn on 2.4G Wifi hotspot on the secondary phone.
-    wifi_hotspot_device.sl4a.wifiSetWifiApConfiguration(
-        wifi_hotspot_2_4g_config)
-    wifi_hotspot_device.sl4a.connectivityStartTethering(0, False)
-    # Connect the 2.4G Wifi on the primary phone.
-    self._ad.mbs.wifiEnable()
-    self._ad.mbs.wifiConnectSimple(
-        wifi_hotspot_2_4g_config['SSID'],
-        wifi_hotspot_2_4g_config['password'])
+        wifi_hotspot_2_4g_config = bt_constants.WIFI_HOTSPOT_2_4G.copy()
+        if int(wifi_hotspot_device.build_info['build_version_sdk']) > 29:
+            wifi_hotspot_2_4g_config['apBand'] = 1
+        # Turn on 2.4G Wifi hotspot on the secondary phone.
+        wifi_hotspot_device.sl4a.wifiSetWifiApConfiguration(wifi_hotspot_2_4g_config)
+        wifi_hotspot_device.sl4a.connectivityStartTethering(0, False)
+        # Connect the 2.4G Wifi on the primary phone.
+        self._ad.mbs.wifiEnable()
+        self._ad.mbs.wifiConnectSimple(wifi_hotspot_2_4g_config['SSID'], wifi_hotspot_2_4g_config['password'])
 
-  def get_paired_device_supported_codecs(self, mac_address: str) -> List[str]:
-    """Gets the supported A2DP codecs of the paired Bluetooth device.
+    def get_paired_device_supported_codecs(self, mac_address: str) -> List[str]:
+        """Gets the supported A2DP codecs of the paired Bluetooth device.
 
     Gets the supported A2DP codecs of the paired Bluetooth device from bluetooth
     manager log.
@@ -1580,18 +1426,17 @@
     Returns:
         A list of the A2DP codecs that the paired Bluetooth device supports.
     """
-    if not self.is_bt_paired(mac_address):
-      raise signals.TestError(
-          f'Devices {self.serial} and {mac_address} are not paired.')
-    cmd = (f'dumpsys bluetooth_manager | '
-           f'egrep -A12 "A2dpStateMachine for {mac_address}" | '
-           f'egrep -A5 "mCodecsSelectableCapabilities"')
-    paired_device_selectable_codecs = self._ad.adb.shell(cmd).decode()
-    pattern = 'codecName:(.*),mCodecType'
-    return re.findall(pattern, paired_device_selectable_codecs)
+        if not self.is_bt_paired(mac_address):
+            raise signals.TestError(f'Devices {self.serial} and {mac_address} are not paired.')
+        cmd = (f'dumpsys bluetooth_manager | '
+               f'egrep -A12 "A2dpStateMachine for {mac_address}" | '
+               f'egrep -A5 "mCodecsSelectableCapabilities"')
+        paired_device_selectable_codecs = self._ad.adb.shell(cmd).decode()
+        pattern = 'codecName:(.*),mCodecType'
+        return re.findall(pattern, paired_device_selectable_codecs)
 
-  def get_current_a2dp_codec(self) -> bt_constants.BluetoothA2dpCodec:
-    """Gets current A2DP codec type.
+    def get_current_a2dp_codec(self) -> bt_constants.BluetoothA2dpCodec:
+        """Gets current A2DP codec type.
 
     Returns:
         A number representing the current A2DP codec type.
@@ -1602,20 +1447,19 @@
         3: aptX HD
         4: LDAC
     """
-    codec_type = self._ad.sl4a.bluetoothA2dpGetCurrentCodecConfig()['codecType']
-    return bt_constants.BluetoothA2dpCodec(codec_type)
+        codec_type = self._ad.sl4a.bluetoothA2dpGetCurrentCodecConfig()['codecType']
+        return bt_constants.BluetoothA2dpCodec(codec_type)
 
-  def is_variable_bit_rate_enabled(self) -> bool:
-    """Checks if Variable Bit Rate (VBR) support is enabled for A2DP AAC codec.
+    def is_variable_bit_rate_enabled(self) -> bool:
+        """Checks if Variable Bit Rate (VBR) support is enabled for A2DP AAC codec.
 
     Returns:
       True if Variable Bit Rate support is enabled else False.
     """
-    return bt_constants.TRUE in self._ad.adb.getprop(
-        bt_constants.AAC_VBR_SUPPORTED_PROPERTY)
+        return bt_constants.TRUE in self._ad.adb.getprop(bt_constants.AAC_VBR_SUPPORTED_PROPERTY)
 
-  def toggle_variable_bit_rate(self, enabled: bool = True) -> bool:
-    """Toggles Variable Bit Rate (VBR) support status for A2DP AAC codec.
+    def toggle_variable_bit_rate(self, enabled: bool = True) -> bool:
+        """Toggles Variable Bit Rate (VBR) support status for A2DP AAC codec.
 
     After calling this method, the android device needs to restart Bluetooth for
     taking effect.
@@ -1629,14 +1473,12 @@
     Returns:
       True if the status is changed successfully else False.
     """
-    self._ad.adb.shell(
-        f'su root setprop {bt_constants.AAC_VBR_SUPPORTED_PROPERTY} '
-        f'{bt_constants.TRUE if enabled else bt_constants.FALSE}')
-    return enabled == self.is_variable_bit_rate_enabled()
+        self._ad.adb.shell(f'su root setprop {bt_constants.AAC_VBR_SUPPORTED_PROPERTY} '
+                           f'{bt_constants.TRUE if enabled else bt_constants.FALSE}')
+        return enabled == self.is_variable_bit_rate_enabled()
 
-  def pair_and_connect_ble_device(
-      self, peripheral_ble_device: android_device.AndroidDevice) -> None:
-    """Pairs Android phone with BLE device.
+    def pair_and_connect_ble_device(self, peripheral_ble_device: android_device.AndroidDevice) -> None:
+        """Pairs Android phone with BLE device.
 
     Initiates pairing from the phone and checks if it is bonded and connected to
     the BLE device.
@@ -1648,24 +1490,22 @@
     Raises:
       signals.ControllerError: raised if it failed to connect BLE device.
     """
-    peripheral_ble_device.activate_ble_pairing_mode()
-    mac_address = self.scan_and_get_ble_device_address(
-        peripheral_ble_device.get_device_name())
-    self.pair_and_connect_bluetooth(mac_address)
+        peripheral_ble_device.activate_ble_pairing_mode()
+        mac_address = self.scan_and_get_ble_device_address(peripheral_ble_device.get_device_name())
+        self.pair_and_connect_bluetooth(mac_address)
 
-  def toggle_charging(self, enabled: bool) -> None:
-    """Toggles charging on the device.
+    def toggle_charging(self, enabled: bool) -> None:
+        """Toggles charging on the device.
 
     Args:
       enabled: Enable charging if True.
     """
-    set_value = '0' if enabled else '1'
-    config_file = bt_constants.CHARGING_CONTROL_CONFIG_DICT[
-        self._ad.build_info['hardware']]
-    self._ad.adb.shell(f'echo {set_value} > {config_file}')
+        set_value = '0' if enabled else '1'
+        config_file = bt_constants.CHARGING_CONTROL_CONFIG_DICT[self._ad.build_info['hardware']]
+        self._ad.adb.shell(f'echo {set_value} > {config_file}')
 
-  def enable_airplane_mode(self, wait_secs=1) -> None:
-    """Enables airplane mode on device.
+    def enable_airplane_mode(self, wait_secs=1) -> None:
+        """Enables airplane mode on device.
 
     Args:
       wait_secs: float, the amount of time to wait after sending the airplane
@@ -1673,15 +1513,12 @@
     Returns:
         None
     """
-    self._ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '1'])
-    self._ad.adb.shell([
-        'am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez',
-        'state', 'true'
-    ])
-    time.sleep(wait_secs)
+        self._ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '1'])
+        self._ad.adb.shell(['am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez', 'state', 'true'])
+        time.sleep(wait_secs)
 
-  def disable_airplane_mode(self, wait_secs=1) -> None:
-    """Disables airplane mode on device.
+    def disable_airplane_mode(self, wait_secs=1) -> None:
+        """Disables airplane mode on device.
 
     Args:
       wait_secs: float, the amount of time to wait after sending the airplane
@@ -1689,22 +1526,19 @@
     Returns:
         None
     """
-    self._ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '0'])
-    self._ad.adb.shell([
-        'am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez',
-        'state', 'false'
-    ])
-    time.sleep(wait_secs)
+        self._ad.adb.shell(['settings', 'put', 'global', 'airplane_mode_on', '0'])
+        self._ad.adb.shell(['am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE', '--ez', 'state', 'false'])
+        time.sleep(wait_secs)
 
-  def disable_verity_check(self) -> None:
-    """Disables Android dm verity check.
+    def disable_verity_check(self) -> None:
+        """Disables Android dm verity check.
 
     Returns:
       None
     """
-    if 'verity is already disabled' in str(self._ad.adb.disable_verity()):
-      return
-    self._ad.reboot()
-    self._ad.root_adb()
-    self._ad.wait_for_boot_completion()
-    self._ad.adb.remount()
+        if 'verity is already disabled' in str(self._ad.adb.disable_verity()):
+            return
+        self._ad.reboot()
+        self._ad.root_adb()
+        self._ad.wait_for_boot_completion()
+        self._ad.adb.remount()
diff --git a/system/blueberry/utils/ble_scan_adv_constants.py b/system/blueberry/utils/ble_scan_adv_constants.py
new file mode 100644
index 0000000..5b45c19
--- /dev/null
+++ b/system/blueberry/utils/ble_scan_adv_constants.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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 enum
+
+
+class BleScanSettingsMatchNums(enum.IntEnum):
+    """Bluetooth Low Energy scan settings match nums"""
+    ONE = 1
+    FEW = 2
+    MAX = 3
+
+
+class BleAdvertiseSettingsTxPower(enum.IntEnum):
+    """Enum class for BLE advertise settings tx power."""
+    ULTRA_LOW = 0
+    LOW = 1
+    MEDIUM = 2
+    HIGH = 3
+
+
+class BleAdvertiseSettingsMode(enum.IntEnum):
+    """Enum class for BLE advertise settings mode."""
+    LOW_POWER = 0
+    BALANCED = 1
+    LOW_LATENCY = 2
+
+
+class BleScanSettingsModes(enum.IntEnum):
+    """Bluetooth Low Energy scan settings mode"""
+    OPPORTUNISTIC = -1
+    LOW_POWER = 0,
+    BALANCED = 1,
+    LOW_LATENCY = 2
+    AMBIENT_DISCOVERY = 3
diff --git a/system/blueberry/utils/bt_constants.py b/system/blueberry/utils/bt_constants.py
index e65e039..5772370 100644
--- a/system/blueberry/utils/bt_constants.py
+++ b/system/blueberry/utils/bt_constants.py
@@ -3,7 +3,6 @@
 
 import enum
 
-
 ### Generic Constants Begin ###
 BT_DEFAULT_TIMEOUT_SECONDS = 15
 DEFAULT_RFCOMM_TIMEOUT_MS = 10000
@@ -55,16 +54,17 @@
 
 # Dict containing charging control config for devices.
 CHARGING_CONTROL_CONFIG_DICT = {
-# Internal codename
+    # Internal codename
 }
 
 
 class AvrcpEvent(enum.Enum):
-  """Enumeration of AVRCP event types."""
-  PLAY = 'State:NOT_PLAYING->PLAYING'
-  PAUSE = 'State:PLAYING->NOT_PLAYING'
-  TRACK_PREVIOUS = 'sendMediaKeyEvent: keyEvent=76'
-  TRACK_NEXT = 'sendMediaKeyEvent: keyEvent=75'
+    """Enumeration of AVRCP event types."""
+    PLAY = 'State:NOT_PLAYING->PLAYING'
+    PAUSE = 'State:PLAYING->NOT_PLAYING'
+    TRACK_PREVIOUS = 'sendMediaKeyEvent: keyEvent=76'
+    TRACK_NEXT = 'sendMediaKeyEvent: keyEvent=75'
+
 
 # Bluetooth RFCOMM UUIDs as defined by the SIG
 BT_RFCOMM_UUIDS = {
@@ -98,115 +98,98 @@
 
 
 class BluetoothAccessLevel(enum.IntEnum):
-  """Enum class for bluetooth profile access levels."""
-  ACCESS_ALLOWED = 1
-  ACCESS_DENIED = 2
+    """Enum class for bluetooth profile access levels."""
+    ACCESS_ALLOWED = 1
+    ACCESS_DENIED = 2
 
 
 class BluetoothProfile(enum.IntEnum):
-  """Enum class for bluetooth profile types.
+    """Enum class for bluetooth profile types.
 
-  Should be kept in sync with
-  //frameworks/base/core/java/android/bluetooth/BluetoothProfile.java
-  """
-
-  HEADSET = 1
-  A2DP = 2
-  HEALTH = 3
-  HID_HOST = 4
-  PAN = 5
-  PBAP = 6
-  GATT = 7
-  GATT_SERVER = 8
-  MAP = 9
-  SAP = 10
-  A2DP_SINK = 11
-  AVRCP_CONTROLLER = 12
-  AVRCP = 13
-  HEADSET_CLIENT = 16
-  PBAP_CLIENT = 17
-  MAP_MCE = 18
-  HID_DEVICE = 19
-  OPP = 20
-  HEARING_AID = 21
+    Should be kept in sync with
+    //frameworks/base/core/java/android/bluetooth/BluetoothProfile.java
+    """
+    HEADSET = 1
+    A2DP = 2
+    HEALTH = 3
+    HID_HOST = 4
+    PAN = 5
+    PBAP = 6
+    GATT = 7
+    GATT_SERVER = 8
+    MAP = 9
+    SAP = 10
+    A2DP_SINK = 11
+    AVRCP_CONTROLLER = 12
+    AVRCP = 13
+    HEADSET_CLIENT = 16
+    PBAP_CLIENT = 17
+    MAP_MCE = 18
+    HID_DEVICE = 19
+    OPP = 20
+    HEARING_AID = 21
 
 
 class BluetoothConnectionPolicy(enum.IntEnum):
-  """Enum class for bluetooth bluetooth connection policy.
+    """Enum class for bluetooth bluetooth connection policy.
 
-  bluetooth connection policy as defined in
-  //frameworks/base/core/java/android/bluetooth/BluetoothProfile.java
-  """
-  CONNECTION_POLICY_UNKNOWN = -1
-  CONNECTION_POLICY_FORBIDDEN = 0
-  CONNECTION_POLICY_ALLOWED = 100
+    bluetooth connection policy as defined in
+    //frameworks/base/core/java/android/bluetooth/BluetoothProfile.java
+    """
+    CONNECTION_POLICY_UNKNOWN = -1
+    CONNECTION_POLICY_FORBIDDEN = 0
+    CONNECTION_POLICY_ALLOWED = 100
 
 
 class BluetoothConnectionStatus(enum.IntEnum):
-  """Enum class for bluetooth connection status.
+    """Enum class for bluetooth connection status.
 
-  Bluetooth connection status as defined in
-  //frameworks/base/core/java/android/bluetooth/BluetoothProfile.java
-  """
-  STATE_DISCONNECTED = 0
-  STATE_CONNECTING = 1
-  STATE_CONNECTED = 2
-  STATE_DISCONNECTING = 3
+    Bluetooth connection status as defined in
+    //frameworks/base/core/java/android/bluetooth/BluetoothProfile.java
+    """
+    STATE_DISCONNECTED = 0
+    STATE_CONNECTING = 1
+    STATE_CONNECTED = 2
+    STATE_DISCONNECTING = 3
 
 
 class BluetoothPriorityLevel(enum.IntEnum):
-  """Enum class for bluetooth priority level.
+    """Enum class for bluetooth priority level.
 
-  Priority levels as defined in
-  //frameworks/base/core/java/android/bluetooth/BluetoothProfile.java
-  """
-
-  PRIORITY_AUTO_CONNECT = 1000
-  PRIORITY_ON = 100
-  PRIORITY_OFF = 0
-  PRIORITY_UNDEFINED = -1
-
-
-class BleAdvertiseSettingsMode(enum.IntEnum):
-  """Enum class for BLE advertise settings mode."""
-  LOW_POWER = 0
-  BALANCED = 1
-  LOW_LATENCY = 2
-
-
-class BleAdvertiseSettingsTxPower(enum.IntEnum):
-  """Enum class for BLE advertise settings tx power."""
-  ULTRA_LOW = 0
-  LOW = 1
-  MEDIUM = 2
-  HIGH = 3
+    Priority levels as defined in
+    //frameworks/base/core/java/android/bluetooth/BluetoothProfile.java
+    """
+    PRIORITY_AUTO_CONNECT = 1000
+    PRIORITY_ON = 100
+    PRIORITY_OFF = 0
+    PRIORITY_UNDEFINED = -1
 
 
 class LogType(enum.Enum):
-  """Enumeration of device log type."""
-  DEFAULT_VALUE = 'GENERIC'
-  BLUETOOTH_DEVICE_SIMULATOR = 'BDS'
-  ICLEVER_HB01 = 'GENERIC'
+    """Enumeration of device log type."""
+    DEFAULT_VALUE = 'GENERIC'
+    BLUETOOTH_DEVICE_SIMULATOR = 'BDS'
+    ICLEVER_HB01 = 'GENERIC'
 
 
 class CallState(enum.IntEnum):
-  """Enum class for phone call state."""
-  IDLE = 0
-  RINGING = 1
-  OFFHOOK = 2
+    """Enum class for phone call state."""
+    IDLE = 0
+    RINGING = 1
+    OFFHOOK = 2
 
 
 class CallLogType(enum.IntEnum):
-  """Enum class for phone call log type."""
-  INCOMING_CALL = 1
-  OUTGOING_CALL = 2
-  MISSED_CALL = 3
+    """Enum class for phone call log type."""
+    INCOMING_CALL = 1
+    OUTGOING_CALL = 2
+    MISSED_CALL = 3
 
 
 class BluetoothA2dpCodec(enum.IntEnum):
-  """Enum class for Bluetooth A2DP codec type."""
-  SBC = 0
-  AAC = 1
-  APTX = 2
-  APTX_HD = 3
-  LDAC = 4
+    """Enum class for Bluetooth A2DP codec type."""
+    SBC = 0
+    AAC = 1
+    APTX = 2
+    APTX_HD = 3
+    LDAC = 4
diff --git a/system/blueberry/utils/bt_gatt_constants.py b/system/blueberry/utils/bt_gatt_constants.py
new file mode 100644
index 0000000..2067ddb
--- /dev/null
+++ b/system/blueberry/utils/bt_gatt_constants.py
@@ -0,0 +1,353 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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 enum
+import sys
+
+
+def _import_str_enum():
+    # StrEnum is only introduced in Python 3.11
+    if sys.version_info >= (3, 11):
+        from enum import StrEnum
+        return StrEnum
+    else:
+        from typing import Type, TypeVar
+        _T = TypeVar("_T")
+
+        class StrEnumInternal(str, enum.Enum):
+            pass
+
+        return StrEnumInternal
+
+
+StrEnum = _import_str_enum()
+
+
+# Gatt Callback error messages
+class GattCallbackError(StrEnum):
+    CHAR_WRITE_REQ_ERR = "Characteristic Write Request event not found. Expected {}"
+    CHAR_WRITE_ERR = "Characteristic Write event not found. Expected {}"
+    DESC_WRITE_REQ_ERR = "Descriptor Write Request event not found. Expected {}"
+    DESC_WRITE_ERR = "Descriptor Write event not found. Expected {}"
+    CHAR_READ_ERR = "Characteristic Read event not found. Expected {}"
+    CHAR_READ_REQ_ERR = "Characteristic Read Request not found. Expected {}"
+    DESC_READ_ERR = "Descriptor Read event not found. Expected {}"
+    DESC_READ_REQ_ERR = "Descriptor Read Request event not found. Expected {}"
+    RD_REMOTE_RSSI_ERR = "Read Remote RSSI event not found. Expected {}"
+    GATT_SERV_DISC_ERR = "GATT Services Discovered event not found. Expected {}"
+    SERV_ADDED_ERR = "Service Added event not found. Expected {}"
+    MTU_CHANGED_ERR = "MTU Changed event not found. Expected {}"
+    MTU_SERV_CHANGED_ERR = "MTU Server Changed event not found. Expected {}"
+    GATT_CONN_CHANGE_ERR = "GATT Connection Changed event not found. Expected {}"
+    CHAR_CHANGE_ERR = "GATT Characteristic Changed event not fond. Expected {}"
+    PHY_READ_ERR = "Phy Read event not fond. Expected {}"
+    PHY_UPDATE_ERR = "Phy Update event not fond. Expected {}"
+    EXEC_WRITE_ERR = "GATT Execute Write event not found. Expected {}"
+
+
+# GATT callback strings as defined in GattClientFacade.java and
+# GattServerFacade.java implemented callbacks.
+class GattCallbackString(StrEnum):
+    CHAR_WRITE_REQ = "GattServer{}onCharacteristicWriteRequest"
+    EXEC_WRITE = "GattServer{}onExecuteWrite"
+    CHAR_WRITE = "GattConnect{}onCharacteristicWrite"
+    DESC_WRITE_REQ = "GattServer{}onDescriptorWriteRequest"
+    DESC_WRITE = "GattConnect{}onDescriptorWrite"
+    CHAR_READ = "GattConnect{}onCharacteristicRead"
+    CHAR_READ_REQ = "GattServer{}onCharacteristicReadRequest"
+    DESC_READ = "GattConnect{}onDescriptorRead"
+    DESC_READ_REQ = "GattServer{}onDescriptorReadRequest"
+    RD_REMOTE_RSSI = "GattConnect{}onReadRemoteRssi"
+    GATT_SERV_DISC = "GattConnect{}onServicesDiscovered"
+    SERV_ADDED = "GattServer{}onServiceAdded"
+    MTU_CHANGED = "GattConnect{}onMtuChanged"
+    MTU_SERV_CHANGED = "GattServer{}onMtuChanged"
+    GATT_CONN_CHANGE = "GattConnect{}onConnectionStateChange"
+    CHAR_CHANGE = "GattConnect{}onCharacteristicChanged"
+    PHY_READ = "GattConnect{}onPhyRead"
+    PHY_UPDATE = "GattConnect{}onPhyUpdate"
+    SERV_PHY_READ = "GattServer{}onPhyRead"
+    SERV_PHY_UPDATE = "GattServer{}onPhyUpdate"
+
+
+# yapf: disable
+# GATT event dictionary of expected callbacks and errors.
+class GattEvent(dict, enum.Enum):
+
+    def __getitem__(self, item):
+        return self._value_[item]
+
+    CHAR_WRITE_REQ = {
+            "evt": GattCallbackString.CHAR_WRITE_REQ,
+            "err": GattCallbackError.CHAR_WRITE_REQ_ERR
+    }
+    EXEC_WRITE = {
+            "evt": GattCallbackString.EXEC_WRITE,
+            "err": GattCallbackError.EXEC_WRITE_ERR
+    }
+    CHAR_WRITE = {
+            "evt": GattCallbackString.CHAR_WRITE,
+            "err": GattCallbackError.CHAR_WRITE_ERR
+    }
+    DESC_WRITE_REQ = {
+            "evt": GattCallbackString.DESC_WRITE_REQ,
+            "err": GattCallbackError.DESC_WRITE_REQ_ERR
+    }
+    DESC_WRITE = {
+            "evt": GattCallbackString.DESC_WRITE,
+            "err": GattCallbackError.DESC_WRITE_ERR
+    }
+    CHAR_READ = {
+            "evt": GattCallbackString.CHAR_READ,
+            "err": GattCallbackError.CHAR_READ_ERR
+    }
+    CHAR_READ_REQ = {
+            "evt": GattCallbackString.CHAR_READ_REQ,
+            "err": GattCallbackError.CHAR_READ_REQ_ERR
+    }
+    DESC_READ = {
+            "evt": GattCallbackString.DESC_READ,
+            "err": GattCallbackError.DESC_READ_ERR
+    }
+    DESC_READ_REQ = {
+            "evt": GattCallbackString.DESC_READ_REQ,
+            "err": GattCallbackError.DESC_READ_REQ_ERR
+    }
+    RD_REMOTE_RSSI = {
+            "evt": GattCallbackString.RD_REMOTE_RSSI,
+            "err": GattCallbackError.RD_REMOTE_RSSI_ERR
+    }
+    GATT_SERV_DISC = {
+            "evt": GattCallbackString.GATT_SERV_DISC,
+            "err": GattCallbackError.GATT_SERV_DISC_ERR
+    }
+    SERV_ADDED = {
+            "evt": GattCallbackString.SERV_ADDED,
+            "err": GattCallbackError.SERV_ADDED_ERR
+    }
+    MTU_CHANGED = {
+            "evt": GattCallbackString.MTU_CHANGED,
+            "err": GattCallbackError.MTU_CHANGED_ERR
+    }
+    GATT_CONN_CHANGE = {
+            "evt": GattCallbackString.GATT_CONN_CHANGE,
+            "err": GattCallbackError.GATT_CONN_CHANGE_ERR
+    }
+    CHAR_CHANGE = {
+            "evt": GattCallbackString.CHAR_CHANGE,
+            "err": GattCallbackError.CHAR_CHANGE_ERR
+    }
+    PHY_READ = {
+            "evt": GattCallbackString.PHY_READ,
+            "err": GattCallbackError.PHY_READ_ERR
+    }
+    PHY_UPDATE = {
+            "evt": GattCallbackString.PHY_UPDATE,
+            "err": GattCallbackError.PHY_UPDATE_ERR
+    }
+    SERV_PHY_READ = {
+            "evt": GattCallbackString.SERV_PHY_READ,
+            "err": GattCallbackError.PHY_READ_ERR
+    }
+    SERV_PHY_UPDATE = {
+            "evt": GattCallbackString.SERV_PHY_UPDATE,
+            "err": GattCallbackError.PHY_UPDATE_ERR
+    }
+# yapf: enable
+
+
+# Matches constants of connection states defined in BluetoothGatt.java
+class GattConnectionState(enum.IntEnum):
+    STATE_DISCONNECTED = 0
+    STATE_CONNECTING = 1
+    STATE_CONNECTED = 2
+    STATE_DISCONNECTING = 3
+
+
+# Matches constants of Bluetooth GATT Characteristic values as defined
+# in BluetoothGattCharacteristic.java
+class GattCharacteristic(enum.IntEnum):
+    PROPERTY_BROADCAST = 0x01
+    PROPERTY_READ = 0x02
+    PROPERTY_WRITE_NO_RESPONSE = 0x04
+    PROPERTY_WRITE = 0x08
+    PROPERTY_NOTIFY = 0x10
+    PROPERTY_INDICATE = 0x20
+    PROPERTY_SIGNED_WRITE = 0x40
+    PROPERTY_EXTENDED_PROPS = 0x80
+    PERMISSION_READ = 0x01
+    PERMISSION_READ_ENCRYPTED = 0x02
+    PERMISSION_READ_ENCRYPTED_MITM = 0x04
+    PERMISSION_WRITE = 0x10
+    PERMISSION_WRITE_ENCRYPTED = 0x20
+    PERMISSION_WRITE_ENCRYPTED_MITM = 0x40
+    PERMISSION_WRITE_SIGNED = 0x80
+    PERMISSION_WRITE_SIGNED_MITM = 0x100
+    WRITE_TYPE_DEFAULT = 0x02
+    WRITE_TYPE_NO_RESPONSE = 0x01
+    WRITE_TYPE_SIGNED = 0x04
+    FORMAT_UINT8 = 0x11
+    FORMAT_UINT16 = 0x12
+    FORMAT_UINT32 = 0x14
+    FORMAT_SINT8 = 0x21
+    FORMAT_SINT16 = 0x22
+    FORMAT_SINT32 = 0x24
+    FORMAT_SFLOAT = 0x32
+    FORMAT_FLOAT = 0x34
+
+
+# Matches constants of Bluetooth GATT Characteristic values as defined
+# in BluetoothGattDescriptor.java
+class GattDescriptor(list, enum.Enum):
+
+    def __getitem__(self, item):
+        return self._value_[item]
+
+    ENABLE_NOTIFICATION_VALUE = [0x01, 0x00]
+    ENABLE_INDICATION_VALUE = [0x02, 0x00]
+    DISABLE_NOTIFICATION_VALUE = [0x00, 0x00]
+    PERMISSION_READ = [0x01]
+    PERMISSION_READ_ENCRYPTED = [0x02]
+    PERMISSION_READ_ENCRYPTED_MITM = [0x04]
+    PERMISSION_WRITE = [0x10]
+    PERMISSION_WRITE_ENCRYPTED = [0x20]
+    PERMISSION_WRITE_ENCRYPTED_MITM = [0x40]
+    PERMISSION_WRITE_SIGNED = [0x80]
+    PERMISSION_WRITE_SIGNED_MITM = [0x100]
+
+
+# https://www.bluetooth.com/specifications/gatt/descriptors
+class GattCharDesc(StrEnum):
+    GATT_CHARAC_EXT_PROPER_UUID = '00002900-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_USER_DESC_UUID = '00002901-0000-1000-8000-00805f9b34fb'
+    GATT_CLIENT_CHARAC_CFG_UUID = '00002902-0000-1000-8000-00805f9b34fb'
+    GATT_SERVER_CHARAC_CFG_UUID = '00002903-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_FMT_UUID = '00002904-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_AGREG_FMT_UUID = '00002905-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_VALID_RANGE_UUID = '00002906-0000-1000-8000-00805f9b34fb'
+    GATT_EXTERNAL_REPORT_REFERENCE = '00002907-0000-1000-8000-00805f9b34fb'
+    GATT_REPORT_REFERENCE = '00002908-0000-1000-8000-00805f9b34fb'
+
+
+# https://www.bluetooth.com/specifications/gatt/characteristics
+class GattCharTypes(StrEnum):
+    GATT_CHARAC_DEVICE_NAME = '00002a00-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_APPEARANCE = '00002a01-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_PERIPHERAL_PRIV_FLAG = '00002a02-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_RECONNECTION_ADDRESS = '00002a03-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_PERIPHERAL_PREF_CONN = '00002a04-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_SERVICE_CHANGED = '00002a05-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_SYSTEM_ID = '00002a23-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_MODEL_NUMBER_STRING = '00002a24-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_SERIAL_NUMBER_STRING = '00002a25-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_FIRMWARE_REVISION_STRING = '00002a26-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_HARDWARE_REVISION_STRING = '00002a27-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_SOFTWARE_REVISION_STRING = '00002a28-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_MANUFACTURER_NAME_STRING = '00002a29-0000-1000-8000-00805f9b34fb'
+    GATT_CHARAC_PNP_ID = '00002a50-0000-1000-8000-00805f9b34fb'
+
+
+# Matches constants of Bluetooth GATT Characteristic values as defined
+# in BluetoothGattCharacteristic.java
+class CharacteristicValueFormat(enum.Enum):
+    STRING = 0x1
+    BYTE = 0x2
+    FORMAT_SINT8 = 0x21
+    FORMAT_UINT8 = 0x11
+    FORMAT_SINT16 = 0x22
+    FORMAT_UINT16 = 0x12
+    FORMAT_SINT32 = 0x24
+    FORMAT_UINT32 = 0x14
+
+
+# Matches constants of Bluetooth Gatt Service types as defined in
+# BluetoothGattService.java
+class GattServiceType(enum.IntEnum):
+    SERVICE_TYPE_PRIMARY = 0
+    SERVICE_TYPE_SECONDARY = 1
+
+
+# Matches constants of Bluetooth Gatt Connection Priority values as defined in
+# BluetoothGatt.java
+class GattConnectionPriority(enum.IntEnum):
+    CONNECTION_PRIORITY_BALANCED = 0
+    CONNECTION_PRIORITY_HIGH = 1
+    CONNECTION_PRIORITY_LOW_POWER = 2
+
+
+# Min and max MTU values
+class GattMtuSize(enum.IntEnum):
+    MIN = 23
+    MAX = 217
+
+
+# Gatt Characteristic attribute lengths
+class GattCharacteristicAttrLength(enum.IntEnum):
+    MTU_ATTR_1 = 1
+    MTU_ATTR_2 = 3
+    MTU_ATTR_3 = 15
+
+
+# Matches constants of Bluetooth Gatt operations status as defined in
+# BluetoothGatt.java
+class BluetoothGatt(enum.IntEnum):
+    GATT_SUCCESS = 0
+    GATT_FAILURE = 0x101
+
+
+# Matches constants of Bluetooth transport values as defined in
+# BluetoothDevice.java
+class GattTransport(enum.IntEnum):
+    TRANSPORT_AUTO = 0x00
+    TRANSPORT_BREDR = 0x01
+    TRANSPORT_LE = 0x02
+
+
+# Matches constants of Bluetooth physical channeling values as defined in
+# BluetoothDevice.java
+class GattPhy(enum.IntEnum):
+    PHY_LE_1M = 1
+    PHY_LE_2M = 2
+    PHY_LE_CODED = 3
+
+
+# Matches constants of Bluetooth physical channeling bitmask values as defined
+# in BluetoothDevice.java
+class GattPhyMask(enum.IntEnum):
+    PHY_LE_1M_MASK = 1
+    PHY_LE_2M_MASK = 2
+    PHY_LE_CODED_MASK = 4
+
+
+# Values as defined in the Bluetooth GATT specification
+GattServerResponses = {
+    "GATT_SUCCESS": 0x0,
+    "GATT_FAILURE": 0x1,
+    "GATT_READ_NOT_PERMITTED": 0x2,
+    "GATT_WRITE_NOT_PERMITTED": 0x3,
+    "GATT_INVALID_PDU": 0x4,
+    "GATT_INSUFFICIENT_AUTHENTICATION": 0x5,
+    "GATT_REQUEST_NOT_SUPPORTED": 0x6,
+    "GATT_INVALID_OFFSET": 0x7,
+    "GATT_INSUFFICIENT_AUTHORIZATION": 0x8,
+    "GATT_INVALID_ATTRIBUTE_LENGTH": 0xd,
+    "GATT_INSUFFICIENT_ENCRYPTION": 0xf,
+    "GATT_CONNECTION_CONGESTED": 0x8f,
+    "GATT_13_ERR": 0x13,
+    "GATT_12_ERR": 0x12,
+    "GATT_0C_ERR": 0x0C,
+    "GATT_16": 0x16
+}
diff --git a/system/blueberry/utils/bt_gatt_utils.py b/system/blueberry/utils/bt_gatt_utils.py
new file mode 100644
index 0000000..4ad42eb
--- /dev/null
+++ b/system/blueberry/utils/bt_gatt_utils.py
@@ -0,0 +1,349 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""
+Original file:
+    tools/test/connectivity/acts_tests/acts_contrib/test_utils/bt/bt_gatt_utils.py
+"""
+
+import logging
+import pprint
+from queue import Empty
+
+from blueberry.utils.bt_gatt_constants import GattCallbackError
+from blueberry.utils.bt_gatt_constants import GattCallbackString
+from blueberry.utils.bt_gatt_constants import GattCharacteristic
+from blueberry.utils.bt_gatt_constants import GattConnectionState
+from blueberry.utils.bt_gatt_constants import GattDescriptor
+from blueberry.utils.bt_gatt_constants import GattPhyMask
+from blueberry.utils.bt_gatt_constants import GattServiceType
+from blueberry.utils.bt_gatt_constants import GattTransport
+from blueberry.utils.bt_test_utils import BtTestUtilsError
+from blueberry.utils.bt_test_utils import get_mac_address_of_generic_advertisement
+from mobly.controllers.android_device import AndroidDevice
+from mobly.controllers.android_device_lib.event_dispatcher import EventDispatcher
+from mobly.controllers.android_device_lib.sl4a_client import Sl4aClient
+
+default_timeout = 10
+log = logging
+
+
+class GattTestUtilsError(Exception):
+    pass
+
+
+def setup_gatt_connection(central: AndroidDevice,
+                          mac_address,
+                          autoconnect,
+                          transport=GattTransport.TRANSPORT_AUTO,
+                          opportunistic=False):
+    gatt_callback = central.sl4a.gattCreateGattCallback()
+    log.info("Gatt Connect to mac address {}.".format(mac_address))
+    bluetooth_gatt = central.sl4a.gattClientConnectGatt(gatt_callback, mac_address, autoconnect, transport,
+                                                        opportunistic, GattPhyMask.PHY_LE_1M_MASK)
+    expected_event = GattCallbackString.GATT_CONN_CHANGE.format(gatt_callback)
+    try:
+        event = central.ed.pop_event(expected_event, default_timeout)
+    except Empty:
+        close_gatt_client(central, bluetooth_gatt)
+        raise GattTestUtilsError("Could not establish a connection to "
+                                 "peripheral. Expected event: {}".format(expected_event))
+    if event['data']['State'] != GattConnectionState.STATE_CONNECTED:
+        close_gatt_client(central, bluetooth_gatt)
+        try:
+            central.sl4a.gattClientClose(bluetooth_gatt)
+        except Exception:
+            logging.debug("Failed to close gatt client.")
+        raise GattTestUtilsError("Could not establish a connection to "
+                                 "peripheral. Event Details: {}".format(pprint.pformat(event)))
+    return bluetooth_gatt, gatt_callback
+
+
+def close_gatt_client(central: AndroidDevice, bluetooth_gatt):
+    try:
+        central.sl4a.gattClientClose(bluetooth_gatt)
+    except Exception:
+        log.debug("Failed to close gatt client.")
+
+
+def disconnect_gatt_connection(central: AndroidDevice, bluetooth_gatt, gatt_callback):
+    central.sl4a.gattClientDisconnect(bluetooth_gatt)
+    wait_for_gatt_disconnect_event(central, gatt_callback)
+    return
+
+
+def wait_for_gatt_disconnect_event(central: AndroidDevice, gatt_callback):
+    expected_event = GattCallbackString.GATT_CONN_CHANGE.format(gatt_callback)
+    try:
+        event = central.ed.pop_event(expected_event, default_timeout)
+    except Empty:
+        raise GattTestUtilsError(GattCallbackError.GATT_CONN_CHANGE_ERR.format(expected_event))
+    found_state = event['data']['State']
+    expected_state = GattConnectionState.STATE_DISCONNECTED
+    if found_state != expected_state:
+        raise GattTestUtilsError("GATT connection state change expected {}, found {}".format(
+            expected_event, found_state))
+    return
+
+
+def orchestrate_gatt_connection(central: AndroidDevice,
+                                peripheral: AndroidDevice,
+                                transport=GattTransport.TRANSPORT_LE,
+                                mac_address=None,
+                                autoconnect=False,
+                                opportunistic=False):
+    adv_callback = None
+    if mac_address is None:
+        if transport == GattTransport.TRANSPORT_LE:
+            try:
+                mac_address, adv_callback, scan_callback = (get_mac_address_of_generic_advertisement(
+                    central, peripheral))
+            except BtTestUtilsError as err:
+                raise GattTestUtilsError("Error in getting mac address: {}".format(err))
+        else:
+            mac_address = peripheral.sl4a.bluetoothGetLocalAddress()
+            adv_callback = None
+    bluetooth_gatt, gatt_callback = setup_gatt_connection(central, mac_address, autoconnect, transport, opportunistic)
+    return bluetooth_gatt, gatt_callback, adv_callback
+
+
+def run_continuous_write_descriptor(cen_droid: Sl4aClient,
+                                    cen_ed: EventDispatcher,
+                                    per_droid: Sl4aClient,
+                                    per_ed: EventDispatcher,
+                                    gatt_server,
+                                    gatt_server_callback,
+                                    bluetooth_gatt,
+                                    services_count,
+                                    discovered_services_index,
+                                    number_of_iterations=100000):
+    log.info("Starting continuous write")
+    bt_device_id = 0
+    status = 1
+    offset = 1
+    test_value = [1, 2, 3, 4, 5, 6, 7]
+    test_value_return = [1, 2, 3]
+    for _ in range(number_of_iterations):
+        try:
+            for i in range(services_count):
+                characteristic_uuids = (cen_droid.gattClientGetDiscoveredCharacteristicUuids(
+                    discovered_services_index, i))
+                log.info(characteristic_uuids)
+                for characteristic in characteristic_uuids:
+                    descriptor_uuids = (cen_droid.gattClientGetDiscoveredDescriptorUuids(
+                        discovered_services_index, i, characteristic))
+                    log.info(descriptor_uuids)
+                    for descriptor in descriptor_uuids:
+                        cen_droid.gattClientDescriptorSetValue(bluetooth_gatt, discovered_services_index, i,
+                                                               characteristic, descriptor, test_value)
+                        cen_droid.gattClientWriteDescriptor(bluetooth_gatt, discovered_services_index, i,
+                                                            characteristic, descriptor)
+                        expected_event = \
+                            GattCallbackString.DESC_WRITE_REQ.format(
+                                gatt_server_callback)
+                        try:
+                            event = per_ed.pop_event(expected_event, default_timeout)
+                        except Empty:
+                            log.error(GattCallbackError.DESC_WRITE_REQ_ERR.format(expected_event))
+                            return False
+                        request_id = event['data']['requestId']
+                        found_value = event['data']['value']
+                        if found_value != test_value:
+                            log.error("Values didn't match. Found: {}, Expected: " "{}".format(found_value, test_value))
+                        per_droid.gattServerSendResponse(gatt_server, bt_device_id, request_id, status, offset,
+                                                         test_value_return)
+                        expected_event = GattCallbackString.DESC_WRITE.format(bluetooth_gatt)
+                        try:
+                            cen_ed.pop_event(expected_event, default_timeout)
+                        except Empty:
+                            log.error(GattCallbackError.DESC_WRITE_ERR.format(expected_event))
+                            raise Exception("Thread ended prematurely.")
+        except Exception as err:
+            log.error("Continuing but found exception: {}".format(err))
+
+
+def setup_characteristics_and_descriptors_read_write(droid: Sl4aClient):
+    characteristic_input = [
+        {
+            'uuid': "aa7edd5a-4d1d-4f0e-883a-d145616a1630",
+            'property': GattCharacteristic.PROPERTY_WRITE | GattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
+            'permission': GattCharacteristic.PERMISSION_WRITE
+        },
+        {
+            'uuid': "21c0a0bf-ad51-4a2d-8124-b74003e4e8c8",
+            'property': GattCharacteristic.PROPERTY_WRITE | GattCharacteristic.PROPERTY_READ,
+            'permission': GattCharacteristic.PERMISSION_READ
+        },
+        {
+            'uuid': "6774191f-6ec3-4aa2-b8a8-cf830e41fda6",
+            'property': GattCharacteristic.PROPERTY_NOTIFY | GattCharacteristic.PROPERTY_READ,
+            'permission': GattCharacteristic.PERMISSION_READ
+        },
+    ]
+    descriptor_input = [{
+        'uuid': "aa7edd5a-4d1d-4f0e-883a-d145616a1630",
+        'property': GattDescriptor.PERMISSION_READ[0] | GattDescriptor.PERMISSION_WRITE[0],
+    }, {
+        'uuid': "76d5ed92-ca81-4edb-bb6b-9f019665fb32",
+        'property': GattDescriptor.PERMISSION_READ[0] | GattDescriptor.PERMISSION_WRITE[0],
+    }]
+    characteristic_list = setup_gatt_characteristics(droid, characteristic_input)
+    descriptor_list = setup_gatt_descriptors(droid, descriptor_input)
+    return characteristic_list, descriptor_list
+
+
+def setup_multiple_services(peripheral: AndroidDevice):
+    per_droid, per_ed = peripheral.sl4a, peripheral.sl4a.ed
+    gatt_server_callback = per_droid.gattServerCreateGattServerCallback()
+    gatt_server = per_droid.gattServerOpenGattServer(gatt_server_callback)
+    characteristic_list, descriptor_list = (setup_characteristics_and_descriptors_read_write(per_droid))
+    per_droid.gattServerCharacteristicAddDescriptor(characteristic_list[1], descriptor_list[0])
+    per_droid.gattServerCharacteristicAddDescriptor(characteristic_list[2], descriptor_list[1])
+    gattService = per_droid.gattServerCreateService("00000000-0000-1000-8000-00805f9b34fb",
+                                                    GattServiceType.SERVICE_TYPE_PRIMARY)
+    gattService2 = per_droid.gattServerCreateService("FFFFFFFF-0000-1000-8000-00805f9b34fb",
+                                                     GattServiceType.SERVICE_TYPE_PRIMARY)
+    gattService3 = per_droid.gattServerCreateService("3846D7A0-69C8-11E4-BA00-0002A5D5C51B",
+                                                     GattServiceType.SERVICE_TYPE_PRIMARY)
+    for characteristic in characteristic_list:
+        per_droid.gattServerAddCharacteristicToService(gattService, characteristic)
+    per_droid.gattServerAddService(gatt_server, gattService)
+    expected_event = GattCallbackString.SERV_ADDED.format(gatt_server_callback)
+    try:
+        per_ed.pop_event(expected_event, default_timeout)
+    except Empty:
+        peripheral.sl4a.gattServerClose(gatt_server)
+        raise GattTestUtilsError(GattCallbackError.SERV_ADDED_ERR.format(expected_event))
+    for characteristic in characteristic_list:
+        per_droid.gattServerAddCharacteristicToService(gattService2, characteristic)
+    per_droid.gattServerAddService(gatt_server, gattService2)
+    try:
+        per_ed.pop_event(expected_event, default_timeout)
+    except Empty:
+        peripheral.sl4a.gattServerClose(gatt_server)
+        raise GattTestUtilsError(GattCallbackError.SERV_ADDED_ERR.format(expected_event))
+    for characteristic in characteristic_list:
+        per_droid.gattServerAddCharacteristicToService(gattService3, characteristic)
+    per_droid.gattServerAddService(gatt_server, gattService3)
+    try:
+        per_ed.pop_event(expected_event, default_timeout)
+    except Empty:
+        peripheral.sl4a.gattServerClose(gatt_server)
+        raise GattTestUtilsError(GattCallbackError.SERV_ADDED_ERR.format(expected_event))
+    return gatt_server_callback, gatt_server
+
+
+def setup_characteristics_and_descriptors_notify_read(droid: Sl4aClient):
+    characteristic_input = [
+        {
+            'uuid': "aa7edd5a-4d1d-4f0e-883a-d145616a1630",
+            'property': GattCharacteristic.PROPERTY_WRITE | GattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
+            'permission': GattCharacteristic.PROPERTY_WRITE
+        },
+        {
+            'uuid': "21c0a0bf-ad51-4a2d-8124-b74003e4e8c8",
+            'property': GattCharacteristic.PROPERTY_NOTIFY | GattCharacteristic.PROPERTY_READ,
+            'permission': GattCharacteristic.PERMISSION_READ
+        },
+        {
+            'uuid': "6774191f-6ec3-4aa2-b8a8-cf830e41fda6",
+            'property': GattCharacteristic.PROPERTY_NOTIFY | GattCharacteristic.PROPERTY_READ,
+            'permission': GattCharacteristic.PERMISSION_READ
+        },
+    ]
+    descriptor_input = [{
+        'uuid': "aa7edd5a-4d1d-4f0e-883a-d145616a1630",
+        'property': GattDescriptor.PERMISSION_READ[0] | GattDescriptor.PERMISSION_WRITE[0],
+    }, {
+        'uuid': "76d5ed92-ca81-4edb-bb6b-9f019665fb32",
+        'property': GattDescriptor.PERMISSION_READ[0] | GattDescriptor.PERMISSION_WRITE[0],
+    }]
+    characteristic_list = setup_gatt_characteristics(droid, characteristic_input)
+    descriptor_list = setup_gatt_descriptors(droid, descriptor_input)
+    return characteristic_list, descriptor_list
+
+
+def setup_gatt_characteristics(droid: Sl4aClient, input):
+    characteristic_list = []
+    for item in input:
+        index = droid.gattServerCreateBluetoothGattCharacteristic(item['uuid'], item['property'], item['permission'])
+        characteristic_list.append(index)
+    return characteristic_list
+
+
+def setup_gatt_descriptors(droid: Sl4aClient, input):
+    descriptor_list = []
+    for item in input:
+        index = droid.gattServerCreateBluetoothGattDescriptor(
+            item['uuid'],
+            item['property'],
+        )
+        descriptor_list.append(index)
+    log.info("setup descriptor list: {}".format(descriptor_list))
+    return descriptor_list
+
+
+def setup_gatt_mtu(central: AndroidDevice, bluetooth_gatt, gatt_callback, mtu):
+    """utility function to set mtu for GATT connection.
+
+    Steps:
+    1. Request mtu change.
+    2. Check if the mtu is changed to the new value
+
+    Args:
+        central: test device for client to scan.
+        bluetooth_gatt: GATT object
+        mtu: new mtu value to be set
+
+    Returns:
+        If success, return True.
+        if fail, return False
+    """
+    central.sl4a.gattClientRequestMtu(bluetooth_gatt, mtu)
+    expected_event = GattCallbackString.MTU_CHANGED.format(gatt_callback)
+    try:
+        mtu_event = central.ed.pop_event(expected_event, default_timeout)
+        mtu_size_found = mtu_event['data']['MTU']
+        if mtu_size_found != mtu:
+            log.error("MTU size found: {}, expected: {}".format(mtu_size_found, mtu))
+            return False
+    except Empty:
+        log.error(GattCallbackError.MTU_CHANGED_ERR.format(expected_event))
+        return False
+    return True
+
+
+def log_gatt_server_uuids(central: AndroidDevice, discovered_services_index, bluetooth_gatt=None):
+    services_count = central.sl4a.gattClientGetDiscoveredServicesCount(discovered_services_index)
+    for i in range(services_count):
+        service = central.sl4a.gattClientGetDiscoveredServiceUuid(discovered_services_index, i)
+        log.info("Discovered service uuid {}".format(service))
+        characteristic_uuids = (central.sl4a.gattClientGetDiscoveredCharacteristicUuids(discovered_services_index, i))
+        for j in range(len(characteristic_uuids)):
+            descriptor_uuids = (central.sl4a.gattClientGetDiscoveredDescriptorUuidsByIndex(
+                discovered_services_index, i, j))
+            if bluetooth_gatt:
+                char_inst_id = central.sl4a.gattClientGetCharacteristicInstanceId(bluetooth_gatt,
+                                                                                  discovered_services_index, i, j)
+                log.info("Discovered characteristic handle uuid: {} {}".format(
+                    hex(char_inst_id), characteristic_uuids[j]))
+                for k in range(len(descriptor_uuids)):
+                    desc_inst_id = central.sl4a.gattClientGetDescriptorInstanceId(bluetooth_gatt,
+                                                                                  discovered_services_index, i, j, k)
+                    log.info("Discovered descriptor handle uuid: {} {}".format(hex(desc_inst_id), descriptor_uuids[k]))
+            else:
+                log.info("Discovered characteristic uuid: {}".format(characteristic_uuids[j]))
+                for k in range(len(descriptor_uuids)):
+                    log.info("Discovered descriptor uuid {}".format(descriptor_uuids[k]))
diff --git a/system/blueberry/utils/bt_test_utils.py b/system/blueberry/utils/bt_test_utils.py
index bfd184d..c1948e7 100644
--- a/system/blueberry/utils/bt_test_utils.py
+++ b/system/blueberry/utils/bt_test_utils.py
@@ -13,21 +13,33 @@
 import random
 import string
 import time
-from typing import Optional
 import wave
+from queue import Empty
+from typing import Optional
+
+from blueberry.tests.gd_sl4a.lib.ble_lib import generate_ble_advertise_objects
+from blueberry.tests.gd_sl4a.lib.bt_constants import adv_succ
+from blueberry.tests.gd_sl4a.lib.bt_constants import ble_advertise_settings_modes
+from blueberry.tests.gd_sl4a.lib.bt_constants import ble_advertise_settings_tx_powers
+from blueberry.tests.gd_sl4a.lib.bt_constants import bt_default_timeout
+from mobly.controllers.android_device import AndroidDevice
+
+
+class BtTestUtilsError(Exception):
+    pass
 
 
 def convert_pcm_to_wav(pcm_file_path, wave_file_path, audio_params):
-  """Converts raw pcm data into wave file.
+    """Converts raw pcm data into wave file.
 
-  Args:
-      pcm_file_path: File path of origin pcm file.
-      wave_file_path: File path of converted wave file.
-      audio_params: A dict with audio configuration.
-  """
-  with open(pcm_file_path, 'rb') as pcm_file:
-    frames = pcm_file.read()
-  write_record_file(wave_file_path, audio_params, frames)
+    Args:
+        pcm_file_path: File path of origin pcm file.
+        wave_file_path: File path of converted wave file.
+        audio_params: A dict with audio configuration.
+    """
+    with open(pcm_file_path, 'rb') as pcm_file:
+        frames = pcm_file.read()
+    write_record_file(wave_file_path, audio_params, frames)
 
 
 def create_vcf_from_vcard(output_path: str,
@@ -35,167 +47,223 @@
                           first_name: Optional[str] = None,
                           last_name: Optional[str] = None,
                           phone_number: Optional[int] = None) -> str:
-  """Creates a vcf file from vCard.
+    """Creates a vcf file from vCard.
 
-  Args:
-    output_path: Path of the output vcf file.
-    num_of_contacts: Number of contacts to be generated.
-    first_name: First name of the contacts.
-    last_name: Last name of the contacts.
-    phone_number: Phone number of the contacts.
+    Args:
+        output_path: Path of the output vcf file.
+        num_of_contacts: Number of contacts to be generated.
+        first_name: First name of the contacts.
+        last_name: Last name of the contacts.
+        phone_number: Phone number of the contacts.
 
-  Returns:
-    vcf_file_path: Path of the output vcf file. E.g.
-        "/<output_path>/contacts_<time>.vcf".
-  """
-  file_name = f'contacts_{int(time.time())}.vcf'
-  vcf_file_path = os.path.join(output_path, file_name)
-  with open(vcf_file_path, 'w+') as f:
-    for i in range(num_of_contacts):
-      lines = []
-      if first_name is None:
-        first_name = 'Person'
-      vcard_last_name = last_name
-      if last_name is None:
-        vcard_last_name = i
-      vcard_phone_number = phone_number
-      if phone_number is None:
-        vcard_phone_number = random.randrange(int(10e10))
-      lines.append('BEGIN:VCARD\n')
-      lines.append('VERSION:2.1\n')
-      lines.append(f'N:{vcard_last_name};{first_name};;;\n')
-      lines.append(f'FN:{first_name} {vcard_last_name}\n')
-      lines.append(f'TEL;CELL:{vcard_phone_number}\n')
-      lines.append(f'EMAIL;PREF:{first_name}{vcard_last_name}@gmail.com\n')
-      lines.append('END:VCARD\n')
-      f.write(''.join(lines))
-  return vcf_file_path
+    Returns:
+        vcf_file_path: Path of the output vcf file. E.g.
+            "/<output_path>/contacts_<time>.vcf".
+    """
+    file_name = f'contacts_{int(time.time())}.vcf'
+    vcf_file_path = os.path.join(output_path, file_name)
+    with open(vcf_file_path, 'w+') as f:
+        for i in range(num_of_contacts):
+            lines = []
+            if first_name is None:
+                first_name = 'Person'
+            vcard_last_name = last_name
+            if last_name is None:
+                vcard_last_name = i
+            vcard_phone_number = phone_number
+            if phone_number is None:
+                vcard_phone_number = random.randrange(int(10e10))
+            lines.append('BEGIN:VCARD\n')
+            lines.append('VERSION:2.1\n')
+            lines.append(f'N:{vcard_last_name};{first_name};;;\n')
+            lines.append(f'FN:{first_name} {vcard_last_name}\n')
+            lines.append(f'TEL;CELL:{vcard_phone_number}\n')
+            lines.append(f'EMAIL;PREF:{first_name}{vcard_last_name}@gmail.com\n')
+            lines.append('END:VCARD\n')
+            f.write(''.join(lines))
+    return vcf_file_path
 
 
-def generate_id_by_size(size,
-                        chars=(string.ascii_lowercase + string.ascii_uppercase +
-                               string.digits)):
-  """Generate random ascii characters of input size and input char types.
+def generate_id_by_size(size, chars=(string.ascii_lowercase + string.ascii_uppercase + string.digits)):
+    """Generate random ascii characters of input size and input char types.
 
-  Args:
-      size: Input size of string.
-      chars: (Optional) Chars to use in generating a random string.
+    Args:
+        size: Input size of string.
+        chars: (Optional) Chars to use in generating a random string.
 
-  Returns:
-      String of random input chars at the input size.
-  """
-  return ''.join(random.choice(chars) for _ in range(size))
+    Returns:
+        String of random input chars at the input size.
+    """
+    return ''.join(random.choice(chars) for _ in range(size))
 
 
 def get_duration_seconds(wav_file_path):
-  """Get duration of most recently recorded file.
+    """Get duration of most recently recorded file.
 
-  Args:
-      wav_file_path: path of the wave file.
+    Args:
+        wav_file_path: path of the wave file.
 
-  Returns:
-      duration (float): duration of recorded file in seconds.
-  """
-  f = wave.open(wav_file_path, 'r')
-  frames = f.getnframes()
-  rate = f.getframerate()
-  duration = (frames / float(rate))
-  f.close()
-  return duration
+    Returns:
+        duration (float): duration of recorded file in seconds.
+    """
+    f = wave.open(wav_file_path, 'r')
+    frames = f.getnframes()
+    rate = f.getframerate()
+    duration = (frames / float(rate))
+    f.close()
+    return duration
 
 
-def wait_until(timeout_sec,
-               condition_func,
-               func_args,
-               expected_value,
-               exception=None,
-               interval_sec=0.5):
-  """Waits until a function returns a expected value or timeout is reached.
+def wait_until(timeout_sec, condition_func, func_args, expected_value, exception=None, interval_sec=0.5):
+    """Waits until a function returns a expected value or timeout is reached.
 
-  Example usage:
-    ```
-    def is_bluetooth_enabled(device) -> bool:
-      do something and return something...
+    Example usage:
+        ```
+        def is_bluetooth_enabled(device) -> bool:
+          do something and return something...
 
-    # Waits and checks if Bluetooth is turned on.
-    bt_test_utils.wait_until(
-        timeout_sec=10,
-        condition_func=is_bluetooth_enabled,
-        func_args=[dut],
-        expected_value=True,
-        exception=signals.TestFailure('Failed to turn on Bluetooth.'),
-        interval_sec=1)
-    ```
+        # Waits and checks if Bluetooth is turned on.
+        bt_test_utils.wait_until(
+            timeout_sec=10,
+            condition_func=is_bluetooth_enabled,
+            func_args=[dut],
+            expected_value=True,
+            exception=signals.TestFailure('Failed to turn on Bluetooth.'),
+            interval_sec=1)
+        ```
 
-  Args:
-    timeout_sec: float, max waiting time in seconds.
-    condition_func: function, when the condiction function returns the expected
-        value, the waiting mechanism will be interrupted.
-    func_args: tuple or list, the arguments for the condition function.
-    expected_value: a expected value that the condition function returns.
-    exception: Exception, an exception will be raised when timed out if needed.
-    interval_sec: float, interval time between calls of the condition function
-        in seconds.
+    Args:
+        timeout_sec: float, max waiting time in seconds.
+        condition_func: function, when the condiction function returns the expected
+            value, the waiting mechanism will be interrupted.
+        func_args: tuple or list, the arguments for the condition function.
+        expected_value: a expected value that the condition function returns.
+        exception: Exception, an exception will be raised when timed out if needed.
+        interval_sec: float, interval time between calls of the condition function
+            in seconds.
 
-  Returns:
-    True if the function returns the expected value else False.
-  """
-  start_time = time.time()
-  end_time = start_time + timeout_sec
-  while time.time() < end_time:
-    if condition_func(*func_args) == expected_value:
-      return True
-    time.sleep(interval_sec)
-  args_string = ', '.join(list(map(str, func_args)))
-  log.warning('Timed out after %.1fs waiting for "%s(%s)" to be "%s".',
-              timeout_sec, condition_func.__name__, args_string, expected_value)
-  if exception:
-    raise exception
-  return False
+    Returns:
+        True if the function returns the expected value else False.
+    """
+    start_time = time.time()
+    end_time = start_time + timeout_sec
+    while time.time() < end_time:
+        if condition_func(*func_args) == expected_value:
+            return True
+        time.sleep(interval_sec)
+    args_string = ', '.join(list(map(str, func_args)))
+    log.warning('Timed out after %.1fs waiting for "%s(%s)" to be "%s".', timeout_sec, condition_func.__name__,
+                args_string, expected_value)
+    if exception:
+        raise exception
+    return False
 
 
 def write_read_verify_data_sl4a(client_ad, server_ad, msg, binary=False):
-  """Verify that the client wrote data to the server Android device correctly.
+    """Verify that the client wrote data to the server Android device correctly.
 
-  Args:
-      client_ad: the Android device to perform the write.
-      server_ad: the Android device to read the data written.
-      msg: the message to write.
-      binary: if the msg arg is binary or not.
+    Args:
+        client_ad: the Android device to perform the write.
+        server_ad: the Android device to read the data written.
+        msg: the message to write.
+        binary: if the msg arg is binary or not.
 
-  Returns:
-      True if the data written matches the data read, false if not.
-  """
-  client_ad.log.info('Write message %s.', msg)
-  if binary:
-    client_ad.sl4a.bluetoothSocketConnWriteBinary(msg)
-  else:
-    client_ad.sl4a.bluetoothSocketConnWrite(msg)
-  server_ad.log.info('Read message %s.', msg)
-  if binary:
-    read_msg = server_ad.sl4a.bluetoothSocketConnReadBinary().rstrip('\r\n')
-  else:
-    read_msg = server_ad.sl4a.bluetoothSocketConnRead()
-  log.info('Verify message.')
-  if msg != read_msg:
-    log.error('Mismatch! Read: %s, Expected: %s', read_msg, msg)
-    return False
-  log.info('Matched! Read: %s, Expected: %s', read_msg, msg)
-  return True
+    Returns:
+        True if the data written matches the data read, false if not.
+    """
+    client_ad.log.info('Write message %s.', msg)
+    if binary:
+        client_ad.sl4a.bluetoothSocketConnWriteBinary(msg)
+    else:
+        client_ad.sl4a.bluetoothSocketConnWrite(msg)
+    server_ad.log.info('Read message %s.', msg)
+    if binary:
+        read_msg = server_ad.sl4a.bluetoothSocketConnReadBinary().rstrip('\r\n')
+    else:
+        read_msg = server_ad.sl4a.bluetoothSocketConnRead()
+    log.info('Verify message.')
+    if msg != read_msg:
+        log.error('Mismatch! Read: %s, Expected: %s', read_msg, msg)
+        return False
+    log.info('Matched! Read: %s, Expected: %s', read_msg, msg)
+    return True
 
 
 def write_record_file(file_name, audio_params, frames):
-  """Writes the recorded audio into the file.
+    """Writes the recorded audio into the file.
 
-  Args:
-      file_name: The file name for writing the recorded audio.
-      audio_params: A dict with audio configuration.
-      frames: Recorded audio frames.
-  """
-  log.debug('writing frame to %s', file_name)
-  wf = wave.open(file_name, 'wb')
-  wf.setnchannels(audio_params['channel'])
-  wf.setsampwidth(audio_params.get('sample_width', 1))
-  wf.setframerate(audio_params['sample_rate'])
-  wf.writeframes(frames)
-  wf.close()
+    Args:
+        file_name: The file name for writing the recorded audio.
+        audio_params: A dict with audio configuration.
+        frames: Recorded audio frames.
+    """
+    log.debug('writing frame to %s', file_name)
+    wf = wave.open(file_name, 'wb')
+    wf.setnchannels(audio_params['channel'])
+    wf.setsampwidth(audio_params.get('sample_width', 1))
+    wf.setframerate(audio_params['sample_rate'])
+    wf.writeframes(frames)
+    wf.close()
+
+
+def get_mac_address_of_generic_advertisement(scan_device, adv_device):
+    """Start generic advertisement and get it's mac address by LE scanning.
+
+    Args:
+        scan_ad: The Android device to use as the scanner.
+        adv_device: The Android device to use as the advertiser.
+
+    Returns:
+        mac_address: The mac address of the advertisement.
+        advertise_callback: The advertise callback id of the active
+            advertisement.
+    """
+    adv_device.sl4a.bleSetAdvertiseDataIncludeDeviceName(True)
+    adv_device.sl4a.bleSetAdvertiseSettingsAdvertiseMode(ble_advertise_settings_modes['low_latency'])
+    adv_device.sl4a.bleSetAdvertiseSettingsIsConnectable(True)
+    adv_device.sl4a.bleSetAdvertiseSettingsTxPowerLevel(ble_advertise_settings_tx_powers['high'])
+    advertise_callback, advertise_data, advertise_settings = (generate_ble_advertise_objects(adv_device.sl4a))
+    adv_device.sl4a.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings)
+    try:
+        adv_device.ed.pop_event(adv_succ.format(advertise_callback), bt_default_timeout)
+    except Empty as err:
+        raise BtTestUtilsError("Advertiser did not start successfully {}".format(err))
+    filter_list = scan_device.sl4a.bleGenFilterList()
+    scan_settings = scan_device.sl4a.bleBuildScanSetting()
+    scan_callback = scan_device.sl4a.bleGenScanCallback()
+    scan_device.sl4a.bleSetScanFilterDeviceName(adv_device.sl4a.bluetoothGetLocalName())
+    scan_device.sl4a.bleBuildScanFilter(filter_list)
+    scan_device.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
+    try:
+        event = scan_device.sl4a.ed.pop_event("BleScan{}onScanResults".format(scan_callback), bt_default_timeout)
+    except Empty as err:
+        raise BtTestUtilsError("Scanner did not find advertisement {}".format(err))
+    mac_address = event['data']['Result']['deviceInfo']['address']
+    return mac_address, advertise_callback, scan_callback
+
+
+def clear_bonded_devices(ad: AndroidDevice):
+    """Clear bonded devices from the input Android device.
+
+    Args:
+        ad: the Android device performing the connection.
+    Returns:
+        True if clearing bonded devices was successful, false if unsuccessful.
+    """
+    bonded_device_list = ad.sl4a.bluetoothGetBondedDevices()
+    while bonded_device_list:
+        device_address = bonded_device_list[0]['address']
+        if not ad.sl4a.bluetoothUnbond(device_address):
+            ad.log.error("Failed to unbond {} from {}".format(device_address, ad.serial))
+            return False
+        ad.log.info("Successfully unbonded {} from {}".format(device_address, ad.serial))
+        #TODO: wait for BOND_STATE_CHANGED intent instead of waiting
+        time.sleep(1)
+
+        # If device was first connected using LE transport, after bonding it is
+        # accessible through it's LE address, and through it classic address.
+        # Unbonding it will unbond two devices representing different
+        # "addresses". Attempt to unbond such already unbonded devices will
+        # result in bluetoothUnbond returning false.
+        bonded_device_list = ad.sl4a.bluetoothGetBondedDevices()
+    return True
diff --git a/system/blueberry/utils/mobly_sl4a_utils.py b/system/blueberry/utils/mobly_sl4a_utils.py
new file mode 100644
index 0000000..0d06d16
--- /dev/null
+++ b/system/blueberry/utils/mobly_sl4a_utils.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import logging
+
+import mobly.controllers.android_device_lib.sl4a_client as sl4a_client
+from mobly.controllers.android_device import AndroidDevice
+from mobly.controllers.android_device_lib.adb import AdbError
+from mobly.controllers.android_device_lib.jsonrpc_client_base import AppRestoreConnectionError
+from mobly.controllers.android_device_lib.services.sl4a_service import Sl4aService
+
+
+class FakeFuture:
+    """
+    A fake Future object to override default mobly behavior
+    """
+
+    def set_result(self, result):
+        logging.debug("Setting fake result {}".format(result))
+
+
+def setup_sl4a(device: AndroidDevice, server_port: int, forwarded_port: int):
+    """
+    A method that setups up SL4A instance on mobly
+    :param device: an AndroidDevice instance
+    :param server_port: Preferred server port used by SL4A on Android device
+    :param forwarded_port: Preferred server port number forwarded from Android to
+                           the host PC via adb for SL4A connections
+    :return: None
+    """
+    sl4a_client._DEVICE_SIDE_PORT = server_port
+    sl4a_client._APP_START_WAIT_TIME = 0.5
+    if device.sl4a is not None:
+        device.log.error("SL4A is not none when registering")
+    device.services.register('sl4a', Sl4aService, start_service=False)
+    # Start the SL4A service and event dispatcher
+    try:
+        device.sl4a.start()
+    except AppRestoreConnectionError as exp:
+        device.log.debug("AppRestoreConnectionError {}".format(exp))
+    # Pause the dispatcher, but do not stop the service
+    try:
+        device.sl4a.pause()
+    except AdbError as exp:
+        device.log.debug("Failed to pause() {}".format(exp))
+    sl4a_client._APP_START_WAIT_TIME = 2 * 60
+    # Restart the service with a new host port
+    device.sl4a.restore_app_connection(port=forwarded_port)
+
+
+def teardown_sl4a(device: AndroidDevice):
+    """
+    A method to tear down SL4A interface on mobly
+    :param device: an AndroidDevice instance that already contains SL4a
+    :return: None
+    """
+    if device.sl4a.is_alive:
+        # Both self.dut.sl4a and self.dut.sl4a.ed._sl4a are Sl4aClient instances
+        # If we do not set host_port to None here, host_poart will be removed twice from Android
+        # The 2nd removal will trigger and exception that spam the test result
+        # TODO: Resolve this issue in mobly
+        device.log.info("Clearing host_port to prevent mobly crash {}".format(device.sl4a._sl4a_client.host_port))
+        device.sl4a._sl4a_client.host_port = None
+        # Moreover concurrent.Future.set_result() should never be called from thread that is
+        # waiting for the future. However, mobly calls it and cause InvalidStateError when it
+        # tries to do that after the thread pool has stopped, overriding it here
+        # TODO: Resolve this issue in mobly
+        device.sl4a.ed.poller = FakeFuture()
+    try:
+        # Guarded by is_alive internally
+        device.sl4a.stop()
+    except AdbError as exp:
+        device.log.warning("Failed top stop()".format(exp))
diff --git a/system/bta/BUILD.gn b/system/bta/BUILD.gn
index db8db54..1e2d6cd 100644
--- a/system/bta/BUILD.gn
+++ b/system/bta/BUILD.gn
@@ -107,6 +107,7 @@
     "//bt/system/",
     "//bt/system/linux_include",
     "//bt/system/bta",
+    "//bt/system/gd",
     "//bt/system/internal_include",
     "//bt/system/internal_include",
     "//bt/system/stack/include",
diff --git a/system/bta/hearing_aid/hearing_aid.cc b/system/bta/hearing_aid/hearing_aid.cc
index 105221d..987aaec 100644
--- a/system/bta/hearing_aid/hearing_aid.cc
+++ b/system/bta/hearing_aid/hearing_aid.cc
@@ -402,15 +402,6 @@
       hearingDevice->connection_update_status = AWAITING;
     }
 
-    if (controller_get_interface()->supports_ble_2m_phy()) {
-      LOG(INFO) << address << " set preferred PHY to 2M";
-      BTM_BleSetPhy(address, PHY_LE_2M, PHY_LE_2M, 0);
-    }
-
-    // Set data length
-    // TODO(jpawlowski: for 16khz only 87 is required, optimize
-    BTM_SetBleDataLength(address, 167);
-
     if (BTM_SecIsSecurityPending(address)) {
       /* if security collision happened, wait for encryption done
        * (BTA_GATTC_ENC_CMPL_CB_EVT) */
@@ -510,6 +501,12 @@
       hearingDevice->connection_update_status = NONE;
     }
 
+    if (!hearingDevice->accepting_audio &&
+        hearingDevice->connection_update_status == COMPLETED &&
+        hearingDevice->gap_opened) {
+      OnDeviceReady(hearingDevice->address);
+    }
+
     for (auto& device : hearingDevices.devices) {
       if (device.conn_id && (device.connection_update_status == AWAITING)) {
         device.connection_update_status = STARTED;
@@ -578,6 +575,24 @@
     }
   }
 
+  void OnPhyUpdateEvent(uint16_t conn_id, uint8_t tx_phys, uint8_t rx_phys,
+                        tGATT_STATUS status) {
+    HearingDevice* hearingDevice = hearingDevices.FindByConnId(conn_id);
+    if (!hearingDevice) {
+      DVLOG(2) << "Skipping unknown device, conn_id=" << loghex(conn_id);
+      return;
+    }
+    if (status == GATT_SUCCESS && tx_phys == PHY_LE_2M &&
+        rx_phys == PHY_LE_2M) {
+      LOG(INFO) << hearingDevice->address << " phy update to 2M successful";
+      return;
+    }
+    LOG(INFO) << hearingDevice->address
+              << " phy update to 2M fail, try again. status: " << status
+              << ", tx_phys: " << tx_phys << ", rx_phys: " << rx_phys;
+    BTM_BleSetPhy(hearingDevice->address, PHY_LE_2M, PHY_LE_2M, 0);
+  }
+
   void OnServiceChangeEvent(const RawAddress& address) {
     HearingDevice* hearingDevice = hearingDevices.FindByAddress(address);
     if (!hearingDevice) {
@@ -875,6 +890,15 @@
   void ConnectSocket(HearingDevice* hearingDevice, uint16_t psm) {
     tL2CAP_CFG_INFO cfg_info = tL2CAP_CFG_INFO{.mtu = 512};
 
+    if (controller_get_interface()->supports_ble_2m_phy()) {
+      LOG(INFO) << hearingDevice->address << " set preferred PHY to 2M";
+      BTM_BleSetPhy(hearingDevice->address, PHY_LE_2M, PHY_LE_2M, 0);
+    }
+
+    // Set data length
+    // TODO(jpawlowski: for 16khz only 87 is required, optimize
+    BTM_SetBleDataLength(hearingDevice->address, 167);
+
     SendEnableServiceChangedInd(hearingDevice);
 
     uint8_t service_id = hearingDevice->isLeft()
@@ -915,8 +939,8 @@
       instance->OnPsmRead(conn_id, status, handle, len, value, data);
   }
 
-  /* CoC Socket is ready */
-  void OnGapConnection(const RawAddress& address) {
+  /* CoC Socket, BLE connection parameter are ready */
+  void OnDeviceReady(const RawAddress& address) {
     HearingDevice* hearingDevice = hearingDevices.FindByAddress(address);
     if (!hearingDevice) {
       LOG(INFO) << "Device not connected to profile" << address;
@@ -1398,7 +1422,16 @@
 
         LOG(INFO) << "GAP_EVT_CONN_OPENED " << address << ", tx_mtu=" << tx_mtu
                   << ", init_credit=" << init_credit;
-        OnGapConnection(address);
+
+        HearingDevice* hearingDevice = hearingDevices.FindByAddress(address);
+        if (!hearingDevice) {
+          LOG(INFO) << "Skipping unknown device" << address;
+          return;
+        }
+        hearingDevice->gap_opened = true;
+        if (hearingDevice->connection_update_status == COMPLETED) {
+          OnDeviceReady(address);
+        }
         break;
       }
 
@@ -1417,6 +1450,7 @@
           hearingDevice->accepting_audio = false;
           hearingDevice->playback_started = false;
           hearingDevice->command_acked = false;
+          hearingDevice->gap_opened = false;
         }
         break;
       case GAP_EVT_CONN_DATA_AVAIL: {
@@ -1631,6 +1665,7 @@
       }
     }
     hearingDevice->connection_update_status = NONE;
+    hearingDevice->gap_opened = false;
 
     if (hearingDevice->conn_id) {
       BtaGattQueue::Clean(hearingDevice->conn_id);
@@ -1850,6 +1885,12 @@
       if (!instance) return;
       instance->OnServiceDiscDoneEvent(p_data->service_changed.remote_bda);
       break;
+    case BTA_GATTC_PHY_UPDATE_EVT: {
+      if (!instance) return;
+      tBTA_GATTC_PHY_UPDATE& p = p_data->phy_update;
+      instance->OnPhyUpdateEvent(p.conn_id, p.tx_phy, p.rx_phy, p.status);
+      break;
+    }
 
     default:
       break;
diff --git a/system/bta/include/bta_hearing_aid_api.h b/system/bta/include/bta_hearing_aid_api.h
index d1b8cb3..90803c5 100644
--- a/system/bta/include/bta_hearing_aid_api.h
+++ b/system/bta/include/bta_hearing_aid_api.h
@@ -158,6 +158,8 @@
   int read_rssi_count;
   int num_intervals_since_last_rssi_read;
 
+  bool gap_opened;
+
   HearingDevice(const RawAddress& address, uint8_t capabilities,
                 uint16_t codecs, uint16_t audio_control_point_handle,
                 uint16_t audio_status_handle, uint16_t audio_status_ccc_handle,
@@ -185,7 +187,8 @@
         codecs(codecs),
         playback_started(false),
         command_acked(false),
-        read_rssi_count(0) {}
+        read_rssi_count(0),
+        gap_opened(false) {}
 
   HearingDevice(const RawAddress& address, bool first_connection)
       : address(address),
@@ -207,7 +210,8 @@
         codecs(0),
         playback_started(false),
         command_acked(false),
-        read_rssi_count(0) {}
+        read_rssi_count(0),
+        gap_opened(false) {}
 
   HearingDevice() : HearingDevice(RawAddress::kEmpty, false) {}
 
diff --git a/system/bta/le_audio/audio_set_configurations.json b/system/bta/le_audio/audio_set_configurations.json
index ef744c6..d0f9cbd 100644
--- a/system/bta/le_audio/audio_set_configurations.json
+++ b/system/bta/le_audio/audio_set_configurations.json
@@ -233,6 +233,16 @@
             "qos_config_name": ["QoS_Config_16_1_1"]
         },
         {
+            "name": "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32_2_Server_Preferred",
+            "codec_config_name": "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32_2",
+            "qos_config_name": ["QoS_Config_Server_Preferred"]
+        },
+        {
+            "name": "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32_2_1",
+            "codec_config_name": "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32_2",
+            "qos_config_name": ["QoS_Config_32_2_1"]
+        },
+        {
             "name": "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_16_2_Server_Preferred",
             "codec_config_name": "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_16_2",
             "qos_config_name": ["QoS_Config_Server_Preferred"]
@@ -413,6 +423,66 @@
             "qos_config_name": ["QoS_Config_24_2_2"]
         },
         {
+            "name": "DualDev_OneChanStereoSnk_OneChanStereoSrc_32_2_Server_Preferred",
+            "codec_config_name": "DualDev_OneChanStereoSnk_OneChanStereoSrc_32_2",
+            "qos_config_name": ["QoS_Config_Server_Preferred"]
+        },
+        {
+            "name": "DualDev_OneChanStereoSnk_OneChanStereoSrc_32_2_1",
+            "codec_config_name": "DualDev_OneChanStereoSnk_OneChanStereoSrc_32_2",
+            "qos_config_name": ["QoS_Config_32_2_1"]
+        },
+        {
+            "name": "DualDev_OneChanStereoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+            "codec_config_name": "DualDev_OneChanStereoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_Server_Preferred"]
+        },
+        {
+            "name": "DualDev_OneChanStereoSnk_OneChanMonoSrc_32_2_1",
+            "codec_config_name": "DualDev_OneChanStereoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_32_2_1"]
+        },
+        {
+            "name": "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+            "codec_config_name": "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_Server_Preferred"]
+        },
+        {
+            "name": "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_32_2_1",
+            "codec_config_name": "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_32_2_1"]
+        },
+        {
+            "name": "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+            "codec_config_name": "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_Server_Preferred"]
+        },
+        {
+            "name": "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_32_2_1",
+            "codec_config_name": "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_32_2_1"]
+        },
+        {
+            "name": "SingleDev_OneChanStereoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+            "codec_config_name": "SingleDev_OneChanStereoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_Server_Preferred"]
+        },
+        {
+            "name": "SingleDev_OneChanStereoSnk_OneChanMonoSrc_32_2_1",
+            "codec_config_name": "SingleDev_OneChanStereoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_32_2_1"]
+        },
+        {
+            "name": "SingleDev_OneChanMonoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+            "codec_config_name": "SingleDev_OneChanMonoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_Server_Preferred"]
+        },
+        {
+            "name": "SingleDev_OneChanMonoSnk_OneChanMonoSrc_32_2_1",
+            "codec_config_name": "SingleDev_OneChanMonoSnk_OneChanMonoSrc_32_2",
+            "qos_config_name": ["QoS_Config_32_2_1"]
+        },
+        {
             "name": "DualDev_OneChanStereoSnk_48_4_Server_Preferred",
             "codec_config_name": "DualDev_OneChanStereoSnk_48_4",
             "qos_config_name": ["QoS_Config_Server_Preferred"]
@@ -1845,6 +1915,135 @@
             ]
         },
         {
+            "name": "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32_2",
+            "subconfigurations": [
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 1,
+                    "direction": "SINK",
+                    "configuration_strategy": "STEREO_ONE_CIS_PER_DEVICE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    3,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                },
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 1,
+                    "direction": "SOURCE",
+                    "configuration_strategy": "STEREO_ONE_CIS_PER_DEVICE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    3,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        {
             "name": "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_16_2",
             "subconfigurations": [
                 {
@@ -3995,6 +4194,771 @@
                     ]
                 }
             ]
+        },
+        {
+            "name": "DualDev_OneChanStereoSnk_OneChanStereoSrc_32_2",
+            "subconfigurations": [
+                {
+                    "device_cnt": 2,
+                    "ase_cnt": 2,
+                    "direction": "SINK",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                },
+                {
+                    "device_cnt": 2,
+                    "ase_cnt": 2,
+                    "direction": "SOURCE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "DualDev_OneChanStereoSnk_OneChanMonoSrc_32_2",
+            "subconfigurations": [
+                {
+                    "device_cnt": 2,
+                    "ase_cnt": 2,
+                    "direction": "SINK",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                },
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 1,
+                    "direction": "SOURCE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_32_2",
+            "subconfigurations": [
+                {
+                    "device_cnt": 2,
+                    "ase_cnt": 4,
+                    "direction": "SINK",
+                    "configuration_strategy": "STEREO_TWO_CISES_PER_DEVICE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                },
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 1,
+                    "direction": "SOURCE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_32_2",
+            "subconfigurations": [
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 1,
+                    "direction": "SINK",
+                    "configuration_strategy": "STEREO_ONE_CIS_PER_DEVICE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    3,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                },
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 1,
+                    "direction": "SOURCE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "SingleDev_OneChanStereoSnk_OneChanMonoSrc_32_2",
+            "subconfigurations": [
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 2,
+                    "direction": "SINK",
+                    "configuration_strategy": "STEREO_TWO_CISES_PER_DEVICE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                },
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 1,
+                    "direction": "SOURCE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "SingleDev_OneChanMonoSnk_OneChanMonoSrc_32_2",
+            "subconfigurations": [
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 1,
+                    "direction": "SINK",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                },
+                {
+                    "device_cnt": 1,
+                    "ase_cnt": 1,
+                    "direction": "SOURCE",
+                    "codec_id": {
+                        "coding_format": 6,
+                        "vendor_company_id": 0,
+                        "vendor_codec_id": 0
+                    },
+                    "codec_configuration": [
+                        {
+                            "name": "sampling_frequency",
+                            "type": 1,
+                            "compound_value": {
+                                "value": [
+                                    6
+                                ]
+                            }
+                        },
+                        {
+                            "name": "frame_duration",
+                            "type": 2,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        },
+                        {
+                            "name": "audio_channel_allocation",
+                            "type": 3,
+                            "compound_value": {
+                                "value": [
+                                    1,
+                                    0,
+                                    0,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "octets_per_codec_frame",
+                            "type": 4,
+                            "compound_value": {
+                                "value": [
+                                    80,
+                                    0
+                                ]
+                            }
+                        },
+                        {
+                            "name": "codec_frame_blocks_per_sdu",
+                            "type": 5,
+                            "compound_value": {
+                                "value": [
+                                    1
+                                ]
+                            }
+                        }
+                    ]
+                }
+            ]
         }
     ],
     "qos_configurations": [
@@ -4024,6 +4988,11 @@
             "max_transport_latency": 95
         },
         {
+            "name": "QoS_Config_32_2_1",
+            "retransmission_number": 2,
+            "max_transport_latency": 10
+        },
+        {
             "name": "QoS_Config_48_4_1",
             "retransmission_number": 5,
             "max_transport_latency": 20
diff --git a/system/bta/le_audio/audio_set_scenarios.json b/system/bta/le_audio/audio_set_scenarios.json
index a79f47b..ee946b4 100644
--- a/system/bta/le_audio/audio_set_scenarios.json
+++ b/system/bta/le_audio/audio_set_scenarios.json
@@ -29,34 +29,48 @@
         {
             "name": "Conversational",
             "configurations": [
+                "DualDev_OneChanStereoSnk_OneChanStereoSrc_32_2_Server_Preferred",
+                "DualDev_OneChanStereoSnk_OneChanStereoSrc_32_2_1",
                 "DualDev_OneChanStereoSnk_OneChanStereoSrc_16_2_Server_Preferred",
                 "DualDev_OneChanStereoSnk_OneChanStereoSrc_16_2_1",
                 "DualDev_OneChanStereoSnk_OneChanStereoSrc_16_2_2",
                 "DualDev_OneChanStereoSnk_OneChanStereoSrc_16_1_Server_Preferred",
                 "DualDev_OneChanStereoSnk_OneChanStereoSrc_16_1_1",
                 "DualDev_OneChanStereoSnk_OneChanStereoSrc_16_1_2",
+                "DualDev_OneChanStereoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+                "DualDev_OneChanStereoSnk_OneChanMonoSrc_32_2_1",
                 "DualDev_OneChanStereoSnk_OneChanMonoSrc_16_2_Server_Preferred",
                 "DualDev_OneChanStereoSnk_OneChanMonoSrc_16_2_1",
                 "DualDev_OneChanStereoSnk_OneChanMonoSrc_16_1_Server_Preferred",
                 "DualDev_OneChanStereoSnk_OneChanMonoSrc_16_1_1",
+                "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+                "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_32_2_1",
                 "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_2_Server_Preferred",
                 "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_2_1",
                 "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_1_Server_Preferred",
                 "DualDev_OneChanDoubleStereoSnk_OneChanMonoSrc_16_1_1",
+                "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32_2_Server_Preferred",
+                "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_32_2_1",
                 "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_16_2_Server_Preferred",
                 "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_16_2_1",
                 "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_16_2_2",
                 "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_16_1_Server_Preferred",
                 "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_16_1_1",
                 "SingleDev_TwoChanStereoSnk_TwoChanStereoSrc_16_1_2",
+                "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+                "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_32_2_1",
                 "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_2_Server_Preferred",
                 "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_2_1",
                 "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_1_Server_Preferred",
                 "SingleDev_TwoChanStereoSnk_OneChanMonoSrc_16_1_1",
+                "SingleDev_OneChanStereoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+                "SingleDev_OneChanStereoSnk_OneChanMonoSrc_32_2_1",
                 "SingleDev_OneChanStereoSnk_OneChanMonoSrc_16_2_Server_Preferred",
                 "SingleDev_OneChanStereoSnk_OneChanMonoSrc_16_2_1",
                 "SingleDev_OneChanStereoSnk_OneChanMonoSrc_16_1_Server_Preferred",
                 "SingleDev_OneChanStereoSnk_OneChanMonoSrc_16_1_1",
+                "SingleDev_OneChanMonoSnk_OneChanMonoSrc_32_2_Server_Preferred",
+                "SingleDev_OneChanMonoSnk_OneChanMonoSrc_32_2_1",
                 "SingleDev_OneChanMonoSnk_OneChanMonoSrc_16_2_Server_Preferred",
                 "SingleDev_OneChanMonoSnk_OneChanMonoSrc_16_2_1",
                 "SingleDev_OneChanMonoSnk_OneChanMonoSrc_16_1_Server_Preferred",
diff --git a/system/bta/le_audio/client.cc b/system/bta/le_audio/client.cc
index 7547a97..7417115 100644
--- a/system/bta/le_audio/client.cc
+++ b/system/bta/le_audio/client.cc
@@ -116,6 +116,23 @@
 namespace {
 void le_audio_gattc_callback(tBTA_GATTC_EVT event, tBTA_GATTC* p_data);
 
+inline uint8_t bits_to_bytes_per_sample(uint8_t bits_per_sample) {
+  // 24 bit audio stream is sent as unpacked, each sample takes 4 bytes.
+  if (bits_per_sample == 24) return 4;
+
+  return bits_per_sample / 8;
+}
+
+inline lc3_pcm_format bits_to_lc3_bits(uint8_t bits_per_sample) {
+  if (bits_per_sample == 16) return LC3_PCM_FORMAT_S16;
+
+  if (bits_per_sample == 24) return LC3_PCM_FORMAT_S24;
+
+  LOG_ALWAYS_FATAL("Encoder/decoder don't know how to handle %d",
+                   bits_per_sample);
+  return LC3_PCM_FORMAT_S16;
+}
+
 class LeAudioClientImpl;
 LeAudioClientImpl* instance;
 LeAudioClientAudioSinkReceiver* audioSinkReceiver;
@@ -684,6 +701,52 @@
     // TODO Implement
   }
 
+  void StartAudioSession(LeAudioDeviceGroup* group,
+                          LeAudioCodecConfiguration* source_config,
+                          LeAudioCodecConfiguration* sink_config) {
+    /* This function is called when group is not yet set to active.
+     * This is why we don't have to check if session is started already.
+     * Just check if it is acquired.
+     */
+    ASSERT_LOG(active_group_id_ == bluetooth::groups::kGroupUnknown,
+               "Active group is not set.");
+    ASSERT_LOG(audio_source_instance_, "Source session not acquired");
+    ASSERT_LOG(audio_sink_instance_, "Sink session not acquired");
+
+    /* We assume that peer device always use same frame duration */
+    uint32_t frame_duration_us = 0;
+    if (!source_config->IsInvalid()) {
+      frame_duration_us = source_config->data_interval_us;
+    } else if (!sink_config->IsInvalid()) {
+      frame_duration_us = sink_config->data_interval_us;
+    } else {
+      ASSERT_LOG(true, "Both configs are invalid");
+    }
+
+    audio_framework_source_config.data_interval_us = frame_duration_us;
+    leAudioClientAudioSource->Start(audio_framework_source_config,
+                                    audioSinkReceiver);
+
+    /* We use same frame duration for sink/source */
+    audio_framework_sink_config.data_interval_us = frame_duration_us;
+
+    /* If group supports more than 16kHz for the microphone in converstional
+     * case let's use that also for Audio Framework.
+     */
+    std::optional<LeAudioCodecConfiguration> sink_configuration =
+        group->GetCodecConfigurationByDirection(
+            LeAudioContextType::CONVERSATIONAL,
+            le_audio::types::kLeAudioDirectionSource);
+    if (sink_configuration &&
+        sink_configuration->sample_rate >
+            bluetooth::audio::le_audio::kSampleRate16000) {
+      audio_framework_sink_config.sample_rate = sink_configuration->sample_rate;
+    }
+
+    leAudioClientAudioSink->Start(audio_framework_sink_config,
+                                  audioSourceReceiver);
+  }
+
   void GroupSetActive(const int group_id) override {
     DLOG(INFO) << __func__ << " group_id: " << group_id;
 
@@ -752,16 +815,8 @@
 
     if (active_group_id_ == bluetooth::groups::kGroupUnknown) {
       /* Expose audio sessions if there was no previous active group */
-      audio_framework_source_config.data_interval_us =
-          current_source_codec_config.data_interval_us;
-      leAudioClientAudioSource->Start(audio_framework_source_config,
-                                      audioSinkReceiver);
-
-      audio_framework_sink_config.data_interval_us =
-          current_source_codec_config.data_interval_us;
-
-      leAudioClientAudioSink->Start(audio_framework_sink_config,
-                                    audioSourceReceiver);
+      StartAudioSession(group, &current_source_codec_config,
+                         &current_sink_codec_config);
     } else {
       /* In case there was an active group. Stop the stream */
       GroupStop(active_group_id_);
@@ -1885,27 +1940,36 @@
     return true;
   }
 
-  void get_mono_stream(const std::vector<uint8_t>& data,
-                       std::vector<int16_t>& chan_mono, int pitch = 1) {
-    uint16_t num_of_frames_per_ch;
+  // mix stero signal into mono
+  std::vector<uint8_t> mono_blend(const std::vector<uint8_t>& buf,
+                                  int bytes_per_sample, size_t frames) {
+    std::vector<uint8_t> mono_out;
+    mono_out.resize(frames * bytes_per_sample);
 
-    int dt_us = current_source_codec_config.data_interval_us;
-    int af_hz = audio_framework_source_config.sample_rate;
-    num_of_frames_per_ch = lc3_frame_samples(dt_us, af_hz);
-
-    chan_mono.reserve(num_of_frames_per_ch);
-    for (int i = 0; i < pitch * num_of_frames_per_ch; i += pitch) {
-      const uint8_t* sample = data.data() + i * 4;
-
-      int16_t left = (int16_t)((*(sample + 1) << 8) + *sample) >> 1;
-
-      sample += 2;
-      int16_t right = (int16_t)((*(sample + 1) << 8) + *sample) >> 1;
-
-      uint16_t mono_data = (int16_t)(((uint32_t)left + (uint32_t)right) >> 1);
-
-      chan_mono.push_back(mono_data);
+    if (bytes_per_sample == 2) {
+      int16_t* out = (int16_t*)mono_out.data();
+      for (size_t i = 0; i < frames; ++i) {
+        const int16_t* in = (int16_t*)(buf.data());
+        int accum = 0;
+        accum += *in++;
+        accum += *in++;
+        accum /= 2;  // round to 0
+        *out++ = accum;
+      }
+    } else if (bytes_per_sample == 4) {
+      int32_t* out = (int32_t*)mono_out.data();
+      for (size_t i = 0; i < frames; ++i) {
+        const int32_t* in = (int32_t*)(buf.data());
+        int accum = 0;
+        accum += *in++;
+        accum += *in++;
+        accum /= 2;  // round to 0
+        *out++ = accum;
+      }
+    } else {
+      LOG_ERROR("Don't know how to mono blend that %d!", bytes_per_sample);
     }
+    return mono_out;
   }
 
   void PrepareAndSendToTwoDevices(
@@ -1920,6 +1984,11 @@
     int af_hz = audio_framework_source_config.sample_rate;
     number_of_required_samples_per_channel = lc3_frame_samples(dt_us, af_hz);
 
+    lc3_pcm_format bits_per_sample =
+        bits_to_lc3_bits(audio_framework_source_config.bits_per_sample);
+    uint8_t bytes_per_sample =
+        bits_to_bytes_per_sample(audio_framework_source_config.bits_per_sample);
+
     for (auto [cis_handle, audio_location] : stream_conf->sink_streams) {
       if (audio_location & le_audio::codec_spec_conf::kLeAudioLocationAnyLeft)
         left_cis_handle = cis_handle;
@@ -1927,11 +1996,12 @@
         right_cis_handle = cis_handle;
     }
 
-    if (data.size() < 2 /* bytes per sample */ * 2 /* channels */ *
+    if (data.size() < bytes_per_sample * 2 /* channels */ *
                           number_of_required_samples_per_channel) {
       LOG(ERROR) << __func__ << " Missing samples. Data size: " << +data.size()
                  << " expected: "
-                 << 2 * 2 * number_of_required_samples_per_channel;
+                 << bytes_per_sample * 2 *
+                        number_of_required_samples_per_channel;
       return;
     }
 
@@ -1941,26 +2011,22 @@
     bool mono = (left_cis_handle == 0) || (right_cis_handle == 0);
 
     if (!mono) {
-      lc3_encode(lc3_encoder_left, LC3_PCM_FORMAT_S16,
-                 (const int16_t*)data.data(), 2, chan_left_enc.size(),
-                 chan_left_enc.data());
-      lc3_encode(lc3_encoder_right, LC3_PCM_FORMAT_S16,
-                 ((const int16_t*)data.data()) + 1, 2, chan_right_enc.size(),
+      lc3_encode(lc3_encoder_left, bits_per_sample, data.data(), 2,
+                 chan_left_enc.size(), chan_left_enc.data());
+      lc3_encode(lc3_encoder_right, bits_per_sample,
+                 data.data() + bytes_per_sample, 2, chan_right_enc.size(),
                  chan_right_enc.data());
     } else {
-      std::vector<int16_t> chan_mono;
-      get_mono_stream(data, chan_mono);
-
+      std::vector<uint8_t> mono = mono_blend(
+          data, bytes_per_sample, number_of_required_samples_per_channel);
       if (left_cis_handle) {
-        lc3_encode(lc3_encoder_left, LC3_PCM_FORMAT_S16,
-                   (const int16_t*)chan_mono.data(), 1, chan_left_enc.size(),
-                   chan_left_enc.data());
+        lc3_encode(lc3_encoder_left, bits_per_sample, mono.data(), 1,
+                   chan_left_enc.size(), chan_left_enc.data());
       }
 
       if (right_cis_handle) {
-        lc3_encode(lc3_encoder_right, LC3_PCM_FORMAT_S16,
-                   (const int16_t*)chan_mono.data(), 1, chan_right_enc.size(),
-                   chan_right_enc.data());
+        lc3_encode(lc3_encoder_right, bits_per_sample, mono.data(), 1,
+                   chan_right_enc.size(), chan_right_enc.data());
       }
     }
 
@@ -1987,6 +2053,10 @@
     int dt_us = current_source_codec_config.data_interval_us;
     int af_hz = audio_framework_source_config.sample_rate;
     number_of_required_samples_per_channel = lc3_frame_samples(dt_us, af_hz);
+    lc3_pcm_format bits_per_sample =
+        bits_to_lc3_bits(audio_framework_source_config.bits_per_sample);
+    uint8_t bytes_per_sample =
+        bits_to_bytes_per_sample(audio_framework_source_config.bits_per_sample);
 
     if ((int)data.size() < (2 /* bytes per sample */ * num_channels *
                             number_of_required_samples_per_channel)) {
@@ -1998,21 +2068,19 @@
     if (num_channels == 1) {
       /* Since we always get two channels from framework, lets make it mono here
        */
-      std::vector<int16_t> chan_mono;
-      get_mono_stream(data, chan_mono);
+      std::vector<uint8_t> mono = mono_blend(
+          data, bytes_per_sample, number_of_required_samples_per_channel);
 
-      auto err = lc3_encode(lc3_encoder_left, LC3_PCM_FORMAT_S16,
-                            (const int16_t*)chan_mono.data(), 1, byte_count,
-                            chan_encoded.data());
+      auto err = lc3_encode(lc3_encoder_left, bits_per_sample, mono.data(), 1,
+                            byte_count, chan_encoded.data());
 
       if (err < 0) {
         LOG(ERROR) << " error while encoding, error code: " << +err;
       }
     } else {
-      lc3_encode(lc3_encoder_left, LC3_PCM_FORMAT_S16,
-                 (const int16_t*)data.data(), 2, byte_count,
-                 chan_encoded.data());
-      lc3_encode(lc3_encoder_right, LC3_PCM_FORMAT_S16,
+      lc3_encode(lc3_encoder_left, bits_per_sample, (const int16_t*)data.data(),
+                 2, byte_count, chan_encoded.data());
+      lc3_encode(lc3_encoder_right, bits_per_sample,
                  (const int16_t*)data.data() + 1, 2, byte_count,
                  chan_encoded.data() + byte_count);
     }
@@ -2212,6 +2280,8 @@
 
     int dt_us = current_sink_codec_config.data_interval_us;
     int af_hz = audio_framework_sink_config.sample_rate;
+    lc3_pcm_format bits_per_sample =
+        bits_to_lc3_bits(audio_framework_sink_config.bits_per_sample);
 
     int pcm_size;
     if (dt_us == 10000) {
@@ -2244,7 +2314,7 @@
     lc3_decoder_t decoder_to_use =
         is_left ? lc3_decoder_left : lc3_decoder_right;
 
-    err = lc3_decode(decoder_to_use, data, size, LC3_PCM_FORMAT_S16,
+    err = lc3_decode(decoder_to_use, data, size, bits_per_sample,
                      pcm_data_decoded.data(), 1 /* pitch */);
 
     if (err < 0) {
@@ -3623,10 +3693,6 @@
       << ", LE Audio Client requires Bluetooth Audio HAL V2.1 at least. Either "
          "disable LE Audio Profile, or update your HAL";
 
-  // TODO: The capability list should pass to the codec manager once it's ready
-  std::vector<::le_audio::set_configurations::AudioSetConfiguration>
-      capabilities = ::bluetooth::audio::le_audio::get_offload_capabilities();
-
   IsoManager::GetInstance()->Start();
 
   if (leAudioClientAudioSource == nullptr)
@@ -3642,7 +3708,7 @@
   instance = new LeAudioClientImpl(callbacks_, stateMachineCallbacks, initCb);
 
   IsoManager::GetInstance()->RegisterCigCallbacks(stateMachineHciCallbacks);
-  CodecManager::GetInstance()->Start(offloading_preference, capabilities);
+  CodecManager::GetInstance()->Start(offloading_preference);
 }
 
 void LeAudioClient::DebugDump(int fd) {
diff --git a/system/bta/le_audio/client_audio.h b/system/bta/le_audio/client_audio.h
index ae6614c..f812d12 100644
--- a/system/bta/le_audio/client_audio.h
+++ b/system/bta/le_audio/client_audio.h
@@ -96,6 +96,13 @@
              (data_interval_us == other.data_interval_us));
   }
 
+  bool operator==(const LeAudioCodecConfiguration& other) const {
+    return ((num_channels == other.num_channels) &&
+            (sample_rate == other.sample_rate) &&
+            (bits_per_sample == other.bits_per_sample) &&
+            (data_interval_us == other.data_interval_us));
+  }
+
   bool IsInvalid() {
     return (num_channels == 0) || (sample_rate == 0) ||
            (bits_per_sample == 0) || (data_interval_us == 0);
diff --git a/system/bta/le_audio/codec_manager.cc b/system/bta/le_audio/codec_manager.cc
index e34d19c..44b625f 100644
--- a/system/bta/le_audio/codec_manager.cc
+++ b/system/bta/le_audio/codec_manager.cc
@@ -45,8 +45,7 @@
 struct codec_manager_impl {
  public:
   codec_manager_impl(
-      const std::vector<btle_audio_codec_config_t>& offloading_preference,
-      const std::vector<AudioSetConfiguration>& adsp_capabilities) {
+      const std::vector<btle_audio_codec_config_t>& offloading_preference) {
     offload_enable_ = osi_property_get_bool(
                           "ro.bluetooth.leaudio_offload.supported", false) &&
                       !osi_property_get_bool(
@@ -71,7 +70,7 @@
                             kIsoDataPathPlatformDefault, {});
     btm_configure_data_path(btm_data_direction::CONTROLLER_TO_HOST,
                             kIsoDataPathPlatformDefault, {});
-    UpdateOffloadCapability(offloading_preference, adsp_capabilities);
+    UpdateOffloadCapability(offloading_preference);
     SetCodecLocation(CodecLocation::ADSP);
   }
   ~codec_manager_impl() {
@@ -218,8 +217,7 @@
   }
 
   void UpdateOffloadCapability(
-      const std::vector<btle_audio_codec_config_t>& offloading_preference,
-      const std::vector<AudioSetConfiguration>& adsp_capabilities) {
+      const std::vector<btle_audio_codec_config_t>& offloading_preference) {
     LOG(INFO) << __func__;
     std::unordered_set<uint8_t> offload_preference_set;
 
@@ -228,6 +226,10 @@
       return;
     }
 
+    std::vector<::le_audio::set_configurations::AudioSetConfiguration>
+        adsp_capabilities =
+            ::bluetooth::audio::le_audio::get_offload_capabilities();
+
     for (auto codec : offloading_preference) {
       auto it = btle_audio_codec_type_map_.find(codec.codec_type);
 
@@ -272,12 +274,10 @@
   impl(const CodecManager& codec_manager) : codec_manager_(codec_manager) {}
 
   void Start(
-      const std::vector<btle_audio_codec_config_t>& offloading_preference,
-      const std::vector<set_configurations::AudioSetConfiguration>&
-          adsp_capabilities) {
+      const std::vector<btle_audio_codec_config_t>& offloading_preference) {
     LOG_ASSERT(!codec_manager_impl_);
-    codec_manager_impl_ = std::make_unique<codec_manager_impl>(
-        offloading_preference, adsp_capabilities);
+    codec_manager_impl_ =
+        std::make_unique<codec_manager_impl>(offloading_preference);
   }
 
   void Stop() {
@@ -294,11 +294,8 @@
 CodecManager::CodecManager() : pimpl_(std::make_unique<impl>(*this)) {}
 
 void CodecManager::Start(
-    const std::vector<btle_audio_codec_config_t>& offloading_preference,
-    const std::vector<set_configurations::AudioSetConfiguration>&
-        adsp_capabilities) {
-  if (!pimpl_->IsRunning())
-    pimpl_->Start(offloading_preference, adsp_capabilities);
+    const std::vector<btle_audio_codec_config_t>& offloading_preference) {
+  if (!pimpl_->IsRunning()) pimpl_->Start(offloading_preference);
 }
 
 void CodecManager::Stop() {
diff --git a/system/bta/le_audio/codec_manager.h b/system/bta/le_audio/codec_manager.h
index d0416e1..1eed274 100644
--- a/system/bta/le_audio/codec_manager.h
+++ b/system/bta/le_audio/codec_manager.h
@@ -38,11 +38,8 @@
     static CodecManager* instance = new CodecManager();
     return instance;
   }
-  void Start(
-      const std::vector<bluetooth::le_audio::btle_audio_codec_config_t>&
-          offloading_preference,
-      const std::vector<::le_audio::set_configurations::AudioSetConfiguration>&
-          adsp_capabilities);
+  void Start(const std::vector<bluetooth::le_audio::btle_audio_codec_config_t>&
+                 offloading_preference);
   void Stop(void);
   virtual types::CodecLocation GetCodecLocation(void) const;
   virtual void UpdateActiveSourceAudioConfig(
diff --git a/system/bta/le_audio/devices.cc b/system/bta/le_audio/devices.cc
index 3d344ce..6731c6c 100644
--- a/system/bta/le_audio/devices.cc
+++ b/system/bta/le_audio/devices.cc
@@ -1081,6 +1081,9 @@
       /* Skip if device has ASE configured in this direction already */
       if (device->GetFirstActiveAseByDirection(ent.direction)) continue;
 
+      /* For the moment, we configure only connected devices. */
+      if (device->conn_id_ == GATT_INVALID_CONN_ID) continue;
+
       if (!device->ConfigureAses(ent, context_type, &active_ase_num,
                                  group_snk_audio_locations,
                                  group_src_audio_locations, reuse_cis_id))
diff --git a/system/bta/le_audio/devices_test.cc b/system/bta/le_audio/devices_test.cc
index 166b598..7628182 100644
--- a/system/bta/le_audio/devices_test.cc
+++ b/system/bta/le_audio/devices_test.cc
@@ -199,12 +199,18 @@
   /* Update those values, on any change of codec linked with content type */
   switch (context_type) {
     case LeAudioContextType::RINGTONE:
-    case LeAudioContextType::CONVERSATIONAL:
       if (id == Lc3SettingId::LC3_16_1 || id == Lc3SettingId::LC3_16_2)
         return true;
 
       break;
 
+    case LeAudioContextType::CONVERSATIONAL:
+      if (id == Lc3SettingId::LC3_16_1 || id == Lc3SettingId::LC3_16_2 ||
+          id == Lc3SettingId::LC3_32_2)
+        return true;
+
+      break;
+
     case LeAudioContextType::MEDIA:
       if (id == Lc3SettingId::LC3_16_1 || id == Lc3SettingId::LC3_16_2 ||
           id == Lc3SettingId::LC3_48_4 || id == Lc3SettingId::LC3_48_2 ||
diff --git a/system/bta/le_audio/le_audio_client_test.cc b/system/bta/le_audio/le_audio_client_test.cc
index f3db83a..6adc967 100644
--- a/system/bta/le_audio/le_audio_client_test.cc
+++ b/system/bta/le_audio/le_audio_client_test.cc
@@ -1032,10 +1032,12 @@
                          bool connect_through_csis = false,
                          bool new_device = true) {
     SetSampleDatabaseEarbudsValid(conn_id, addr, sink_audio_allocation,
-                                  source_audio_allocation, true, /*add_csis*/
-                                  true,                          /*add_cas*/
-                                  true,                          /*add_pacs*/
-                                  true,                          /*add_ascs*/
+                                  source_audio_allocation,
+                                  0x0004, /* source sample freq 16khz */
+                                  true,   /*add_csis*/
+                                  true,   /*add_cas*/
+                                  true,   /*add_pacs*/
+                                  true,   /*add_ascs*/
                                   group_size, rank);
     EXPECT_CALL(mock_client_callbacks_,
                 OnConnectionState(ConnectionState::CONNECTED, addr))
@@ -1066,12 +1068,13 @@
   void ConnectNonCsisDevice(const RawAddress& addr, uint16_t conn_id,
                             uint32_t sink_audio_allocation,
                             uint32_t source_audio_allocation) {
-    SetSampleDatabaseEarbudsValid(conn_id, addr, sink_audio_allocation,
-                                  source_audio_allocation, false, /*add_csis*/
-                                  true,                           /*add_cas*/
-                                  true,                           /*add_pacs*/
-                                  true,                           /*add_ascs*/
-                                  0, 0);
+    SetSampleDatabaseEarbudsValid(
+        conn_id, addr, sink_audio_allocation, source_audio_allocation, 0x0004,
+        /* source sample freq 16khz */ false, /*add_csis*/
+        true,                                 /*add_cas*/
+        true,                                 /*add_pacs*/
+        true,                                 /*add_ascs*/
+        0, 0);
     EXPECT_CALL(mock_client_callbacks_,
                 OnConnectionState(ConnectionState::CONNECTED, addr))
         .Times(1);
@@ -1358,6 +1361,7 @@
   void SetSampleDatabaseEarbudsValid(uint16_t conn_id, RawAddress addr,
                                      uint32_t sink_audio_allocation,
                                      uint32_t source_audio_allocation,
+                                     uint16_t sample_freq_mask = 0x0004,
                                      bool add_csis = true, bool add_cas = true,
                                      bool add_pacs = true, bool add_ascs = true,
                                      uint8_t set_size = 2, uint8_t rank = 1) {
@@ -1438,10 +1442,14 @@
       src_allocation[2] = (uint8_t)(source_audio_allocation >> 16);
       src_allocation[3] = (uint8_t)(source_audio_allocation >> 24);
 
+      uint8_t sample_freq[2];
+      sample_freq[0] = (uint8_t)(sample_freq_mask);
+      sample_freq[1] = (uint8_t)(sample_freq_mask >> 8);
+
       // Set pacs default read values
       ON_CALL(*peer_devices.at(conn_id)->pacs, OnReadCharacteristic(_, _, _))
           .WillByDefault(
-              [this, conn_id, snk_allocation, src_allocation](
+              [this, conn_id, snk_allocation, src_allocation, sample_freq](
                   uint16_t handle, GATT_READ_OP_CB cb, void* cb_data) {
                 auto& pacs = peer_devices.at(conn_id)->pacs;
                 std::vector<uint8_t> value;
@@ -1459,8 +1467,8 @@
                       0x10,
                       0x03,
                       0x01,
-                      0x04,
-                      0x00,
+                      sample_freq[0],
+                      sample_freq[1],
                       0x02,
                       0x02,
                       0x03,
@@ -1471,7 +1479,7 @@
                       0x04,
                       0x1E,
                       0x00,
-                      0x28,
+                      0x78,
                       0x00,
                       // Metadata Length
                       0x00,
@@ -1524,8 +1532,8 @@
                       0x10,
                       0x03,
                       0x01,
-                      0x04,
-                      0x00,
+                      sample_freq[0],
+                      sample_freq[1],
                       0x02,
                       0x02,
                       0x03,
@@ -1536,7 +1544,7 @@
                       0x04,
                       0x1E,
                       0x00,
-                      0x28,
+                      0x78,
                       0x00,
                       // Metadata Length
                       0x00,
@@ -1850,9 +1858,10 @@
   const RawAddress test_address0 = GetTestAddress(0);
   SetSampleDatabaseEarbudsValid(
       1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
-      codec_spec_conf::kLeAudioLocationStereo, true, /*add_csis*/
-      true,                                          /*add_cas*/
-      false,                                         /*add_pacs*/
+      codec_spec_conf::kLeAudioLocationStereo, 0x0004,
+      /* source sample freq 16khz */ true, /*add_csis*/
+      true,                                /*add_cas*/
+      false,                               /*add_pacs*/
       true /*add_ascs*/);
   EXPECT_CALL(mock_client_callbacks_,
               OnConnectionState(ConnectionState::DISCONNECTED, test_address0))
@@ -1865,9 +1874,10 @@
   const RawAddress test_address0 = GetTestAddress(0);
   SetSampleDatabaseEarbudsValid(
       1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
-      codec_spec_conf::kLeAudioLocationStereo, true, /*add_csis*/
-      true,                                          /*add_cas*/
-      true,                                          /*add_pacs*/
+      codec_spec_conf::kLeAudioLocationStereo, 0x0004,
+      /* source sample freq 16khz */ true, /*add_csis*/
+      true,                                /*add_cas*/
+      true,                                /*add_pacs*/
       false /*add_ascs*/);
   EXPECT_CALL(mock_client_callbacks_,
               OnConnectionState(ConnectionState::DISCONNECTED, test_address0))
@@ -1881,9 +1891,10 @@
   uint16_t conn_id = 1;
   SetSampleDatabaseEarbudsValid(
       conn_id, test_address0, codec_spec_conf::kLeAudioLocationStereo,
-      codec_spec_conf::kLeAudioLocationStereo, true, /*add_csis*/
-      false,                                         /*add_cas*/
-      true,                                          /*add_pacs*/
+      codec_spec_conf::kLeAudioLocationStereo, 0x0004,
+      /* source sample freq 16khz */ true, /*add_csis*/
+      false,                               /*add_cas*/
+      true,                                /*add_pacs*/
       true /*add_ascs*/);
 
   EXPECT_CALL(mock_client_callbacks_,
@@ -1896,9 +1907,10 @@
   const RawAddress test_address0 = GetTestAddress(0);
   SetSampleDatabaseEarbudsValid(
       1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
-      codec_spec_conf::kLeAudioLocationStereo, false, /*add_csis*/
-      true,                                           /*add_cas*/
-      true,                                           /*add_pacs*/
+      codec_spec_conf::kLeAudioLocationStereo, 0x0004,
+      /* source sample freq 16khz */ false, /*add_csis*/
+      true,                                 /*add_cas*/
+      true,                                 /*add_pacs*/
       true /*add_ascs*/);
   EXPECT_CALL(mock_client_callbacks_,
               OnConnectionState(ConnectionState::CONNECTED, test_address0))
@@ -2039,19 +2051,21 @@
   const RawAddress test_address0 = GetTestAddress(0);
   SetSampleDatabaseEarbudsValid(
       1, test_address0, codec_spec_conf::kLeAudioLocationFrontLeft,
-      codec_spec_conf::kLeAudioLocationFrontLeft, true, /*add_csis*/
-      true,                                             /*add_cas*/
-      true,                                             /*add_pacs*/
-      true,                                             /*add_ascs*/
+      codec_spec_conf::kLeAudioLocationFrontLeft, 0x0004,
+      /* source sample freq 16khz */ true, /*add_csis*/
+      true,                                /*add_cas*/
+      true,                                /*add_pacs*/
+      true,                                /*add_ascs*/
       group_size, 1);
 
   const RawAddress test_address1 = GetTestAddress(1);
   SetSampleDatabaseEarbudsValid(
       2, test_address1, codec_spec_conf::kLeAudioLocationFrontRight,
-      codec_spec_conf::kLeAudioLocationFrontRight, true, /*add_csis*/
-      true,                                              /*add_cas*/
-      true,                                              /*add_pacs*/
-      true,                                              /*add_ascs*/
+      codec_spec_conf::kLeAudioLocationFrontRight, 0x0004,
+      /* source sample freq 16khz */ true, /*add_csis*/
+      true,                                /*add_cas*/
+      true,                                /*add_pacs*/
+      true,                                /*add_ascs*/
       group_size, 2);
 
   // Load devices from the storage when storage API is called
@@ -2134,10 +2148,11 @@
   const RawAddress test_address0 = GetTestAddress(0);
   SetSampleDatabaseEarbudsValid(
       1, test_address0, codec_spec_conf::kLeAudioLocationFrontLeft,
-      codec_spec_conf::kLeAudioLocationFrontLeft, true, /*add_csis*/
-      true,                                             /*add_cas*/
-      true,                                             /*add_pacs*/
-      true,                                             /*add_ascs*/
+      codec_spec_conf::kLeAudioLocationFrontLeft, 0x0004,
+      /* source sample freq 16khz */ true, /*add_csis*/
+      true,                                /*add_cas*/
+      true,                                /*add_pacs*/
+      true,                                /*add_ascs*/
       group_size, 1);
 
   ON_CALL(mock_groups_module_, GetGroupId(test_address0, _))
@@ -2149,10 +2164,11 @@
   const RawAddress test_address1 = GetTestAddress(1);
   SetSampleDatabaseEarbudsValid(
       2, test_address1, codec_spec_conf::kLeAudioLocationFrontRight,
-      codec_spec_conf::kLeAudioLocationFrontRight, true, /*add_csis*/
-      true,                                              /*add_cas*/
-      true,                                              /*add_pacs*/
-      true,                                              /*add_ascs*/
+      codec_spec_conf::kLeAudioLocationFrontRight, 0x0004,
+      /* source sample freq 16khz */ true, /*add_csis*/
+      true,                                /*add_cas*/
+      true,                                /*add_pacs*/
+      true,                                /*add_ascs*/
       group_size, 2);
 
   ON_CALL(mock_groups_module_, GetGroupId(test_address1, _))
@@ -2469,9 +2485,9 @@
 
   SetSampleDatabaseEarbudsValid(
       1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
-      codec_spec_conf::kLeAudioLocationStereo, false /*add_csis*/,
-      true /*add_cas*/, true /*add_pacs*/, true /*add_ascs*/, 1 /*set_size*/,
-      0 /*rank*/);
+      codec_spec_conf::kLeAudioLocationStereo, 0x0004,
+      /* source sample freq 16khz */ false /*add_csis*/, true /*add_cas*/,
+      true /*add_pacs*/, true /*add_ascs*/, 1 /*set_size*/, 0 /*rank*/);
   EXPECT_CALL(mock_client_callbacks_,
               OnConnectionState(ConnectionState::CONNECTED, test_address0))
       .Times(1);
@@ -2534,9 +2550,9 @@
 
   SetSampleDatabaseEarbudsValid(
       1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
-      codec_spec_conf::kLeAudioLocationStereo, false /*add_csis*/,
-      true /*add_cas*/, true /*add_pacs*/, true /*add_ascs*/, 1 /*set_size*/,
-      0 /*rank*/);
+      codec_spec_conf::kLeAudioLocationStereo, 0x0004,
+      /* source sample freq 16khz */ false /*add_csis*/, true /*add_cas*/,
+      true /*add_pacs*/, true /*add_ascs*/, 1 /*set_size*/, 0 /*rank*/);
   EXPECT_CALL(mock_client_callbacks_,
               OnConnectionState(ConnectionState::CONNECTED, test_address0))
       .Times(1);
@@ -2594,9 +2610,9 @@
 
   SetSampleDatabaseEarbudsValid(
       1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
-      codec_spec_conf::kLeAudioLocationStereo, false /*add_csis*/,
-      true /*add_cas*/, true /*add_pacs*/, true /*add_ascs*/, 1 /*set_size*/,
-      0 /*rank*/);
+      codec_spec_conf::kLeAudioLocationStereo, 0x0004,
+      /* source sample freq 16khz */ false /*add_csis*/, true /*add_cas*/,
+      true /*add_pacs*/, true /*add_ascs*/, 1 /*set_size*/, 0 /*rank*/);
   EXPECT_CALL(mock_client_callbacks_,
               OnConnectionState(ConnectionState::CONNECTED, test_address0))
       .Times(1);
@@ -3008,5 +3024,35 @@
   Mock::VerifyAndClearExpectations(&mock_client_callbacks_);
 }
 
+TEST_F(UnicastTest, TwoEarbudsWithSourceSupporting32kHz) {
+  const RawAddress test_address0 = GetTestAddress(0);
+  int group_id = 0;
+  SetSampleDatabaseEarbudsValid(
+      1, test_address0, codec_spec_conf::kLeAudioLocationStereo,
+      codec_spec_conf::kLeAudioLocationStereo, 0x0024,
+      /* source sample freq 32/16khz */ true, /*add_csis*/
+      true,                                   /*add_cas*/
+      true,                                   /*add_pacs*/
+      true /*add_ascs*/);
+  EXPECT_CALL(mock_client_callbacks_,
+              OnConnectionState(ConnectionState::CONNECTED, test_address0))
+      .Times(1);
+  ConnectLeAudio(test_address0);
+
+  // LeAudioCodecConfiguration received_af_sink_config;
+  const LeAudioCodecConfiguration expected_af_sink_config = {
+      .num_channels = 2,
+      .sample_rate = bluetooth::audio::le_audio::kSampleRate32000,
+      .bits_per_sample = bluetooth::audio::le_audio::kBitsPerSample16,
+      .data_interval_us = LeAudioCodecConfiguration::kInterval10000Us,
+  };
+
+  // Audio sessions are started only when device gets active
+  EXPECT_CALL(*mock_unicast_audio_source_, Start(_, _)).Times(1);
+  EXPECT_CALL(*mock_audio_sink_, Start(expected_af_sink_config, _)).Times(1);
+  LeAudioClient::Get()->GroupSetActive(group_id);
+  SyncOnMainLoop();
+}
+
 }  // namespace
 }  // namespace le_audio
diff --git a/system/bta/le_audio/le_audio_types.h b/system/bta/le_audio/le_audio_types.h
index 629e352..b5067d4 100644
--- a/system/bta/le_audio/le_audio_types.h
+++ b/system/bta/le_audio/le_audio_types.h
@@ -173,6 +173,7 @@
 constexpr uint16_t kLeAudioCodecLC3FrameLen30 = 30;
 constexpr uint16_t kLeAudioCodecLC3FrameLen40 = 40;
 constexpr uint16_t kLeAudioCodecLC3FrameLen60 = 60;
+constexpr uint16_t kLeAudioCodecLC3FrameLen80 = 80;
 constexpr uint16_t kLeAudioCodecLC3FrameLen120 = 120;
 
 }  // namespace codec_spec_conf
diff --git a/system/bta/le_audio/mock_codec_manager.cc b/system/bta/le_audio/mock_codec_manager.cc
index 6b2dd3d..d433b5f 100644
--- a/system/bta/le_audio/mock_codec_manager.cc
+++ b/system/bta/le_audio/mock_codec_manager.cc
@@ -63,9 +63,7 @@
 
 void CodecManager::Start(
     const std::vector<bluetooth::le_audio::btle_audio_codec_config_t>&
-        offloading_preference,
-    const std::vector<set_configurations::AudioSetConfiguration>&
-        adsp_capabilities) {
+        offloading_preference) {
   // It is needed here as CodecManager which is a singleton creates it, but in
   // this mock we want to destroy and recreate the mock on each test case.
   if (!pimpl_) {
diff --git a/system/bta/le_audio/state_machine_test.cc b/system/bta/le_audio/state_machine_test.cc
index ba8d267..ff33fd3 100644
--- a/system/bta/le_audio/state_machine_test.cc
+++ b/system/bta/le_audio/state_machine_test.cc
@@ -393,9 +393,7 @@
     ASSERT_NE(codec_manager_, nullptr);
     std::vector<bluetooth::le_audio::btle_audio_codec_config_t>
         mock_offloading_preference(0);
-    std::vector<set_configurations::AudioSetConfiguration>
-        mock_adsp_capabilities(0);
-    codec_manager_->Start(mock_offloading_preference, mock_adsp_capabilities);
+    codec_manager_->Start(mock_offloading_preference);
     mock_codec_manager_ = MockCodecManager::GetInstance();
     ASSERT_NE(mock_codec_manager_, nullptr);
     ON_CALL(*mock_codec_manager_, GetCodecLocation())
diff --git a/system/btif/src/btif_hf.cc b/system/btif/src/btif_hf.cc
index cc09d24..995ba83 100644
--- a/system/btif/src/btif_hf.cc
+++ b/system/btif/src/btif_hf.cc
@@ -27,12 +27,16 @@
 
 #define LOG_TAG "bt_btif_hf"
 
+#include <base/logging.h>
+#include <frameworks/proto_logging/stats/enums/bluetooth/enums.pb.h>
+
 #include <cstdint>
 #include <string>
 
 #include "bta/include/bta_ag_api.h"
 #include "bta/include/utl.h"
 #include "btif/include/btif_common.h"
+#include "btif/include/btif_metrics_logging.h"
 #include "btif/include/btif_profile_queue.h"
 #include "btif/include/btif_util.h"
 #include "common/metrics.h"
@@ -44,8 +48,6 @@
 #include "stack/include/btm_api.h"
 #include "types/raw_address.h"
 
-#include <base/logging.h>
-
 namespace {
 constexpr char kBtmLogTag[] = "HFP";
 }
@@ -339,6 +341,10 @@
                          << ", report disconnect state for p_data bda.";
             bt_hf_callbacks->ConnectionStateCallback(
                 BTHF_CONNECTION_STATE_DISCONNECTED, &(p_data->open.bd_addr));
+            log_counter_metrics_btif(
+                android::bluetooth::CodePathCounterKeyEnum::
+                    HFP_COLLISON_AT_AG_OPEN,
+                1);
           }
           break;
         }
@@ -358,6 +364,9 @@
           bt_hf_callbacks->ConnectionStateCallback(
               BTHF_CONNECTION_STATE_DISCONNECTED,
               &(btif_hf_cb[idx].connected_bda));
+          log_counter_metrics_btif(android::bluetooth::CodePathCounterKeyEnum::
+                                       HFP_COLLISON_AT_CONNECTING,
+                                   1);
           reset_control_block(&btif_hf_cb[idx]);
           btif_queue_advance();
         }
@@ -387,6 +396,9 @@
         reset_control_block(&btif_hf_cb[idx]);
         bt_hf_callbacks->ConnectionStateCallback(btif_hf_cb[idx].state,
                                                  &connected_bda);
+        log_counter_metrics_btif(android::bluetooth::CodePathCounterKeyEnum::
+                                     HFP_SELF_INITIATED_AG_FAILED,
+                                 1);
         btif_queue_advance();
       }
       break;
@@ -406,6 +418,9 @@
                                                &connected_bda);
       if (failed_to_setup_slc) {
         LOG(ERROR) << __func__ << ": failed to setup SLC for " << connected_bda;
+        log_counter_metrics_btif(
+            android::bluetooth::CodePathCounterKeyEnum::HFP_SLC_SETUP_FAILED,
+            1);
         btif_queue_advance();
       }
       break;
@@ -1075,7 +1090,7 @@
   if (index == 0) {
     ag_res.ok_flag = BTA_AG_OK_DONE;
   } else {
-    std::string cell_number(number);
+    std::string cell_number(number ? number : "");
     BTIF_TRACE_EVENT(
         "clcc_response: [%d] dir %d state %d mode %d number = %s type = %d",
         index, dir, state, mode, PRIVATE_CELL(cell_number), type);
diff --git a/system/btif/src/btif_storage.cc b/system/btif/src/btif_storage.cc
index 338fb24..6c94377 100644
--- a/system/btif/src/btif_storage.cc
+++ b/system/btif/src/btif_storage.cc
@@ -486,7 +486,12 @@
           }
         }
         bt_linkkey_file_found = true;
-        p_bonded_devices->devices[p_bonded_devices->num_devices++] = bd_addr;
+        if (p_bonded_devices->num_devices < BTM_SEC_MAX_DEVICE_RECORDS) {
+          p_bonded_devices->devices[p_bonded_devices->num_devices++] = bd_addr;
+        } else {
+          BTIF_TRACE_WARNING("%s: exceed the max number of bonded devices",
+                             __func__);
+        }
       } else {
         bt_linkkey_file_found = false;
       }
@@ -1369,7 +1374,12 @@
 
     // Fill in the bonded devices
     if (device_added) {
-      p_bonded_devices->devices[p_bonded_devices->num_devices++] = bd_addr;
+      if (p_bonded_devices->num_devices < BTM_SEC_MAX_DEVICE_RECORDS) {
+        p_bonded_devices->devices[p_bonded_devices->num_devices++] = bd_addr;
+      } else {
+        BTIF_TRACE_WARNING("%s: exceed the max number of bonded devices",
+                           __func__);
+      }
       btif_gatts_add_bonded_dev_from_nv(bd_addr);
     }
 
diff --git a/system/gd/.clang-format b/system/gd/.clang-format
index edf31fe..cf9a43a 100644
--- a/system/gd/.clang-format
+++ b/system/gd/.clang-format
@@ -22,7 +22,7 @@
 BasedOnStyle: Google
 CommentPragmas: NOLINT:.*
 DerivePointerAlignment: false
-ColumnLimit: 120
+ColumnLimit: 100
 AllowShortFunctionsOnASingleLine: Empty
 ConstructorInitializerAllOnOneLineOrOnePerLine: true
 BreakConstructorInitializers: BeforeColon
diff --git a/system/gd/BUILD.gn b/system/gd/BUILD.gn
index 11aa01d..0641fcc 100644
--- a/system/gd/BUILD.gn
+++ b/system/gd/BUILD.gn
@@ -63,6 +63,7 @@
   include_dirs = [ "." ]
   configs += [ ":gd_defaults" ]
   deps = [
+    "//bt/system/gd/rust/common:libbt_keystore_cc",
     "//bt/system/gd/rust/topshim:libbluetooth_topshim",
     "//bt/system/gd/rust/shim:libbluetooth_rust_interop",
     "//bt/system/gd:BluetoothGeneratedPackets_h",
diff --git a/system/gd/hci/acl_manager/le_impl_test.cc b/system/gd/hci/acl_manager/le_impl_test.cc
index 1f3c17f..d87ebcb 100644
--- a/system/gd/hci/acl_manager/le_impl_test.cc
+++ b/system/gd/hci/acl_manager/le_impl_test.cc
@@ -88,12 +88,12 @@
 
     void EnqueueCommand(
         std::unique_ptr<T> command, common::ContextualOnceCallback<void(CommandCompleteView)> on_complete) override {
-      hci_.EnqueueCommand(move(command), std::move(on_complete));
+      hci_.EnqueueCommand(std::move(command), std::move(on_complete));
     }
 
     void EnqueueCommand(
         std::unique_ptr<T> command, common::ContextualOnceCallback<void(CommandStatusView)> on_status) override {
-      hci_.EnqueueCommand(move(command), std::move(on_status));
+      hci_.EnqueueCommand(std::move(command), std::move(on_status));
     }
     HciLayer& hci_;
   };
diff --git a/system/gd/hci/hci_layer.cc b/system/gd/hci/hci_layer.cc
index 57d7e55..0d3b77d 100644
--- a/system/gd/hci/hci_layer.cc
+++ b/system/gd/hci/hci_layer.cc
@@ -44,7 +44,6 @@
 using hci::ResetCompleteView;
 using os::Alarm;
 using os::Handler;
-using std::move;
 using std::unique_ptr;
 
 static void fail_if_reset_complete_not_success(CommandCompleteView complete) {
@@ -61,12 +60,18 @@
 class CommandQueueEntry {
  public:
   CommandQueueEntry(
-      unique_ptr<CommandBuilder> command_packet, ContextualOnceCallback<void(CommandCompleteView)> on_complete_function)
-      : command(move(command_packet)), waiting_for_status_(false), on_complete(move(on_complete_function)) {}
+      unique_ptr<CommandBuilder> command_packet,
+      ContextualOnceCallback<void(CommandCompleteView)> on_complete_function)
+      : command(std::move(command_packet)),
+        waiting_for_status_(false),
+        on_complete(std::move(on_complete_function)) {}
 
   CommandQueueEntry(
-      unique_ptr<CommandBuilder> command_packet, ContextualOnceCallback<void(CommandStatusView)> on_status_function)
-      : command(move(command_packet)), waiting_for_status_(true), on_status(move(on_status_function)) {}
+      unique_ptr<CommandBuilder> command_packet,
+      ContextualOnceCallback<void(CommandStatusView)> on_status_function)
+      : command(std::move(command_packet)),
+        waiting_for_status_(true),
+        on_status(std::move(on_status_function)) {}
 
   unique_ptr<CommandBuilder> command;
   unique_ptr<CommandView> command_view;
@@ -139,7 +144,7 @@
 
   template <typename TResponse>
   void enqueue_command(unique_ptr<CommandBuilder> command, ContextualOnceCallback<void(TResponse)> on_response) {
-    command_queue_.emplace_back(move(command), move(on_response));
+    command_queue_.emplace_back(std::move(command), std::move(on_response));
     send_next_command();
   }
 
@@ -174,26 +179,36 @@
     }
     bool is_status = logging_id == "status";
 
-    ASSERT_LOG(!command_queue_.empty(), "Unexpected %s event with OpCode 0x%02hx (%s)", logging_id.c_str(), op_code,
-               OpCodeText(op_code).c_str());
+    ASSERT_LOG(
+        !command_queue_.empty(),
+        "Unexpected %s event with OpCode 0x%02hx (%s)",
+        logging_id.c_str(),
+        op_code,
+        OpCodeText(op_code).c_str());
     if (waiting_command_ == OpCode::CONTROLLER_DEBUG_INFO && op_code != OpCode::CONTROLLER_DEBUG_INFO) {
       LOG_ERROR("Discarding event that came after timeout 0x%02hx (%s)", op_code, OpCodeText(op_code).c_str());
       return;
     }
-    ASSERT_LOG(waiting_command_ == op_code, "Waiting for 0x%02hx (%s), got 0x%02hx (%s)", waiting_command_,
-               OpCodeText(waiting_command_).c_str(), op_code, OpCodeText(op_code).c_str());
+    ASSERT_LOG(
+        waiting_command_ == op_code,
+        "Waiting for 0x%02hx (%s), got 0x%02hx (%s)",
+        waiting_command_,
+        OpCodeText(waiting_command_).c_str(),
+        op_code,
+        OpCodeText(op_code).c_str());
 
     bool is_vendor_specific = static_cast<int>(op_code) & (0x3f << 10);
     CommandStatusView status_view = CommandStatusView::Create(event);
     if (is_vendor_specific && (is_status && !command_queue_.front().waiting_for_status_) &&
         (status_view.IsValid() && status_view.GetStatus() == ErrorCode::UNKNOWN_HCI_COMMAND)) {
-      // If this is a command status of a vendor specific command, and command complete is expected, we can't treat
-      // this as hard failure since we have no way of probing this lack of support at earlier time. Instead we let
-      // the command complete handler handle a empty Command Complete packet, which will be interpreted as invalid
-      // response.
+      // If this is a command status of a vendor specific command, and command complete is expected,
+      // we can't treat this as hard failure since we have no way of probing this lack of support at
+      // earlier time. Instead we let the command complete handler handle a empty Command Complete
+      // packet, which will be interpreted as invalid response.
       CommandCompleteView command_complete_view = CommandCompleteView::Create(
           EventView::Create(PacketView<kLittleEndian>(std::make_shared<std::vector<uint8_t>>(std::vector<uint8_t>()))));
-      command_queue_.front().GetCallback<CommandCompleteView>()->Invoke(move(command_complete_view));
+      command_queue_.front().GetCallback<CommandCompleteView>()->Invoke(
+          std::move(command_complete_view));
     } else {
       ASSERT_LOG(
           command_queue_.front().waiting_for_status_ == is_status,
@@ -202,7 +217,7 @@
           OpCodeText(op_code).c_str(),
           logging_id.c_str());
 
-      command_queue_.front().GetCallback<TResponse>()->Invoke(move(response_view));
+      command_queue_.front().GetCallback<TResponse>()->Invoke(std::move(response_view));
     }
 
     command_queue_.pop_front();
@@ -275,8 +290,11 @@
         "Can not register handler for %02hhx (%s)",
         EventCode::LE_META_EVENT,
         EventCodeText(EventCode::LE_META_EVENT).c_str());
-    ASSERT_LOG(event_handlers_.count(event) == 0, "Can not register a second handler for %02hhx (%s)", event,
-               EventCodeText(event).c_str());
+    ASSERT_LOG(
+        event_handlers_.count(event) == 0,
+        "Can not register a second handler for %02hhx (%s)",
+        event,
+        EventCodeText(event).c_str());
     event_handlers_[event] = handler;
   }
 
@@ -298,8 +316,11 @@
   }
 
   void register_le_event(SubeventCode event, ContextualCallback<void(LeMetaEventView)> handler) {
-    ASSERT_LOG(subevent_handlers_.count(event) == 0, "Can not register a second handler for %02hhx (%s)", event,
-               SubeventCodeText(event).c_str());
+    ASSERT_LOG(
+        subevent_handlers_.count(event) == 0,
+        "Can not register a second handler for %02hhx (%s)",
+        event,
+        SubeventCodeText(event).c_str());
     subevent_handlers_[event] = handler;
   }
 
@@ -336,22 +357,28 @@
       // BT Core spec 5.2 (Volume 4, Part E section 4.4) allows anytime
       // COMMAND_COMPLETE and COMMAND_STATUS with opcode 0x0 for flow control
       if (event_code == EventCode::COMMAND_COMPLETE) {
-          auto view = CommandCompleteView::Create(event);
-          ASSERT(view.IsValid());
-          auto op_code = view.GetCommandOpCode();
-          ASSERT_LOG(op_code == OpCode::NONE,
+        auto view = CommandCompleteView::Create(event);
+        ASSERT(view.IsValid());
+        auto op_code = view.GetCommandOpCode();
+        ASSERT_LOG(
+            op_code == OpCode::NONE,
             "Received %s event with OpCode 0x%02hx (%s) without a waiting command"
             "(is the HAL sending commands, but not handling the events?)",
-            EventCodeText(event_code).c_str(), op_code, OpCodeText(op_code).c_str());
+            EventCodeText(event_code).c_str(),
+            op_code,
+            OpCodeText(op_code).c_str());
       }
       if (event_code == EventCode::COMMAND_STATUS) {
-          auto view = CommandStatusView::Create(event);
-          ASSERT(view.IsValid());
-          auto op_code = view.GetCommandOpCode();
-          ASSERT_LOG(op_code == OpCode::NONE,
+        auto view = CommandStatusView::Create(event);
+        ASSERT(view.IsValid());
+        auto op_code = view.GetCommandOpCode();
+        ASSERT_LOG(
+            op_code == OpCode::NONE,
             "Received %s event with OpCode 0x%02hx (%s) without a waiting command"
             "(is the HAL sending commands, but not handling the events?)",
-            EventCodeText(event_code).c_str(), op_code, OpCodeText(op_code).c_str());
+            EventCodeText(event_code).c_str(),
+            op_code,
+            OpCodeText(op_code).c_str());
       }
       std::unique_ptr<CommandView> no_waiting_command{nullptr};
       log_hci_event(no_waiting_command, event, module_.GetDependency<storage::StorageModule>());
@@ -423,25 +450,28 @@
   void hciEventReceived(hal::HciPacket event_bytes) override {
     auto packet = packet::PacketView<packet::kLittleEndian>(std::make_shared<std::vector<uint8_t>>(event_bytes));
     EventView event = EventView::Create(packet);
-    module_.CallOn(module_.impl_, &impl::on_hci_event, move(event));
+    module_.CallOn(module_.impl_, &impl::on_hci_event, std::move(event));
   }
 
   void aclDataReceived(hal::HciPacket data_bytes) override {
-    auto packet = packet::PacketView<packet::kLittleEndian>(std::make_shared<std::vector<uint8_t>>(move(data_bytes)));
+    auto packet = packet::PacketView<packet::kLittleEndian>(
+        std::make_shared<std::vector<uint8_t>>(std::move(data_bytes)));
     auto acl = std::make_unique<AclView>(AclView::Create(packet));
-    module_.impl_->incoming_acl_buffer_.Enqueue(move(acl), module_.GetHandler());
+    module_.impl_->incoming_acl_buffer_.Enqueue(std::move(acl), module_.GetHandler());
   }
 
   void scoDataReceived(hal::HciPacket data_bytes) override {
-    auto packet = packet::PacketView<packet::kLittleEndian>(std::make_shared<std::vector<uint8_t>>(move(data_bytes)));
+    auto packet = packet::PacketView<packet::kLittleEndian>(
+        std::make_shared<std::vector<uint8_t>>(std::move(data_bytes)));
     auto sco = std::make_unique<ScoView>(ScoView::Create(packet));
-    module_.impl_->incoming_sco_buffer_.Enqueue(move(sco), module_.GetHandler());
+    module_.impl_->incoming_sco_buffer_.Enqueue(std::move(sco), module_.GetHandler());
   }
 
   void isoDataReceived(hal::HciPacket data_bytes) override {
-    auto packet = packet::PacketView<packet::kLittleEndian>(std::make_shared<std::vector<uint8_t>>(move(data_bytes)));
+    auto packet = packet::PacketView<packet::kLittleEndian>(
+        std::make_shared<std::vector<uint8_t>>(std::move(data_bytes)));
     auto iso = std::make_unique<IsoView>(IsoView::Create(packet));
-    module_.impl_->incoming_iso_buffer_.Enqueue(move(iso), module_.GetHandler());
+    module_.impl_->incoming_iso_buffer_.Enqueue(std::move(iso), module_.GetHandler());
   }
 
   HciLayer& module_;
@@ -449,8 +479,7 @@
 
 HciLayer::HciLayer() : impl_(nullptr), hal_callbacks_(nullptr) {}
 
-HciLayer::~HciLayer() {
-}
+HciLayer::~HciLayer() {}
 
 common::BidiQueueEnd<AclBuilder, AclView>* HciLayer::GetAclQueueEnd() {
   return impl_->acl_queue_.GetUpEnd();
@@ -466,12 +495,17 @@
 
 void HciLayer::EnqueueCommand(
     unique_ptr<CommandBuilder> command, ContextualOnceCallback<void(CommandCompleteView)> on_complete) {
-  CallOn(impl_, &impl::enqueue_command<CommandCompleteView>, move(command), move(on_complete));
+  CallOn(
+      impl_,
+      &impl::enqueue_command<CommandCompleteView>,
+      std::move(command),
+      std::move(on_complete));
 }
 
 void HciLayer::EnqueueCommand(
     unique_ptr<CommandBuilder> command, ContextualOnceCallback<void(CommandStatusView)> on_status) {
-  CallOn(impl_, &impl::enqueue_command<CommandStatusView>, move(command), move(on_status));
+  CallOn(
+      impl_, &impl::enqueue_command<CommandStatusView>, std::move(command), std::move(on_status));
 }
 
 void HciLayer::RegisterEventHandler(EventCode event, ContextualCallback<void(EventView)> handler) {
diff --git a/system/gd/hci/hci_packets.pdl b/system/gd/hci/hci_packets.pdl
index fa7373a..1960bf5 100644
--- a/system/gd/hci/hci_packets.pdl
+++ b/system/gd/hci/hci_packets.pdl
@@ -3832,6 +3832,10 @@
 }
 
 test LeSetExtendedAdvertisingEnable {
+  "\x39\x20\x06\x01\x01\x01\x00\x00\x00",
+}
+
+test LeSetExtendedAdvertisingDisable {
   "\x39\x20\x06\x00\x01\x01\x00\x00\x00",
 }
 
diff --git a/system/gd/packet/parser/packet_def.cc b/system/gd/packet/parser/packet_def.cc
index 3fe2218..d857eff 100644
--- a/system/gd/packet/parser/packet_def.cc
+++ b/system/gd/packet/parser/packet_def.cc
@@ -1406,8 +1406,8 @@
     }
     for (size_t i = 1; i < lineage.size(); i++) {
       s << "_ => {";
-      s << "println!(\"Couldn't parse " << util::CamelCaseToUnderScore(lineage[lineage.size() - i]->name_);
-      s << "{:02x?}\", " << util::CamelCaseToUnderScore(lineage[lineage.size() - i - 1]->name_) << "_packet); ";
+      s << "panic!(\"Couldn't parse " << util::CamelCaseToUnderScore(lineage[lineage.size() - i]->name_);
+      s << "\n {:#02x?}\", " << util::CamelCaseToUnderScore(lineage[lineage.size() - i - 1]->name_) << "_packet); ";
       s << "}}}";
     }
 
diff --git a/system/gd/packet/parser/parent_def.cc b/system/gd/packet/parser/parent_def.cc
index 0df4a5d..a72a9d7 100644
--- a/system/gd/packet/parser/parent_def.cc
+++ b/system/gd/packet/parser/parent_def.cc
@@ -613,6 +613,8 @@
       FixedScalarField::kFieldType,
   });
 
+  s << "if bytes.len() < " << this->GetSize(false).bytes() << " { return false; }";
+
   for (auto const& field : fields) {
     auto start_offset = GetOffsetForField(field->GetName(), false);
     auto end_offset = GetOffsetForField(field->GetName(), true);
diff --git a/system/gd/packet/parser/test/rust_test_packets.pdl b/system/gd/packet/parser/test/rust_test_packets.pdl
index d791979..38ee025 100644
--- a/system/gd/packet/parser/test/rust_test_packets.pdl
+++ b/system/gd/packet/parser/test/rust_test_packets.pdl
@@ -82,27 +82,27 @@
 }
 
 test AddRes {
-  "\x00\x04\x04\x01\x04\x04",
+  "\x02\x00",
 }
 
 test SubRes {
-  "\x01\x04\x04\x01\x04\x04",
+  "\x03\x00",
 }
 
 test AddCommand {
-  "\x02\x04\x04\x01\x04\x04",
+  "\x04\x00",
 }
 
 test SubCommand {
-  "\x03\x04\x04\x01\x04\x04",
+  "\x05\x00",
 }
 
 test AddErr {
-  "\x04\x04\x04\x01\x04\x04",
+  "\x00\x00",
 }
 
 test SubErr {
-  "\x05\x04\x04\x01\x04\x04",
+  "\x01\x00",
 }
 
 
@@ -148,17 +148,17 @@
 }
 
 test ChildOneTwo {
-  "\x01\x02\x03\x01",
+  "\x01\x02\x03\x01\x01\x02\x03",
 }
 
 test ChildThreeFour {
-  "\x03\x03\x03\x01",
+  "\x03\x04\x03\x01\x03\x04\x03",
 }
 
 test ChildThree {
-  "\x02\x01\x04\x01",
+  "\x01\x04\x03\x04\x03\x00\x02\x05",
 }
 
 test GrandChildThreeFive {
-  "\x01\x02\x03\x01",
+  "\x01\x04\x03\x04\x03\x00\x02\x05",
 }
diff --git a/system/gd/rust/common/BUILD.gn b/system/gd/rust/common/BUILD.gn
index 80d5a23..9a6e8ca 100644
--- a/system/gd/rust/common/BUILD.gn
+++ b/system/gd/rust/common/BUILD.gn
@@ -15,29 +15,32 @@
 
 import("//common-mk/cxxbridge.gni")
 
-rust_library("libbt_common") {
-  crate_name = "bt_common"
-
-  sources = [ "src/lib.rs" ]
-
-  configs = [
-    "//bt/system/gd/rust/shim:rust_libs",
-    "//bt/system/gd:rust_defaults",
+static_library("libbt_keystore_cc") {
+  complete_static_lib = true
+  sources = [ "keystore/fake_bt_keystore.cc" ]
+  deps = [
+    ":libbt_common_bridge_code"
   ]
+
+  configs += ["//bt/system/gd:gd_defaults"]
 }
 
-cxxbridge_cc("libbt_common_sys_prop_bridge_code") {
-  sources = [ "src/sys_prop.rs" ]
+config("rust_common_config") {
+  include_dirs = [ "//bt/system/gd/rust/common" ]
+}
 
+cxxbridge_header("libbt_common_bridge_header") {
+  sources = [ "src/bridge.rs" ]
+  all_dependent_configs = [ ":rust_common_config" ]
+  deps = [ ":cxxlibheader" ]
+}
+
+cxxbridge_cc("libbt_common_bridge_code") {
+  sources = [ "src/bridge.rs" ]
+  deps = [ ":libbt_common_bridge_header" ]
   configs = [ "//bt/system/gd:gd_defaults" ]
 }
 
-static_library("libbt_common_sys_prop_cxx") {
-  sources = [ "src/ffi/sys_props.cc" ]
-
-  include_dirs = [ "src/ffi" ]
-
-  deps = [ ":libbt_common_sys_prop_bridge_code" ]
-
-  configs += [ "//bt/system/gd:gd_defaults" ]
+cxxbridge_libheader("cxxlibheader") {
+  deps = []
 }
diff --git a/system/gd/rust/common/Cargo.toml b/system/gd/rust/common/Cargo.toml
index 22de79a..88b3918 100644
--- a/system/gd/rust/common/Cargo.toml
+++ b/system/gd/rust/common/Cargo.toml
@@ -21,6 +21,7 @@
 [dependencies]
 cxx = "*"
 env_logger = "*"
+futures = "0.3.13"
 grpcio = "*"
 lazy_static = "*"
 log = "*"
diff --git a/system/gd/rust/common/build.rs b/system/gd/rust/common/build.rs
new file mode 100644
index 0000000..73a5adb
--- /dev/null
+++ b/system/gd/rust/common/build.rs
@@ -0,0 +1,12 @@
+fn main() {
+    let target_dir = std::env::var_os("CARGO_TARGET_DIR").unwrap();
+
+    // Link the keystore static lib and make sure to use -lc++
+    println!("cargo:rustc-link-lib=static=bt_keystore_cc");
+    println!("cargo:rustc-link-search=native={}", target_dir.clone().into_string().unwrap());
+    println!("cargo:rustc-link-lib=c++");
+
+    // Re-run if static libs or this file changed.
+    println!("cargo:rerun-if-changed={}", target_dir.into_string().unwrap());
+    println!("cargo:rerun-if-changed=build.rs");
+}
diff --git a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/main.rs b/system/gd/rust/linux/mgmt/src/bin/btmanagerd/main.rs
index f73629e..f37c614 100644
--- a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/main.rs
+++ b/system/gd/rust/linux/mgmt/src/bin/btmanagerd/main.rs
@@ -1,30 +1,21 @@
-mod bluetooth_manager;
-mod bluetooth_manager_dbus;
-mod config_util;
-mod dbus_arg;
-mod dbus_iface;
-mod powerd_suspend_manager;
-mod service_watcher;
-mod state_machine;
+// The manager binary (btmanagerd) is a fairly barebone bin file that depends on the manager_service
+// library which implements most of the logic. The code is separated in this way so that we can
+// apply certain linker flags (which is applied to the library but not the binary).
+// Please keep main.rs logic light and write the heavy logic in the manager_service library instead.
 
-use crate::bluetooth_manager::BluetoothManager;
-use crate::powerd_suspend_manager::PowerdSuspendManager;
 use dbus::channel::MatchingReceiver;
 use dbus::message::MatchRule;
 use dbus_crossroads::Crossroads;
 use dbus_projection::DisconnectWatcher;
 use dbus_tokio::connection;
 use log::LevelFilter;
+use manager_service::bluetooth_manager::{BluetoothManager, ManagerContext};
+use manager_service::powerd_suspend_manager::PowerdSuspendManager;
+use manager_service::{bluetooth_manager_dbus, config_util, state_machine};
 use std::sync::atomic::AtomicBool;
 use std::sync::{Arc, Mutex};
 use syslog::{BasicLogger, Facility, Formatter3164};
 
-#[derive(Clone)]
-struct ManagerContext {
-    proxy: state_machine::StateMachineProxy,
-    floss_enabled: Arc<AtomicBool>,
-}
-
 #[tokio::main]
 pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
     let formatter = Formatter3164 {
@@ -62,10 +53,8 @@
 
     let context = state_machine::start_new_state_machine_context(invoker);
     let proxy = context.get_proxy();
-    let manager_context = ManagerContext {
-        proxy: proxy,
-        floss_enabled: Arc::new(AtomicBool::new(config_util::is_floss_enabled())),
-    };
+    let manager_context =
+        ManagerContext::new(proxy, Arc::new(AtomicBool::new(config_util::is_floss_enabled())));
 
     // The resource is a task that should be spawned onto a tokio compatible
     // reactor ASAP. If the resource ever finishes, you lost connection to D-Bus.
diff --git a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/bluetooth_manager.rs b/system/gd/rust/linux/mgmt/src/bluetooth_manager.rs
similarity index 91%
rename from system/gd/rust/linux/mgmt/src/bin/btmanagerd/bluetooth_manager.rs
rename to system/gd/rust/linux/mgmt/src/bluetooth_manager.rs
index 706fb11..8de6cd0 100644
--- a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/bluetooth_manager.rs
+++ b/system/gd/rust/linux/mgmt/src/bluetooth_manager.rs
@@ -1,17 +1,29 @@
 use log::{error, info, warn};
 
-use manager_service::iface_bluetooth_manager::{
-    AdapterWithEnabled, IBluetoothManager, IBluetoothManagerCallback,
-};
-
 use std::collections::HashMap;
 use std::process::Command;
-use std::sync::atomic::Ordering;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
 
-use crate::{config_util, state_machine, ManagerContext};
+use crate::iface_bluetooth_manager::{
+    AdapterWithEnabled, IBluetoothManager, IBluetoothManagerCallback,
+};
+use crate::{config_util, state_machine};
 
 const BLUEZ_INIT_TARGET: &str = "bluetoothd";
 
+#[derive(Clone)]
+pub struct ManagerContext {
+    proxy: state_machine::StateMachineProxy,
+    floss_enabled: Arc<AtomicBool>,
+}
+
+impl ManagerContext {
+    pub fn new(proxy: state_machine::StateMachineProxy, floss_enabled: Arc<AtomicBool>) -> Self {
+        Self { proxy, floss_enabled }
+    }
+}
+
 /// Implementation of IBluetoothManager.
 pub struct BluetoothManager {
     manager_context: ManagerContext,
@@ -20,7 +32,7 @@
 }
 
 impl BluetoothManager {
-    pub(crate) fn new(manager_context: ManagerContext) -> BluetoothManager {
+    pub fn new(manager_context: ManagerContext) -> BluetoothManager {
         BluetoothManager {
             manager_context,
             callbacks: HashMap::new(),
diff --git a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/bluetooth_manager_dbus.rs b/system/gd/rust/linux/mgmt/src/bluetooth_manager_dbus.rs
similarity index 97%
rename from system/gd/rust/linux/mgmt/src/bin/btmanagerd/bluetooth_manager_dbus.rs
rename to system/gd/rust/linux/mgmt/src/bluetooth_manager_dbus.rs
index 8ee912f..73b3c03 100644
--- a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/bluetooth_manager_dbus.rs
+++ b/system/gd/rust/linux/mgmt/src/bluetooth_manager_dbus.rs
@@ -4,11 +4,11 @@
 use dbus_projection::{dbus_generated, DisconnectWatcher};
 
 use btstack::RPCProxy;
-use manager_service::iface_bluetooth_manager::{
-    AdapterWithEnabled, IBluetoothManager, IBluetoothManagerCallback,
-};
 
 use crate::dbus_arg::{DBusArg, DBusArgError, RefArgToRust};
+use crate::iface_bluetooth_manager::{
+    AdapterWithEnabled, IBluetoothManager, IBluetoothManagerCallback,
+};
 
 #[dbus_propmap(AdapterWithEnabled)]
 pub struct AdapterWithEnabledDbus {
diff --git a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/config_util.rs b/system/gd/rust/linux/mgmt/src/config_util.rs
similarity index 100%
rename from system/gd/rust/linux/mgmt/src/bin/btmanagerd/config_util.rs
rename to system/gd/rust/linux/mgmt/src/config_util.rs
diff --git a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/dbus_arg.rs b/system/gd/rust/linux/mgmt/src/dbus_arg.rs
similarity index 100%
rename from system/gd/rust/linux/mgmt/src/bin/btmanagerd/dbus_arg.rs
rename to system/gd/rust/linux/mgmt/src/dbus_arg.rs
diff --git a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/dbus_iface.rs b/system/gd/rust/linux/mgmt/src/dbus_iface.rs
similarity index 100%
rename from system/gd/rust/linux/mgmt/src/bin/btmanagerd/dbus_iface.rs
rename to system/gd/rust/linux/mgmt/src/dbus_iface.rs
diff --git a/system/gd/rust/linux/mgmt/src/lib.rs b/system/gd/rust/linux/mgmt/src/lib.rs
index 6a091f2..cd761ea 100644
--- a/system/gd/rust/linux/mgmt/src/lib.rs
+++ b/system/gd/rust/linux/mgmt/src/lib.rs
@@ -1,4 +1,12 @@
+pub mod bluetooth_manager;
+pub mod bluetooth_manager_dbus;
+pub mod config_util;
+pub mod dbus_arg;
+pub mod dbus_iface;
 pub mod iface_bluetooth_manager;
+pub mod powerd_suspend_manager;
+pub mod service_watcher;
+pub mod state_machine;
 
 // protoc-rust generates all modules and exports them in mod.rs
 // We have to include them all here to make them available for crate export.
diff --git a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/powerd_suspend_manager.rs b/system/gd/rust/linux/mgmt/src/powerd_suspend_manager.rs
similarity index 99%
rename from system/gd/rust/linux/mgmt/src/bin/btmanagerd/powerd_suspend_manager.rs
rename to system/gd/rust/linux/mgmt/src/powerd_suspend_manager.rs
index 84c9926..fb9faae 100644
--- a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/powerd_suspend_manager.rs
+++ b/system/gd/rust/linux/mgmt/src/powerd_suspend_manager.rs
@@ -5,16 +5,16 @@
 use dbus::nonblock::SyncConnection;
 use dbus_crossroads::Crossroads;
 use dbus_projection::DisconnectWatcher;
-use manager_service::suspend::{
-    RegisterSuspendDelayReply, RegisterSuspendDelayRequest, SuspendDone, SuspendImminent,
-    SuspendImminent_Reason, SuspendReadinessInfo,
-};
 use protobuf::{CodedInputStream, CodedOutputStream, Message};
 use std::sync::{Arc, Mutex};
 use std::time::Duration;
 
 use crate::dbus_iface::{export_suspend_callback_dbus_obj, SuspendDBus};
 use crate::service_watcher::ServiceWatcher;
+use crate::suspend::{
+    RegisterSuspendDelayReply, RegisterSuspendDelayRequest, SuspendDone, SuspendImminent,
+    SuspendImminent_Reason, SuspendReadinessInfo,
+};
 
 const POWERD_SERVICE: &str = "org.chromium.PowerManager";
 const POWERD_INTERFACE: &str = "org.chromium.PowerManager";
diff --git a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/service_watcher.rs b/system/gd/rust/linux/mgmt/src/service_watcher.rs
similarity index 100%
rename from system/gd/rust/linux/mgmt/src/bin/btmanagerd/service_watcher.rs
rename to system/gd/rust/linux/mgmt/src/service_watcher.rs
diff --git a/system/gd/rust/linux/mgmt/src/bin/btmanagerd/state_machine.rs b/system/gd/rust/linux/mgmt/src/state_machine.rs
similarity index 100%
rename from system/gd/rust/linux/mgmt/src/bin/btmanagerd/state_machine.rs
rename to system/gd/rust/linux/mgmt/src/state_machine.rs
diff --git a/system/gd/rust/packets/test_lib.rs b/system/gd/rust/packets/test_lib.rs
index 960178c..db6fbe6 100644
--- a/system/gd/rust/packets/test_lib.rs
+++ b/system/gd/rust/packets/test_lib.rs
@@ -98,4 +98,11 @@
         let res = TestBodySizePacket::parse(&input);
         assert!(res.is_ok());
     }
+
+    #[test]
+    fn test_invalid_grand_child_three_five_size() {
+        let input = [0x1, 0x4, 0x3, 0x4, 0x3, 0x0, 0x2 /*, 0x5*/];
+        let res = GrandParentPacket::parse(&input);
+        assert!(res.is_err());
+    }
 }
diff --git a/system/main/shim/acl.cc b/system/main/shim/acl.cc
index 21678c8..95fce55 100644
--- a/system/main/shim/acl.cc
+++ b/system/main/shim/acl.cc
@@ -653,7 +653,7 @@
       return;
     }
 
-    if (page_number != max_page_number)
+    if (max_page_number != 0 && page_number != max_page_number)
       connection_->ReadRemoteExtendedFeatures(page_number + 1);
   }
 
diff --git a/system/main/test/main_shim_test.cc b/system/main/test/main_shim_test.cc
index a701ea6..5de3b7a 100644
--- a/system/main/test/main_shim_test.cc
+++ b/system/main/test/main_shim_test.cc
@@ -128,6 +128,10 @@
   mock_connection_le_on_disconnected_promise.set_value(handle);
 }
 
+void mock_link_classic_on_read_remote_extended_features_complete(
+    uint16_t handle, uint8_t current_page_number, uint8_t max_page_number,
+    uint64_t features) {}
+
 const shim::legacy::acl_interface_t GetMockAclInterface() {
   shim::legacy::acl_interface_t acl_interface{
       .on_send_data_upwards = mock_on_send_data_upwards,
@@ -165,7 +169,8 @@
       .link.classic.on_read_link_quality_complete = nullptr,
       .link.classic.on_read_link_supervision_timeout_complete = nullptr,
       .link.classic.on_read_remote_version_information_complete = nullptr,
-      .link.classic.on_read_remote_extended_features_complete = nullptr,
+      .link.classic.on_read_remote_extended_features_complete =
+          mock_link_classic_on_read_remote_extended_features_complete,
       .link.classic.on_read_rssi_complete = nullptr,
       .link.classic.on_read_transmit_power_level_complete = nullptr,
       .link.classic.on_role_change = nullptr,
@@ -235,6 +240,15 @@
   bool ReadRemoteVersionInformation() override { return true; }
   bool ReadRemoteSupportedFeatures() override { return true; }
 
+  std::function<void(uint8_t)> read_remote_extended_features_function_{};
+
+  bool ReadRemoteExtendedFeatures(uint8_t page_number) override {
+    if (read_remote_extended_features_function_) {
+      read_remote_extended_features_function_(page_number);
+    }
+    return true;
+  }
+
   bool Disconnect(hci::DisconnectReason reason) override {
     disconnect_cnt_++;
     disconnect_promise_.set_value(handle_);
@@ -280,6 +294,7 @@
   MockDeQueue<packet::PacketView<hci::kLittleEndian>> rx_;
 
   bool ReadRemoteVersionInformation() override { return true; }
+  bool LeReadRemoteFeatures() override { return true; }
 
   void Disconnect(hci::DisconnectReason reason) override {
     disconnect_cnt_++;
@@ -379,6 +394,65 @@
   }
 };
 
+class MainShimTestWithClassicConnection : public MainShimTest {
+ protected:
+  void SetUp() override {
+    MainShimTest::SetUp();
+    hci::Address address({0x11, 0x22, 0x33, 0x44, 0x55, 0x66});
+
+    acl_ = MakeAcl();
+
+    // Create connection
+    EXPECT_CALL(*test::mock_acl_manager_, CreateConnection(_)).Times(1);
+    acl_->CreateClassicConnection(address);
+
+    // Respond with a mock connection created
+    auto connection = std::make_unique<MockClassicAclConnection>(address, 123);
+    ASSERT_EQ(123, connection->GetHandle());
+    ASSERT_EQ(hci::Address({0x11, 0x22, 0x33, 0x44, 0x55, 0x66}),
+              connection->GetAddress());
+    raw_connection_ = connection.get();
+
+    acl_->OnConnectSuccess(std::move(connection));
+    ASSERT_EQ(nullptr, connection);
+    ASSERT_NE(nullptr, raw_connection_->callbacks_);
+  }
+
+  void TearDown() override {
+    // Specify local disconnect request
+    auto tx_disconnect_future =
+        raw_connection_->disconnect_promise_.get_future();
+    acl_->DisconnectClassic(123, HCI_SUCCESS, {});
+
+    // Wait for disconnect to be received
+    uint16_t result = tx_disconnect_future.get();
+    ASSERT_EQ(123, result);
+
+    // Now emulate the remote disconnect response
+    auto handle_promise = std::promise<uint16_t>();
+    auto rx_disconnect_future = handle_promise.get_future();
+    mock_function_handle_promise_map
+        ["mock_connection_classic_on_disconnected"] = std::move(handle_promise);
+    raw_connection_->callbacks_->OnDisconnection(hci::ErrorCode::SUCCESS);
+
+    result = rx_disconnect_future.get();
+    ASSERT_EQ(123, result);
+
+    // *Our* task completing indicates reactor is done
+    std::promise<void> done;
+    auto future = done.get_future();
+    handler_->Call([](std::promise<void> done) { done.set_value(); },
+                   std::move(done));
+    future.wait();
+
+    acl_.reset();
+
+    MainShimTest::TearDown();
+  }
+  std::unique_ptr<shim::legacy::Acl> acl_;
+  MockClassicAclConnection* raw_connection_{nullptr};
+};
+
 TEST_F(MainShimTest, Nop) {}
 
 TEST_F(MainShimTest, Acl_Lifecycle) {
@@ -512,7 +586,7 @@
   void OnBatchScanThresholdCrossed(int client_if) override {}
 };
 
-TEST_F(MainShimTest, BleScannerInterfaceImpl_OnScanResult) {
+TEST_F(MainShimTest, DISABLED_BleScannerInterfaceImpl_OnScanResult) {
   auto* ble = static_cast<bluetooth::shim::BleScannerInterfaceImpl*>(
       bluetooth::shim::get_ble_scanner_instance());
 
@@ -554,7 +628,7 @@
     nullptr,
 };
 
-TEST_F(MainShimTest, LeShimAclConnection_local_disconnect) {
+TEST_F(MainShimTest, DISABLED_LeShimAclConnection_local_disconnect) {
   bluetooth::common::InitFlags::Load(test_flags);
   auto acl = MakeAcl();
   EXPECT_CALL(*test::mock_acl_manager_, CreateLeConnection(_, _)).Times(1);
@@ -579,6 +653,8 @@
                                                           remote_address, role);
   auto raw_connection = connection.get();
   acl->OnLeConnectSuccess(remote_address, std::move(connection));
+  ASSERT_EQ(nullptr, connection);
+  ASSERT_NE(nullptr, raw_connection->callbacks_);
 
   // Initiate local LE disconnect
   mock_connection_le_on_disconnected_promise = std::promise<uint16_t>();
@@ -595,3 +671,50 @@
 
   ASSERT_EQ(0x1234, disconnect_future.get());
 }
+
+TEST_F(MainShimTestWithClassicConnection, nop) {}
+
+TEST_F(MainShimTestWithClassicConnection, read_extended_feature) {
+  int read_remote_extended_feature_call_count = 0;
+  raw_connection_->read_remote_extended_features_function_ =
+      [&read_remote_extended_feature_call_count](uint8_t page_number) {
+        read_remote_extended_feature_call_count++;
+      };
+
+  // Handle typical case
+  {
+    read_remote_extended_feature_call_count = 0;
+    const uint8_t max_page = 3;
+    raw_connection_->callbacks_->OnReadRemoteExtendedFeaturesComplete(
+        1, max_page, 0xabcdef9876543210);
+    raw_connection_->callbacks_->OnReadRemoteExtendedFeaturesComplete(
+        2, max_page, 0xbcdef9876543210a);
+    raw_connection_->callbacks_->OnReadRemoteExtendedFeaturesComplete(
+        3, max_page, 0xcdef9876543210ab);
+    ASSERT_EQ(static_cast<int>(max_page) - 1,
+              read_remote_extended_feature_call_count);
+  }
+
+  // Handle extreme case
+  {
+    read_remote_extended_feature_call_count = 0;
+    const uint8_t max_page = 255;
+    for (int page = 1; page < static_cast<int>(max_page) + 1; page++) {
+      raw_connection_->callbacks_->OnReadRemoteExtendedFeaturesComplete(
+          static_cast<uint8_t>(page), max_page, 0xabcdef9876543210);
+    }
+    ASSERT_EQ(static_cast<int>(max_page - 1),
+              read_remote_extended_feature_call_count);
+  }
+
+  // Handle case where device returns max page of zero
+  {
+    read_remote_extended_feature_call_count = 0;
+    const uint8_t max_page = 0;
+    raw_connection_->callbacks_->OnReadRemoteExtendedFeaturesComplete(
+        1, max_page, 0xabcdef9876543210);
+    ASSERT_EQ(0, read_remote_extended_feature_call_count);
+  }
+
+  raw_connection_->read_remote_extended_features_function_ = {};
+}
diff --git a/system/stack/Android.bp b/system/stack/Android.bp
index a244f14..f132f28 100644
--- a/system/stack/Android.bp
+++ b/system/stack/Android.bp
@@ -1142,3 +1142,70 @@
         },
     },
 }
+
+cc_test {
+    name: "net_test_stack_acl",
+    test_suites: ["device-tests"],
+    host_supported: true,
+    defaults: ["fluoride_defaults"],
+    local_include_dirs: [
+        "include",
+        "test/common",
+    ],
+    include_dirs: [
+        "packages/modules/Bluetooth/system",
+        "packages/modules/Bluetooth/system/gd",
+        "packages/modules/Bluetooth/system/utils/include",
+    ],
+    generated_headers: [
+        "BluetoothGeneratedDumpsysDataSchema_h",
+        "BluetoothGeneratedPackets_h",
+    ],
+    srcs: [
+        ":OsiCompatSources",
+        ":TestCommonMainHandler",
+        ":TestCommonMockFunctions",
+        ":TestCommonStackConfig",
+        ":TestMockBta",
+        ":TestMockBtif",
+        ":TestMockDevice",
+        ":TestMockHci",
+        ":TestMockLegacyHciCommands",
+        ":TestMockLegacyHciInterface",
+        ":TestMockMainShim",
+        ":TestMockStackBtm",
+        ":TestMockStackBtu",
+        ":TestMockStackCryptotoolbox",
+        ":TestMockStackGatt",
+        ":TestMockStackHcic",
+        ":TestMockStackL2cap",
+        ":TestMockStackMetrics",
+        ":TestMockStackSdp",
+        ":TestMockStackSmp",
+        "acl/*.cc",
+        "test/stack_acl_test.cc",
+    ],
+    static_libs: [
+        "libbt-common",
+        "libbt-protos-lite",
+        "libgmock",
+        "liblog",
+        "libosi",
+    ],
+    shared_libs: [
+        "libbinder_ndk",
+        "libcrypto",
+        "libflatbuffers-cpp",
+        "libprotobuf-cpp-lite",
+    ],
+    sanitize: {
+        address: true,
+        all_undefined: true,
+        cfi: true,
+        integer_overflow: true,
+        scs: true,
+        diag: {
+            undefined : true
+        },
+    },
+}
diff --git a/system/stack/acl/btm_acl.cc b/system/stack/acl/btm_acl.cc
index df6ec16..5632151 100644
--- a/system/stack/acl/btm_acl.cc
+++ b/system/stack/acl/btm_acl.cc
@@ -2761,21 +2761,21 @@
 }
 
 bool acl_create_le_connection_with_id(uint8_t id, const RawAddress& bd_addr) {
-    tBLE_BD_ADDR address_with_type{
-        .bda = bd_addr,
-        .type = BLE_ADDR_RANDOM,
-    };
-    gatt_find_in_device_record(bd_addr, &address_with_type);
-    LOG_DEBUG("Creating le direct connection to:%s",
-              PRIVATE_ADDRESS(address_with_type));
+  tBLE_BD_ADDR address_with_type{
+      .bda = bd_addr,
+      .type = BLE_ADDR_PUBLIC,
+  };
+  gatt_find_in_device_record(bd_addr, &address_with_type);
+  LOG_DEBUG("Creating le direct connection to:%s",
+            PRIVATE_ADDRESS(address_with_type));
 
-    if (address_with_type.type == BLE_ADDR_ANONYMOUS) {
-      LOG_WARN(
-          "Creating le direct connection to:%s, address type 'anonymous' is "
-          "invalid",
-          PRIVATE_ADDRESS(address_with_type));
-      return false;
-    }
+  if (address_with_type.type == BLE_ADDR_ANONYMOUS) {
+    LOG_WARN(
+        "Creating le direct connection to:%s, address type 'anonymous' is "
+        "invalid",
+        PRIVATE_ADDRESS(address_with_type));
+    return false;
+  }
 
     bluetooth::shim::ACL_AcceptLeConnectionFrom(address_with_type,
                                                 /* is_direct */ true);
@@ -2897,7 +2897,7 @@
       bd_features_text(p_acl->peer_lmp_feature_pages[current_page_number])
           .c_str());
 
-  if (max_page_number == current_page_number) {
+  if (max_page_number == 0 || max_page_number == current_page_number) {
     NotifyAclFeaturesReadComplete(*p_acl, max_page_number);
   }
 }
diff --git a/system/stack/test/stack_acl_test.cc b/system/stack/test/stack_acl_test.cc
new file mode 100644
index 0000000..6b741e2
--- /dev/null
+++ b/system/stack/test/stack_acl_test.cc
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 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.
+ */
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <cstdint>
+#include <deque>
+
+#include "common/init_flags.h"
+#include "osi/include/log.h"
+#include "stack/acl/acl.h"
+#include "stack/btm/btm_int_types.h"
+#include "stack/btm/security_device_record.h"
+#include "stack/include/acl_api.h"
+#include "stack/include/acl_hci_link_interface.h"
+#include "stack/include/hci_error_code.h"
+#include "test/common/mock_functions.h"
+#include "test/mock/mock_main_shim_acl_api.h"
+#include "types/ble_address_with_type.h"
+#include "types/hci_role.h"
+#include "types/raw_address.h"
+
+tBTM_CB btm_cb;
+
+void LogMsg(uint32_t trace_set_mask, const char* fmt_str, ...) {}
+
+namespace {
+const char* test_flags[] = {
+    "INIT_logging_debug_enabled_for_all=true",
+    nullptr,
+};
+
+const RawAddress kRawAddress = RawAddress({0x11, 0x22, 0x33, 0x44, 0x55, 0x66});
+}  // namespace
+
+namespace bluetooth {
+namespace testing {
+
+std::set<const RawAddress> copy_of_connected_with_both_public_and_random_set();
+
+}  // namespace testing
+}  // namespace bluetooth
+
+void BTM_update_version_info(const RawAddress& bd_addr,
+                             const remote_version_info& remote_version_info) {}
+
+void btm_sec_role_changed(tHCI_STATUS hci_status, const RawAddress& bd_addr,
+                          tHCI_ROLE new_role) {}
+
+class StackAclTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    mock_function_count_map.clear();
+    bluetooth::common::InitFlags::Load(test_flags);
+  }
+  void TearDown() override {}
+
+  tBTM_SEC_DEV_REC device_record_;
+};
+
+TEST_F(StackAclTest, nop) {}
+
+TEST_F(StackAclTest, acl_process_extended_features) {
+  const uint16_t hci_handle = 0x123;
+  const tBT_TRANSPORT transport = BT_TRANSPORT_LE;
+  const tHCI_ROLE link_role = HCI_ROLE_CENTRAL;
+
+  btm_acl_created(kRawAddress, hci_handle, link_role, transport);
+  tACL_CONN* p_acl = btm_acl_for_bda(kRawAddress, transport);
+  ASSERT_NE(nullptr, p_acl);
+
+  // Handle typical case
+  {
+    const uint8_t max_page = 3;
+    memset((void*)p_acl->peer_lmp_feature_valid, 0,
+           HCI_EXT_FEATURES_PAGE_MAX + 1);
+    acl_process_extended_features(hci_handle, 1, max_page, 0xf123456789abcde);
+    acl_process_extended_features(hci_handle, 2, max_page, 0xef123456789abcd);
+    acl_process_extended_features(hci_handle, 3, max_page, 0xdef123456789abc);
+
+    /* page 0 is the standard feature set */
+    ASSERT_FALSE(p_acl->peer_lmp_feature_valid[0]);
+    ASSERT_TRUE(p_acl->peer_lmp_feature_valid[1]);
+    ASSERT_TRUE(p_acl->peer_lmp_feature_valid[2]);
+    ASSERT_TRUE(p_acl->peer_lmp_feature_valid[3]);
+  }
+
+  // Handle extreme case
+  {
+    const uint8_t max_page = 255;
+    memset((void*)p_acl->peer_lmp_feature_valid, 0,
+           HCI_EXT_FEATURES_PAGE_MAX + 1);
+    for (int i = 1; i < HCI_EXT_FEATURES_PAGE_MAX + 1; i++) {
+      acl_process_extended_features(hci_handle, static_cast<uint8_t>(i),
+                                    max_page, 0x123456789abcdef);
+    }
+    /* page 0 is the standard feature set */
+    ASSERT_FALSE(p_acl->peer_lmp_feature_valid[0]);
+    ASSERT_TRUE(p_acl->peer_lmp_feature_valid[1]);
+    ASSERT_TRUE(p_acl->peer_lmp_feature_valid[2]);
+    ASSERT_TRUE(p_acl->peer_lmp_feature_valid[3]);
+  }
+
+  // Handle case where device returns max page of zero
+  {
+    memset((void*)p_acl->peer_lmp_feature_valid, 0,
+           HCI_EXT_FEATURES_PAGE_MAX + 1);
+    acl_process_extended_features(hci_handle, 1, 0, 0xdef123456789abc);
+    ASSERT_FALSE(p_acl->peer_lmp_feature_valid[0]);
+    ASSERT_TRUE(p_acl->peer_lmp_feature_valid[1]);
+    ASSERT_FALSE(p_acl->peer_lmp_feature_valid[2]);
+    ASSERT_FALSE(p_acl->peer_lmp_feature_valid[3]);
+  }
+
+  btm_acl_removed(hci_handle);
+}
diff --git a/system/test/mock/mock_main_shim_acl_api.h b/system/test/mock/mock_main_shim_acl_api.h
new file mode 100644
index 0000000..cb08047
--- /dev/null
+++ b/system/test/mock/mock_main_shim_acl_api.h
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2022 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.
+ */
+#pragma once
+
+/*
+ * Generated mock file from original source file
+ *   Functions generated:14
+ *
+ *  mockcify.pl ver 0.5.0
+ */
+
+#include <cstdint>
+#include <functional>
+#include <map>
+#include <string>
+
+extern std::map<std::string, int> mock_function_count_map;
+
+// Original included files, if any
+// NOTE: Since this is a mock file with mock definitions some number of
+//       include files may not be required.  The include-what-you-use
+//       still applies, but crafting proper inclusion is out of scope
+//       for this effort.  This compilation unit may compile as-is, or
+//       may need attention to prune from (or add to ) the inclusion set.
+#include <cstddef>
+#include <cstdint>
+#include <future>
+
+#include "gd/hci/acl_manager.h"
+#include "main/shim/acl_api.h"
+#include "main/shim/dumpsys.h"
+#include "main/shim/helpers.h"
+#include "main/shim/stack.h"
+#include "osi/include/allocator.h"
+#include "stack/include/bt_hdr.h"
+#include "types/ble_address_with_type.h"
+#include "types/raw_address.h"
+
+// Original usings
+
+// Mocked compile conditionals, if any
+
+namespace test {
+namespace mock {
+namespace main_shim_acl_api {
+
+// Shared state between mocked functions and tests
+// Name: ACL_AcceptLeConnectionFrom
+// Params: const tBLE_BD_ADDR& legacy_address_with_type, bool is_direct
+// Return: bool
+struct ACL_AcceptLeConnectionFrom {
+  static bool return_value;
+  std::function<bool(const tBLE_BD_ADDR& legacy_address_with_type,
+                     bool is_direct)>
+      body{[](const tBLE_BD_ADDR& legacy_address_with_type, bool is_direct) {
+        return return_value;
+      }};
+  bool operator()(const tBLE_BD_ADDR& legacy_address_with_type,
+                  bool is_direct) {
+    return body(legacy_address_with_type, is_direct);
+  };
+};
+extern struct ACL_AcceptLeConnectionFrom ACL_AcceptLeConnectionFrom;
+
+// Name: ACL_AddToAddressResolution
+// Params: const tBLE_BD_ADDR& legacy_address_with_type, const Octet16&
+// peer_irk, const Octet16& local_irk Return: void
+struct ACL_AddToAddressResolution {
+  std::function<void(const tBLE_BD_ADDR& legacy_address_with_type,
+                     const Octet16& peer_irk, const Octet16& local_irk)>
+      body{[](const tBLE_BD_ADDR& legacy_address_with_type,
+              const Octet16& peer_irk, const Octet16& local_irk) {}};
+  void operator()(const tBLE_BD_ADDR& legacy_address_with_type,
+                  const Octet16& peer_irk, const Octet16& local_irk) {
+    body(legacy_address_with_type, peer_irk, local_irk);
+  };
+};
+extern struct ACL_AddToAddressResolution ACL_AddToAddressResolution;
+
+// Name: ACL_CancelClassicConnection
+// Params: const RawAddress& raw_address
+// Return: void
+struct ACL_CancelClassicConnection {
+  std::function<void(const RawAddress& raw_address)> body{
+      [](const RawAddress& raw_address) {}};
+  void operator()(const RawAddress& raw_address) { body(raw_address); };
+};
+extern struct ACL_CancelClassicConnection ACL_CancelClassicConnection;
+
+// Name: ACL_ClearAddressResolution
+// Params:
+// Return: void
+struct ACL_ClearAddressResolution {
+  std::function<void()> body{[]() {}};
+  void operator()() { body(); };
+};
+extern struct ACL_ClearAddressResolution ACL_ClearAddressResolution;
+
+// Name: ACL_ClearFilterAcceptList
+// Params:
+// Return: void
+struct ACL_ClearFilterAcceptList {
+  std::function<void()> body{[]() {}};
+  void operator()() { body(); };
+};
+extern struct ACL_ClearFilterAcceptList ACL_ClearFilterAcceptList;
+
+// Name: ACL_ConfigureLePrivacy
+// Params: bool is_le_privacy_enabled
+// Return: void
+struct ACL_ConfigureLePrivacy {
+  std::function<void(bool is_le_privacy_enabled)> body{
+      [](bool is_le_privacy_enabled) {}};
+  void operator()(bool is_le_privacy_enabled) { body(is_le_privacy_enabled); };
+};
+extern struct ACL_ConfigureLePrivacy ACL_ConfigureLePrivacy;
+
+// Name: ACL_CreateClassicConnection
+// Params: const RawAddress& raw_address
+// Return: void
+struct ACL_CreateClassicConnection {
+  std::function<void(const RawAddress& raw_address)> body{
+      [](const RawAddress& raw_address) {}};
+  void operator()(const RawAddress& raw_address) { body(raw_address); };
+};
+extern struct ACL_CreateClassicConnection ACL_CreateClassicConnection;
+
+// Name: ACL_Disconnect
+// Params: uint16_t handle, bool is_classic, tHCI_STATUS reason, std::string
+// comment Return: void
+struct ACL_Disconnect {
+  std::function<void(uint16_t handle, bool is_classic, tHCI_STATUS reason,
+                     std::string comment)>
+      body{[](uint16_t handle, bool is_classic, tHCI_STATUS reason,
+              std::string comment) {}};
+  void operator()(uint16_t handle, bool is_classic, tHCI_STATUS reason,
+                  std::string comment) {
+    body(handle, is_classic, reason, comment);
+  };
+};
+extern struct ACL_Disconnect ACL_Disconnect;
+
+// Name: ACL_IgnoreAllLeConnections
+// Params:
+// Return: void
+struct ACL_IgnoreAllLeConnections {
+  std::function<void()> body{[]() {}};
+  void operator()() { body(); };
+};
+extern struct ACL_IgnoreAllLeConnections ACL_IgnoreAllLeConnections;
+
+// Name: ACL_IgnoreLeConnectionFrom
+// Params: const tBLE_BD_ADDR& legacy_address_with_type
+// Return: void
+struct ACL_IgnoreLeConnectionFrom {
+  std::function<void(const tBLE_BD_ADDR& legacy_address_with_type)> body{
+      [](const tBLE_BD_ADDR& legacy_address_with_type) {}};
+  void operator()(const tBLE_BD_ADDR& legacy_address_with_type) {
+    body(legacy_address_with_type);
+  };
+};
+extern struct ACL_IgnoreLeConnectionFrom ACL_IgnoreLeConnectionFrom;
+
+// Name: ACL_ReadConnectionAddress
+// Params: const RawAddress& pseudo_addr, RawAddress& conn_addr, tBLE_ADDR_TYPE*
+// p_addr_type Return: void
+struct ACL_ReadConnectionAddress {
+  std::function<void(const RawAddress& pseudo_addr, RawAddress& conn_addr,
+                     tBLE_ADDR_TYPE* p_addr_type)>
+      body{[](const RawAddress& pseudo_addr, RawAddress& conn_addr,
+              tBLE_ADDR_TYPE* p_addr_type) {}};
+  void operator()(const RawAddress& pseudo_addr, RawAddress& conn_addr,
+                  tBLE_ADDR_TYPE* p_addr_type) {
+    body(pseudo_addr, conn_addr, p_addr_type);
+  };
+};
+extern struct ACL_ReadConnectionAddress ACL_ReadConnectionAddress;
+
+// Name: ACL_RemoveFromAddressResolution
+// Params: const tBLE_BD_ADDR& legacy_address_with_type
+// Return: void
+struct ACL_RemoveFromAddressResolution {
+  std::function<void(const tBLE_BD_ADDR& legacy_address_with_type)> body{
+      [](const tBLE_BD_ADDR& legacy_address_with_type) {}};
+  void operator()(const tBLE_BD_ADDR& legacy_address_with_type) {
+    body(legacy_address_with_type);
+  };
+};
+extern struct ACL_RemoveFromAddressResolution ACL_RemoveFromAddressResolution;
+
+// Name: ACL_Shutdown
+// Params:
+// Return: void
+struct ACL_Shutdown {
+  std::function<void()> body{[]() {}};
+  void operator()() { body(); };
+};
+extern struct ACL_Shutdown ACL_Shutdown;
+
+// Name: ACL_WriteData
+// Params: uint16_t handle, BT_HDR* p_buf
+// Return: void
+struct ACL_WriteData {
+  std::function<void(uint16_t handle, BT_HDR* p_buf)> body{
+      [](uint16_t handle, BT_HDR* p_buf) {}};
+  void operator()(uint16_t handle, BT_HDR* p_buf) { body(handle, p_buf); };
+};
+extern struct ACL_WriteData ACL_WriteData;
+
+}  // namespace main_shim_acl_api
+}  // namespace mock
+}  // namespace test
+
+// END mockcify generation
diff --git a/system/types/ble_address_with_type.h b/system/types/ble_address_with_type.h
index ecd62a8..38a52cc 100644
--- a/system/types/ble_address_with_type.h
+++ b/system/types/ble_address_with_type.h
@@ -119,5 +119,24 @@
   std::string ToString() const {
     return std::string(bda.ToString() + "[" + AddressTypeText(type) + "]");
   }
+  bool operator==(const tBLE_BD_ADDR rhs) const {
+    return rhs.type == type && rhs.bda == bda;
+  }
+  bool operator!=(const tBLE_BD_ADDR rhs) const { return !(*this == rhs); }
 };
+
+template <>
+struct std::hash<tBLE_BD_ADDR> {
+  std::size_t operator()(const tBLE_BD_ADDR& val) const {
+    static_assert(sizeof(uint64_t) >=
+                  (RawAddress::kLength + sizeof(tBLE_ADDR_TYPE)));
+    uint64_t int_addr = 0;
+    memcpy(reinterpret_cast<uint8_t*>(&int_addr), val.bda.address,
+           RawAddress::kLength);
+    memcpy(reinterpret_cast<uint8_t*>(&int_addr) + RawAddress::kLength,
+           (const void*)&val.type, sizeof(tBLE_ADDR_TYPE));
+    return std::hash<uint64_t>{}(int_addr);
+  }
+};
+
 #endif
diff --git a/tools/pdl/Android.bp b/tools/pdl/Android.bp
index b1cbcd4..e4d05fd 100644
--- a/tools/pdl/Android.bp
+++ b/tools/pdl/Android.bp
@@ -1,4 +1,3 @@
-
 package {
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
@@ -17,6 +16,11 @@
         "libserde_json",
         "libstructopt",
         "libcodespan_reporting",
+        "libquote",
+        "libsyn",
+        "libproc_macro2",
+        "libanyhow",
+        "libtempfile",
     ],
     proc_macros: [
         "libpest_derive",
@@ -32,4 +36,8 @@
     name: "pdl_inline_tests",
     defaults: ["pdl_defaults"],
     test_suites: ["general-tests"],
+    data: [
+        "rustfmt",
+        "rustfmt.toml",
+    ],
 }
diff --git a/system/gd/packet/parser/doc/reference.md b/tools/pdl/doc/reference.md
similarity index 100%
rename from system/gd/packet/parser/doc/reference.md
rename to tools/pdl/doc/reference.md
diff --git a/tools/pdl/rustfmt b/tools/pdl/rustfmt
new file mode 120000
index 0000000..a390621
--- /dev/null
+++ b/tools/pdl/rustfmt
@@ -0,0 +1 @@
+../../../../../prebuilts/rust/linux-x86/stable/rustfmt
\ No newline at end of file
diff --git a/tools/pdl/rustfmt.toml b/tools/pdl/rustfmt.toml
new file mode 120000
index 0000000..760eb84
--- /dev/null
+++ b/tools/pdl/rustfmt.toml
@@ -0,0 +1 @@
+../../rustfmt.toml
\ No newline at end of file
diff --git a/tools/pdl/src/generator.rs b/tools/pdl/src/generator.rs
new file mode 100644
index 0000000..a6de22b
--- /dev/null
+++ b/tools/pdl/src/generator.rs
@@ -0,0 +1,640 @@
+use crate::ast;
+use anyhow::{anyhow, bail, Context, Result};
+use quote::{format_ident, quote};
+use std::collections::HashMap;
+use std::path::Path;
+use syn::parse_quote;
+
+/// Generate a block of code.
+///
+/// Like `quote!`, but the code block will be followed by an empty
+/// line of code. This makes the generated code more readable.
+macro_rules! quote_block {
+    ($($tt:tt)*) => {
+        format!("{}\n\n", quote!($($tt)*))
+    }
+}
+
+/// Generate the file preamble.
+fn generate_preamble(path: &Path) -> Result<String> {
+    let mut code = String::new();
+    let filename = path
+        .file_name()
+        .and_then(|path| path.to_str())
+        .ok_or_else(|| anyhow!("could not find filename in {:?}", path))?;
+    code.push_str(&format!("// @generated rust packets from {filename}\n\n"));
+
+    code.push_str(&quote_block! {
+        use bytes::{BufMut, Bytes, BytesMut};
+        use num_derive::{FromPrimitive, ToPrimitive};
+        use num_traits::{FromPrimitive, ToPrimitive};
+        use std::convert::{TryFrom, TryInto};
+        use std::fmt;
+        use std::sync::Arc;
+        use thiserror::Error;
+    });
+
+    code.push_str(&quote_block! {
+        type Result<T> = std::result::Result<T, Error>;
+    });
+
+    code.push_str(&quote_block! {
+        #[derive(Debug, Error)]
+        pub enum Error {
+            #[error("Packet parsing failed")]
+            InvalidPacketError,
+            #[error("{field} was {value:x}, which is not known")]
+            ConstraintOutOfBounds { field: String, value: u64 },
+            #[error("when parsing {obj}.{field} needed length of {wanted} but got {got}")]
+            InvalidLengthError {
+                obj: String,
+                field: String,
+                wanted: usize,
+                got: usize,
+            },
+            #[error("Due to size restrictions a struct could not be parsed.")]
+            ImpossibleStructError,
+            #[error("when parsing field {obj}.{field}, {value} is not a valid {type_} value")]
+            InvalidEnumValueError {
+                obj: String,
+                field: String,
+                value: u64,
+                type_: String,
+            },
+        }
+    });
+
+    code.push_str(&quote_block! {
+        #[derive(Debug, Error)]
+        #[error("{0}")]
+        pub struct TryFromError(&'static str);
+    });
+
+    code.push_str(&quote_block! {
+        pub trait Packet {
+            fn to_bytes(self) -> Bytes;
+            fn to_vec(self) -> Vec<u8>;
+        }
+    });
+
+    Ok(code)
+}
+
+/// Round up the bit width to a Rust integer size.
+fn round_bit_width(width: usize) -> Result<usize> {
+    match width {
+        8 => Ok(8),
+        16 => Ok(16),
+        24 | 32 => Ok(32),
+        40 | 48 | 56 | 64 => Ok(64),
+        _ => bail!("unsupported field width: {width}"),
+    }
+}
+
+/// Generate a Rust unsigned integer type large enough to hold
+/// integers of the given bit width.
+fn type_for_width(width: usize) -> Result<syn::Type> {
+    let rounded_width = round_bit_width(width)?;
+    syn::parse_str(&format!("u{rounded_width}")).map_err(anyhow::Error::from)
+}
+
+fn generate_field(
+    field: &ast::Field,
+    visibility: syn::Visibility,
+) -> Result<proc_macro2::TokenStream> {
+    match field {
+        ast::Field::Scalar { id, width, .. } => {
+            let field_name = format_ident!("{id}");
+            let field_type = type_for_width(*width)?;
+            Ok(quote! {
+                #visibility #field_name: #field_type
+            })
+        }
+        _ => todo!("unsupported field: {:?}", field),
+    }
+}
+
+fn generate_field_getter(
+    packet_name: &syn::Ident,
+    field: &ast::Field,
+) -> Result<proc_macro2::TokenStream> {
+    match field {
+        ast::Field::Scalar { id, width, .. } => {
+            // TODO(mgeisler): refactor with generate_field above.
+            let getter_name = format_ident!("get_{id}");
+            let field_name = format_ident!("{id}");
+            let field_type = type_for_width(*width)?;
+            Ok(quote! {
+                pub fn #getter_name(&self) -> #field_type {
+                    self.#packet_name.as_ref().#field_name
+                }
+            })
+        }
+        _ => todo!("unsupported field: {:?}", field),
+    }
+}
+
+fn generate_field_parser(
+    endianness_value: &ast::EndiannessValue,
+    packet_name: &str,
+    field: &ast::Field,
+    offset: usize,
+) -> Result<proc_macro2::TokenStream> {
+    match field {
+        ast::Field::Scalar { id, width, .. } => {
+            let field_name = format_ident!("{id}");
+            let type_width = round_bit_width(*width)?;
+            let field_type = type_for_width(*width)?;
+
+            let getter = match endianness_value {
+                ast::EndiannessValue::BigEndian => format_ident!("from_be_bytes"),
+                ast::EndiannessValue::LittleEndian => format_ident!("from_le_bytes"),
+            };
+
+            let wanted_len = syn::Index::from(offset + width / 8);
+            let indices = (offset..offset + width / 8).map(syn::Index::from);
+            let padding = vec![syn::Index::from(0); (type_width - width) / 8];
+            let mask = if *width != type_width {
+                Some(quote! {
+                    let #field_name = #field_name & 0xfff;
+                })
+            } else {
+                None
+            };
+
+            Ok(quote! {
+                // TODO(mgeisler): call a function instead to avoid
+                // generating so much code for this.
+                if bytes.len() < #wanted_len {
+                    return Err(Error::InvalidLengthError {
+                        obj: #packet_name.to_string(),
+                        field: #id.to_string(),
+                        wanted: #wanted_len,
+                        got: bytes.len(),
+                    });
+                }
+                let #field_name = #field_type::#getter([#(bytes[#indices]),* #(, #padding)*]);
+                #mask
+            })
+        }
+        _ => todo!("unsupported field: {:?}", field),
+    }
+}
+
+fn generate_field_writer(
+    grammar: &ast::Grammar,
+    field: &ast::Field,
+    offset: usize,
+) -> Result<proc_macro2::TokenStream> {
+    match field {
+        ast::Field::Scalar { id, width, .. } => {
+            let field_name = format_ident!("{id}");
+            let bit_width = round_bit_width(*width)?;
+            let start = syn::Index::from(offset);
+            let end = syn::Index::from(offset + bit_width / 8);
+            let byte_width = syn::Index::from(bit_width / 8);
+            let writer = match grammar.endianness.value {
+                ast::EndiannessValue::BigEndian => format_ident!("to_be_bytes"),
+                ast::EndiannessValue::LittleEndian => format_ident!("to_le_bytes"),
+            };
+            Ok(quote! {
+                let #field_name = self.#field_name;
+                buffer[#start..#end].copy_from_slice(&#field_name.#writer()[0..#byte_width]);
+            })
+        }
+        _ => todo!("unsupported field: {:?}", field),
+    }
+}
+
+fn get_field_size(field: &ast::Field) -> usize {
+    match field {
+        ast::Field::Scalar { width, .. } => width / 8,
+        _ => todo!("unsupported field: {:?}", field),
+    }
+}
+
+/// Generate code for an `ast::Decl::Packet` enum value.
+fn generate_packet_decl(
+    grammar: &ast::Grammar,
+    packets: &HashMap<&str, &ast::Decl>,
+    child_ids: &[&str],
+    id: &str,
+    fields: &[ast::Field],
+    parent_id: &Option<String>,
+) -> Result<String> {
+    // TODO(mgeisler): use the convert_case crate to convert between
+    // `FooBar` and `foo_bar` in the code below.
+    let mut code = String::new();
+
+    let has_children = !child_ids.is_empty();
+    let child_idents = child_ids.iter().map(|id| format_ident!("{id}")).collect::<Vec<_>>();
+
+    let ident = format_ident!("{}", id.to_lowercase());
+    let data_child_ident = format_ident!("{id}DataChild");
+    let child_decl_packet_name =
+        child_idents.iter().map(|ident| format_ident!("{ident}Packet")).collect::<Vec<_>>();
+    let child_name = format_ident!("{id}Child");
+    if has_children {
+        let child_data_idents = child_idents.iter().map(|ident| format_ident!("{ident}Data"));
+        code.push_str(&quote_block! {
+            #[derive(Debug)]
+            enum #data_child_ident {
+                #(#child_idents(Arc<#child_data_idents>),)*
+                None,
+            }
+
+            impl #data_child_ident {
+                fn get_total_size(&self) -> usize {
+                    // TODO(mgeisler): use Self instad of #data_child_ident.
+                    match self {
+                        #(#data_child_ident::#child_idents(value) => value.get_total_size(),)*
+                        #data_child_ident::None => 0,
+                    }
+                }
+            }
+
+            #[derive(Debug)]
+            pub enum #child_name {
+                #(#child_idents(#child_decl_packet_name),)*
+                None,
+            }
+        });
+    }
+
+    let data_name = format_ident!("{id}Data");
+    let child_field = has_children.then(|| {
+        quote! {
+            child: #data_child_ident,
+        }
+    });
+    let plain_fields = fields
+        .iter()
+        .map(|field| generate_field(field, parse_quote!()))
+        .collect::<Result<Vec<_>>>()?;
+    code.push_str(&quote_block! {
+        #[derive(Debug)]
+        struct #data_name {
+            #(#plain_fields,)*
+            #child_field
+        }
+    });
+
+    let parent = parent_id.as_ref().map(|parent_id| match packets.get(parent_id.as_str()) {
+        Some(ast::Decl::Packet { id, .. }) => {
+            let parent_ident = format_ident!("{}", id.to_lowercase());
+            let parent_data = format_ident!("{id}Data");
+            quote! {
+                #parent_ident: Arc<#parent_data>,
+            }
+        }
+        _ => panic!("Could not find {parent_id}"),
+    });
+
+    let packet_name = format_ident!("{id}Packet");
+    code.push_str(&quote_block! {
+        #[derive(Debug, Clone)]
+        pub struct #packet_name {
+            #parent
+            #ident: Arc<#data_name>,
+        }
+    });
+
+    let builder_name = format_ident!("{id}Builder");
+    let pub_fields = fields
+        .iter()
+        .map(|field| generate_field(field, parse_quote!(pub)))
+        .collect::<Result<Vec<_>>>()?;
+    code.push_str(&quote_block! {
+        #[derive(Debug)]
+        pub struct #builder_name {
+            #(#pub_fields,)*
+        }
+    });
+
+    // TODO(mgeisler): use the `Buf` trait instead of tracking
+    // the offset manually.
+    let mut offset = 0;
+    let field_parsers = fields
+        .iter()
+        .map(|field| {
+            let parser = generate_field_parser(&grammar.endianness.value, id, field, offset);
+            offset += get_field_size(field);
+            parser
+        })
+        .collect::<Result<Vec<_>>>()?;
+    let field_names = fields
+        .iter()
+        .map(|field| match field {
+            ast::Field::Scalar { id, .. } => format_ident!("{id}"),
+            _ => todo!("unsupported field: {:?}", field),
+        })
+        .collect::<Vec<_>>();
+    let mut offset = 0;
+    let field_writers = fields
+        .iter()
+        .map(|field| {
+            let writer = generate_field_writer(grammar, field, offset);
+            offset += get_field_size(field);
+            writer
+        })
+        .collect::<Result<Vec<_>>>()?;
+    let total_field_size = syn::Index::from(fields.iter().map(get_field_size).sum::<usize>());
+
+    code.push_str(&quote_block! {
+        impl #data_name {
+            fn conforms(bytes: &[u8]) -> bool {
+                true
+            }
+
+            fn parse(bytes: &[u8]) -> Result<Self> {
+                #(#field_parsers)*
+                Ok(Self { #(#field_names),* })
+            }
+
+            fn write_to(&self, buffer: &mut BytesMut) {
+                #(#field_writers)*
+            }
+
+            fn get_total_size(&self) -> usize {
+                self.get_size()
+            }
+
+            fn get_size(&self) -> usize {
+                let ret = 0;
+                let ret = ret + #total_field_size;
+                ret
+            }
+        }
+    });
+
+    code.push_str(&quote_block! {
+        impl Packet for #packet_name {
+            fn to_bytes(self) -> Bytes {
+                let mut buffer = BytesMut::new();
+                buffer.resize(self.#ident.get_total_size(), 0);
+                self.#ident.write_to(&mut buffer);
+                buffer.freeze()
+            }
+            fn to_vec(self) -> Vec<u8> {
+                self.to_bytes().to_vec()
+            }
+        }
+        impl From<#packet_name> for Bytes {
+            fn from(packet: #packet_name) -> Self {
+                packet.to_bytes()
+            }
+        }
+        impl From<#packet_name> for Vec<u8> {
+            fn from(packet: #packet_name) -> Self {
+                packet.to_vec()
+            }
+        }
+    });
+
+    let specialize = has_children.then(|| {
+        quote! {
+            pub fn specialize(&self) -> #child_name {
+                match &self.#ident.child {
+                    #(#data_child_ident::#child_idents(_) =>
+                      #child_name::#child_idents(
+                          #child_decl_packet_name::new(self.#ident.clone()).unwrap()),)*
+                    #data_child_ident::None => #child_name::None,
+                }
+            }
+        }
+    });
+    let field_getters = fields
+        .iter()
+        .map(|field| generate_field_getter(&ident, field))
+        .collect::<Result<Vec<_>>>()?;
+    code.push_str(&quote_block! {
+        impl #packet_name {
+            pub fn parse(bytes: &[u8]) -> Result<Self> {
+                Ok(Self::new(Arc::new(#data_name::parse(bytes)?)).unwrap())
+            }
+
+            #specialize
+
+            fn new(root: Arc<#data_name>) -> std::result::Result<Self, &'static str> {
+                let #ident = root;
+                Ok(Self { #ident })
+            }
+
+            #(#field_getters)*
+        }
+    });
+
+    let child = has_children.then(|| {
+        quote! {
+            child: #data_child_ident::None,
+        }
+    });
+    code.push_str(&quote_block! {
+        impl #builder_name {
+            pub fn build(self) -> #packet_name {
+                let #ident = Arc::new(#data_name {
+                    #(#field_names: self.#field_names,)*
+                    #child
+                });
+                #packet_name::new(#ident).unwrap()
+            }
+        }
+    });
+
+    Ok(code)
+}
+
+fn generate_decl(
+    grammar: &ast::Grammar,
+    packets: &HashMap<&str, &ast::Decl>,
+    children: &HashMap<&str, Vec<&str>>,
+    decl: &ast::Decl,
+) -> Result<String> {
+    let empty: Vec<&str> = vec![];
+    match decl {
+        ast::Decl::Packet { id, fields, parent_id, .. } => generate_packet_decl(
+            grammar,
+            packets,
+            children.get(id.as_str()).unwrap_or(&empty),
+            id,
+            fields,
+            parent_id,
+        ),
+        _ => todo!("unsupported Decl::{:?}", decl),
+    }
+}
+
+/// Generate Rust code from `grammar`.
+///
+/// The code is not formatted, pipe it through `rustfmt` to get
+/// readable source code.
+pub fn generate_rust(sources: &ast::SourceDatabase, grammar: &ast::Grammar) -> Result<String> {
+    let source =
+        sources.get(grammar.file).with_context(|| format!("could not read {}", grammar.file))?;
+
+    let mut children = HashMap::new();
+    let mut packets = HashMap::new();
+    for decl in &grammar.declarations {
+        if let ast::Decl::Packet { id, parent_id, .. } = decl {
+            packets.insert(id.as_str(), decl);
+            if let Some(parent_id) = parent_id {
+                children.entry(parent_id.as_str()).or_insert_with(Vec::new).push(id.as_str());
+            }
+        }
+    }
+
+    let mut code = String::new();
+
+    code.push_str(&generate_preamble(Path::new(source.name()))?);
+
+    for decl in &grammar.declarations {
+        let decl_code = generate_decl(grammar, &packets, &children, decl)
+            .with_context(|| format!("failed to generating code for {:?}", decl))?;
+        code.push_str(&decl_code);
+        code.push_str("\n\n");
+    }
+
+    Ok(code)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::parser::parse_inline;
+    use std::io::Write;
+    use std::process::Command;
+    use std::process::Stdio;
+    use tempfile::NamedTempFile;
+
+    fn parse(text: &str) -> ast::Grammar {
+        let mut db = ast::SourceDatabase::new();
+        parse_inline(&mut db, String::from("stdin"), String::from(text)).expect("parsing failure")
+    }
+
+    fn format_with_rustfmt(unformatted: &str) -> String {
+        // We expect to find `rustfmt` as a sibling to the test
+        // executable. It ends up there when referenced using the
+        // `data` property in an Android.pb file.
+        let mut rustfmt_path = std::env::current_exe().unwrap();
+        rustfmt_path.set_file_name("rustfmt");
+        let mut rustfmt = Command::new(&rustfmt_path)
+            .stdin(Stdio::piped())
+            .stdout(Stdio::piped())
+            .spawn()
+            .unwrap_or_else(|_| panic!("failed to start {:?}", &rustfmt_path));
+
+        let mut stdin = rustfmt.stdin.take().unwrap();
+        // Owned copy which we can move into the writing thread.
+        let unformatted = String::from(unformatted);
+        std::thread::spawn(move || {
+            stdin.write_all(unformatted.as_bytes()).expect("could not write to stdin");
+        });
+
+        let output = rustfmt.wait_with_output().expect("error executing rustfmt");
+        assert!(output.status.success(), "rustfmt failed: {}", output.status);
+        String::from_utf8(output.stdout).expect("rustfmt output was not UTF-8")
+    }
+
+    fn unified_diff(left: &str, right: &str) -> String {
+        let mut temp_left = NamedTempFile::new().unwrap();
+        temp_left.write_all(left.as_bytes()).unwrap();
+        let mut temp_right = NamedTempFile::new().unwrap();
+        temp_right.write_all(right.as_bytes()).unwrap();
+
+        // We expect `diff` to be available on PATH.
+        let output = Command::new("diff")
+            .arg("--unified")
+            .arg("--label")
+            .arg("left")
+            .arg("--label")
+            .arg("right")
+            .arg(temp_left.path())
+            .arg(temp_right.path())
+            .output()
+            .expect("failed to run diff");
+        let diff_trouble_exit_code = 2; // from diff(1)
+        assert_ne!(
+            output.status.code().unwrap(),
+            diff_trouble_exit_code,
+            "diff failed: {}",
+            output.status
+        );
+        String::from_utf8(output.stdout).expect("diff output was not UTF-8")
+    }
+
+    #[track_caller]
+    fn assert_eq_with_diff(left: &str, right: &str) {
+        assert!(
+            left == right,
+            "texts did not match, left:\n{}\n\n\
+             right:\n{}\n\n\
+             diff:\n{}\n",
+            left,
+            right,
+            unified_diff(left, right)
+        );
+    }
+
+    #[test]
+    fn test_generate_preamble() {
+        let actual_code = generate_preamble(Path::new("some/path/foo.pdl")).unwrap();
+        let expected_code = include_str!("../test/generated/preamble.rs");
+        assert_eq_with_diff(&format_with_rustfmt(&actual_code), expected_code);
+    }
+
+    #[test]
+    fn test_generate_packet_decl_empty() {
+        let grammar = parse(
+            r#"
+              big_endian_packets
+              packet Foo {}
+            "#,
+        );
+        let packets = HashMap::new();
+        let children = HashMap::new();
+        let decl = &grammar.declarations[0];
+        let actual_code = generate_decl(&grammar, &packets, &children, decl).unwrap();
+        let expected_code = include_str!("../test/generated/packet_decl_empty.rs");
+        assert_eq_with_diff(&format_with_rustfmt(&actual_code), expected_code);
+    }
+
+    #[test]
+    fn test_generate_packet_decl_little_endian() {
+        let grammar = parse(
+            r#"
+              little_endian_packets
+
+              packet Foo {
+                x: 8,
+                y: 16,
+              }
+            "#,
+        );
+        let packets = HashMap::new();
+        let children = HashMap::new();
+        let decl = &grammar.declarations[0];
+        let actual_code = generate_decl(&grammar, &packets, &children, decl).unwrap();
+        let expected_code = include_str!("../test/generated/packet_decl_simple_little_endian.rs");
+        assert_eq_with_diff(&format_with_rustfmt(&actual_code), expected_code);
+    }
+
+    #[test]
+    fn test_generate_packet_decl_simple_big_endian() {
+        let grammar = parse(
+            r#"
+              big_endian_packets
+
+              packet Foo {
+                x: 8,
+                y: 16,
+              }
+            "#,
+        );
+        let packets = HashMap::new();
+        let children = HashMap::new();
+        let decl = &grammar.declarations[0];
+        let actual_code = generate_decl(&grammar, &packets, &children, decl).unwrap();
+        let expected_code = include_str!("../test/generated/packet_decl_simple_big_endian.rs");
+        assert_eq_with_diff(&format_with_rustfmt(&actual_code), expected_code);
+    }
+}
diff --git a/tools/pdl/src/main.rs b/tools/pdl/src/main.rs
index ff5c585..7bd8c03 100644
--- a/tools/pdl/src/main.rs
+++ b/tools/pdl/src/main.rs
@@ -4,11 +4,30 @@
 use structopt::StructOpt;
 
 mod ast;
+mod generator;
 mod lint;
 mod parser;
 
 use crate::lint::Lintable;
 
+#[derive(Debug)]
+enum OutputFormat {
+    JSON,
+    Rust,
+}
+
+impl std::str::FromStr for OutputFormat {
+    type Err = String;
+
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        match input.to_lowercase().as_str() {
+            "json" => Ok(Self::JSON),
+            "rust" => Ok(Self::Rust),
+            _ => Err(format!("could not parse {:?}, valid option are 'json' and 'rust'.", input)),
+        }
+    }
+}
+
 #[derive(Debug, StructOpt)]
 #[structopt(name = "pdl-parser", about = "Packet Description Language parser tool.")]
 struct Opt {
@@ -16,6 +35,11 @@
     #[structopt(short, long = "--version")]
     version: bool,
 
+    /// Generate output in this format ("json" or "rust"). The output
+    /// will be printed on stdout in both cases.
+    #[structopt(short, long = "--output-format", name = "FORMAT", default_value = "JSON")]
+    output_format: OutputFormat,
+
     /// Input file.
     #[structopt(name = "FILE")]
     input_file: String,
@@ -33,7 +57,16 @@
     match parser::parse_file(&mut sources, opt.input_file) {
         Ok(grammar) => {
             let _ = grammar.lint().print(&sources, termcolor::ColorChoice::Always);
-            println!("{}", serde_json::to_string_pretty(&grammar).unwrap())
+
+            match opt.output_format {
+                OutputFormat::JSON => {
+                    println!("{}", serde_json::to_string_pretty(&grammar).unwrap())
+                }
+                OutputFormat::Rust => match generator::generate_rust(&sources, &grammar) {
+                    Ok(code) => println!("{}", &code),
+                    Err(err) => println!("failed to generate code: {}", err),
+                },
+            }
         }
         Err(err) => {
             let writer = termcolor::StandardStream::stderr(termcolor::ColorChoice::Always);
diff --git a/tools/pdl/src/parser.rs b/tools/pdl/src/parser.rs
index edeecfe..dd4e702 100644
--- a/tools/pdl/src/parser.rs
+++ b/tools/pdl/src/parser.rs
@@ -187,7 +187,7 @@
     Err(format!("expected rule {:?}, got nothing", expected))
 }
 
-fn expect<'i>(iter: &mut NodeIterator<'i>, rule: Rule) -> Result<Node<'i>, String> {
+fn expect<'i>(iter: &mut impl Iterator<Item = Node<'i>>, rule: Rule) -> Result<Node<'i>, String> {
     match iter.next() {
         Some(node) if node.as_rule() == rule => Ok(node),
         Some(node) => err_unexpected_rule(rule, node.as_rule()),
@@ -233,8 +233,12 @@
     }
 }
 
-fn parse_string(iter: &mut NodeIterator<'_>) -> Result<String, String> {
-    expect(iter, Rule::string).map(|n| n.as_string())
+fn parse_string<'i>(iter: &mut impl Iterator<Item = Node<'i>>) -> Result<String, String> {
+    expect(iter, Rule::string)
+        .map(|n| n.as_str())
+        .and_then(|s| s.strip_prefix('"').ok_or_else(|| "expected \" prefix".to_owned()))
+        .and_then(|s| s.strip_suffix('"').ok_or_else(|| "expected \" suffix".to_owned()))
+        .map(|s| s.to_owned())
 }
 
 fn parse_size_modifier_opt(iter: &mut NodeIterator<'_>) -> Option<String> {
@@ -533,4 +537,29 @@
         assert_eq!(grammar.endianness.value, ast::EndiannessValue::BigEndian);
         assert_ne!(grammar.endianness.loc, ast::SourceRange::default());
     }
+
+    #[test]
+    fn test_parse_string_bare() {
+        let mut pairs = PDLParser::parse(Rule::string, r#""test""#).unwrap();
+
+        assert_eq!(parse_string(&mut pairs).as_deref(), Ok("test"));
+        assert_eq!(pairs.next(), None, "pairs is empty");
+    }
+
+    #[test]
+    fn test_parse_string_space() {
+        let mut pairs = PDLParser::parse(Rule::string, r#""test with space""#).unwrap();
+
+        assert_eq!(parse_string(&mut pairs).as_deref(), Ok("test with space"));
+        assert_eq!(pairs.next(), None, "pairs is empty");
+    }
+
+    #[test]
+    #[should_panic] /* This is not supported */
+    fn test_parse_string_escape() {
+        let mut pairs = PDLParser::parse(Rule::string, r#""\"test\"""#).unwrap();
+
+        assert_eq!(parse_string(&mut pairs).as_deref(), Ok(r#""test""#));
+        assert_eq!(pairs.next(), None, "pairs is empty");
+    }
 }
diff --git a/tools/pdl/test/generated/packet_decl_empty.rs b/tools/pdl/test/generated/packet_decl_empty.rs
new file mode 100644
index 0000000..f4a8dcf
--- /dev/null
+++ b/tools/pdl/test/generated/packet_decl_empty.rs
@@ -0,0 +1,67 @@
+#[derive(Debug)]
+struct FooData {}
+
+#[derive(Debug, Clone)]
+pub struct FooPacket {
+    foo: Arc<FooData>,
+}
+
+#[derive(Debug)]
+pub struct FooBuilder {}
+
+impl FooData {
+    fn conforms(bytes: &[u8]) -> bool {
+        true
+    }
+    fn parse(bytes: &[u8]) -> Result<Self> {
+        Ok(Self {})
+    }
+    fn write_to(&self, buffer: &mut BytesMut) {}
+    fn get_total_size(&self) -> usize {
+        self.get_size()
+    }
+    fn get_size(&self) -> usize {
+        let ret = 0;
+        let ret = ret + 0;
+        ret
+    }
+}
+
+impl Packet for FooPacket {
+    fn to_bytes(self) -> Bytes {
+        let mut buffer = BytesMut::new();
+        buffer.resize(self.foo.get_total_size(), 0);
+        self.foo.write_to(&mut buffer);
+        buffer.freeze()
+    }
+    fn to_vec(self) -> Vec<u8> {
+        self.to_bytes().to_vec()
+    }
+}
+impl From<FooPacket> for Bytes {
+    fn from(packet: FooPacket) -> Self {
+        packet.to_bytes()
+    }
+}
+impl From<FooPacket> for Vec<u8> {
+    fn from(packet: FooPacket) -> Self {
+        packet.to_vec()
+    }
+}
+
+impl FooPacket {
+    pub fn parse(bytes: &[u8]) -> Result<Self> {
+        Ok(Self::new(Arc::new(FooData::parse(bytes)?)).unwrap())
+    }
+    fn new(root: Arc<FooData>) -> std::result::Result<Self, &'static str> {
+        let foo = root;
+        Ok(Self { foo })
+    }
+}
+
+impl FooBuilder {
+    pub fn build(self) -> FooPacket {
+        let foo = Arc::new(FooData {});
+        FooPacket::new(foo).unwrap()
+    }
+}
diff --git a/tools/pdl/test/generated/packet_decl_simple_big_endian.rs b/tools/pdl/test/generated/packet_decl_simple_big_endian.rs
new file mode 100644
index 0000000..89b1bab
--- /dev/null
+++ b/tools/pdl/test/generated/packet_decl_simple_big_endian.rs
@@ -0,0 +1,102 @@
+#[derive(Debug)]
+struct FooData {
+    x: u8,
+    y: u16,
+}
+
+#[derive(Debug, Clone)]
+pub struct FooPacket {
+    foo: Arc<FooData>,
+}
+
+#[derive(Debug)]
+pub struct FooBuilder {
+    pub x: u8,
+    pub y: u16,
+}
+
+impl FooData {
+    fn conforms(bytes: &[u8]) -> bool {
+        true
+    }
+    fn parse(bytes: &[u8]) -> Result<Self> {
+        if bytes.len() < 1 {
+            return Err(Error::InvalidLengthError {
+                obj: "Foo".to_string(),
+                field: "x".to_string(),
+                wanted: 1,
+                got: bytes.len(),
+            });
+        }
+        let x = u8::from_be_bytes([bytes[0]]);
+        if bytes.len() < 3 {
+            return Err(Error::InvalidLengthError {
+                obj: "Foo".to_string(),
+                field: "y".to_string(),
+                wanted: 3,
+                got: bytes.len(),
+            });
+        }
+        let y = u16::from_be_bytes([bytes[1], bytes[2]]);
+        Ok(Self { x, y })
+    }
+    fn write_to(&self, buffer: &mut BytesMut) {
+        let x = self.x;
+        buffer[0..1].copy_from_slice(&x.to_be_bytes()[0..1]);
+        let y = self.y;
+        buffer[1..3].copy_from_slice(&y.to_be_bytes()[0..2]);
+    }
+    fn get_total_size(&self) -> usize {
+        self.get_size()
+    }
+    fn get_size(&self) -> usize {
+        let ret = 0;
+        let ret = ret + 3;
+        ret
+    }
+}
+
+impl Packet for FooPacket {
+    fn to_bytes(self) -> Bytes {
+        let mut buffer = BytesMut::new();
+        buffer.resize(self.foo.get_total_size(), 0);
+        self.foo.write_to(&mut buffer);
+        buffer.freeze()
+    }
+    fn to_vec(self) -> Vec<u8> {
+        self.to_bytes().to_vec()
+    }
+}
+impl From<FooPacket> for Bytes {
+    fn from(packet: FooPacket) -> Self {
+        packet.to_bytes()
+    }
+}
+impl From<FooPacket> for Vec<u8> {
+    fn from(packet: FooPacket) -> Self {
+        packet.to_vec()
+    }
+}
+
+impl FooPacket {
+    pub fn parse(bytes: &[u8]) -> Result<Self> {
+        Ok(Self::new(Arc::new(FooData::parse(bytes)?)).unwrap())
+    }
+    fn new(root: Arc<FooData>) -> std::result::Result<Self, &'static str> {
+        let foo = root;
+        Ok(Self { foo })
+    }
+    pub fn get_x(&self) -> u8 {
+        self.foo.as_ref().x
+    }
+    pub fn get_y(&self) -> u16 {
+        self.foo.as_ref().y
+    }
+}
+
+impl FooBuilder {
+    pub fn build(self) -> FooPacket {
+        let foo = Arc::new(FooData { x: self.x, y: self.y });
+        FooPacket::new(foo).unwrap()
+    }
+}
diff --git a/tools/pdl/test/generated/packet_decl_simple_little_endian.rs b/tools/pdl/test/generated/packet_decl_simple_little_endian.rs
new file mode 100644
index 0000000..5b81ec8
--- /dev/null
+++ b/tools/pdl/test/generated/packet_decl_simple_little_endian.rs
@@ -0,0 +1,102 @@
+#[derive(Debug)]
+struct FooData {
+    x: u8,
+    y: u16,
+}
+
+#[derive(Debug, Clone)]
+pub struct FooPacket {
+    foo: Arc<FooData>,
+}
+
+#[derive(Debug)]
+pub struct FooBuilder {
+    pub x: u8,
+    pub y: u16,
+}
+
+impl FooData {
+    fn conforms(bytes: &[u8]) -> bool {
+        true
+    }
+    fn parse(bytes: &[u8]) -> Result<Self> {
+        if bytes.len() < 1 {
+            return Err(Error::InvalidLengthError {
+                obj: "Foo".to_string(),
+                field: "x".to_string(),
+                wanted: 1,
+                got: bytes.len(),
+            });
+        }
+        let x = u8::from_le_bytes([bytes[0]]);
+        if bytes.len() < 3 {
+            return Err(Error::InvalidLengthError {
+                obj: "Foo".to_string(),
+                field: "y".to_string(),
+                wanted: 3,
+                got: bytes.len(),
+            });
+        }
+        let y = u16::from_le_bytes([bytes[1], bytes[2]]);
+        Ok(Self { x, y })
+    }
+    fn write_to(&self, buffer: &mut BytesMut) {
+        let x = self.x;
+        buffer[0..1].copy_from_slice(&x.to_le_bytes()[0..1]);
+        let y = self.y;
+        buffer[1..3].copy_from_slice(&y.to_le_bytes()[0..2]);
+    }
+    fn get_total_size(&self) -> usize {
+        self.get_size()
+    }
+    fn get_size(&self) -> usize {
+        let ret = 0;
+        let ret = ret + 3;
+        ret
+    }
+}
+
+impl Packet for FooPacket {
+    fn to_bytes(self) -> Bytes {
+        let mut buffer = BytesMut::new();
+        buffer.resize(self.foo.get_total_size(), 0);
+        self.foo.write_to(&mut buffer);
+        buffer.freeze()
+    }
+    fn to_vec(self) -> Vec<u8> {
+        self.to_bytes().to_vec()
+    }
+}
+impl From<FooPacket> for Bytes {
+    fn from(packet: FooPacket) -> Self {
+        packet.to_bytes()
+    }
+}
+impl From<FooPacket> for Vec<u8> {
+    fn from(packet: FooPacket) -> Self {
+        packet.to_vec()
+    }
+}
+
+impl FooPacket {
+    pub fn parse(bytes: &[u8]) -> Result<Self> {
+        Ok(Self::new(Arc::new(FooData::parse(bytes)?)).unwrap())
+    }
+    fn new(root: Arc<FooData>) -> std::result::Result<Self, &'static str> {
+        let foo = root;
+        Ok(Self { foo })
+    }
+    pub fn get_x(&self) -> u8 {
+        self.foo.as_ref().x
+    }
+    pub fn get_y(&self) -> u16 {
+        self.foo.as_ref().y
+    }
+}
+
+impl FooBuilder {
+    pub fn build(self) -> FooPacket {
+        let foo = Arc::new(FooData { x: self.x, y: self.y });
+        FooPacket::new(foo).unwrap()
+    }
+}
diff --git a/tools/pdl/test/generated/preamble.rs b/tools/pdl/test/generated/preamble.rs
new file mode 100644
index 0000000..0a03fe4
--- /dev/null
+++ b/tools/pdl/test/generated/preamble.rs
@@ -0,0 +1,34 @@
+// @generated rust packets from foo.pdl
+
+use bytes::{BufMut, Bytes, BytesMut};
+use num_derive::{FromPrimitive, ToPrimitive};
+use num_traits::{FromPrimitive, ToPrimitive};
+use std::convert::{TryFrom, TryInto};
+use std::fmt;
+use std::sync::Arc;
+use thiserror::Error;
+
+type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Debug, Error)]
+pub enum Error {
+    #[error("Packet parsing failed")]
+    InvalidPacketError,
+    #[error("{field} was {value:x}, which is not known")]
+    ConstraintOutOfBounds { field: String, value: u64 },
+    #[error("when parsing {obj}.{field} needed length of {wanted} but got {got}")]
+    InvalidLengthError { obj: String, field: String, wanted: usize, got: usize },
+    #[error("Due to size restrictions a struct could not be parsed.")]
+    ImpossibleStructError,
+    #[error("when parsing field {obj}.{field}, {value} is not a valid {type_} value")]
+    InvalidEnumValueError { obj: String, field: String, value: u64, type_: String },
+}
+
+#[derive(Debug, Error)]
+#[error("{0}")]
+pub struct TryFromError(&'static str);
+
+pub trait Packet {
+    fn to_bytes(self) -> Bytes;
+    fn to_vec(self) -> Vec<u8>;
+}
diff --git a/tools/rootcanal/model/devices/device.cc b/tools/rootcanal/model/devices/device.cc
index f5fa8bb..3bcf30b 100644
--- a/tools/rootcanal/model/devices/device.cc
+++ b/tools/rootcanal/model/devices/device.cc
@@ -31,8 +31,7 @@
 }
 
 void Device::UnregisterPhyLayers() {
-  for (auto weak_phy : phy_layers_) {
-    auto phy = weak_phy.lock();
+  for (auto phy : phy_layers_) {
     if (phy != nullptr) {
       phy->Unregister();
     }
@@ -41,8 +40,7 @@
 }
 
 void Device::UnregisterPhyLayer(Phy::Type phy_type, uint32_t factory_id) {
-  for (auto& weak_phy : phy_layers_) {
-    auto phy = weak_phy.lock();
+  for (auto& phy : phy_layers_) {
     if (phy != nullptr && phy->IsFactoryId(factory_id) &&
         phy->GetType() == phy_type) {
       phy->Unregister();
@@ -61,8 +59,7 @@
 void Device::SendLinkLayerPacket(
     std::shared_ptr<model::packets::LinkLayerPacketBuilder> to_send,
     Phy::Type phy_type) {
-  for (auto weak_phy : phy_layers_) {
-    auto phy = weak_phy.lock();
+  for (auto phy : phy_layers_) {
     if (phy != nullptr && phy->GetType() == phy_type) {
       phy->Send(to_send);
     }
@@ -71,8 +68,7 @@
 
 void Device::SendLinkLayerPacket(model::packets::LinkLayerPacketView to_send,
                                  Phy::Type phy_type) {
-  for (auto weak_phy : phy_layers_) {
-    auto phy = weak_phy.lock();
+  for (auto phy : phy_layers_) {
     if (phy != nullptr && phy->GetType() == phy_type) {
       phy->Send(to_send);
     }
diff --git a/tools/rootcanal/model/devices/device.h b/tools/rootcanal/model/devices/device.h
index 541db6d..25d1024 100644
--- a/tools/rootcanal/model/devices/device.h
+++ b/tools/rootcanal/model/devices/device.h
@@ -85,7 +85,7 @@
   void RegisterCloseCallback(std::function<void()>);
 
  protected:
-  std::vector<std::weak_ptr<PhyLayer>> phy_layers_;
+  std::vector<std::shared_ptr<PhyLayer>> phy_layers_;
 
   std::chrono::steady_clock::time_point last_advertisement_;
 
diff --git a/tools/rootcanal/model/setup/async_manager.cc b/tools/rootcanal/model/setup/async_manager.cc
index 01447ec..5ae004a 100644
--- a/tools/rootcanal/model/setup/async_manager.cc
+++ b/tools/rootcanal/model/setup/async_manager.cc
@@ -364,6 +364,7 @@
     std::chrono::steady_clock::time_point time;
     bool periodic;
     std::chrono::milliseconds period{};
+    std::mutex in_callback; // Taken when the callback is active
     TaskCallback callback;
     AsyncTaskId task_id;
     AsyncUserId user_id;
@@ -381,8 +382,22 @@
     if (tasks_by_id_.count(async_task_id) == 0) {
       return false;
     }
-    task_queue_.erase(tasks_by_id_[async_task_id]);
-    tasks_by_id_.erase(async_task_id);
+
+    // Now make sure we are not running this task.
+    // 2 cases:
+    // - This is called from thread_, this means a running
+    //   scheduled task is actually unregistering. All bets are off.
+    // - Another thread is calling us, let's make sure the task is not active.
+    if (thread_.get_id() != std::this_thread::get_id()) {
+      auto task = tasks_by_id_[async_task_id];
+      const std::lock_guard<std::mutex> lock(task->in_callback);
+      task_queue_.erase(task);
+      tasks_by_id_.erase(async_task_id);
+    } else {
+      task_queue_.erase(tasks_by_id_[async_task_id]);
+      tasks_by_id_.erase(async_task_id);
+    }
+
     return true;
   }
 
@@ -437,11 +452,12 @@
   void ThreadRoutine() {
     while (running_) {
       TaskCallback callback;
+      std::shared_ptr<Task> task_p;
       bool run_it = false;
       {
         std::unique_lock<std::mutex> guard(internal_mutex_);
         if (!task_queue_.empty()) {
-          std::shared_ptr<Task> task_p = *(task_queue_.begin());
+          task_p = *(task_queue_.begin());
           if (task_p->time < std::chrono::steady_clock::now()) {
             run_it = true;
             callback = task_p->callback;
@@ -458,6 +474,7 @@
         }
       }
       if (run_it) {
+        const std::lock_guard<std::mutex> lock(task_p->in_callback);
         callback();
       }
       {
diff --git a/tools/rootcanal/model/setup/async_manager.h b/tools/rootcanal/model/setup/async_manager.h
index f6603c4..e82fa37 100644
--- a/tools/rootcanal/model/setup/async_manager.h
+++ b/tools/rootcanal/model/setup/async_manager.h
@@ -77,18 +77,18 @@
                                     std::chrono::milliseconds period,
                                     const TaskCallback& callback);
 
-  // Cancels the/every future occurrence of the action specified by this id. It
-  // is guaranteed that the associated callback will not be called after this
-  // method returns (it could be called during the execution of the method).
-  // The calling thread may block until the scheduling thread acknowledges the
-  // cancellation.
+  // Cancels the/every future occurrence of the action specified by this id.
+  // The following invariants will hold:
+  // - The task will not be invoked after this method returns
+  // - If the task is currently running it will block until the task is
+  //   completed, unless cancel is called from the running task.
   bool CancelAsyncTask(AsyncTaskId async_task_id);
 
-  // Cancels the/every future occurrence of the action specified by this id. It
-  // is guaranteed that the associated callback will not be called after this
-  // method returns (it could be called during the execution of the method).
-  // The calling thread may block until the scheduling thread acknowledges the
-  // cancellation.
+  // Cancels the/every future occurrence of the action specified by this id.
+  // The following invariants will hold:
+  // - The task will not be invoked after this method returns
+  // - If the task is currently running it will block until the task is
+  //   completed, unless cancel is called from the running task.
   bool CancelAsyncTasksFromUser(AsyncUserId user_id);
 
   // Execs the given code in a synchronized manner. It is guaranteed that code
diff --git a/tools/rootcanal/model/setup/test_model.cc b/tools/rootcanal/model/setup/test_model.cc
index 37da755..470734c 100644
--- a/tools/rootcanal/model/setup/test_model.cc
+++ b/tools/rootcanal/model/setup/test_model.cc
@@ -52,6 +52,10 @@
   model_user_id_ = get_user_id_();
 }
 
+TestModel::~TestModel() {
+  StopTimer();
+}
+
 void TestModel::SetTimerPeriod(std::chrono::milliseconds new_period) {
   timer_period_ = new_period;
 
@@ -88,13 +92,13 @@
   schedule_task_(model_user_id_, std::chrono::milliseconds(0),
                  [this, dev_index]() {
                    devices_[dev_index]->UnregisterPhyLayers();
-                   devices_[dev_index].reset();
+                   devices_[dev_index] = nullptr;
                  });
 }
 
 size_t TestModel::AddPhy(Phy::Type phy_type) {
   size_t factory_id = phys_.size();
-  phys_.push_back(std::make_shared<PhyLayerFactory>(phy_type, factory_id));
+  phys_.emplace_back(phy_type, factory_id);
   return factory_id;
 }
 
@@ -103,11 +107,9 @@
     LOG_WARN("Unknown phy at index %zu", phy_index);
     return;
   }
-  schedule_task_(model_user_id_, std::chrono::milliseconds(0),
-                 [this, phy_index]() {
-                   phys_[phy_index]->UnregisterAllPhyLayers();
-                   phys_[phy_index].reset();
-                 });
+  schedule_task_(
+      model_user_id_, std::chrono::milliseconds(0),
+      [this, phy_index]() { phys_[phy_index].UnregisterAllPhyLayers(); });
 }
 
 void TestModel::AddDeviceToPhy(size_t dev_index, size_t phy_index) {
@@ -120,13 +122,9 @@
     return;
   }
   auto dev = devices_[dev_index];
-  std::weak_ptr<Device> weak_dev = dev;
-  dev->RegisterPhyLayer(phys_[phy_index]->GetPhyLayer(
-      [weak_dev](model::packets::LinkLayerPacketView packet) {
-        auto device = weak_dev.lock();
-        if (device != nullptr) {
-          device->IncomingPacket(std::move(packet));
-        }
+  dev->RegisterPhyLayer(phys_[phy_index].GetPhyLayer(
+      [dev](model::packets::LinkLayerPacketView packet) {
+        dev->IncomingPacket(std::move(packet));
       },
       dev_index));
 }
@@ -143,8 +141,8 @@
   schedule_task_(model_user_id_, std::chrono::milliseconds(0),
                  [this, dev_index, phy_index]() {
                    devices_[dev_index]->UnregisterPhyLayer(
-                       phys_[phy_index]->GetType(),
-                       phys_[phy_index]->GetFactoryId());
+                       phys_[phy_index].GetType(),
+                       phys_[phy_index].GetFactoryId());
                  });
 }
 
@@ -156,7 +154,7 @@
   AsyncUserId user_id = get_user_id_();
 
   for (size_t i = 0; i < phys_.size(); i++) {
-    if (phy_type == phys_[i]->GetType()) {
+    if (phy_type == phys_[i].GetType()) {
       AddDeviceToPhy(index, i);
     }
   }
@@ -208,7 +206,8 @@
   }
 
   cancel_tasks_from_user_(user_id);
-  Del(index);
+  devices_[index]->UnregisterPhyLayers();
+  devices_[index] = nullptr;
 }
 
 void TestModel::SetDeviceAddress(size_t index, Address address) {
@@ -233,11 +232,7 @@
   list_string_ += " Phys: \r\n";
   for (size_t i = 0; i < phys_.size(); i++) {
     list_string_ += "  " + std::to_string(i) + ":";
-    if (phys_[i] == nullptr) {
-      list_string_ += " deleted \r\n";
-    } else {
-      list_string_ += phys_[i]->ToString() + " \r\n";
-    }
+    list_string_ += phys_[i].ToString() + " \r\n";
   }
   return list_string_;
 }
diff --git a/tools/rootcanal/model/setup/test_model.h b/tools/rootcanal/model/setup/test_model.h
index 6449b62..2171c53 100644
--- a/tools/rootcanal/model/setup/test_model.h
+++ b/tools/rootcanal/model/setup/test_model.h
@@ -49,7 +49,7 @@
       std::function<void(AsyncTaskId)> cancel,
       std::function<std::shared_ptr<Device>(const std::string&, int, Phy::Type)>
           connect_to_remote);
-  ~TestModel() = default;
+  ~TestModel();
 
   TestModel(TestModel& model) = delete;
   TestModel& operator=(const TestModel& model) = delete;
@@ -100,7 +100,7 @@
   void Reset();
 
  private:
-  std::vector<std::shared_ptr<PhyLayerFactory>> phys_;
+  std::vector<PhyLayerFactory> phys_;
   std::vector<std::shared_ptr<Device>> devices_;
   std::string list_string_;
 
diff --git a/tools/rootcanal/test/async_manager_unittest.cc b/tools/rootcanal/test/async_manager_unittest.cc
index a0a33d4..c7a7444 100644
--- a/tools/rootcanal/test/async_manager_unittest.cc
+++ b/tools/rootcanal/test/async_manager_unittest.cc
@@ -33,6 +33,7 @@
 #include <ratio>               // for ratio
 #include <string>              // for string
 #include <tuple>               // for tuple
+#include <thread>
 
 #include "osi/include/osi.h"  // for OSI_NO_INTR
 
@@ -123,6 +124,7 @@
 
   void SetUp() override {
     memset(server_buffer_, 0, kBufferSize);
+    memset(client_buffer_, 0, kBufferSize);
 
     socket_fd_ = StartServer();
 
@@ -137,7 +139,7 @@
   void TearDown() override {
     async_manager_.StopWatchingFileDescriptor(socket_fd_);
     close(socket_fd_);
-    ASSERT_TRUE(CheckBufferEquals());
+    ASSERT_EQ(std::string_view(server_buffer_, kBufferSize), std::string_view(client_buffer_, kBufferSize));
   }
 
   int ConnectClient() {
@@ -215,6 +217,46 @@
   close(socket_cli_fd);
 }
 
+
+TEST_F(AsyncManagerSocketTest, CanUnsubscribeTaskFromWithinTask) {
+  Event running;
+  using namespace std::chrono_literals;
+  async_manager_.ExecAsyncPeriodically(1, 1ms, 2ms, [&running, this]() {
+     EXPECT_TRUE(async_manager_.CancelAsyncTask(1)) << "We were scheduled, so cancel should return true";
+     EXPECT_FALSE(async_manager_.CancelAsyncTask(1)) << "We were not scheduled, so cancel should return false";
+     running.set(true);
+  });
+
+  EXPECT_TRUE(running.wait_for(10ms));
+}
+
+
+TEST_F(AsyncManagerSocketTest, UnsubScribeWaitsUntilCompletion) {
+  using namespace std::chrono_literals;
+  Event running;
+  bool cancel_done = false;
+  bool task_complete = false;
+  async_manager_.ExecAsyncPeriodically(1, 1ms, 2ms, [&running, &cancel_done, &task_complete]() {
+      // Let the other thread now we are in the callback..
+      running.set(true);
+      // Wee bit of a hack that relies on timing..
+      std::this_thread::sleep_for(20ms);
+      EXPECT_FALSE(cancel_done) << "Task cancellation did not wait for us to complete!";
+      task_complete = true;
+  });
+
+  EXPECT_TRUE(running.wait_for(10ms));
+  auto start = std::chrono::system_clock::now();
+
+  // There is a 20ms wait.. so we know that this should take some time.
+  EXPECT_TRUE(async_manager_.CancelAsyncTask(1)) << "We were scheduled, so cancel should return true";
+  cancel_done = true;
+  EXPECT_TRUE(task_complete) << "We managed to cancel a task while it was not yet finished.";
+  auto end = std::chrono::system_clock::now();
+  auto passed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+  EXPECT_GT(passed_ms.count(), 10);
+}
+
 TEST_F(AsyncManagerSocketTest, NoEventsAfterUnsubscribe) {
   // This tests makes sure the AsyncManager never fires an event
   // after calling StopWatchingFileDescriptor.