Merge "Change getEnabledComponentOverrides return type"
diff --git a/jni/com_android_bluetooth_a2dp_sink.cpp b/jni/com_android_bluetooth_a2dp_sink.cpp
index d7cbeb7..a74b18d 100644
--- a/jni/com_android_bluetooth_a2dp_sink.cpp
+++ b/jni/com_android_bluetooth_a2dp_sink.cpp
@@ -256,7 +256,7 @@
 
 int register_com_android_bluetooth_a2dp_sink(JNIEnv* env) {
   return jniRegisterNativeMethods(
-      env, "com/android/bluetooth/a2dpsink/A2dpSinkService", sMethods,
+      env, "com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface", sMethods,
       NELEM(sMethods));
 }
 }
diff --git a/jni/com_android_bluetooth_gatt.cpp b/jni/com_android_bluetooth_gatt.cpp
index bf6cba8..642e589 100644
--- a/jni/com_android_bluetooth_gatt.cpp
+++ b/jni/com_android_bluetooth_gatt.cpp
@@ -302,12 +302,22 @@
                                conn_id, status, p_data->handle, jb.get());
 }
 
-void btgattc_write_characteristic_cb(int conn_id, int status, uint16_t handle) {
+void btgattc_write_characteristic_cb(int conn_id, int status, uint16_t handle,
+                                     uint16_t len, const uint8_t* value) {
   CallbackEnv sCallbackEnv(__func__);
   if (!sCallbackEnv.valid()) return;
 
+  ScopedLocalRef<jbyteArray> jb(sCallbackEnv.get(), NULL);
+  if (status == 0) {  // Success
+    jb.reset(sCallbackEnv->NewByteArray(len));
+    sCallbackEnv->SetByteArrayRegion(jb.get(), 0, len, (jbyte*)value);
+  } else {
+    uint8_t value = 0;
+    jb.reset(sCallbackEnv->NewByteArray(1));
+    sCallbackEnv->SetByteArrayRegion(jb.get(), 0, 1, (jbyte*)&value);
+  }
   sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onWriteCharacteristic,
-                               conn_id, status, handle);
+                               conn_id, status, handle, jb.get());
 }
 
 void btgattc_execute_write_cb(int conn_id, int status) {
@@ -336,12 +346,22 @@
                                status, p_data.handle, jb.get());
 }
 
-void btgattc_write_descriptor_cb(int conn_id, int status, uint16_t handle) {
+void btgattc_write_descriptor_cb(int conn_id, int status, uint16_t handle,
+                                 uint16_t len, const uint8_t* value) {
   CallbackEnv sCallbackEnv(__func__);
   if (!sCallbackEnv.valid()) return;
 
+  ScopedLocalRef<jbyteArray> jb(sCallbackEnv.get(), NULL);
+  if (status == 0) {  // Success
+    jb.reset(sCallbackEnv->NewByteArray(len));
+    sCallbackEnv->SetByteArrayRegion(jb.get(), 0, len, (jbyte*)value);
+  } else {
+    uint8_t value = 0;
+    jb.reset(sCallbackEnv->NewByteArray(1));
+    sCallbackEnv->SetByteArrayRegion(jb.get(), 0, 1, (jbyte*)&value);
+  }
   sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onWriteDescriptor, conn_id,
-                               status, handle);
+                               status, handle, jb.get());
 }
 
 void btgattc_remote_rssi_cb(int client_if, const RawAddress& bda, int rssi,
@@ -983,7 +1003,7 @@
   method_onReadCharacteristic =
       env->GetMethodID(clazz, "onReadCharacteristic", "(III[B)V");
   method_onWriteCharacteristic =
-      env->GetMethodID(clazz, "onWriteCharacteristic", "(III)V");
+      env->GetMethodID(clazz, "onWriteCharacteristic", "(III[B)V");
   method_onExecuteCompleted =
       env->GetMethodID(clazz, "onExecuteCompleted", "(II)V");
   method_onSearchCompleted =
@@ -991,7 +1011,7 @@
   method_onReadDescriptor =
       env->GetMethodID(clazz, "onReadDescriptor", "(III[B)V");
   method_onWriteDescriptor =
-      env->GetMethodID(clazz, "onWriteDescriptor", "(III)V");
+      env->GetMethodID(clazz, "onWriteDescriptor", "(III[B)V");
   method_onNotify =
       env->GetMethodID(clazz, "onNotify", "(ILjava/lang/String;IZ[B)V");
   method_onRegisterForNotifications =
diff --git a/jni/com_android_bluetooth_hfpclient.cpp b/jni/com_android_bluetooth_hfpclient.cpp
index 83c6b20..763885e 100644
--- a/jni/com_android_bluetooth_hfpclient.cpp
+++ b/jni/com_android_bluetooth_hfpclient.cpp
@@ -22,10 +22,15 @@
 #include "hardware/bt_hf_client.h"
 #include "utils/Log.h"
 
+#include <shared_mutex>
+
 namespace android {
 
 static bthf_client_interface_t* sBluetoothHfpClientInterface = NULL;
+static std::shared_mutex interface_mutex;
+
 static jobject mCallbacksObj = NULL;
+static std::shared_mutex callbacks_mutex;
 
 static jmethodID method_onConnectionStateChanged;
 static jmethodID method_onAudioStateChanged;
@@ -68,8 +73,9 @@
                                 bthf_client_connection_state_t state,
                                 unsigned int peer_feat,
                                 unsigned int chld_feat) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -82,8 +88,9 @@
 
 static void audio_state_cb(const RawAddress* bd_addr,
                            bthf_client_audio_state_t state) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -93,8 +100,9 @@
 }
 
 static void vr_cmd_cb(const RawAddress* bd_addr, bthf_client_vr_state_t state) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -105,8 +113,9 @@
 
 static void network_state_cb(const RawAddress* bd_addr,
                              bthf_client_network_state_t state) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -117,7 +126,9 @@
 
 static void network_roaming_cb(const RawAddress* bd_addr,
                                bthf_client_service_type_t type) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -127,8 +138,9 @@
 }
 
 static void network_signal_cb(const RawAddress* bd_addr, int signal) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -138,8 +150,9 @@
 }
 
 static void battery_level_cb(const RawAddress* bd_addr, int level) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -149,8 +162,9 @@
 }
 
 static void current_operator_cb(const RawAddress* bd_addr, const char* name) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -169,8 +183,9 @@
 }
 
 static void call_cb(const RawAddress* bd_addr, bthf_client_call_t call) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -181,8 +196,9 @@
 
 static void callsetup_cb(const RawAddress* bd_addr,
                          bthf_client_callsetup_t callsetup) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -197,8 +213,9 @@
 
 static void callheld_cb(const RawAddress* bd_addr,
                         bthf_client_callheld_t callheld) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -209,8 +226,9 @@
 
 static void resp_and_hold_cb(const RawAddress* bd_addr,
                              bthf_client_resp_and_hold_t resp_and_hold) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -220,8 +238,9 @@
 }
 
 static void clip_cb(const RawAddress* bd_addr, const char* number) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -240,8 +259,9 @@
 }
 
 static void call_waiting_cb(const RawAddress* bd_addr, const char* number) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -264,8 +284,9 @@
                              bthf_client_call_state_t state,
                              bthf_client_call_mpty_type_t mpty,
                              const char* number) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -285,8 +306,9 @@
 
 static void volume_change_cb(const RawAddress* bd_addr,
                              bthf_client_volume_type_t type, int volume) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -296,8 +318,9 @@
 
 static void cmd_complete_cb(const RawAddress* bd_addr,
                             bthf_client_cmd_complete_t type, int cme) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -307,8 +330,9 @@
 
 static void subscriber_info_cb(const RawAddress* bd_addr, const char* name,
                                bthf_client_subscriber_service_type_t type) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -328,8 +352,9 @@
 
 static void in_band_ring_cb(const RawAddress* bd_addr,
                             bthf_client_in_band_ring_state_t in_band) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -339,8 +364,9 @@
 
 static void last_voice_tag_number_cb(const RawAddress* bd_addr,
                                      const char* number) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -359,8 +385,9 @@
 }
 
 static void ring_indication_cb(const RawAddress* bd_addr) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -370,8 +397,9 @@
 
 static void unknown_event_cb(const RawAddress* bd_addr,
                              const char* eventString) {
+  std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
   CallbackEnv sCallbackEnv(__func__);
-  if (!sCallbackEnv.valid()) return;
+  if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
 
   ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
   if (!addr.get()) return;
@@ -446,6 +474,9 @@
 
 static void initializeNative(JNIEnv* env, jobject object) {
   ALOGD("%s: HfpClient", __func__);
+  std::unique_lock<std::shared_mutex> interface_lock(interface_mutex);
+  std::unique_lock<std::shared_mutex> callbacks_lock(callbacks_mutex);
+
   const bt_interface_t* btInf = getBluetoothInterface();
   if (btInf == NULL) {
     ALOGE("Bluetooth module is not loaded");
@@ -484,6 +515,9 @@
 }
 
 static void cleanupNative(JNIEnv* env, jobject object) {
+  std::unique_lock<std::shared_mutex> interface_lock(interface_mutex);
+  std::unique_lock<std::shared_mutex> callbacks_lock(callbacks_mutex);
+
   const bt_interface_t* btInf = getBluetoothInterface();
   if (btInf == NULL) {
     ALOGE("Bluetooth module is not loaded");
@@ -504,6 +538,7 @@
 }
 
 static jboolean connectNative(JNIEnv* env, jobject object, jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -522,6 +557,7 @@
 
 static jboolean disconnectNative(JNIEnv* env, jobject object,
                                  jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -541,6 +577,7 @@
 
 static jboolean connectAudioNative(JNIEnv* env, jobject object,
                                    jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -560,6 +597,7 @@
 
 static jboolean disconnectAudioNative(JNIEnv* env, jobject object,
                                       jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -579,6 +617,7 @@
 
 static jboolean startVoiceRecognitionNative(JNIEnv* env, jobject object,
                                             jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -598,6 +637,7 @@
 
 static jboolean stopVoiceRecognitionNative(JNIEnv* env, jobject object,
                                            jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -617,6 +657,7 @@
 
 static jboolean setVolumeNative(JNIEnv* env, jobject object, jbyteArray address,
                                 jint volume_type, jint volume) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -636,6 +677,7 @@
 
 static jboolean dialNative(JNIEnv* env, jobject object, jbyteArray address,
                            jstring number_str) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -664,6 +706,7 @@
 
 static jboolean dialMemoryNative(JNIEnv* env, jobject object,
                                  jbyteArray address, jint location) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -685,6 +728,7 @@
 static jboolean handleCallActionNative(JNIEnv* env, jobject object,
                                        jbyteArray address, jint action,
                                        jint index) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -705,6 +749,7 @@
 
 static jboolean queryCurrentCallsNative(JNIEnv* env, jobject object,
                                         jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -725,6 +770,7 @@
 
 static jboolean queryCurrentOperatorNameNative(JNIEnv* env, jobject object,
                                                jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -746,6 +792,7 @@
 
 static jboolean retrieveSubscriberInfoNative(JNIEnv* env, jobject object,
                                              jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -766,6 +813,7 @@
 
 static jboolean sendDtmfNative(JNIEnv* env, jobject object, jbyteArray address,
                                jbyte code) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -786,6 +834,7 @@
 
 static jboolean requestLastVoiceTagNumberNative(JNIEnv* env, jobject object,
                                                 jbyteArray address) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -809,6 +858,7 @@
 static jboolean sendATCmdNative(JNIEnv* env, jobject object, jbyteArray address,
                                 jint cmd, jint val1, jint val2,
                                 jstring arg_str) {
+  std::shared_lock<std::shared_mutex> lock(interface_mutex);
   if (!sBluetoothHfpClientInterface) return JNI_FALSE;
 
   jbyte* addr = env->GetByteArrayElements(address, NULL);
diff --git a/jni/com_android_bluetooth_le_audio.cpp b/jni/com_android_bluetooth_le_audio.cpp
index 7b4036c..659a694 100644
--- a/jni/com_android_bluetooth_le_audio.cpp
+++ b/jni/com_android_bluetooth_le_audio.cpp
@@ -218,6 +218,47 @@
   return JNI_TRUE;
 }
 
+static jboolean groupAddNodeNative(JNIEnv* env, jobject object, jint group_id,
+                                   jbyteArray address) {
+  jbyte* addr = env->GetByteArrayElements(address, nullptr);
+
+  if (!sLeAudioClientInterface) {
+    LOG(ERROR) << __func__ << ": Failed to get the Bluetooth LeAudio Interface";
+    return JNI_FALSE;
+  }
+
+  if (!addr) {
+    jniThrowIOException(env, EINVAL);
+    return JNI_FALSE;
+  }
+
+  RawAddress* tmpraw = (RawAddress*)addr;
+  sLeAudioClientInterface->GroupAddNode(group_id, *tmpraw);
+  env->ReleaseByteArrayElements(address, addr, 0);
+
+  return JNI_TRUE;
+}
+
+static jboolean groupRemoveNodeNative(JNIEnv* env, jobject object,
+                                      jint group_id, jbyteArray address) {
+
+  if (!sLeAudioClientInterface) {
+    LOG(ERROR) << __func__ << ": Failed to get the Bluetooth LeAudio Interface";
+    return JNI_FALSE;
+  }
+
+  jbyte* addr = env->GetByteArrayElements(address, nullptr);
+  if (!addr) {
+    jniThrowIOException(env, EINVAL);
+    return JNI_FALSE;
+  }
+
+  RawAddress* tmpraw = (RawAddress*)addr;
+  sLeAudioClientInterface->GroupRemoveNode(group_id, *tmpraw);
+  env->ReleaseByteArrayElements(address, addr, 0);
+  return JNI_TRUE;
+}
+
 static void groupSetActiveNative(JNIEnv* env, jobject object, jint group_id) {
   LOG(INFO) << __func__;
 
@@ -235,6 +276,8 @@
     {"cleanupNative", "()V", (void*)cleanupNative},
     {"connectLeAudioNative", "([B)Z", (void*)connectLeAudioNative},
     {"disconnectLeAudioNative", "([B)Z", (void*)disconnectLeAudioNative},
+    {"groupAddNodeNative", "(I[B)Z", (void*)groupAddNodeNative},
+    {"groupRemoveNodeNative", "(I[B)Z", (void*)groupRemoveNodeNative},
     {"groupSetActiveNative", "(I)V", (void*)groupSetActiveNative},
 };
 
diff --git a/res/values/config.xml b/res/values/config.xml
index b99b8fb..1b0a315 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -125,6 +125,9 @@
     <!-- Flag whether or not to keep polling AG with CLCC for call information every 2 seconds -->
     <bool name="hfp_clcc_poll_during_call">true</bool>
 
+    <!-- Time delay in milliseconds between consecutive polling AG with CLCC for call info -->
+    <integer name="hfp_clcc_poll_interval_during_call">2000</integer>
+
     <!-- Package that is providing the exposure notification service -->
     <string name="exposure_notification_package">com.google.android.gms</string>
 
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface.java
new file mode 100644
index 0000000..75c34d8
--- /dev/null
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.bluetooth.a2dpsink;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * A2DP Sink Native Interface to/from JNI.
+ */
+public class A2dpSinkNativeInterface {
+    private static final String TAG = "A2dpSinkNativeInterface";
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    private BluetoothAdapter mAdapter;
+
+    @GuardedBy("INSTANCE_LOCK")
+    private static A2dpSinkNativeInterface sInstance;
+    private static final Object INSTANCE_LOCK = new Object();
+
+    static {
+        classInitNative();
+    }
+
+    private A2dpSinkNativeInterface() {
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        if (mAdapter == null) {
+            Log.wtf(TAG, "No Bluetooth Adapter Available");
+        }
+    }
+
+    /**
+     * Get singleton instance.
+     */
+    public static A2dpSinkNativeInterface getInstance() {
+        synchronized (INSTANCE_LOCK) {
+            if (sInstance == null) {
+                sInstance = new A2dpSinkNativeInterface();
+            }
+            return sInstance;
+        }
+    }
+
+    /**
+     * Initializes the native interface and sets the max number of connected devices
+     *
+     * @param maxConnectedAudioDevices The maximum number of devices that can be connected at once
+     */
+    public void init(int maxConnectedAudioDevices) {
+        initNative(maxConnectedAudioDevices);
+    }
+
+    /**
+     * Cleanup the native interface.
+     */
+    public void cleanup() {
+        cleanupNative();
+    }
+
+    /**
+     * Initiates an A2DP connection to a remote device.
+     *
+     * @param device the remote device
+     * @return true on success, otherwise false.
+     */
+    public boolean connectA2dpSink(BluetoothDevice device) {
+        return connectA2dpNative(Utils.getByteAddress(device));
+    }
+
+    /**
+     * Disconnects A2DP from a remote device.
+     *
+     * @param device the remote device
+     * @return true on success, otherwise false.
+     */
+    public boolean disconnectA2dpSink(BluetoothDevice device) {
+        return disconnectA2dpNative(Utils.getByteAddress(device));
+    }
+
+    /**
+     * Set a BluetoothDevice as the active device
+     *
+     * The active device is the only one that will receive passthrough commands and the only one
+     * that will have its audio decoded.
+     *
+     * Sending null for the active device will make no device active.
+     *
+     * @param device
+     * @return True if the active device request has been scheduled
+     */
+    public boolean setActiveDevice(BluetoothDevice device) {
+        // Translate to byte address for JNI. Use an all 0 MAC for no active device
+        byte[] address = null;
+        if (device != null) {
+            address = Utils.getByteAddress(device);
+        } else {
+            address = Utils.getBytesFromAddress("00:00:00:00:00:00");
+        }
+        return setActiveDeviceNative(address);
+    }
+
+    /**
+     * Inform A2DP decoder of the current audio focus
+     *
+     * @param focusGranted
+     */
+    public void informAudioFocusState(int focusGranted) {
+        informAudioFocusStateNative(focusGranted);
+    }
+
+    /**
+     * Inform A2DP decoder the desired audio gain
+     *
+     * @param gain
+     */
+    public void informAudioTrackGain(float gain) {
+        informAudioTrackGainNative(gain);
+    }
+
+    /**
+     * Send a stack event up to the A2DP Sink Service
+     */
+    private void sendMessageToService(StackEvent event) {
+        A2dpSinkService service = A2dpSinkService.getA2dpSinkService();
+        if (service != null) {
+            service.messageFromNative(event);
+        } else {
+            Log.e(TAG, "Event ignored, service not available: " + event);
+        }
+    }
+
+    /**
+     * For the JNI to send messages about connection state changes
+     */
+    public void onConnectionStateChanged(byte[] address, int state) {
+        StackEvent event =
+                StackEvent.connectionStateChanged(mAdapter.getRemoteDevice(address), state);
+        if (DBG) {
+            Log.d(TAG, "onConnectionStateChanged: " + event);
+        }
+        sendMessageToService(event);
+    }
+
+    /**
+     * For the JNI to send messages about audio stream state changes
+     */
+    public void onAudioStateChanged(byte[] address, int state) {
+        StackEvent event = StackEvent.audioStateChanged(mAdapter.getRemoteDevice(address), state);
+        if (DBG) {
+            Log.d(TAG, "onAudioStateChanged: " + event);
+        }
+        sendMessageToService(event);
+    }
+
+    /**
+     * For the JNI to send messages about audio configuration changes
+     */
+    public void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
+        StackEvent event = StackEvent.audioConfigChanged(
+                mAdapter.getRemoteDevice(address), sampleRate, channelCount);
+        if (DBG) {
+            Log.d(TAG, "onAudioConfigChanged: " + event);
+        }
+        sendMessageToService(event);
+    }
+
+    // Native methods that call into the JNI interface
+    private static native void classInitNative();
+    private native void initNative(int maxConnectedAudioDevices);
+    private native void cleanupNative();
+    private native boolean connectA2dpNative(byte[] address);
+    private native boolean disconnectA2dpNative(byte[] address);
+    private native boolean setActiveDeviceNative(byte[] address);
+    private native void informAudioFocusStateNative(int focusGranted);
+    private native void informAudioTrackGainNative(float gain);
+}
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
index 5e0a164..fc54444 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
@@ -49,7 +49,7 @@
 
     private AdapterService mAdapterService;
     private DatabaseManager mDatabaseManager;
-    protected Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
+    private Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
             new ConcurrentHashMap<>(1);
 
     private final Object mStreamHandlerLock = new Object();
@@ -60,9 +60,7 @@
     private A2dpSinkStreamHandler mA2dpSinkStreamHandler;
     private static A2dpSinkService sService;
 
-    static {
-        classInitNative();
-    }
+    A2dpSinkNativeInterface mNativeInterface;
 
     @Override
     protected boolean start() {
@@ -70,12 +68,15 @@
                 "AdapterService cannot be null when A2dpSinkService starts");
         mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),
                 "DatabaseManager cannot be null when A2dpSinkService starts");
+        mNativeInterface = A2dpSinkNativeInterface.getInstance();
+
+        mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();
+        mNativeInterface.init(mMaxConnectedAudioDevices);
 
         synchronized (mStreamHandlerLock) {
-            mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, this);
+            mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, mNativeInterface);
         }
-        mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();
-        initNative(mMaxConnectedAudioDevices);
+
         setA2dpSinkService(this);
         return true;
     }
@@ -83,7 +84,7 @@
     @Override
     protected boolean stop() {
         setA2dpSinkService(null);
-        cleanupNative();
+        mNativeInterface.cleanup();
         for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
             stateMachine.quitNow();
         }
@@ -117,16 +118,8 @@
      * Set the device that should be allowed to actively stream
      */
     public boolean setActiveDevice(BluetoothDevice device) {
-        // Translate to byte address for JNI. Use an all 0 MAC for no active device
-        byte[] address = null;
-        if (device != null) {
-            address = Utils.getByteAddress(device);
-        } else {
-            address = Utils.getBytesFromAddress("00:00:00:00:00:00");
-        }
-
         synchronized (mActiveDeviceLock) {
-            if (setActiveDeviceNative(address)) {
+            if (mNativeInterface.setActiveDevice(device)) {
                 mActiveDevice = device;
                 return true;
             }
@@ -340,6 +333,10 @@
                     + ", InstanceMap start state: " + sb.toString());
         }
 
+        if (device == null) {
+            throw new IllegalArgumentException("Null device");
+        }
+
         A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
         // a state machine instance doesn't exist. maybe it is already gone?
         if (stateMachine == null) {
@@ -356,7 +353,15 @@
         return true;
     }
 
-    void removeStateMachine(A2dpSinkStateMachine stateMachine) {
+    /**
+     * Remove a device's state machine.
+     *
+     * Called by the state machines when they disconnect.
+     *
+     * Visible for testing so it can be mocked and verified on.
+     */
+    @VisibleForTesting
+    public void removeStateMachine(A2dpSinkStateMachine stateMachine) {
         mDeviceStateMap.remove(stateMachine.getDevice());
     }
 
@@ -365,7 +370,8 @@
     }
 
     protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) {
-        A2dpSinkStateMachine newStateMachine = new A2dpSinkStateMachine(device, this);
+        A2dpSinkStateMachine newStateMachine =
+                new A2dpSinkStateMachine(device, this, mNativeInterface);
         A2dpSinkStateMachine existingStateMachine =
                 mDeviceStateMap.putIfAbsent(device, newStateMachine);
         // Given null is not a valid value in our map, ConcurrentHashMap will return null if the
@@ -377,6 +383,11 @@
         return existingStateMachine;
     }
 
+    @VisibleForTesting
+    protected A2dpSinkStateMachine getStateMachineForDevice(BluetoothDevice device) {
+        return mDeviceStateMap.get(device);
+    }
+
     List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
         if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
         List<BluetoothDevice> deviceList = new ArrayList<>();
@@ -406,6 +417,7 @@
      * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
      */
     public int getConnectionState(BluetoothDevice device) {
+        if (device == null) return BluetoothProfile.STATE_DISCONNECTED;
         A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
         return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED
                 : stateMachine.getState();
@@ -475,6 +487,7 @@
     }
 
     BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
+        if (device == null) return null;
         A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
         // a state machine instance doesn't exist. maybe it is already gone?
         if (stateMachine == null) {
@@ -483,53 +496,37 @@
         return stateMachine.getAudioConfig();
     }
 
-    /* JNI interfaces*/
-
-    private static native void classInitNative();
-
-    private native void initNative(int maxConnectedAudioDevices);
-
-    private native void cleanupNative();
-
-    native boolean connectA2dpNative(byte[] address);
-
-    native boolean disconnectA2dpNative(byte[] address);
-
     /**
-     * set A2DP state machine as the active device
-     * the active device is the only one that will receive passthrough commands and the only one
-     * that will have its audio decoded
-     *
-     * @hide
-     * @param address
-     * @return active device request has been scheduled
+     * Receive and route a stack event from the JNI
      */
-    public native boolean setActiveDeviceNative(byte[] address);
+    protected void messageFromNative(StackEvent event) {
+        switch (event.mType) {
+            case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                onConnectionStateChanged(event);
+                return;
+            case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
+                onAudioStateChanged(event);
+                return;
+            case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED:
+                onAudioConfigChanged(event);
+                return;
+            default:
+                Log.e(TAG, "Received unknown stack event of type " + event.mType);
+                return;
+        }
+    }
 
-    /**
-     * inform A2DP decoder of the current audio focus
-     *
-     * @param focusGranted
-     */
-    @VisibleForTesting
-    public native void informAudioFocusStateNative(int focusGranted);
-
-    /**
-     * inform A2DP decoder the desired audio gain
-     *
-     * @param gain
-     */
-    @VisibleForTesting
-    public native void informAudioTrackGainNative(float gain);
-
-    private void onConnectionStateChanged(byte[] address, int state) {
-        StackEvent event = StackEvent.connectionStateChanged(
-                BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address), state);
-        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
+    private void onConnectionStateChanged(StackEvent event) {
+        BluetoothDevice device = event.mDevice;
+        if (device == null) {
+            return;
+        }
+        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
         stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
     }
 
-    private void onAudioStateChanged(byte[] address, int state) {
+    private void onAudioStateChanged(StackEvent event) {
+        int state = event.mState;
         synchronized (mStreamHandlerLock) {
             if (mA2dpSinkStreamHandler == null) {
                 Log.e(TAG, "Received audio state change before we've been started");
@@ -541,15 +538,18 @@
                     || state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) {
                 mA2dpSinkStreamHandler.obtainMessage(
                         A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget();
+            } else {
+                Log.w(TAG, "Unhandled audio state change, state=" + state);
             }
         }
     }
 
-    private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
-        StackEvent event = StackEvent.audioConfigChanged(
-                BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address), sampleRate,
-                channelCount);
-        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
+    private void onAudioConfigChanged(StackEvent event) {
+        BluetoothDevice device = event.mDevice;
+        if (device == null) {
+            return;
+        }
+        A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
         stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
     }
 }
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
index 8fba61a..921753c 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
@@ -55,6 +55,7 @@
     protected final BluetoothDevice mDevice;
     protected final byte[] mDeviceAddress;
     protected final A2dpSinkService mService;
+    protected final A2dpSinkNativeInterface mNativeInterface;
     protected final Disconnected mDisconnected;
     protected final Connecting mConnecting;
     protected final Connected mConnected;
@@ -63,11 +64,13 @@
     protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
     protected BluetoothAudioConfig mAudioConfig = null;
 
-    A2dpSinkStateMachine(BluetoothDevice device, A2dpSinkService service) {
+    A2dpSinkStateMachine(BluetoothDevice device, A2dpSinkService service,
+            A2dpSinkNativeInterface nativeInterface) {
         super(TAG);
         mDevice = device;
         mDeviceAddress = Utils.getByteAddress(mDevice);
         mService = service;
+        mNativeInterface = nativeInterface;
         if (DBG) Log.d(TAG, device.toString());
 
         mDisconnected = new Disconnected();
@@ -83,10 +86,6 @@
         setInitialState(mDisconnected);
     }
 
-    protected String getConnectionStateChangedIntent() {
-        return BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED;
-    }
-
     /**
      * Get the current connection state
      *
@@ -177,7 +176,7 @@
                                     == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
                                 Log.w(TAG, "Ignore incoming connection, profile is"
                                         + " turned off for " + mDevice);
-                                mService.disconnectA2dpNative(mDeviceAddress);
+                                mNativeInterface.disconnectA2dpSink(mDevice);
                             } else {
                                 mConnecting.mIncomingConnection = true;
                                 transitionTo(mConnecting);
@@ -204,7 +203,7 @@
             sendMessageDelayed(CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS);
 
             if (!mIncomingConnection) {
-                mService.connectA2dpNative(mDeviceAddress);
+                mNativeInterface.connectA2dpSink(mDevice);
             }
 
             super.enter();
@@ -256,7 +255,7 @@
             switch (message.what) {
                 case DISCONNECT:
                     transitionTo(mDisconnecting);
-                    mService.disconnectA2dpNative(mDeviceAddress);
+                    mNativeInterface.disconnectA2dpSink(mDevice);
                     return true;
                 case STACK_EVENT:
                     processStackEvent((StackEvent) message.obj);
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
index 84a6fcd..2ba66a2 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
@@ -18,7 +18,6 @@
 
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadsetClientCall;
-import android.content.Context;
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
 import android.media.AudioFocusRequest;
@@ -81,7 +80,7 @@
 
     // Private variables.
     private A2dpSinkService mA2dpSinkService;
-    private Context mContext;
+    private A2dpSinkNativeInterface mNativeInterface;
     private AudioManager mAudioManager;
     // Keep track if the remote device is providing audio
     private boolean mStreamAvailable = false;
@@ -111,10 +110,11 @@
         }
     };
 
-    public A2dpSinkStreamHandler(A2dpSinkService a2dpSinkService, Context context) {
+    public A2dpSinkStreamHandler(A2dpSinkService a2dpSinkService,
+            A2dpSinkNativeInterface nativeInterface) {
         mA2dpSinkService = a2dpSinkService;
-        mContext = context;
-        mAudioManager = context.getSystemService(AudioManager.class);
+        mNativeInterface = nativeInterface;
+        mAudioManager = mA2dpSinkService.getSystemService(AudioManager.class);
     }
 
     /**
@@ -206,7 +206,7 @@
 
                     case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                         // Make the volume duck.
-                        int duckPercent = mContext.getResources()
+                        int duckPercent = mA2dpSinkService.getResources()
                                 .getInteger(R.integer.a2dp_sink_duck_percent);
                         if (duckPercent < 0 || duckPercent > 100) {
                             Log.e(TAG, "Invalid duck percent using default.");
@@ -300,7 +300,7 @@
                     .setUsage(AudioAttributes.USAGE_MEDIA)
                     .build();
 
-            mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent, attrs,
+            mMediaPlayer = MediaPlayer.create(mA2dpSinkService, R.raw.silent, attrs,
                     mAudioManager.generateAudioSessionId());
             if (mMediaPlayer == null) {
                 Log.e(TAG, "Failed to initialize media player. You may not get media key events");
@@ -342,18 +342,18 @@
     }
 
     private void startFluorideStreaming() {
-        mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_GRANTED);
-        mA2dpSinkService.informAudioTrackGainNative(1.0f);
+        mNativeInterface.informAudioFocusState(STATE_FOCUS_GRANTED);
+        mNativeInterface.informAudioTrackGain(1.0f);
         requestMediaKeyFocus();
     }
 
     private void stopFluorideStreaming() {
         releaseMediaKeyFocus();
-        mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_LOST);
+        mNativeInterface.informAudioFocusState(STATE_FOCUS_LOST);
     }
 
     private void setFluorideAudioTrackGain(float gain) {
-        mA2dpSinkService.informAudioTrackGainNative(gain);
+        mNativeInterface.informAudioTrackGain(gain);
     }
 
     private void sendAvrcpPause() {
@@ -380,20 +380,18 @@
         return false;
     }
 
-    synchronized int getAudioFocus() {
-        return mAudioFocus;
-    }
-
     private boolean isIotDevice() {
-        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_EMBEDDED);
+        return mA2dpSinkService.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_EMBEDDED);
     }
 
     private boolean isTvDevice() {
-        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
+        return mA2dpSinkService.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_LEANBACK);
     }
 
     private boolean shouldRequestFocus() {
-        return mContext.getResources()
+        return mA2dpSinkService.getResources()
                 .getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
     }
 
diff --git a/src/com/android/bluetooth/a2dpsink/StackEvent.java b/src/com/android/bluetooth/a2dpsink/StackEvent.java
index 68a6ce9..5d0947f 100644
--- a/src/com/android/bluetooth/a2dpsink/StackEvent.java
+++ b/src/com/android/bluetooth/a2dpsink/StackEvent.java
@@ -47,16 +47,24 @@
 
     @Override
     public String toString() {
+        String s = "StackEvent<device=" + mDevice + ", type =";
         switch (mType) {
             case EVENT_TYPE_CONNECTION_STATE_CHANGED:
-                return "EVENT_TYPE_CONNECTION_STATE_CHANGED " + mState;
+                s += "EVENT_TYPE_CONNECTION_STATE_CHANGED, state=" + mState;
+                break;
             case EVENT_TYPE_AUDIO_STATE_CHANGED:
-                return "EVENT_TYPE_AUDIO_STATE_CHANGED " + mState;
+                s += "EVENT_TYPE_AUDIO_STATE_CHANGED, state=" + mState;
+                break;
             case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
-                return "EVENT_TYPE_AUDIO_CONFIG_CHANGED " + mSampleRate + ":" + mChannelCount;
+                s += "EVENT_TYPE_AUDIO_CONFIG_CHANGED, sampleRate=" + mSampleRate
+                        + ", channelCount=" + mChannelCount;
+                break;
             default:
-                return "Unknown";
+                s += "Unknown";
+                break;
         }
+        s += ">";
+        return s;
     }
 
     static StackEvent connectionStateChanged(BluetoothDevice device, int state) {
diff --git a/src/com/android/bluetooth/audio_util/helpers/PlayStatus.java b/src/com/android/bluetooth/audio_util/helpers/PlayStatus.java
index 922b151..75a760b 100644
--- a/src/com/android/bluetooth/audio_util/helpers/PlayStatus.java
+++ b/src/com/android/bluetooth/audio_util/helpers/PlayStatus.java
@@ -31,7 +31,7 @@
     static final byte REV_SEEK = 4;
     static final byte ERROR = -1;
 
-    public long position = 0xFFFFFFFFFFFFFFFFL;
+    public long position = 0;
     public long duration = 0x00L;
     public byte state = STOPPED;
 
@@ -41,7 +41,7 @@
         if (state == null) return ret;
 
         ret.state = playbackStateToAvrcpState(state.getState());
-        ret.position = state.getPosition();
+        ret.position = (state.getPosition() > 0) ? state.getPosition() : 0;
         ret.duration = duration;
         return ret;
     }
diff --git a/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java b/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java
index c834a5f..39f1f14 100644
--- a/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java
+++ b/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java
@@ -22,6 +22,7 @@
 import android.bluetooth.BluetoothDevice;
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceCallback;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
@@ -76,7 +77,11 @@
     private void switchVolumeDevice(@NonNull BluetoothDevice device) {
         // Inform the audio manager that the device has changed
         d("switchVolumeDevice: Set Absolute volume support to " + mDeviceMap.get(device));
-        mAudioManager.avrcpSupportsAbsoluteVolume(device.getAddress(), mDeviceMap.get(device));
+        mAudioManager.setDeviceVolumeBehavior(new AudioDeviceAttributes(
+                    AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
+                    device.getAddress()),
+                 mDeviceMap.get(device) ? AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE
+                 : AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE);
 
         // Get the current system volume and try to get the preference volume
         int savedVolume = getVolume(device, sNewDeviceVolume);
diff --git a/src/com/android/bluetooth/btservice/AdapterService.java b/src/com/android/bluetooth/btservice/AdapterService.java
index d615402..fc8906f 100644
--- a/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/src/com/android/bluetooth/btservice/AdapterService.java
@@ -456,8 +456,6 @@
         mJniCallbacks = new JniCallbacks(this, mAdapterProperties);
         mBluetoothKeystoreService = new BluetoothKeystoreService(isCommonCriteriaMode());
         mBluetoothKeystoreService.start();
-        mActivityAttributionService = new ActivityAttributionService();
-        mActivityAttributionService.start();
         int configCompareResult = mBluetoothKeystoreService.getCompareResult();
 
         // Start tracking Binder latency for the bluetooth process.
@@ -512,6 +510,9 @@
 
         mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this);
 
+        mActivityAttributionService = new ActivityAttributionService();
+        mActivityAttributionService.start();
+
         setAdapterService(this);
 
         invalidateBluetoothCaches();
@@ -820,10 +821,6 @@
             mSdpManager = null;
         }
 
-        if (mBluetoothKeystoreService != null) {
-            mBluetoothKeystoreService.cleanup();
-        }
-
         if (mActivityAttributionService != null) {
             mActivityAttributionService.cleanup();
         }
@@ -842,6 +839,11 @@
             mJniCallbacks.cleanup();
         }
 
+        if (mBluetoothKeystoreService != null) {
+            debugLog("cleanup(): mBluetoothKeystoreService.cleanup()");
+            mBluetoothKeystoreService.cleanup();
+        }
+
         if (mPhonePolicy != null) {
             mPhonePolicy.cleanup();
         }
@@ -2376,6 +2378,34 @@
         }
 
         @Override
+        public int isCisCentralSupported() {
+            AdapterService service = getService();
+            if (service == null) {
+                return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+            }
+
+            if (service.mAdapterProperties.isLeConnectedIsochronousStreamCentralSupported()) {
+                return BluetoothStatusCodes.SUCCESS;
+            }
+
+            return BluetoothStatusCodes.ERROR_FEATURE_NOT_SUPPORTED;
+        }
+
+        @Override
+        public int isLePeriodicAdvertisingSyncTransferSenderSupported() {
+            AdapterService service = getService();
+            if (service == null) {
+                return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+            }
+
+            if (service.mAdapterProperties.isLePeriodicAdvertisingSyncTransferSenderSupported()) {
+                return BluetoothStatusCodes.SUCCESS;
+            }
+
+            return BluetoothStatusCodes.ERROR_FEATURE_NOT_SUPPORTED;
+        }
+
+        @Override
         public int getLeMaximumAdvertisingDataLength() {
             AdapterService service = getService();
             if (service == null) {
@@ -3394,12 +3424,12 @@
             }
 
             // Copy the traffic objects whose byte counts are > 0
-            final UidTraffic[] result = arrayLen > 0 ? new UidTraffic[arrayLen] : null;
+            final List<UidTraffic> result = new ArrayList<>();
             int putIdx = 0;
             for (int i = 0; i < mUidTraffic.size(); i++) {
                 final UidTraffic traffic = mUidTraffic.valueAt(i);
                 if (traffic.getTxBytes() != 0 || traffic.getRxBytes() != 0) {
-                    result[putIdx++] = traffic.clone();
+                    result.add(traffic.clone());
                 }
             }
 
@@ -3716,6 +3746,7 @@
     private static final String GD_L2CAP_FLAG = "INIT_gd_l2cap";
     private static final String GD_RUST_FLAG = "INIT_gd_rust";
     private static final String GD_LINK_POLICY_FLAG = "INIT_gd_link_policy";
+    private static final String GATT_ROBUST_CACHING_FLAG = "INIT_gatt_robust_caching";
 
     /**
      * Logging flags logic (only applies to DEBUG and VERBOSE levels):
@@ -3768,6 +3799,9 @@
         if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_LINK_POLICY_FLAG, false)) {
             initFlags.add(String.format("%s=%s", GD_LINK_POLICY_FLAG, "true"));
         }
+        if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GATT_ROBUST_CACHING_FLAG, true)) {
+            initFlags.add(String.format("%s=%s", GATT_ROBUST_CACHING_FLAG, "true"));
+        }
         if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH,
                 LOGGING_DEBUG_ENABLED_FOR_ALL_FLAG, false)) {
             initFlags.add(String.format("%s=%s", LOGGING_DEBUG_ENABLED_FOR_ALL_FLAG, "true"));
@@ -3784,7 +3818,7 @@
             initFlags.add(String.format("%s=%s", LOGGING_DEBUG_DISABLED_FOR_TAGS_FLAG,
                     debugLoggingDisabledTags));
         }
-        if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, BTAA_HCI_LOG_FLAG, false)) {
+        if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, BTAA_HCI_LOG_FLAG, true)) {
             initFlags.add(String.format("%s=%s", BTAA_HCI_LOG_FLAG, "true"));
         }
         return initFlags.toArray(new String[0]);
diff --git a/src/com/android/bluetooth/btservice/AdapterState.java b/src/com/android/bluetooth/btservice/AdapterState.java
index fd81208..7216d78 100644
--- a/src/com/android/bluetooth/btservice/AdapterState.java
+++ b/src/com/android/bluetooth/btservice/AdapterState.java
@@ -18,9 +18,9 @@
 
 import android.bluetooth.BluetoothAdapter;
 import android.os.Message;
+import android.os.SystemProperties;
 import android.util.Log;
 
-import com.android.bluetooth.R;
 import com.android.bluetooth.telephony.BluetoothInCallService;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
@@ -72,6 +72,11 @@
     static final int BLE_STOP_TIMEOUT = 11;
     static final int BLE_START_TIMEOUT = 12;
 
+    static final String BLE_START_TIMEOUT_DELAY_PROPERTY =
+            "ro.bluetooth.ble_start_timeout_delay";
+    static final String BLE_STOP_TIMEOUT_DELAY_PROPERTY =
+            "ro.bluetooth.ble_stop_timeout_delay";
+
     static final int BLE_START_TIMEOUT_DELAY = 4000;
     static final int BLE_STOP_TIMEOUT_DELAY = 1000;
     static final int BREDR_START_TIMEOUT_DELAY = 4000;
@@ -264,7 +269,10 @@
         @Override
         public void enter() {
             super.enter();
-            sendMessageDelayed(BLE_START_TIMEOUT, BLE_START_TIMEOUT_DELAY);
+            final int timeoutDelay = SystemProperties.getInt(
+                    BLE_START_TIMEOUT_DELAY_PROPERTY, BLE_START_TIMEOUT_DELAY);
+            Log.d(TAG, "Start Timeout Delay: " + timeoutDelay);
+            sendMessageDelayed(BLE_START_TIMEOUT, timeoutDelay);
             mAdapterService.bringUpBle();
         }
 
@@ -385,7 +393,10 @@
         public void enter() {
             super.enter();
             mAdapterService.enableBluetoothInCallService(false);
-            sendMessageDelayed(BLE_STOP_TIMEOUT, BLE_STOP_TIMEOUT_DELAY);
+            final int timeoutDelay = SystemProperties.getInt(
+                    BLE_STOP_TIMEOUT_DELAY_PROPERTY, BLE_STOP_TIMEOUT_DELAY);
+            Log.d(TAG, "Stop Timeout Delay: " + timeoutDelay);
+            sendMessageDelayed(BLE_STOP_TIMEOUT, timeoutDelay);
             mAdapterService.bringDownBle();
         }
 
diff --git a/src/com/android/bluetooth/btservice/RemoteDevices.java b/src/com/android/bluetooth/btservice/RemoteDevices.java
index a28105a..2f0a8d4 100644
--- a/src/com/android/bluetooth/btservice/RemoteDevices.java
+++ b/src/com/android/bluetooth/btservice/RemoteDevices.java
@@ -600,6 +600,9 @@
                             if (sAdapterService.getState() == BluetoothAdapter.STATE_ON) {
                                 sAdapterService.deviceUuidUpdated(bdDevice);
                                 sendUuidIntent(bdDevice, device);
+                            } else if (sAdapterService.getState()
+                                    == BluetoothAdapter.STATE_BLE_ON) {
+                                sAdapterService.deviceUuidUpdated(bdDevice);
                             }
                             break;
                         case AbstractionLayer.BT_PROPERTY_TYPE_OF_DEVICE:
diff --git a/src/com/android/bluetooth/btservice/bluetoothKeystore/BluetoothKeystoreService.java b/src/com/android/bluetooth/btservice/bluetoothKeystore/BluetoothKeystoreService.java
index df40e35..3375797 100644
--- a/src/com/android/bluetooth/btservice/bluetoothKeystore/BluetoothKeystoreService.java
+++ b/src/com/android/bluetooth/btservice/bluetoothKeystore/BluetoothKeystoreService.java
@@ -192,7 +192,6 @@
             debugLog("cleanup() called before start()");
             return;
         }
-
         // Mark service as stopped
         setBluetoothKeystoreService(null);
 
@@ -213,7 +212,6 @@
     @VisibleForTesting
     public void cleanupForCommonCriteriaModeEnable() {
         try {
-            Thread.sleep(100);
             setEncryptKeyOrRemoveKey(CONFIG_FILE_PREFIX, CONFIG_FILE_HASH);
         } catch (InterruptedException e) {
             reportBluetoothKeystoreException(e, "Interrupted while operating.");
@@ -362,16 +360,10 @@
             if (decryptedString.isEmpty()) {
                 cleanupAll();
             } else if (decryptedString.equals(CONFIG_FILE_HASH)) {
-                backupConfigEncryptionFile();
                 readHashFile(CONFIG_FILE_PATH, CONFIG_FILE_PREFIX);
-                //save Map
-                if (mNameDecryptKey.containsKey(CONFIG_FILE_PREFIX)
-                        && mNameDecryptKey.get(CONFIG_FILE_PREFIX).equals(
-                        mNameDecryptKey.get(CONFIG_BACKUP_PREFIX))) {
-                    infoLog("Since the hash is same with previous, don't need encrypt again.");
-                } else {
-                    mPendingEncryptKey.put(prefixString);
-                }
+                mPendingEncryptKey.put(CONFIG_FILE_PREFIX);
+                readHashFile(CONFIG_BACKUP_PATH, CONFIG_BACKUP_PREFIX);
+                mPendingEncryptKey.put(CONFIG_BACKUP_PREFIX);
                 saveEncryptedKey();
             }
             return;
@@ -474,6 +466,7 @@
             }
             if (!keyEncryptedLines.isEmpty()) {
                 Files.write(Paths.get(CONFIG_FILE_ENCRYPTION_PATH), keyEncryptedLines);
+                Files.write(Paths.get(CONFIG_BACKUP_ENCRYPTION_PATH), keyEncryptedLines);
             }
         } catch (IOException e) {
             throw new RuntimeException("write encryption file fail");
@@ -503,20 +496,6 @@
         return mNameDecryptKey;
     }
 
-    private void backupConfigEncryptionFile() throws IOException {
-        if (Files.exists(Paths.get(CONFIG_FILE_ENCRYPTION_PATH))) {
-            Files.move(Paths.get(CONFIG_FILE_ENCRYPTION_PATH),
-                    Paths.get(CONFIG_BACKUP_ENCRYPTION_PATH),
-                    StandardCopyOption.REPLACE_EXISTING);
-        }
-        if (mNameEncryptKey.containsKey(CONFIG_FILE_PREFIX)) {
-            mNameEncryptKey.put(CONFIG_BACKUP_PREFIX, mNameEncryptKey.get(CONFIG_FILE_PREFIX));
-        }
-        if (mNameDecryptKey.containsKey(CONFIG_FILE_PREFIX)) {
-            mNameDecryptKey.put(CONFIG_BACKUP_PREFIX, mNameDecryptKey.get(CONFIG_FILE_PREFIX));
-        }
-    }
-
     private boolean doesComparePass(int item) {
         return (mCompareResult & item) == item;
     }
diff --git a/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java b/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
index e34dca1..61f4cdf 100644
--- a/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
+++ b/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
@@ -592,6 +592,10 @@
         }
 
         ParcelUuid uuid = mGroupIdToUuidMap.get(groupId);
+        if (mCallbacks.get(uuid) == null) {
+            Log.e(TAG, " There is no clients for uuid: " + uuid);
+            return;
+        }
 
         for (Map.Entry<Executor, IBluetoothCsipSetCoordinatorCallback> entry :
                 mCallbacks.get(uuid).entrySet()) {
diff --git a/src/com/android/bluetooth/gatt/CallbackInfo.java b/src/com/android/bluetooth/gatt/CallbackInfo.java
index 330422d..f6035b3 100644
--- a/src/com/android/bluetooth/gatt/CallbackInfo.java
+++ b/src/com/android/bluetooth/gatt/CallbackInfo.java
@@ -26,16 +26,38 @@
     public String address;
     public int status;
     public int handle;
+    public byte[] value;
 
-    CallbackInfo(String address, int status, int handle) {
+    static class Builder {
+        private String mAddress;
+        private int mStatus;
+        private int mHandle;
+        private byte[] mValue;
+
+        Builder(String address, int status) {
+            mAddress = address;
+            mStatus = status;
+        }
+
+        Builder setHandle(int handle) {
+            mHandle = handle;
+            return this;
+        }
+
+        Builder setValue(byte[] value) {
+            mValue = value;
+            return this;
+        }
+
+        CallbackInfo build() {
+            return new CallbackInfo(mAddress, mStatus, mHandle, mValue);
+        }
+    }
+
+    private CallbackInfo(String address, int status, int handle, byte[] value) {
         this.address = address;
         this.status = status;
         this.handle = handle;
     }
-
-    CallbackInfo(String address, int status) {
-        this.address = address;
-        this.status = status;
-    }
 }
 
diff --git a/src/com/android/bluetooth/gatt/GattService.java b/src/com/android/bluetooth/gatt/GattService.java
index 17f7fca..e7b6aac 100644
--- a/src/com/android/bluetooth/gatt/GattService.java
+++ b/src/com/android/bluetooth/gatt/GattService.java
@@ -30,6 +30,7 @@
 import android.bluetooth.BluetoothGattDescriptor;
 import android.bluetooth.BluetoothGattService;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
 import android.bluetooth.IBluetoothGatt;
 import android.bluetooth.IBluetoothGattCallback;
 import android.bluetooth.IBluetoothGattServerCallback;
@@ -734,7 +735,7 @@
                 int authReq, byte[] value, AttributionSource attributionSource) {
             GattService service = getService();
             if (service == null) {
-                return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+                return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
             }
             return service.writeCharacteristic(clientIf, address, handle, writeType, authReq, value,
                     attributionSource);
@@ -751,13 +752,14 @@
         }
 
         @Override
-        public void writeDescriptor(int clientIf, String address, int handle, int authReq,
+        public int writeDescriptor(int clientIf, String address, int handle, int authReq,
                 byte[] value, AttributionSource attributionSource) {
             GattService service = getService();
             if (service == null) {
-                return;
+                return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
             }
-            service.writeDescriptor(clientIf, address, handle, authReq, value, attributionSource);
+            return service.writeDescriptor(clientIf, address, handle, authReq, value,
+                    attributionSource);
         }
 
         @Override
@@ -1694,7 +1696,8 @@
         }
     }
 
-    void onWriteCharacteristic(int connId, int status, int handle) throws RemoteException {
+    void onWriteCharacteristic(int connId, int status, int handle, byte[] data)
+            throws RemoteException {
         String address = mClientMap.addressByConnId(connId);
         synchronized (mPermits) {
             Log.d(TAG, "onWriteCharacteristic() - increasing permit for address="
@@ -1703,7 +1706,8 @@
         }
 
         if (VDBG) {
-            Log.d(TAG, "onWriteCharacteristic() - address=" + address + ", status=" + status);
+            Log.d(TAG, "onWriteCharacteristic() - address=" + address + ", status=" + status
+                    + ", length=" + data.length);
         }
 
         ClientMap.App app = mClientMap.getByConnId(connId);
@@ -1712,12 +1716,15 @@
         }
 
         if (!app.isCongested) {
-            app.callback.onCharacteristicWrite(address, status, handle);
+            app.callback.onCharacteristicWrite(address, status, handle, data);
         } else {
             if (status == BluetoothGatt.GATT_CONNECTION_CONGESTED) {
                 status = BluetoothGatt.GATT_SUCCESS;
             }
-            CallbackInfo callbackInfo = new CallbackInfo(address, status, handle);
+            CallbackInfo callbackInfo = new CallbackInfo.Builder(address, status)
+                    .setHandle(handle)
+                    .setValue(data)
+                    .build();
             app.queueCallback(callbackInfo);
         }
     }
@@ -1749,16 +1756,18 @@
         }
     }
 
-    void onWriteDescriptor(int connId, int status, int handle) throws RemoteException {
+    void onWriteDescriptor(int connId, int status, int handle, byte[] data)
+            throws RemoteException {
         String address = mClientMap.addressByConnId(connId);
 
         if (VDBG) {
-            Log.d(TAG, "onWriteDescriptor() - address=" + address + ", status=" + status);
+            Log.d(TAG, "onWriteDescriptor() - address=" + address + ", status=" + status
+                    + ", length=" + data.length);
         }
 
         ClientMap.App app = mClientMap.getByConnId(connId);
         if (app != null) {
-            app.callback.onDescriptorWrite(address, status, handle);
+            app.callback.onDescriptorWrite(address, status, handle, data);
         }
     }
 
@@ -2181,7 +2190,7 @@
                     return;
                 }
                 app.callback.onCharacteristicWrite(callbackInfo.address, callbackInfo.status,
-                        callbackInfo.handle);
+                        callbackInfo.handle, callbackInfo.value);
             }
         }
     }
@@ -2915,7 +2924,7 @@
             byte[] value, AttributionSource attributionSource) {
         if (!Utils.checkConnectPermissionForDataDelivery(
                 this, attributionSource, "GattService writeCharacteristic")) {
-            return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+            return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION;
         }
 
         if (VDBG) {
@@ -2929,12 +2938,12 @@
         Integer connId = mClientMap.connIdByAddress(clientIf, address);
         if (connId == null) {
             Log.e(TAG, "writeCharacteristic() - No connection for " + address + "...");
-            return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+            return BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED;
         }
 
         if (!permissionCheck(connId, handle)) {
             Log.w(TAG, "writeCharacteristic() - permission check failed!");
-            return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+            return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_PRIVILEGED_PERMISSION;
         }
 
         Log.d(TAG, "writeCharacteristic() - trying to acquire permit.");
@@ -2943,19 +2952,19 @@
             AtomicBoolean atomicBoolean = mPermits.get(address);
             if (atomicBoolean == null) {
                 Log.d(TAG, "writeCharacteristic() -  atomicBoolean uninitialized!");
-                return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+                return BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED;
             }
 
             boolean success = atomicBoolean.get();
             if (!success) {
-                 Log.d(TAG, "writeCharacteristic() - no permit available.");
-                 return BluetoothGatt.GATT_WRITE_REQUEST_BUSY;
+                Log.d(TAG, "writeCharacteristic() - no permit available.");
+                return BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY;
             }
             atomicBoolean.set(false);
         }
 
         gattClientWriteCharacteristicNative(connId, handle, writeType, authReq, value);
-        return BluetoothGatt.GATT_WRITE_REQUEST_SUCCESS;
+        return BluetoothStatusCodes.SUCCESS;
     }
 
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
@@ -2985,11 +2994,11 @@
     }
 
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
-    void writeDescriptor(int clientIf, String address, int handle, int authReq, byte[] value,
+    int writeDescriptor(int clientIf, String address, int handle, int authReq, byte[] value,
             AttributionSource attributionSource) {
         if (!Utils.checkConnectPermissionForDataDelivery(
                 this, attributionSource, "GattService writeDescriptor")) {
-            return;
+            return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION;
         }
         if (VDBG) {
             Log.d(TAG, "writeDescriptor() - address=" + address);
@@ -2998,15 +3007,16 @@
         Integer connId = mClientMap.connIdByAddress(clientIf, address);
         if (connId == null) {
             Log.e(TAG, "writeDescriptor() - No connection for " + address + "...");
-            return;
+            return BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED;
         }
 
         if (!permissionCheck(connId, handle)) {
             Log.w(TAG, "writeDescriptor() - permission check failed!");
-            return;
+            return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_PRIVILEGED_PERMISSION;
         }
 
         gattClientWriteDescriptorNative(connId, handle, authReq, value);
+        return BluetoothStatusCodes.SUCCESS;
     }
 
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
@@ -3410,7 +3420,7 @@
             if (status == BluetoothGatt.GATT_CONNECTION_CONGESTED) {
                 status = BluetoothGatt.GATT_SUCCESS;
             }
-            app.queueCallback(new CallbackInfo(address, status));
+            app.queueCallback(new CallbackInfo.Builder(address, status).build());
         }
     }
 
diff --git a/src/com/android/bluetooth/hfp/HeadsetPhoneState.java b/src/com/android/bluetooth/hfp/HeadsetPhoneState.java
index 50094cf1..c1c43c8 100644
--- a/src/com/android/bluetooth/hfp/HeadsetPhoneState.java
+++ b/src/com/android/bluetooth/hfp/HeadsetPhoneState.java
@@ -22,6 +22,7 @@
 import android.telephony.PhoneStateListener;
 import android.telephony.ServiceState;
 import android.telephony.SignalStrength;
+import android.telephony.SignalStrengthUpdateRequest;
 import android.telephony.SubscriptionManager;
 import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
 import android.telephony.TelephonyManager;
@@ -70,6 +71,7 @@
     private final HashMap<BluetoothDevice, Integer> mDeviceEventMap = new HashMap<>();
     private PhoneStateListener mPhoneStateListener;
     private final OnSubscriptionsChangedListener mOnSubscriptionsChangedListener;
+    private SignalStrengthUpdateRequest mSignalStrengthUpdateRequest;
 
     HeadsetPhoneState(HeadsetService headsetService) {
         Objects.requireNonNull(headsetService, "headsetService is null");
@@ -85,6 +87,9 @@
         mOnSubscriptionsChangedListener = new HeadsetPhoneStateOnSubscriptionChangedListener();
         mSubscriptionManager.addOnSubscriptionsChangedListener(command -> mHandler.post(command),
                 mOnSubscriptionsChangedListener);
+        mSignalStrengthUpdateRequest = new SignalStrengthUpdateRequest.Builder()
+                .setSystemThresholdReportingRequestedWhileIdle(true)
+                .build();
     }
 
     /**
@@ -156,6 +161,9 @@
         Log.i(TAG, "startListenForPhoneState(), subId=" + subId + ", enabled_events=" + events);
         mPhoneStateListener = new HeadsetPhoneStateListener(command -> mHandler.post(command));
         mTelephonyManager.listen(mPhoneStateListener, events);
+        if ((events & PhoneStateListener.LISTEN_SIGNAL_STRENGTHS) != 0) {
+            mTelephonyManager.setSignalStrengthUpdateRequest(mSignalStrengthUpdateRequest);
+        }
     }
 
     private void stopListenForPhoneState() {
@@ -167,6 +175,7 @@
                 + getTelephonyEventsToListen());
         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
         mPhoneStateListener = null;
+        mTelephonyManager.clearSignalStrengthUpdateRequest(mSignalStrengthUpdateRequest);
     }
 
     int getCindService() {
diff --git a/src/com/android/bluetooth/hfp/HeadsetService.java b/src/com/android/bluetooth/hfp/HeadsetService.java
index 29117ef..7c3784e 100644
--- a/src/com/android/bluetooth/hfp/HeadsetService.java
+++ b/src/com/android/bluetooth/hfp/HeadsetService.java
@@ -1645,7 +1645,7 @@
             // Suspend A2DP when call about is about to become active
             if (mActiveDevice != null && callState != HeadsetHalConstants.CALL_STATE_DISCONNECTED
                     && !mSystemInterface.isCallIdle() && isCallIdleBefore) {
-                mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true");
+                mSystemInterface.getAudioManager().setA2dpSuspended(true);
             }
         });
         doForEachConnectedStateMachine(
@@ -1655,7 +1655,7 @@
             if (callState == HeadsetHalConstants.CALL_STATE_IDLE
                     && mSystemInterface.isCallIdle() && !isAudioOn()) {
                 // Resume A2DP when call ended and SCO is not connected
-                mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
+                mSystemInterface.getAudioManager().setA2dpSuspended(false);
             }
         });
 
@@ -1813,7 +1813,7 @@
                 }
                 // Unsuspend A2DP when SCO connection is gone and call state is idle
                 if (mSystemInterface.isCallIdle()) {
-                    mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
+                    mSystemInterface.getAudioManager().setA2dpSuspended(false);
                 }
             }
         }
@@ -1928,6 +1928,7 @@
 
     @Override
     public void dump(StringBuilder sb) {
+        boolean isScoOn = mSystemInterface.getAudioManager().isBluetoothScoOn();
         synchronized (mStateMachines) {
             super.dump(sb);
             ProfileService.println(sb, "mMaxHeadsetConnections: " + mMaxHeadsetConnections);
@@ -1948,9 +1949,7 @@
             ProfileService.println(sb, "mForceScoAudio: " + mForceScoAudio);
             ProfileService.println(sb, "mCreated: " + mCreated);
             ProfileService.println(sb, "mStarted: " + mStarted);
-            ProfileService.println(sb,
-                    "AudioManager.isBluetoothScoOn(): " + mSystemInterface.getAudioManager()
-                            .isBluetoothScoOn());
+            ProfileService.println(sb, "AudioManager.isBluetoothScoOn(): " + isScoOn);
             ProfileService.println(sb, "Telecom.isInCall(): " + mSystemInterface.isInCall());
             ProfileService.println(sb, "Telecom.isRinging(): " + mSystemInterface.isRinging());
             for (HeadsetStateMachine stateMachine : mStateMachines.values()) {
diff --git a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
index fe3f98a..bbc7878 100644
--- a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
+++ b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
@@ -80,12 +80,6 @@
     private static final String TAG = "HeadsetStateMachine";
     private static final boolean DBG = false;
 
-    private static final String HEADSET_NAME = "bt_headset_name";
-    private static final String HEADSET_NREC = "bt_headset_nrec";
-    private static final String HEADSET_WBS = "bt_wbs";
-    private static final String HEADSET_AUDIO_FEATURE_ON = "on";
-    private static final String HEADSET_AUDIO_FEATURE_OFF = "off";
-
     static final int CONNECT = 1;
     static final int DISCONNECT = 2;
     static final int CONNECT_AUDIO = 3;
@@ -114,6 +108,10 @@
     // NOTE: the value is not "final" - it is modified in the unit tests
     @VisibleForTesting static int sConnectTimeoutMs = 30000;
 
+    // Number of times we should retry disconnecting audio before
+    // disconnecting the device.
+    private static final int MAX_RETRY_DISCONNECT_AUDIO = 3;
+
     private static final HeadsetAgIndicatorEnableState DEFAULT_AG_INDICATOR_ENABLE_STATE =
             new HeadsetAgIndicatorEnableState(true, true, true, true);
 
@@ -143,12 +141,15 @@
     private HeadsetAgIndicatorEnableState mAgIndicatorEnableState;
     // The timestamp when the device entered connecting/connected state
     private long mConnectingTimestampMs = Long.MIN_VALUE;
-    // Audio Parameters like NREC
-    private final HashMap<String, String> mAudioParams = new HashMap<>();
+    // Audio Parameters
+    private boolean mHasNrecEnabled = false;
+    private boolean mHasWbsEnabled = false;
     // AT Phone book keeps a group of states used by AT+CPBR commands
     private final AtPhonebook mPhonebook;
     // HSP specific
     private boolean mNeedDialingOutReply;
+    // Audio disconnect timeout retry count
+    private int mAudioDisconnectRetry = 0;
 
     // Keys are AT commands, and values are the company IDs.
     private static final Map<String, Integer> VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID;
@@ -221,7 +222,8 @@
         if (mPhonebook != null) {
             mPhonebook.cleanup();
         }
-        mAudioParams.clear();
+        mHasWbsEnabled = false;
+        mHasNrecEnabled = false;
     }
 
     public void dump(StringBuilder sb) {
@@ -316,8 +318,7 @@
             BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_SCO_CONNECTION_STATE_CHANGED,
                     mAdapterService.obfuscateAddress(device),
                     getConnectionStateFromAudioState(toState),
-                    TextUtils.equals(mAudioParams.get(HEADSET_WBS), HEADSET_AUDIO_FEATURE_ON)
-                            ? BluetoothHfpProtoEnums.SCO_CODEC_MSBC
+                    mHasWbsEnabled ? BluetoothHfpProtoEnums.SCO_CODEC_MSBC
                             : BluetoothHfpProtoEnums.SCO_CODEC_CVSD,
                     mAdapterService.getMetricId(device));
             mHeadsetService.onAudioStateChangedFromStateMachine(device, fromState, toState);
@@ -450,7 +451,8 @@
             mPhonebook.resetAtState();
             updateAgIndicatorEnableState(null);
             mNeedDialingOutReply = false;
-            mAudioParams.clear();
+            mHasWbsEnabled = false;
+            mHasNrecEnabled = false;
             broadcastStateTransitions();
             // Remove the state machine for unbonded devices
             if (mPrevState != null
@@ -1042,6 +1044,10 @@
                 // state. This is to prevent auto connect attempts from disconnecting
                 // devices that previously successfully connected.
                 removeDeferredMessages(CONNECT);
+            } else if (mPrevState == mAudioDisconnecting) {
+                // Reset audio disconnecting retry count. Either the disconnection was successful
+                // or the retry count reached MAX_RETRY_DISCONNECT_AUDIO.
+                mAudioDisconnectRetry = 0;
             }
             broadcastStateTransitions();
         }
@@ -1073,9 +1079,9 @@
                 break;
                 case CONNECT_AUDIO:
                     stateLogD("CONNECT_AUDIO, device=" + mDevice);
-                    mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true");
+                    mSystemInterface.getAudioManager().setA2dpSuspended(true);
                     if (!mNativeInterface.connectAudio(mDevice)) {
-                        mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
+                        mSystemInterface.getAudioManager().setA2dpSuspended(false);
                         stateLogE("Failed to connect SCO audio for " + mDevice);
                         // No state change involved, fire broadcast immediately
                         broadcastAudioState(mDevice, BluetoothHeadset.STATE_AUDIO_DISCONNECTED,
@@ -1283,7 +1289,7 @@
                         stateLogW("CONNECT_AUDIO device is not connected " + device);
                         break;
                     }
-                    stateLogW("CONNECT_AUDIO device auido is already connected " + device);
+                    stateLogW("CONNECT_AUDIO device audio is already connected " + device);
                     break;
                 }
                 case DISCONNECT_AUDIO: {
@@ -1385,8 +1391,18 @@
                         stateLogW("CONNECT_TIMEOUT for unknown device " + device);
                         break;
                     }
-                    stateLogW("CONNECT_TIMEOUT");
-                    transitionTo(mConnected);
+                    if (mAudioDisconnectRetry == MAX_RETRY_DISCONNECT_AUDIO) {
+                        stateLogW("CONNECT_TIMEOUT: Disconnecting device");
+                        // Restoring state to Connected with message DISCONNECT
+                        deferMessage(obtainMessage(DISCONNECT, mDevice));
+                        transitionTo(mConnected);
+                    } else {
+                        mAudioDisconnectRetry += 1;
+                        stateLogW("CONNECT_TIMEOUT: retrying "
+                                + (MAX_RETRY_DISCONNECT_AUDIO - mAudioDisconnectRetry)
+                                + " more time(s)");
+                        transitionTo(mAudioOn);
+                    }
                     break;
                 }
                 default:
@@ -1407,6 +1423,8 @@
                     break;
                 case HeadsetHalConstants.AUDIO_STATE_CONNECTED:
                     stateLogW("processAudioEvent: audio disconnection failed");
+                    // Audio connected, resetting disconnect retry.
+                    mAudioDisconnectRetry = 0;
                     transitionTo(mAudioOn);
                     break;
                 case HeadsetHalConstants.AUDIO_STATE_CONNECTING:
@@ -1508,15 +1526,12 @@
     }
 
     private void setAudioParameters() {
-        String keyValuePairs = String.join(";", new String[]{
-                HEADSET_NAME + "=" + getCurrentDeviceName(),
-                HEADSET_NREC + "=" + mAudioParams.getOrDefault(HEADSET_NREC,
-                        HEADSET_AUDIO_FEATURE_OFF),
-                HEADSET_WBS + "=" + mAudioParams.getOrDefault(HEADSET_WBS,
-                        HEADSET_AUDIO_FEATURE_OFF)
-        });
-        Log.i(TAG, "setAudioParameters for " + mDevice + ": " + keyValuePairs);
-        mSystemInterface.getAudioManager().setParameters(keyValuePairs);
+        AudioManager am = mSystemInterface.getAudioManager();
+        Log.i(TAG, "setAudioParameters for " + mDevice + ":"
+                + " Name=" + getCurrentDeviceName()
+                + " hasNrecEnabled=" + mHasNrecEnabled
+                + " hasWbsEnabled=" + mHasWbsEnabled);
+        am.setBluetoothHeadsetProperties(getCurrentDeviceName(), mHasNrecEnabled, mHasWbsEnabled);
     }
 
     private String parseUnknownAt(String atString) {
@@ -1645,32 +1660,28 @@
     }
 
     private void processNoiseReductionEvent(boolean enable) {
-        String prevNrec = mAudioParams.getOrDefault(HEADSET_NREC, HEADSET_AUDIO_FEATURE_OFF);
-        String newNrec = enable ? HEADSET_AUDIO_FEATURE_ON : HEADSET_AUDIO_FEATURE_OFF;
-        mAudioParams.put(HEADSET_NREC, newNrec);
-        log("processNoiseReductionEvent: " + HEADSET_NREC + " change " + prevNrec + " -> "
-                + newNrec);
+        log("processNoiseReductionEvent: " + mHasNrecEnabled + " -> " + enable);
+        mHasNrecEnabled = enable;
         if (getAudioState() == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
             setAudioParameters();
         }
     }
 
     private void processWBSEvent(int wbsConfig) {
-        String prevWbs = mAudioParams.getOrDefault(HEADSET_WBS, HEADSET_AUDIO_FEATURE_OFF);
+        boolean prevWbs = mHasWbsEnabled;
         switch (wbsConfig) {
             case HeadsetHalConstants.BTHF_WBS_YES:
-                mAudioParams.put(HEADSET_WBS, HEADSET_AUDIO_FEATURE_ON);
+                mHasWbsEnabled = true;
                 break;
             case HeadsetHalConstants.BTHF_WBS_NO:
             case HeadsetHalConstants.BTHF_WBS_NONE:
-                mAudioParams.put(HEADSET_WBS, HEADSET_AUDIO_FEATURE_OFF);
+                mHasWbsEnabled = false;
                 break;
             default:
                 Log.e(TAG, "processWBSEvent: unknown wbsConfig " + wbsConfig);
                 return;
         }
-        log("processWBSEvent: " + HEADSET_NREC + " change " + prevWbs + " -> " + mAudioParams.get(
-                HEADSET_WBS));
+        log("processWBSEvent: " + prevWbs + " -> " + mHasWbsEnabled);
     }
 
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -2053,7 +2064,7 @@
             events |= PhoneStateListener.LISTEN_SERVICE_STATE;
         }
         if (mAgIndicatorEnableState != null && mAgIndicatorEnableState.signal) {
-            events |= PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH;
+            events |= PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
         }
         mSystemInterface.getHeadsetPhoneState().listenForPhoneState(mDevice, events);
     }
diff --git a/src/com/android/bluetooth/hfpclient/HeadsetClientService.java b/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
index 319372a..8c45847 100644
--- a/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
+++ b/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
@@ -39,6 +39,7 @@
 import com.android.bluetooth.btservice.ProfileService;
 import com.android.bluetooth.btservice.storage.DatabaseManager;
 import com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
@@ -59,7 +60,11 @@
     private static final boolean DBG = false;
     private static final String TAG = "HeadsetClientService";
 
-    private HashMap<BluetoothDevice, HeadsetClientStateMachine> mStateMachineMap = new HashMap<>();
+    // This is also used as a lock for shared data in {@link HeadsetClientService}
+    @GuardedBy("mStateMachineMap")
+    private final HashMap<BluetoothDevice, HeadsetClientStateMachine> mStateMachineMap =
+            new HashMap<>();
+
     private static HeadsetClientService sHeadsetClientService;
     private NativeInterface mNativeInterface = null;
     private HandlerThread mSmThread = null;
@@ -71,6 +76,8 @@
     // Maxinum number of devices we can try connecting to in one session
     private static final int MAX_STATE_MACHINES_POSSIBLE = 100;
 
+    private final Object mStartStopLock = new Object();
+
     public static final String HFP_CLIENT_STOP_TAG = "hfp_client_stop_tag";
 
     @Override
@@ -79,83 +86,94 @@
     }
 
     @Override
-    protected synchronized boolean start() {
-        if (DBG) {
-            Log.d(TAG, "start()");
+    protected boolean start() {
+        synchronized (mStartStopLock) {
+            if (DBG) {
+                Log.d(TAG, "start()");
+            }
+            if (getHeadsetClientService() != null) {
+                Log.w(TAG, "start(): start called without stop");
+                return false;
+            }
+
+            mDatabaseManager = Objects.requireNonNull(
+                    AdapterService.getAdapterService().getDatabase(),
+                    "DatabaseManager cannot be null when HeadsetClientService starts");
+
+            // Setup the JNI service
+            mNativeInterface = NativeInterface.getInstance();
+            mNativeInterface.initialize();
+
+            mBatteryManager = getSystemService(BatteryManager.class);
+
+            mAudioManager = getSystemService(AudioManager.class);
+            if (mAudioManager == null) {
+                Log.e(TAG, "AudioManager service doesn't exist?");
+            } else {
+                // start AudioManager in a known state
+                mAudioManager.setHfpEnabled(false);
+            }
+
+            mSmFactory = new HeadsetClientStateMachineFactory();
+            synchronized (mStateMachineMap) {
+                mStateMachineMap.clear();
+            }
+
+            IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
+            filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+            registerReceiver(mBroadcastReceiver, filter);
+
+            // Start the HfpClientConnectionService to create connection with telecom when HFP
+            // connection is available.
+            Intent startIntent = new Intent(this, HfpClientConnectionService.class);
+            startService(startIntent);
+
+            // Create the thread on which all State Machines will run
+            mSmThread = new HandlerThread("HeadsetClient.SM");
+            mSmThread.start();
+
+            setHeadsetClientService(this);
+            return true;
         }
-        if (sHeadsetClientService != null) {
-            Log.w(TAG, "start(): start called without stop");
-            return false;
-        }
-
-        mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),
-                "DatabaseManager cannot be null when HeadsetClientService starts");
-
-        // Setup the JNI service
-        mNativeInterface = NativeInterface.getInstance();
-        mNativeInterface.initialize();
-
-        mBatteryManager = getSystemService(BatteryManager.class);
-
-        mAudioManager = getSystemService(AudioManager.class);
-        if (mAudioManager == null) {
-            Log.e(TAG, "AudioManager service doesn't exist?");
-        } else {
-            // start AudioManager in a known state
-            mAudioManager.setParameters("hfp_enable=false");
-        }
-
-        mSmFactory = new HeadsetClientStateMachineFactory();
-        mStateMachineMap.clear();
-
-        IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
-        filter.addAction(Intent.ACTION_BATTERY_CHANGED);
-        registerReceiver(mBroadcastReceiver, filter);
-
-        // Start the HfpClientConnectionService to create connection with telecom when HFP
-        // connection is available.
-        Intent startIntent = new Intent(this, HfpClientConnectionService.class);
-        startService(startIntent);
-
-        // Create the thread on which all State Machines will run
-        mSmThread = new HandlerThread("HeadsetClient.SM");
-        mSmThread.start();
-
-        setHeadsetClientService(this);
-        return true;
     }
 
     @Override
-    protected synchronized boolean stop() {
-        if (sHeadsetClientService == null) {
-            Log.w(TAG, "stop() called without start()");
-            return false;
+    protected boolean stop() {
+        synchronized (mStartStopLock) {
+            synchronized (HeadsetClientService.class) {
+                if (sHeadsetClientService == null) {
+                    Log.w(TAG, "stop() called without start()");
+                    return false;
+                }
+
+                // Stop the HfpClientConnectionService.
+                Intent stopIntent = new Intent(this, HfpClientConnectionService.class);
+                sHeadsetClientService.stopService(stopIntent);
+            }
+
+            setHeadsetClientService(null);
+
+            unregisterReceiver(mBroadcastReceiver);
+
+            synchronized (mStateMachineMap) {
+                for (Iterator<Map.Entry<BluetoothDevice, HeadsetClientStateMachine>> it =
+                        mStateMachineMap.entrySet().iterator(); it.hasNext(); ) {
+                    HeadsetClientStateMachine sm =
+                            mStateMachineMap.get((BluetoothDevice) it.next().getKey());
+                    sm.doQuit();
+                    it.remove();
+                }
+            }
+
+            // Stop the handler thread
+            mSmThread.quit();
+            mSmThread = null;
+
+            mNativeInterface.cleanup();
+            mNativeInterface = null;
+
+            return true;
         }
-
-        // Stop the HfpClientConnectionService.
-        Intent stopIntent = new Intent(this, HfpClientConnectionService.class);
-        sHeadsetClientService.stopService(stopIntent);
-
-        setHeadsetClientService(null);
-
-        unregisterReceiver(mBroadcastReceiver);
-
-        for (Iterator<Map.Entry<BluetoothDevice, HeadsetClientStateMachine>> it =
-                mStateMachineMap.entrySet().iterator(); it.hasNext(); ) {
-            HeadsetClientStateMachine sm =
-                    mStateMachineMap.get((BluetoothDevice) it.next().getKey());
-            sm.doQuit();
-            it.remove();
-        }
-
-        // Stop the handler thread
-        mSmThread.quit();
-        mSmThread = null;
-
-        mNativeInterface.cleanup();
-        mNativeInterface = null;
-
-        return true;
     }
 
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -182,8 +200,8 @@
                                 "Setting volume to audio manager: " + streamValue + " hands free: "
                                         + hfVol);
                     }
-                    mAudioManager.setParameters("hfp_volume=" + hfVol);
-                    synchronized (this) {
+                    mAudioManager.setHfpVolume(hfVol);
+                    synchronized (mStateMachineMap) {
                         for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
                             if (sm != null) {
                                 sm.sendMessage(HeadsetClientStateMachine.SET_SPEAKER_VOLUME,
@@ -206,7 +224,7 @@
                             "Send battery level update BIEV(2," + batteryLevel + ") command");
                 }
 
-                synchronized (this) {
+                synchronized (mStateMachineMap) {
                     for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
                         if (sm != null) {
                             sm.sendMessage(HeadsetClientStateMachine.SEND_BIEV,
@@ -527,17 +545,16 @@
         if (DBG) {
             Log.d(TAG, "connect " + device);
         }
-        HeadsetClientStateMachine sm = getStateMachine(device);
-        if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
-            return false;
-        }
-
         if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
             Log.w(TAG, "Connection not allowed: <" + device.getAddress()
                     + "> is CONNECTION_POLICY_FORBIDDEN");
             return false;
         }
+        HeadsetClientStateMachine sm = getStateMachine(device, true);
+        if (sm == null) {
+            Log.e(TAG, "Cannot allocate SM for device " + device);
+            return false;
+        }
 
         sm.sendMessage(HeadsetClientStateMachine.CONNECT, device);
         return true;
@@ -552,7 +569,7 @@
     public boolean disconnect(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -566,24 +583,31 @@
         return true;
     }
 
-    public synchronized List<BluetoothDevice> getConnectedDevices() {
+    /**
+     * @return A list of connected {@link BluetoothDevice}.
+     */
+    public List<BluetoothDevice> getConnectedDevices() {
         ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>();
-        for (BluetoothDevice bd : mStateMachineMap.keySet()) {
-            HeadsetClientStateMachine sm = mStateMachineMap.get(bd);
-            if (sm != null && sm.getConnectionState(bd) == BluetoothProfile.STATE_CONNECTED) {
-                connectedDevices.add(bd);
+        synchronized (mStateMachineMap) {
+            for (BluetoothDevice bd : mStateMachineMap.keySet()) {
+                HeadsetClientStateMachine sm = mStateMachineMap.get(bd);
+                if (sm != null && sm.getConnectionState(bd) == BluetoothProfile.STATE_CONNECTED) {
+                    connectedDevices.add(bd);
+                }
             }
         }
         return connectedDevices;
     }
 
-    private synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+    private List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
         List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
-        for (BluetoothDevice bd : mStateMachineMap.keySet()) {
-            for (int state : states) {
-                HeadsetClientStateMachine sm = mStateMachineMap.get(bd);
-                if (sm != null && sm.getConnectionState(bd) == state) {
-                    devices.add(bd);
+        synchronized (mStateMachineMap) {
+            for (BluetoothDevice bd : mStateMachineMap.keySet()) {
+                for (int state : states) {
+                    HeadsetClientStateMachine sm = mStateMachineMap.get(bd);
+                    if (sm != null && sm.getConnectionState(bd) == state) {
+                        devices.add(bd);
+                    }
                 }
             }
         }
@@ -599,11 +623,12 @@
      * {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or
      * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
      */
-    public synchronized int getConnectionState(BluetoothDevice device) {
-        HeadsetClientStateMachine sm = mStateMachineMap.get(device);
+    public int getConnectionState(BluetoothDevice device) {
+        HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm != null) {
             return sm.getConnectionState(device);
         }
+
         return BluetoothProfile.STATE_DISCONNECTED;
     }
 
@@ -659,7 +684,7 @@
     boolean startVoiceRecognition(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
         int connectionState = sm.getConnectionState(device);
@@ -673,7 +698,7 @@
     boolean stopVoiceRecognition(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
         int connectionState = sm.getConnectionState(device);
@@ -693,7 +718,7 @@
     public int getAudioState(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return -1;
         }
 
@@ -703,7 +728,7 @@
     boolean connectAudio(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -720,7 +745,7 @@
     boolean disconnectAudio(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -734,7 +759,7 @@
     boolean holdCall(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -750,7 +775,7 @@
 
     boolean acceptCall(BluetoothDevice device, int flag) {
         /* Phonecalls from a single device are supported, hang up any calls on the other phone */
-        synchronized (this) {
+        synchronized (mStateMachineMap) {
             for (Map.Entry<BluetoothDevice, HeadsetClientStateMachine> entry : mStateMachineMap
                     .entrySet()) {
                 if (entry.getValue() == null || entry.getKey().equals(device)) {
@@ -771,7 +796,7 @@
         }
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -788,7 +813,7 @@
     boolean rejectCall(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -806,7 +831,7 @@
     boolean terminateCall(BluetoothDevice device, UUID uuid) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -825,7 +850,7 @@
     boolean enterPrivateMode(BluetoothDevice device, int index) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -844,7 +869,7 @@
     BluetoothHeadsetClientCall dial(BluetoothDevice device, String number) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return null;
         }
 
@@ -867,7 +892,7 @@
     public boolean sendDTMF(BluetoothDevice device, byte code) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -889,7 +914,7 @@
     public List<BluetoothHeadsetClientCall> getCurrentCalls(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return null;
         }
 
@@ -903,7 +928,7 @@
     public boolean explicitCallTransfer(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -921,7 +946,7 @@
     public boolean sendVendorAtCommand(BluetoothDevice device, int vendorId, String atCommand) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return false;
         }
 
@@ -939,7 +964,7 @@
     public Bundle getCurrentAgEvents(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return null;
         }
 
@@ -953,7 +978,7 @@
     public Bundle getCurrentAgFeatures(BluetoothDevice device) {
         HeadsetClientStateMachine sm = getStateMachine(device);
         if (sm == null) {
-            Log.e(TAG, "Cannot allocate SM for device " + device);
+            Log.e(TAG, "SM does not exist for device " + device);
             return null;
         }
         int connectionState = sm.getConnectionState(device);
@@ -965,57 +990,115 @@
 
     // Handle messages from native (JNI) to java
     public void messageFromNative(StackEvent stackEvent) {
-        HeadsetClientStateMachine sm = getStateMachine(stackEvent.device);
-        if (sm == null) {
-            Log.w(TAG, "No SM found for event " + stackEvent);
-        }
+        Objects.requireNonNull(stackEvent.device,
+                "Device should never be null, event: " + stackEvent);
 
+        HeadsetClientStateMachine sm = getStateMachine(stackEvent.device,
+                isConnectionEvent(stackEvent));
+        if (sm == null) {
+            throw new IllegalStateException(
+                    "State machine not found for stack event: " + stackEvent);
+        }
         sm.sendMessage(StackEvent.STACK_EVENT, stackEvent);
     }
 
-    // State machine management
-    private synchronized HeadsetClientStateMachine getStateMachine(BluetoothDevice device) {
+    private boolean isConnectionEvent(StackEvent stackEvent) {
+        if (stackEvent.type == StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
+            if ((stackEvent.valueInt == HeadsetClientHalConstants.CONNECTION_STATE_CONNECTING)
+                    || (stackEvent.valueInt
+                    == HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private HeadsetClientStateMachine getStateMachine(BluetoothDevice device) {
+        return getStateMachine(device, false);
+    }
+
+    private HeadsetClientStateMachine getStateMachine(BluetoothDevice device,
+            boolean isConnectionEvent) {
         if (device == null) {
             Log.e(TAG, "getStateMachine failed: Device cannot be null");
             return null;
         }
 
-        HeadsetClientStateMachine sm = mStateMachineMap.get(device);
+        HeadsetClientStateMachine sm;
+        synchronized (mStateMachineMap) {
+            sm = mStateMachineMap.get(device);
+        }
+
         if (sm != null) {
             if (DBG) {
                 Log.d(TAG, "Found SM for device " + device);
             }
-            return sm;
+        } else if (isConnectionEvent) {
+            // The only time a new state machine should be created when none was found is for
+            // connection events.
+            sm = allocateStateMachine(device);
+            if (sm == null) {
+                Log.e(TAG, "SM could not be allocated for device " + device);
+            }
         }
-
-        // There is a possibility of a DOS attack if someone populates here with a lot of fake
-        // BluetoothAddresses. If it so happens instead of blowing up we can atleast put a limit on
-        // how long the attack would survive
-        if (mStateMachineMap.keySet().size() > MAX_STATE_MACHINES_POSSIBLE) {
-            Log.e(TAG, "Max state machines reached, possible DOS attack "
-                    + MAX_STATE_MACHINES_POSSIBLE);
-            return null;
-        }
-
-        // Allocate a new SM
-        Log.d(TAG, "Creating a new state machine");
-        sm = mSmFactory.make(this, mSmThread, mNativeInterface);
-        mStateMachineMap.put(device, sm);
         return sm;
     }
 
+    private HeadsetClientStateMachine allocateStateMachine(BluetoothDevice device) {
+        if (device == null) {
+            Log.e(TAG, "allocateStateMachine failed: Device cannot be null");
+            return null;
+        }
+
+        if (getHeadsetClientService() == null) {
+            // Preconditions: {@code setHeadsetClientService(this)} is the last thing {@code start}
+            // does, and {@code setHeadsetClientService(null)} is (one of) the first thing
+            // {@code stop does}.
+            Log.e(TAG, "Cannot allocate SM if service has begun stopping or has not completed"
+                    + " startup.");
+            return null;
+        }
+
+        synchronized (mStateMachineMap) {
+            HeadsetClientStateMachine sm = mStateMachineMap.get(device);
+            if (sm != null) {
+                if (DBG) {
+                    Log.d(TAG, "allocateStateMachine: SM already exists for device " + device);
+                }
+                return sm;
+            }
+
+            // There is a possibility of a DOS attack if someone populates here with a lot of fake
+            // BluetoothAddresses. If it so happens instead of blowing up we can at least put a
+            // limit on how long the attack would survive
+            if (mStateMachineMap.keySet().size() > MAX_STATE_MACHINES_POSSIBLE) {
+                Log.e(TAG, "Max state machines reached, possible DOS attack "
+                        + MAX_STATE_MACHINES_POSSIBLE);
+                return null;
+            }
+
+            // Allocate a new SM
+            Log.d(TAG, "Creating a new state machine");
+            sm = mSmFactory.make(this, mSmThread, mNativeInterface);
+            mStateMachineMap.put(device, sm);
+            return sm;
+        }
+    }
+
     // Check if any of the state machines have routed the SCO audio stream.
-    synchronized boolean isScoRouted() {
-        for (Map.Entry<BluetoothDevice, HeadsetClientStateMachine> entry : mStateMachineMap
-                .entrySet()) {
-            if (entry.getValue() != null) {
-                int audioState = entry.getValue().getAudioState(entry.getKey());
-                if (audioState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) {
-                    if (DBG) {
-                        Log.d(TAG, "Device " + entry.getKey() + " audio state " + audioState
-                                + " Connected");
+    boolean isScoRouted() {
+        synchronized (mStateMachineMap) {
+            for (Map.Entry<BluetoothDevice, HeadsetClientStateMachine> entry : mStateMachineMap
+                    .entrySet()) {
+                if (entry.getValue() != null) {
+                    int audioState = entry.getValue().getAudioState(entry.getKey());
+                    if (audioState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) {
+                        if (DBG) {
+                            Log.d(TAG, "Device " + entry.getKey() + " audio state " + audioState
+                                    + " Connected");
+                        }
+                        return true;
                     }
-                    return true;
                 }
             }
         }
@@ -1023,18 +1106,22 @@
     }
 
     @Override
-    public synchronized void dump(StringBuilder sb) {
+    public void dump(StringBuilder sb) {
         super.dump(sb);
-        for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
-            if (sm != null) {
-                sm.dump(sb);
+        synchronized (mStateMachineMap) {
+            for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
+                if (sm != null) {
+                    sm.dump(sb);
+                }
             }
         }
     }
 
     // For testing
-    protected synchronized Map<BluetoothDevice, HeadsetClientStateMachine> getStateMachineMap() {
-        return mStateMachineMap;
+    protected Map<BluetoothDevice, HeadsetClientStateMachine> getStateMachineMap() {
+        synchronized (mStateMachineMap) {
+            return mStateMachineMap;
+        }
     }
 
     protected void setSMFactory(HeadsetClientStateMachineFactory factory) {
@@ -1049,7 +1136,7 @@
         int batteryLevel = mBatteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
         int batteryIndicatorID = 2;
 
-        synchronized (this) {
+        synchronized (mStateMachineMap) {
             for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
                 if (sm != null) {
                     sm.sendMessage(HeadsetClientStateMachine.SEND_BIEV,
diff --git a/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java b/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
index 2703831..13eeee9 100644
--- a/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
+++ b/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
@@ -483,7 +483,9 @@
 
         if (mCalls.size() > 0) {
             if (mService.getResources().getBoolean(R.bool.hfp_clcc_poll_during_call)) {
-                sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
+                sendMessageDelayed(QUERY_CURRENT_CALLS,
+                        mService.getResources().getInteger(
+                        R.integer.hfp_clcc_poll_interval_during_call));
             } else {
                 if (getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING) != null) {
                     logD("Still have incoming call; polling");
@@ -813,9 +815,9 @@
         }
         logD("hfp_enable=" + enable);
         if (enable && !sAudioIsRouted) {
-            mAudioManager.setParameters("hfp_enable=true");
+            mAudioManager.setHfpEnabled(true);
         } else if (!enable) {
-            mAudioManager.setParameters("hfp_enable=false");
+            mAudioManager.setHfpEnabled(false);
         }
         sAudioIsRouted = enable;
     }
@@ -1330,9 +1332,16 @@
                     break;
                 case QUERY_CURRENT_CALLS:
                     removeMessages(QUERY_CURRENT_CALLS);
-                    if (mCalls.size() > 0) {
-                        // If there are ongoing calls periodically check their status.
-                        sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
+                    // If there are ongoing calls periodically check their status.
+                    if (mCalls.size() > 1
+                            && mService.getResources().getBoolean(
+                            R.bool.hfp_clcc_poll_during_call)) {
+                        sendMessageDelayed(QUERY_CURRENT_CALLS,
+                                mService.getResources().getInteger(
+                                R.integer.hfp_clcc_poll_interval_during_call));
+                    } else if (mCalls.size() > 0) {
+                        sendMessageDelayed(QUERY_CURRENT_CALLS,
+                                QUERY_CURRENT_CALLS_WAIT_MILLIS);
                     }
                     queryCallsStart();
                     break;
@@ -1583,7 +1592,7 @@
                     // routing is handled by the bluetooth stack itself. The only reason to do so is
                     // because Bluetooth SCO connection from the HF role is not entirely supported
                     // for routing and volume purposes.
-                    // NOTE: All calls here are routed via the setParameters which changes the
+                    // NOTE: All calls here are routed via AudioManager methods which changes the
                     // routing at the Audio HAL level.
 
                     if (mService.isScoRouted()) {
@@ -1605,15 +1614,15 @@
                     logD("hfp_enable=true mAudioWbs is " + mAudioWbs);
                     if (mAudioWbs) {
                         logD("Setting sampling rate as 16000");
-                        mAudioManager.setParameters("hfp_set_sampling_rate=16000");
+                        mAudioManager.setHfpSamplingRate(16000);
                     } else {
                         logD("Setting sampling rate as 8000");
-                        mAudioManager.setParameters("hfp_set_sampling_rate=8000");
+                        mAudioManager.setHfpSamplingRate(8000);
                     }
                     logD("hf_volume " + hfVol);
                     routeHfpAudio(true);
                     mAudioFocusRequest = requestAudioFocus();
-                    mAudioManager.setParameters("hfp_volume=" + hfVol);
+                    mAudioManager.setHfpVolume(hfVol);
                     transitionTo(mAudioOn);
                     break;
 
diff --git a/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java b/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
index c104c3d..0048d4f 100644
--- a/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
+++ b/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
@@ -177,6 +177,24 @@
     }
 
     /**
+     * Add new Node into a group.
+     * @param groupId group identifier
+     * @param device remote device
+     */
+     public boolean groupAddNode(int groupId, BluetoothDevice device) {
+        return groupAddNodeNative(groupId, getByteAddress(device));
+    }
+
+    /**
+     * Add new Node into a group.
+     * @param groupId group identifier
+     * @param device remote device
+     */
+    public boolean groupRemoveNode(int groupId, BluetoothDevice device) {
+        return groupRemoveNodeNative(groupId, getByteAddress(device));
+    }
+
+    /**
      * Set active group.
      * @param groupId group ID to set as active
      */
@@ -190,5 +208,7 @@
     private native void cleanupNative();
     private native boolean connectLeAudioNative(byte[] address);
     private native boolean disconnectLeAudioNative(byte[] address);
+    private native boolean groupAddNodeNative(int groupId, byte[] address);
+    private native boolean groupRemoveNodeNative(int groupId, byte[] address);
     private native void groupSetActiveNative(int groupId);
 }
diff --git a/src/com/android/bluetooth/le_audio/LeAudioService.java b/src/com/android/bluetooth/le_audio/LeAudioService.java
index 05513c9..5983c33 100644
--- a/src/com/android/bluetooth/le_audio/LeAudioService.java
+++ b/src/com/android/bluetooth/le_audio/LeAudioService.java
@@ -23,18 +23,27 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothCsipSetCoordinator;
 import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothUuid;
+import android.bluetooth.BluetoothVolumeControl;
+import android.bluetooth.IBluetoothCsipSetCoordinator;
+import android.bluetooth.IBluetoothCsipSetCoordinatorCallback;
 import android.bluetooth.IBluetoothLeAudio;
 import android.content.AttributionSource;
+import android.bluetooth.IBluetoothVolumeControl;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.ServiceConnection;
 import android.media.AudioManager;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.ParcelUuid;
+import android.os.RemoteException;
 import android.util.Log;
 
 import com.android.bluetooth.Utils;
@@ -42,7 +51,9 @@
 import com.android.bluetooth.btservice.ProfileService;
 import com.android.bluetooth.btservice.ServiceFactory;
 import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.bluetooth.csip.CsipSetCoordinatorService;
 import com.android.bluetooth.mcp.McpService;
+import com.android.bluetooth.vc.VolumeControlService;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
@@ -105,15 +116,101 @@
     private final Map<BluetoothDevice, LeAudioStateMachine> mStateMachines = new HashMap<>();
 
     private final Map<BluetoothDevice, Integer> mDeviceGroupIdMap = new ConcurrentHashMap<>();
+    private final Map<BluetoothDevice, Integer> mSetMemberAvailable = new ConcurrentHashMap<>();
     private int mActiveDeviceGroupId = LE_AUDIO_GROUP_ID_INVALID;
     private final int mContextSupportingInputAudio =
-            BluetoothLeAudio.CONTEXT_TYPE_COMMUNICATION;
+            BluetoothLeAudio.CONTEXT_TYPE_COMMUNICATION |
+            BluetoothLeAudio.CONTEXT_TYPE_MAN_MACHINE;
+
     private final int mContextSupportingOutputAudio = BluetoothLeAudio.CONTEXT_TYPE_COMMUNICATION |
-            BluetoothLeAudio.CONTEXT_TYPE_MEDIA;
+            BluetoothLeAudio.CONTEXT_TYPE_MEDIA |
+            BluetoothLeAudio.CONTEXT_TYPE_INSTRUCTIONAL |
+            BluetoothLeAudio.CONTEXT_TYPE_ATTENTION_SEEKING |
+            BluetoothLeAudio.CONTEXT_TYPE_IMMEDIATE_ALERT |
+            BluetoothLeAudio.CONTEXT_TYPE_MAN_MACHINE |
+            BluetoothLeAudio.CONTEXT_TYPE_EMERGENCY_ALERT |
+            BluetoothLeAudio.CONTEXT_TYPE_RINGTONE |
+            BluetoothLeAudio.CONTEXT_TYPE_TV |
+            BluetoothLeAudio.CONTEXT_TYPE_LIVE |
+            BluetoothLeAudio.CONTEXT_TYPE_GAME;
 
     private BroadcastReceiver mBondStateChangedReceiver;
     private BroadcastReceiver mConnectionStateChangedReceiver;
 
+    class MyCsipSetCoordinatorCallbacks extends IBluetoothCsipSetCoordinatorCallback.Stub {
+        @Override
+        public void onCsisSetMemberAvailable(BluetoothDevice device, int groupId) {
+            synchronized (LeAudioService.this) {
+                LeAudioService.this.setMemberAvailable(device, groupId);
+            }
+        }
+    };
+
+    private volatile MyCsipSetCoordinatorCallbacks mCsipSetCoordinatorCallback =
+                                                            new MyCsipSetCoordinatorCallbacks();
+    private volatile IBluetoothCsipSetCoordinator mCsipSetCoordinatorProxy;
+    private final ServiceConnection mCsipSetCoordinatorProxyConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            if (DBG) {
+                Log.d(TAG, "CsisClientProxy connected");
+            }
+            synchronized (LeAudioService.this) {
+                mCsipSetCoordinatorProxy = IBluetoothCsipSetCoordinator.Stub.asInterface(service);
+                CsipSetCoordinatorService mCsipSetCoordinatorService =
+                    CsipSetCoordinatorService.getCsipSetCoordinatorService();
+                if (mCsipSetCoordinatorService == null) {
+                    Log.e(TAG, "CsisClientService is null on LeAudioService starts");
+                    return;
+                }
+                mCsipSetCoordinatorService.registerCsisMemberObserver(
+                                                    LeAudioService.this.getMainExecutor(),
+                                                    BluetoothUuid.CAP,
+                                                    mCsipSetCoordinatorCallback);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName className) {
+            if (DBG) {
+                Log.d(TAG, "CsisClientProxy disconnected");
+            }
+            synchronized (LeAudioService.this) {
+                mCsipSetCoordinatorProxy = null;
+            }
+        }
+    };
+
+    private volatile IBluetoothVolumeControl mVolumeControlProxy;
+    VolumeControlService mVolumeControlService = null;
+    private final ServiceConnection mVolumeControlProxyConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            if (DBG) {
+                Log.d(TAG, "mVolumeControlProxyConnection connected");
+            }
+            synchronized (LeAudioService.this) {
+
+                mVolumeControlProxy = IBluetoothVolumeControl.Stub.asInterface(service);
+                mVolumeControlService =
+                    VolumeControlService.getVolumeControlService();
+                if (mVolumeControlService == null) {
+                    Log.e(TAG, "VolumeControlService is null when LeAudioService starts");
+                }
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName className) {
+            if (DBG) {
+                Log.d(TAG, "mVolumeControlProxy disconnected");
+            }
+            synchronized (LeAudioService.this) {
+                mVolumeControlProxy = null;
+            }
+        }
+    };
+
     @Override
     protected IProfileServiceBinder initBinder() {
         return new BluetoothLeAudioBinder(this);
@@ -148,6 +245,7 @@
         mStateMachinesThread.start();
 
         mDeviceGroupIdMap.clear();
+        mSetMemberAvailable.clear();
         mGroupDescriptors.clear();
 
         // Setup broadcast receivers
@@ -160,6 +258,12 @@
         mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
         registerReceiver(mConnectionStateChangedReceiver, filter);
 
+        /* Bind Csis Service */
+        bindCsisClientService();
+
+        /* Bind Volume control service */
+        bindVolumeControlService();
+
         // Mark service as started
         setLeAudioService(this);
 
@@ -176,6 +280,20 @@
             return true;
         }
 
+        setActiveDevice(null);
+        //Don't wait for async call with INACTIVE group status, clean active
+        //device for active group.
+        for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) {
+            LeAudioGroupDescriptor descriptor = entry.getValue();
+            Integer group_id = entry.getKey();
+            if (descriptor.mIsActive) {
+                descriptor.mIsActive = false;
+                updateActiveDevices(group_id, descriptor.mActiveContexts,
+                        ACTIVE_CONTEXTS_NONE, descriptor.mIsActive);
+                break;
+            }
+        }
+
         // Cleanup native interfaces
         mLeAudioNativeInterface.cleanup();
         mLeAudioNativeInterface = null;
@@ -199,6 +317,7 @@
         }
 
         mDeviceGroupIdMap.clear();
+        mSetMemberAvailable.clear();
         mGroupDescriptors.clear();
 
         if (mStateMachinesThread != null) {
@@ -208,6 +327,10 @@
 
         mAudioManager = null;
         mAdapterService = null;
+        mAudioManager = null;
+
+        unbindCsisClientService();
+        unbindVolumeControlService();
         return true;
     }
 
@@ -235,6 +358,54 @@
         sLeAudioService = instance;
     }
 
+    private void bindVolumeControlService() {
+        synchronized (mVolumeControlProxyConnection) {
+            Intent intent = new Intent(IBluetoothVolumeControl.class.getName());
+            ComponentName comp = intent.resolveSystemService(getPackageManager(), 0);
+            intent.setComponent(comp);
+            if (comp == null || !bindService(intent, mVolumeControlProxyConnection, 0)) {
+                Log.wtf(TAG, "Could not bind to IBluetoothVolumeControl Service with " +
+                        intent);
+            }
+        }
+    }
+    private void unbindVolumeControlService() {
+        synchronized (mVolumeControlProxyConnection) {
+            if (mVolumeControlProxy != null) {
+                if (DBG) {
+                    Log.d(TAG, "Unbinding mVolumeControlProxyConnection");
+                }
+                mVolumeControlProxy = null;
+                // Synchronization should make sure unbind can be successful
+                unbindService(mVolumeControlProxyConnection);
+            }
+        }
+    }
+
+    private void bindCsisClientService() {
+        synchronized (mCsipSetCoordinatorProxyConnection) {
+            Intent intent = new Intent(IBluetoothCsipSetCoordinator.class.getName());
+            ComponentName comp = intent.resolveSystemService(getPackageManager(), 0);
+            intent.setComponent(comp);
+            if (comp == null || !bindService(intent, mCsipSetCoordinatorProxyConnection, 0)) {
+                Log.wtf(TAG, "Could not bind to IBluetoothCsisClient Service with " +
+                        intent);
+            }
+        }
+    }
+    private void unbindCsisClientService() {
+        synchronized (mCsipSetCoordinatorProxyConnection) {
+            if (mCsipSetCoordinatorProxy != null) {
+                if (DBG) {
+                    Log.d(TAG, "Unbinding CsisClientProxyConnection");
+                }
+                mCsipSetCoordinatorProxy = null;
+                // Synchronization should make sure unbind can be successful
+                unbindService(mCsipSetCoordinatorProxyConnection);
+            }
+        }
+    }
+
     public boolean connect(BluetoothDevice device) {
         if (DBG) {
             Log.d(TAG, "connect(): " + device);
@@ -262,7 +433,7 @@
                 Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
                 return false;
             }
-            sm.sendMessage(LeAudioStateMachine.CONNECT, groupId);
+            sm.sendMessage(LeAudioStateMachine.CONNECT);
         }
 
         // Connect other devices from this group
@@ -275,16 +446,17 @@
                     continue;
                 }
                 synchronized (mStateMachines) {
-                    LeAudioStateMachine sm = getOrCreateStateMachine(storedDevice);
-                    if (sm == null) {
-                        Log.e(TAG, "Ignored connect request for " + storedDevice
-                                + " : no state machine");
-                        continue;
-                    }
-                    sm.sendMessage(LeAudioStateMachine.CONNECT, groupId);
-                }
-            }
-        }
+                     LeAudioStateMachine sm = getOrCreateStateMachine(storedDevice);
+                     if (sm == null) {
+                         Log.e(TAG, "Ignored connect request for " + storedDevice
+                                 + " : no state machine");
+                         continue;
+                     }
+                     sm.sendMessage(LeAudioStateMachine.CONNECT);
+                 }
+             }
+         }
+
         return true;
     }
 
@@ -414,6 +586,55 @@
     }
 
     /**
+     * Add device to the given group.
+     * @param groupId group ID the device is being added to
+     * @param device the active device
+     * @return true on success, otherwise false
+     */
+    public boolean groupAddNode(int groupId, BluetoothDevice device) {
+        return mLeAudioNativeInterface.groupAddNode(groupId, device);
+    }
+
+    /**
+     * Remove device from a given group.
+     * @param groupId group ID the device is being removed from
+     * @param device the active device
+     * @return true on success, otherwise false
+     */
+    public boolean groupRemoveNode(int groupId, BluetoothDevice device) {
+        return mLeAudioNativeInterface.groupRemoveNode(groupId, device);
+    }
+
+    /**
+     * Checks if given group exists.
+     * @param group_id group Id to verify
+     * @return true given group exists, otherwise false
+     */
+    public boolean isValidDeviceGroup(int group_id) {
+        return (group_id != LE_AUDIO_GROUP_ID_INVALID) ?
+                mDeviceGroupIdMap.containsValue(group_id) :
+                false;
+    }
+
+    /**
+     * Get all the devices within a given group.
+     * @param group_id group Id to verify
+     * @return all devices within a given group or empty list
+     */
+    public List<BluetoothDevice> getGroupDevices(int group_id) {
+        List<BluetoothDevice> result = new ArrayList<>();
+
+        if (group_id != LE_AUDIO_GROUP_ID_INVALID) {
+            for (BluetoothDevice storedDevice : mDeviceGroupIdMap.keySet()) {
+                if (getGroupId(storedDevice) == group_id) {
+                    result.add(storedDevice);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
      * Get supported group audio direction from available context.
      *
      * @param activeContext bitset of active context to be matched with possible audio direction
@@ -528,7 +749,7 @@
         if (device != null && mPreviousAudioOutDevice != null) {
             int previousGroupId = getGroupId(mPreviousAudioOutDevice);
             if (previousGroupId == groupId) {
-                /* This is thes same group as aleady notified to the system.
+                /* This is the same group as already notified to the system.
                 * Therefore do not change the device we have connected to the group,
                 * unless, previous one is disconnected now
                 */
@@ -755,14 +976,9 @@
         } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED) {
             int group_id = stackEvent.valueInt1;
             int group_status = stackEvent.valueInt2;
+            boolean send_intent = false;
 
             switch (group_status) {
-                case LeAudioStackEvent.GROUP_STATUS_IDLE:
-                case LeAudioStackEvent.GROUP_STATUS_RECONFIGURED:
-                case LeAudioStackEvent.GROUP_STATUS_DESTROYED:
-                case LeAudioStackEvent.GROUP_STATUS_SUSPENDED:
-                case LeAudioStackEvent.GROUP_STATUS_STREAMING:
-                    break;
                 case LeAudioStackEvent.GROUP_STATUS_ACTIVE: {
                     LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(group_id);
                     if (descriptor != null) {
@@ -770,6 +986,7 @@
                             descriptor.mIsActive = true;
                             updateActiveDevices(group_id, ACTIVE_CONTEXTS_NONE,
                                                 descriptor.mActiveContexts, descriptor.mIsActive);
+                            send_intent = true;
                         }
                     } else {
                         Log.e(TAG, "no descriptors for group: " + group_id);
@@ -783,6 +1000,7 @@
                             descriptor.mIsActive = false;
                             updateActiveDevices(group_id, descriptor.mActiveContexts,
                                     ACTIVE_CONTEXTS_NONE, descriptor.mIsActive);
+                            send_intent = true;
                         }
                     } else {
                         Log.e(TAG, "no descriptors for group: " + group_id);
@@ -793,10 +1011,11 @@
                     break;
             }
 
-            intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED);
-            intent.putExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID, group_id);
-            intent.putExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_STATUS, group_status);
-
+            if (send_intent) {
+                intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED);
+                intent.putExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID, group_id);
+                intent.putExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_STATUS, group_status);
+            }
         }
 
         if (intent != null) {
@@ -863,6 +1082,18 @@
         if (bondState != BluetoothDevice.BOND_NONE) {
             return;
         }
+
+        /* Remove bonded set member from outstanding list */
+        if (mSetMemberAvailable.containsKey(device)) {
+            mSetMemberAvailable.remove(device);
+        }
+
+        int groupId = getGroupId(device);
+        if (groupId != LE_AUDIO_GROUP_ID_INVALID) {
+            /* In case device is still in the group, let's remove it */
+            mLeAudioNativeInterface.groupRemoveNode(groupId, device);
+        }
+
         mDeviceGroupIdMap.remove(device);
         synchronized (mStateMachines) {
             LeAudioStateMachine sm = mStateMachines.get(device);
@@ -994,6 +1225,22 @@
         }
     }
 
+    synchronized void setMemberAvailable(BluetoothDevice device, int groupId) {
+        if (device == null) {
+            Log.e(TAG, "unexpected invocation. device=" + device);
+            return;
+        }
+
+        if (mSetMemberAvailable.containsKey(device)) {
+            if (DBG) {
+                Log.d(TAG, "Device " + device + " is already notified- drop it");
+            }
+            return;
+        }
+
+        mSetMemberAvailable.put(device, groupId);
+    }
+
    /**
      * Check whether can connect to a peer device.
      * The check considers a number of factors during the evaluation.
@@ -1086,8 +1333,7 @@
         if (device == null) {
             return LE_AUDIO_GROUP_ID_INVALID;
         }
-        //TODO: implement
-        return LE_AUDIO_GROUP_ID_INVALID;
+        return mDeviceGroupIdMap.getOrDefault(device, LE_AUDIO_GROUP_ID_INVALID);
     }
 
     /**
@@ -1098,6 +1344,23 @@
         if (DBG) {
             Log.d(TAG, "SetVolume " + volume);
         }
+
+        if (mActiveDeviceGroupId == LE_AUDIO_GROUP_ID_INVALID) {
+            Log.e(TAG, "There is no active group ");
+            return;
+        }
+
+        if (mVolumeControlService == null) {
+            Log.e(TAG, "VolumeControl no available ");
+            return;
+        }
+
+        try {
+            mVolumeControlProxy.setVolumeGroup(mActiveDeviceGroupId, volume,
+                                               this.getAttributionSource());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Set Volume failed: " + e);
+        }
     }
 
     /**
@@ -1221,6 +1484,26 @@
         }
 
         @Override
+        public boolean groupAddNode(int group_id, BluetoothDevice device,
+                                    AttributionSource source) {
+            LeAudioService service = getService(source);
+            if (service == null) {
+                return false;
+            }
+            return service.groupAddNode(group_id, device);
+        }
+
+        @Override
+        public boolean groupRemoveNode(int groupId, BluetoothDevice device,
+                                       AttributionSource source) {
+            LeAudioService service = getService(source);
+            if (service == null) {
+                return false;
+            }
+            return service.groupRemoveNode(groupId, device);
+        }
+
+        @Override
         public void setVolume(int volume, AttributionSource source) {
             LeAudioService service = getService(source);
             if (service == null) {
@@ -1234,6 +1517,8 @@
     @Override
     public void dump(StringBuilder sb) {
         super.dump(sb);
-        // TODO: Dump all state machines
+        for (LeAudioStateMachine sm : mStateMachines.values()) {
+            sm.dump(sb);
+        }
     }
 }
diff --git a/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java b/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java
index 992afae..ab40c8b 100644
--- a/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java
+++ b/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java
@@ -40,13 +40,8 @@
     static final int CONNECTION_STATE_CONNECTED = 2;
     static final int CONNECTION_STATE_DISCONNECTING = 3;
 
-    static final int GROUP_STATUS_IDLE = 0;
+    static final int GROUP_STATUS_INACTIVE = 0;
     static final int GROUP_STATUS_ACTIVE = 1;
-    static final int GROUP_STATUS_INACTIVE = 2;
-    static final int GROUP_STATUS_STREAMING = 3;
-    static final int GROUP_STATUS_SUSPENDED = 4;
-    static final int GROUP_STATUS_RECONFIGURED = 5;
-    static final int GROUP_STATUS_DESTROYED = 6;
 
     static final int GROUP_NODE_ADDED = 1;
     static final int GROUP_NODE_REMOVED = 2;
@@ -113,7 +108,7 @@
             case EVENT_TYPE_GROUP_NODE_STATUS_CHANGED:
                 // same as EVENT_TYPE_GROUP_STATUS_CHANGED
             case EVENT_TYPE_GROUP_STATUS_CHANGED:
-                // same as EVENT_TYPE_GROUP_STATUS_CHANGED
+                return "{group_id:" + Integer.toString(value) + "}";
             case EVENT_TYPE_AUDIO_CONF_CHANGED:
                 // FIXME: It should have proper direction names here
                 return "{direction:" + value + "}";
@@ -127,20 +122,10 @@
         switch (type) {
             case EVENT_TYPE_GROUP_STATUS_CHANGED:
                 switch (value) {
-                    case GROUP_STATUS_IDLE:
-                        return "GROUP_STATUS_IDLE";
                     case GROUP_STATUS_ACTIVE:
                         return "GROUP_STATUS_ACTIVE";
                     case GROUP_STATUS_INACTIVE:
                         return "GROUP_STATUS_INACTIVE";
-                    case GROUP_STATUS_STREAMING:
-                        return "GROUP_STATUS_STREAMING";
-                    case GROUP_STATUS_SUSPENDED:
-                        return "GROUP_STATUS_SUSPENDED";
-                    case GROUP_STATUS_RECONFIGURED:
-                        return "GROUP_STATUS_RECONFIGURED";
-                    case GROUP_STATUS_DESTROYED:
-                        return "GROUP_STATUS_DESTROYED";
                     default:
                         break;
                 }
@@ -164,8 +149,6 @@
 
     private static String eventTypeValue3ToString(int type, int value) {
         switch (type) {
-            case EVENT_TYPE_GROUP_STATUS_CHANGED:
-                return "{group_flags:" + Integer.toString(value) + "}";
             case EVENT_TYPE_AUDIO_CONF_CHANGED:
                 // FIXME: It should have proper location names here
                 return "{snk_audio_loc:" + value + "}";
diff --git a/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java b/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java
index 1cfcd91..a838cbb 100644
--- a/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java
+++ b/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java
@@ -63,6 +63,11 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Scanner;
+
 final class LeAudioStateMachine extends StateMachine {
     private static final boolean DBG = false;
     private static final String TAG = "LeAudioStateMachine";
@@ -157,8 +162,7 @@
 
             switch (message.what) {
                 case CONNECT:
-                    int groupId = message.arg1;
-                    log("Connecting to " + mDevice + " group " + groupId);
+                    log("Connecting to " + mDevice);
                     if (!mNativeInterface.connectLeAudio(mDevice)) {
                         Log.e(TAG, "Disconnected: error connecting to " + mDevice);
                         break;
@@ -184,7 +188,7 @@
                     }
                     switch (event.type) {
                         case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
-                            processConnectionEvent(event.valueInt1, event.valueInt2);
+                            processConnectionEvent(event.valueInt1);
                             break;
                         default:
                             Log.e(TAG, "Disconnected: ignoring stack event: " + event);
@@ -198,7 +202,7 @@
         }
 
         // in Disconnected state
-        private void processConnectionEvent(int state, int groupId) {
+        private void processConnectionEvent(int state) {
             switch (state) {
                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
                     Log.w(TAG, "Ignore LeAudio DISCONNECTED event: " + mDevice);
@@ -285,7 +289,7 @@
                     }
                     switch (event.type) {
                         case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
-                            processConnectionEvent(event.valueInt1, event.valueInt2);
+                            processConnectionEvent(event.valueInt1);
                             break;
                         default:
                             Log.e(TAG, "Connecting: ignoring stack event: " + event);
@@ -299,7 +303,7 @@
         }
 
         // in Connecting state
-        private void processConnectionEvent(int state, int groupId) {
+        private void processConnectionEvent(int state) {
             switch (state) {
                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
                     Log.w(TAG, "Connecting device disconnected: " + mDevice);
@@ -371,7 +375,7 @@
                     }
                     switch (event.type) {
                         case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
-                            processConnectionEvent(event.valueInt1, event.valueInt2);
+                            processConnectionEvent(event.valueInt1);
                             break;
                         default:
                             Log.e(TAG, "Disconnecting: ignoring stack event: " + event);
@@ -385,7 +389,7 @@
         }
 
         // in Disconnecting state
-        private void processConnectionEvent(int state, int groupId) {
+        private void processConnectionEvent(int state) {
             switch (state) {
                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
                     Log.i(TAG, "Disconnected: " + mDevice);
@@ -465,7 +469,7 @@
                     }
                     switch (event.type) {
                         case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
-                            processConnectionEvent(event.valueInt1, event.valueInt2);
+                            processConnectionEvent(event.valueInt1);
                             break;
                         default:
                             Log.e(TAG, "Connected: ignoring stack event: " + event);
@@ -479,7 +483,7 @@
         }
 
         // in Connected state
-        private void processConnectionEvent(int state, int groupId) {
+        private void processConnectionEvent(int state) {
             switch (state) {
                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
                     Log.i(TAG, "Disconnected from " + mDevice);
@@ -554,6 +558,24 @@
         return Integer.toString(state);
     }
 
+    public void dump(StringBuilder sb) {
+        ProfileService.println(sb, "mDevice: " + mDevice);
+        ProfileService.println(sb, "  StateMachine: " + this);
+        // Dump the state machine logs
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(stringWriter);
+        super.dump(new FileDescriptor(), printWriter, new String[]{});
+        printWriter.flush();
+        stringWriter.flush();
+        ProfileService.println(sb, "  StateMachineLog:");
+        Scanner scanner = new Scanner(stringWriter.toString());
+        while (scanner.hasNextLine()) {
+            String line = scanner.nextLine();
+            ProfileService.println(sb, "    " + line);
+        }
+        scanner.close();
+    }
+
     @Override
     protected void log(String msg) {
         if (DBG) {
diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java b/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
index 8801c16..ed4eac9 100755
--- a/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
+++ b/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
@@ -352,18 +352,24 @@
                     mContext.getString(android.R.string.unknownName), contactCursor);
 
             if (contactCursor != null) {
-                if (!composer.initWithCallback(contactCursor,
-                        new EnterpriseRawContactEntitlesInfoCallback())) {
+                if (!composer.init(contactCursor)) {
                     return nameList;
                 }
+                int idColumn = contactCursor.getColumnIndex(Data.CONTACT_ID);
+                if (idColumn < 0) {
+                    idColumn = contactCursor.getColumnIndex(Contacts._ID);
+                }
 
                 int i = 0;
                 contactCursor.moveToFirst();
-                while (!composer.isAfterLast()) {
-                    String vcard = composer.createOneEntry();
+                while (!contactCursor.isAfterLast()) {
+                    String vcard = composer.buildVCard(RawContactsEntity.queryRawContactEntity(
+                                mResolver, contactCursor.getLong(idColumn)));
+                    if (!contactCursor.moveToNext()) {
+                        Log.e(TAG, "Cursor#moveToNext() returned false");
+                    }
                     if (vcard == null) {
-                        Log.e(TAG, "Failed to read a contact. Error reason: "
-                                + composer.getErrorReason());
+                        Log.e(TAG, "Failed to read a contact.");
                         return nameList;
                     } else if (vcard.isEmpty()) {
                         Log.i(TAG, "Contact may have been deleted during operation");
@@ -689,23 +695,6 @@
         }
     }
 
-    /**
-     * Handler enterprise contact id in VCardComposer
-     */
-    private static class EnterpriseRawContactEntitlesInfoCallback
-            implements VCardComposer.RawContactEntitlesInfoCallback {
-        @Override
-        public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) {
-            if (Contacts.isEnterpriseContactId(contactId)) {
-                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI,
-                        contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE);
-            } else {
-                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI,
-                        contactId);
-            }
-        }
-    }
-
     private int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor,
             final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) {
         long timestamp = 0;
@@ -754,21 +743,27 @@
             });
             buffer = new HandlerForStringBuffer(op, ownerVCard);
             Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount());
-            if (!composer.initWithCallback(contactIdCursor,
-                    new EnterpriseRawContactEntitlesInfoCallback()) || !buffer.onInit(mContext)) {
+            if (!composer.init(contactIdCursor) || !buffer.onInit(mContext)) {
                 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
             }
+            int idColumn = contactIdCursor.getColumnIndex(Data.CONTACT_ID);
+            if (idColumn < 0) {
+                idColumn = contactIdCursor.getColumnIndex(Contacts._ID);
+            }
 
-            while (!composer.isAfterLast()) {
+            while (!contactIdCursor.isAfterLast()) {
                 if (BluetoothPbapObexServer.sIsAborted) {
                     ((ServerOperation) op).isAborted = true;
                     BluetoothPbapObexServer.sIsAborted = false;
                     break;
                 }
-                String vcard = composer.createOneEntry();
+                String vcard = composer.buildVCard(RawContactsEntity.queryRawContactEntity(
+                            mResolver, contactIdCursor.getLong(idColumn)));
+                if (!contactIdCursor.moveToNext()) {
+                    Log.e(TAG, "Cursor#moveToNext() returned false");
+                }
                 if (vcard == null) {
-                    Log.e(TAG,
-                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
+                    Log.e(TAG, "Failed to read a contact.");
                     return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
                 } else if (vcard.isEmpty()) {
                     Log.i(TAG, "Contact may have been deleted during operation");
@@ -853,21 +848,27 @@
             });
             buffer = new HandlerForStringBuffer(op, ownerVCard);
             Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount());
-            if (!composer.initWithCallback(contactIdCursor,
-                    new EnterpriseRawContactEntitlesInfoCallback()) || !buffer.onInit(mContext)) {
+            if (!composer.init(contactIdCursor) || !buffer.onInit(mContext)) {
                 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
             }
+            int idColumn = contactIdCursor.getColumnIndex(Data.CONTACT_ID);
+            if (idColumn < 0) {
+                idColumn = contactIdCursor.getColumnIndex(Contacts._ID);
+            }
 
-            while (!composer.isAfterLast()) {
+            while (!contactIdCursor.isAfterLast()) {
                 if (BluetoothPbapObexServer.sIsAborted) {
                     ((ServerOperation) op).isAborted = true;
                     BluetoothPbapObexServer.sIsAborted = false;
                     break;
                 }
-                String vcard = composer.createOneEntry();
+                String vcard = composer.buildVCard(RawContactsEntity.queryRawContactEntity(
+                            mResolver, contactIdCursor.getLong(idColumn)));
+                if (!contactIdCursor.moveToNext()) {
+                    Log.e(TAG, "Cursor#moveToNext() returned false");
+                }
                 if (vcard == null) {
-                    Log.e(TAG,
-                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
+                    Log.e(TAG, "Failed to read a contact.");
                     return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
                 } else if (vcard.isEmpty()) {
                     Log.i(TAG, "Contact may have been deleted during operation");
diff --git a/src/com/android/bluetooth/telephony/BluetoothInCallService.java b/src/com/android/bluetooth/telephony/BluetoothInCallService.java
index 410dc09..dacfa06 100644
--- a/src/com/android/bluetooth/telephony/BluetoothInCallService.java
+++ b/src/com/android/bluetooth/telephony/BluetoothInCallService.java
@@ -26,6 +26,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.media.AudioManager;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
@@ -55,6 +56,9 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Used to receive updates about calls from the Telecom component. This service is bound to Telecom
@@ -89,6 +93,8 @@
     // Indicates that no BluetoothCall is ringing
     private static final int DEFAULT_RINGING_ADDRESS_TYPE = 128;
 
+    private static final int DISCONNECT_TONE_TIMEOUT_SECONDS = 1;
+
     private int mNumActiveCalls = 0;
     private int mNumHeldCalls = 0;
     private int mNumChildrenOfActiveCall = 0;
@@ -102,6 +108,13 @@
     private static final Object LOCK = new Object();
     private BluetoothHeadsetProxy mBluetoothHeadset;
 
+    private Semaphore mDisconnectionToneSemaphore = new Semaphore(0);
+    private int mAudioMode = AudioManager.MODE_INVALID;
+    private final Object mAudioModeLock = new Object();
+
+    @VisibleForTesting
+    public AudioManager mAudioManager;
+
     @VisibleForTesting
     public TelephonyManager mTelephonyManager;
 
@@ -291,6 +304,19 @@
         }
     }
 
+    class BluetoothOnModeChangedListener implements AudioManager.OnModeChangedListener {
+        @Override
+        public void onModeChanged(int mode) {
+            synchronized (mAudioModeLock) {
+                mAudioMode = mode;
+            }
+            if (mode == AudioManager.MODE_NORMAL) {
+                mDisconnectionToneSemaphore.release();
+            }
+        }
+    }
+    private BluetoothOnModeChangedListener mBluetoothOnModeChangedListener;
+
     @Override
     public IBinder onBind(Intent intent) {
         Log.i(TAG, "onBind. Intent: " + intent);
@@ -525,6 +551,26 @@
             return;
         }
         Log.d(TAG, "onCallRemoved");
+        BluetoothCall heldCall = mCallInfo.getHeldCall();
+        if (mCallInfo.isNullCall(heldCall)) {
+            // current call is the only call
+
+            mDisconnectionToneSemaphore.drainPermits();
+            boolean isAudioModeNormal = false;
+            synchronized (mAudioModeLock) {
+                isAudioModeNormal = (mAudioMode == AudioManager.MODE_NORMAL);
+            }
+            if (!isAudioModeNormal) {
+                Log.d(TAG, "Acquiring mDisconnectionToneSemaphore");
+                try {
+                  boolean result = mDisconnectionToneSemaphore.tryAcquire(
+                    DISCONNECT_TONE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+                  Log.d(TAG, "Acquiring mDisconnectionToneSemaphore result " + result);
+                } catch (InterruptedException e) {
+                  Log.w(TAG, "Failed to acquire mDisconnectionToneSemaphore");
+                }
+            }
+        }
         CallStateCallback callback = getCallback(call);
         if (callback != null) {
             call.unregisterCallback(callback);
@@ -565,11 +611,19 @@
         mBluetoothAdapterReceiver = new BluetoothAdapterReceiver();
         IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
         registerReceiver(mBluetoothAdapterReceiver, intentFilter);
+        mBluetoothOnModeChangedListener = new BluetoothOnModeChangedListener();
+        mAudioManager = getSystemService(AudioManager.class);
+        mAudioManager.addOnModeChangedListener(
+                Executors.newSingleThreadExecutor(), mBluetoothOnModeChangedListener);
     }
 
     @Override
     public void onDestroy() {
         Log.d(TAG, "onDestroy");
+        if (mBluetoothOnModeChangedListener != null) {
+            mAudioManager.removeOnModeChangedListener(mBluetoothOnModeChangedListener);
+            mBluetoothOnModeChangedListener = null;
+        }
         if (mBluetoothAdapterReceiver != null) {
             unregisterReceiver(mBluetoothAdapterReceiver);
             mBluetoothAdapterReceiver = null;
diff --git a/src/com/android/bluetooth/vc/VolumeControlService.java b/src/com/android/bluetooth/vc/VolumeControlService.java
index 8c55767..cb22e4d 100644
--- a/src/com/android/bluetooth/vc/VolumeControlService.java
+++ b/src/com/android/bluetooth/vc/VolumeControlService.java
@@ -426,14 +426,17 @@
     }
 
     void messageFromNative(VolumeControlStackEvent stackEvent) {
+
+        if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED) {
+            handleVolumeControlChanged(stackEvent.device, stackEvent.valueInt1,
+                                       stackEvent.valueInt2, stackEvent.valueBool1);
+          return;
+        }
+
         Objects.requireNonNull(stackEvent.device,
                 "Device should never be null, event: " + stackEvent);
 
         Intent intent = null;
-        if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED) {
-            handleVolumeControlChanged(stackEvent.device, stackEvent.valueInt1,
-                                       stackEvent.valueInt2, stackEvent.valueBool1);
-        }
 
         if (intent != null) {
             intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml
index 6edd9a5..8cea43c 100644
--- a/tests/unit/AndroidTest.xml
+++ b/tests/unit/AndroidTest.xml
@@ -22,8 +22,8 @@
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="settings put global ble_scan_always_enabled 0" />
-        <option name="run-command" value="svc bluetooth disable" />
-        <option name="teardown-command" value="svc bluetooth enable" />
+        <option name="run-command" value="su u$(am get-current-user)_system svc bluetooth disable" />
+        <option name="teardown-command" value="su u$(am get-current-user)_system svc bluetooth enable" />
         <option name="teardown-command" value="settings put global ble_scan_always_enabled 1" />
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.FolderSaver">
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java
index 6917e2f..d767156 100644
--- a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java
@@ -15,12 +15,17 @@
  */
 package com.android.bluetooth.a2dpsink;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
 import static org.mockito.Mockito.*;
 
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioConfig;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
+import android.media.AudioFormat;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
@@ -33,7 +38,6 @@
 import com.android.bluetooth.btservice.storage.DatabaseManager;
 
 import org.junit.After;
-import org.junit.Assert;
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
@@ -42,6 +46,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class A2dpSinkServiceTest {
@@ -53,6 +60,17 @@
 
     @Mock private AdapterService mAdapterService;
     @Mock private DatabaseManager mDatabaseManager;
+    @Mock private A2dpSinkNativeInterface mNativeInterface;
+
+    private BluetoothDevice mDevice1;
+    private BluetoothDevice mDevice2;
+    private BluetoothDevice mDevice3;
+    private BluetoothDevice mDevice4;
+    private BluetoothDevice mDevice5;
+    private BluetoothDevice mDevice6;
+
+    private static final int TEST_SAMPLE_RATE = 44;
+    private static final int TEST_CHANNEL_COUNT = 1;
 
     @Before
     public void setUp() throws Exception {
@@ -60,16 +78,33 @@
         Assume.assumeTrue("Ignore test when A2dpSinkService is not enabled",
                 mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink));
         MockitoAnnotations.initMocks(this);
+
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        assertThat(mAdapter).isNotNull();
+        mDevice1 = makeBluetoothDevice("11:11:11:11:11:11");
+        mDevice2 = makeBluetoothDevice("22:22:22:22:22:22");
+        mDevice3 = makeBluetoothDevice("33:33:33:33:33:33");
+        mDevice4 = makeBluetoothDevice("44:44:44:44:44:44");
+        mDevice5 = makeBluetoothDevice("55:55:55:55:55:55");
+        mDevice6 = makeBluetoothDevice("66:66:66:66:66:66");
+        BluetoothDevice[] bondedDevices = new BluetoothDevice[]{
+            mDevice1, mDevice2, mDevice3, mDevice4, mDevice5, mDevice6
+        };
+
+        // Setup the adapter service and start our service under test
         TestUtils.setAdapterService(mAdapterService);
         doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
         doReturn(true, false).when(mAdapterService).isStartedProfile(anyString());
+        doReturn(bondedDevices).when(mAdapterService).getBondedDevices();
+        when(mDatabaseManager.setProfileConnectionPolicy(any(), anyInt(),
+                anyInt())).thenReturn(true);
         setMaxConnectedAudioDevices(1);
         TestUtils.startService(mServiceRule, A2dpSinkService.class);
         mService = A2dpSinkService.getA2dpSinkService();
-        Assert.assertNotNull(mService);
-        // Try getting the Bluetooth adapter
-        mAdapter = BluetoothAdapter.getDefaultAdapter();
-        Assert.assertNotNull(mAdapter);
+        assertThat(mService).isNotNull();
+
+        mService.mNativeInterface = mNativeInterface;
+        doReturn(true).when(mNativeInterface).setActiveDevice(any());
     }
 
     @After
@@ -79,10 +114,31 @@
         }
         TestUtils.stopService(mServiceRule, A2dpSinkService.class);
         mService = A2dpSinkService.getA2dpSinkService();
-        Assert.assertNull(mService);
+        assertThat(mService).isNull();
         TestUtils.clearAdapterService(mAdapterService);
     }
 
+    private void setupDeviceConnection(BluetoothDevice device) {
+        assertThat(mService.getConnectionState(device)).isEqualTo(
+                BluetoothProfile.STATE_DISCONNECTED);
+        assertThat(mService.connect(device)).isTrue();
+        sendConnectionEvent(device, StackEvent.CONNECTION_STATE_CONNECTED);
+        waitForDeviceProcessing(device);
+        assertThat(mService.getConnectionState(device)).isEqualTo(
+                BluetoothProfile.STATE_CONNECTED);
+    }
+
+    private void sendConnectionEvent(BluetoothDevice device, int newState) {
+        StackEvent event = StackEvent.connectionStateChanged(device, newState);
+        mService.messageFromNative(event);
+    }
+
+    private void waitForDeviceProcessing(BluetoothDevice device) {
+        A2dpSinkStateMachine sm = mService.getStateMachineForDevice(device);
+        if (sm == null) return;
+        TestUtils.waitForLooperToFinishScheduledTask(sm.getHandler().getLooper());
+    }
+
     private BluetoothDevice makeBluetoothDevice(String address) {
         return mAdapter.getRemoteDevice(address);
     }
@@ -105,29 +161,49 @@
                 .thenReturn(priority);
     }
 
+    /**
+     * Test that initialization of the service completes and that we can get a instance
+     */
     @Test
     public void testInitialize() {
-        Assert.assertNotNull(A2dpSinkService.getA2dpSinkService());
+        assertThat(A2dpSinkService.getA2dpSinkService()).isNotNull();
     }
 
     /**
-     * Test that a PRIORITY_ON device is connected to
+     * Test that asking to connect with a null device fails
      */
     @Test
-    public void testConnect() {
-        BluetoothDevice device = makeBluetoothDevice("11:11:11:11:11:11");
-        mockDevicePriority(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
-        Assert.assertTrue(mService.connect(device));
+    public void testConnectNullDevice() {
+        assertThrows(IllegalArgumentException.class, () -> mService.connect(null));
     }
 
     /**
-     * Test that a PRIORITY_OFF device is not connected to
+     * Test that a CONNECTION_POLICY_ALLOWED device can connected
      */
     @Test
-    public void testConnectPriorityOffDevice() {
-        BluetoothDevice device = makeBluetoothDevice("11:11:11:11:11:11");
-        mockDevicePriority(device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
-        Assert.assertFalse(mService.connect(device));
+    public void testConnectPolicyAllowedDevice() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        setupDeviceConnection(mDevice1);
+    }
+
+    /**
+     * Test that a CONNECTION_POLICY_FORBIDDEN device is not allowed to connect
+     */
+    @Test
+    public void testConnectPolicyForbiddenDevice() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+        assertThat(mService.connect(mDevice1)).isFalse();
+        assertThat(mService.getConnectionState(mDevice1)).isEqualTo(
+                BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    /**
+     * Test that a CONNECTION_POLICY_UNKNOWN device is allowed to connect
+     */
+    @Test
+    public void testConnectPolicyUnknownDevice() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+        setupDeviceConnection(mDevice1);
     }
 
     /**
@@ -137,22 +213,259 @@
     public void testConnectMultipleDevices() {
         setMaxConnectedAudioDevices(5);
 
-        BluetoothDevice device1 = makeBluetoothDevice("11:11:11:11:11:11");
-        BluetoothDevice device2 = makeBluetoothDevice("22:22:22:22:22:22");
-        BluetoothDevice device3 = makeBluetoothDevice("33:33:33:33:33:33");
-        BluetoothDevice device4 = makeBluetoothDevice("44:44:44:44:44:44");
-        BluetoothDevice device5 = makeBluetoothDevice("55:55:55:55:55:55");
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        mockDevicePriority(mDevice2, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        mockDevicePriority(mDevice3, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        mockDevicePriority(mDevice4, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        mockDevicePriority(mDevice5, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        mockDevicePriority(mDevice6, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
 
-        mockDevicePriority(device1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
-        mockDevicePriority(device2, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
-        mockDevicePriority(device3, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
-        mockDevicePriority(device4, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
-        mockDevicePriority(device5, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        setupDeviceConnection(mDevice1);
+        setupDeviceConnection(mDevice2);
+        setupDeviceConnection(mDevice3);
+        setupDeviceConnection(mDevice4);
+        setupDeviceConnection(mDevice5);
+    }
 
-        Assert.assertTrue(mService.connect(device1));
-        Assert.assertTrue(mService.connect(device2));
-        Assert.assertTrue(mService.connect(device3));
-        Assert.assertTrue(mService.connect(device4));
-        Assert.assertTrue(mService.connect(device5));
+    /**
+     * Test to make sure we can disconnect a connected device
+     */
+    @Test
+    public void testDisconnect() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        setupDeviceConnection(mDevice1);
+
+        assertThat(mService.disconnect(mDevice1)).isTrue();
+        waitForDeviceProcessing(mDevice1);
+        assertThat(mService.getConnectionState(mDevice1)).isEqualTo(
+                BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    /**
+     * Assure disconnect() fails with a device that's not connected
+     */
+    @Test
+    public void testDisconnectDeviceDoesNotExist() {
+        assertThat(mService.disconnect(mDevice1)).isFalse();
+    }
+
+    /**
+     * Assure disconnect() fails with an invalid device
+     */
+    @Test
+    public void testDisconnectNullDevice() {
+        assertThrows(IllegalArgumentException.class, () -> mService.disconnect(null));
+    }
+
+    /**
+     * Assure dump() returns something and does not crash
+     */
+    @Test
+    public void testDump() {
+        StringBuilder sb = new StringBuilder();
+        mService.dump(sb);
+        assertThat(sb.toString()).isNotNull();
+    }
+
+    /**
+     * Test that we can set the active device to a valid device and receive it back from
+     * GetActiveDevice()
+     */
+    @Test
+    public void testSetActiveDevice() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        assertThat(mService.getActiveDevice()).isNotEqualTo(mDevice1);
+        assertThat(mService.setActiveDevice(mDevice1)).isTrue();
+        assertThat(mService.getActiveDevice()).isEqualTo(mDevice1);
+    }
+
+    /**
+     * Test that calls to set a null active device succeed in unsetting the active device
+     */
+    @Test
+    public void testSetActiveDeviceNullDevice() {
+        assertThat(mService.setActiveDevice(null)).isTrue();
+        assertThat(mService.getActiveDevice()).isNull();
+    }
+
+    /**
+     * Make sure we can receive the set audio configuration
+     */
+    @Test
+    public void testGetAudioConfiguration() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        setupDeviceConnection(mDevice1);
+
+        StackEvent audioConfigChanged =
+                StackEvent.audioConfigChanged(mDevice1, TEST_SAMPLE_RATE, TEST_CHANNEL_COUNT);
+        mService.messageFromNative(audioConfigChanged);
+        waitForDeviceProcessing(mDevice1);
+
+        BluetoothAudioConfig expected = new BluetoothAudioConfig(TEST_SAMPLE_RATE,
+                TEST_CHANNEL_COUNT, AudioFormat.ENCODING_PCM_16BIT);
+        BluetoothAudioConfig config = mService.getAudioConfig(mDevice1);
+        assertThat(config).isEqualTo(expected);
+    }
+
+    /**
+     * Getting an audio config for a device that hasn't received one yet should return null
+     */
+    @Test
+    public void testGetAudioConfigWithConfigUnset() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        setupDeviceConnection(mDevice1);
+        assertThat(mService.getAudioConfig(mDevice1)).isNull();
+    }
+
+    /**
+     * Getting an audio config for a null device should return null
+     */
+    @Test
+    public void testGetAudioConfigNullDevice() {
+        assertThat(mService.getAudioConfig(null)).isNull();
+    }
+
+    /**
+     * Test that a newly connected device ends up in the set returned by
+     * getConnectedDevices
+     */
+    @Test
+    public void testGetConnectedDevices() {
+        ArrayList<BluetoothDevice> expected = new ArrayList<BluetoothDevice>();
+        expected.add(mDevice1);
+
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        setupDeviceConnection(mDevice1);
+
+        List<BluetoothDevice> devices = mService.getConnectedDevices();
+        assertThat(devices).isEqualTo(expected);
+    }
+
+    /**
+     * Test that a newly connected device ends up in the set returned by
+     * testGetDevicesMatchingConnectionStates
+     */
+    @Test
+    public void testGetDevicesMatchingConnectionStatesConnected() {
+        ArrayList<BluetoothDevice> expected = new ArrayList<BluetoothDevice>();
+        expected.add(mDevice1);
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        setupDeviceConnection(mDevice1);
+
+        List<BluetoothDevice> devices = mService.getDevicesMatchingConnectionStates(
+                new int[] {BluetoothProfile.STATE_CONNECTED});
+        assertThat(devices).isEqualTo(expected);
+    }
+
+    /**
+     * Test that a all bonded device end up in the set returned by
+     * testGetDevicesMatchingConnectionStates, even when they're disconnected
+     */
+    @Test
+    public void testGetDevicesMatchingConnectionStatesDisconnected() {
+        ArrayList<BluetoothDevice> expected = new ArrayList<BluetoothDevice>();
+        expected.add(mDevice1);
+        expected.add(mDevice2);
+        expected.add(mDevice3);
+        expected.add(mDevice4);
+        expected.add(mDevice5);
+        expected.add(mDevice6);
+
+        List<BluetoothDevice> devices = mService.getDevicesMatchingConnectionStates(
+                new int[] {BluetoothProfile.STATE_DISCONNECTED});
+        assertThat(devices).isEqualTo(expected);
+    }
+
+    /**
+     * Test that GetConnectionPolicy() can get a device with policy "Allowed"
+     */
+    @Test
+    public void testGetConnectionPolicyDeviceAllowed() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        assertThat(mService.getConnectionPolicy(mDevice1)).isEqualTo(
+                BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+    }
+
+    /**
+     * Test that GetConnectionPolicy() can get a device with policy "Forbidden"
+     */
+    @Test
+    public void testGetConnectionPolicyDeviceForbidden() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+        assertThat(mService.getConnectionPolicy(mDevice1)).isEqualTo(
+                BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+    }
+
+    /**
+     * Test that GetConnectionPolicy() can get a device with policy "Unknown"
+     */
+    @Test
+    public void testGetConnectionPolicyDeviceUnknown() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+        assertThat(mService.getConnectionPolicy(mDevice1)).isEqualTo(
+                BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+    }
+
+    /**
+     * Test that SetConnectionPolicy() can change a device's policy to "Allowed"
+     */
+    @Test
+    public void testSetConnectionPolicyDeviceAllowed() {
+        assertThat(mService.setConnectionPolicy(mDevice1,
+                BluetoothProfile.CONNECTION_POLICY_ALLOWED)).isTrue();
+        verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mDevice1,
+                BluetoothProfile.A2DP_SINK, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+    }
+
+    /**
+     * Test that SetConnectionPolicy() can change a device's policy to "Forbidden"
+     */
+    @Test
+    public void testSetConnectionPolicyDeviceForbiddenWhileNotConnected() {
+        assertThat(mService.setConnectionPolicy(mDevice1,
+                BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)).isTrue();
+        verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mDevice1,
+                BluetoothProfile.A2DP_SINK, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+    }
+
+    /**
+     * Test that SetConnectionPolicy() can change a connected device's policy to "Forbidden"
+     * and that the new "Forbidden" policy causes a disconnect of the device.
+     */
+    @Test
+    public void testSetConnectionPolicyDeviceForbiddenWhileConnected() {
+        mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        setupDeviceConnection(mDevice1);
+
+        assertThat(mService.setConnectionPolicy(mDevice1,
+                BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)).isTrue();
+        verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mDevice1,
+                BluetoothProfile.A2DP_SINK, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+
+        waitForDeviceProcessing(mDevice1);
+        assertThat(mService.getConnectionState(mDevice1)).isEqualTo(
+                BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    /**
+     * Test that SetConnectionPolicy() can change a device's policy to "Unknown"
+     */
+    @Test
+    public void testSetConnectionPolicyDeviceUnknown() {
+        assertThat(mService.setConnectionPolicy(mDevice1,
+                BluetoothProfile.CONNECTION_POLICY_UNKNOWN)).isTrue();
+        verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mDevice1,
+                BluetoothProfile.A2DP_SINK, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+    }
+
+    /**
+     * Test that SetConnectionPolicy is robust to DatabaseManager failures
+     */
+    @Test
+    public void testSetConnectionPolicyDatabaseWriteFails() {
+        when(mDatabaseManager.setProfileConnectionPolicy(any(), anyInt(),
+                anyInt())).thenReturn(false);
+        assertThat(mService.setConnectionPolicy(mDevice1,
+                BluetoothProfile.CONNECTION_POLICY_ALLOWED)).isFalse();
     }
 }
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachineTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachineTest.java
new file mode 100644
index 0000000..74d3e3c
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachineTest.java
@@ -0,0 +1,370 @@
+/*
+ * 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.
+ */
+package com.android.bluetooth.a2dpsink;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioConfig;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioFormat;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class A2dpSinkStateMachineTest {
+    private Context mTargetContext;
+
+    private BluetoothAdapter mAdapter;
+    private BluetoothDevice mDevice;
+    private final String mDeviceAddress = "11:11:11:11:11:11";
+    @Mock private A2dpSinkService mService;
+    @Mock private A2dpSinkNativeInterface mNativeInterface;
+
+    A2dpSinkStateMachine mStateMachine;
+    private static final int TIMEOUT_MS = 1000;
+    private static final int CONNECT_TIMEOUT_MS = 6000;
+    private static final int UNHANDLED_MESSAGE = 9999;
+
+    @Before
+    public void setUp() throws Exception {
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        Assume.assumeTrue("Ignore test when A2dpSinkService is not enabled",
+                mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink));
+        MockitoAnnotations.initMocks(this);
+
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        assertThat(mAdapter).isNotNull();
+        mDevice = mAdapter.getRemoteDevice(mDeviceAddress);
+
+        doNothing().when(mService).removeStateMachine(any(A2dpSinkStateMachine.class));
+
+        mStateMachine = new A2dpSinkStateMachine(mDevice, mService, mNativeInterface);
+        mStateMachine.start();
+        assertThat(mStateMachine.getDevice()).isEqualTo(mDevice);
+        assertThat(mStateMachine.getAudioConfig()).isNull();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink)) {
+            return;
+        }
+        mStateMachine = null;
+        mDevice = null;
+        mAdapter = null;
+    }
+
+    private void mockDeviceConnectionPolicy(BluetoothDevice device, int policy) {
+        doReturn(policy).when(mService).getConnectionPolicy(device);
+    }
+
+    private void sendConnectionEvent(int state) {
+        mStateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT,
+                StackEvent.connectionStateChanged(mDevice, state));
+    }
+
+    private void sendAudioConfigChangedEvent(int sampleRate, int channelCount) {
+        mStateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT,
+                StackEvent.audioConfigChanged(mDevice, sampleRate, channelCount));
+    }
+
+    /**********************************************************************************************
+     * DISCONNECTED STATE TESTS                                                                   *
+     *********************************************************************************************/
+
+    @Test
+    public void testConnectInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        mStateMachine.connect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mNativeInterface, timeout(TIMEOUT_MS).times(1)).connectA2dpSink(mDevice);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    @Test
+    public void testDisconnectInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        mStateMachine.disconnect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testAudioConfigChangedInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendAudioConfigChangedEvent(44, 1);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        assertThat(mStateMachine.getAudioConfig()).isNull();
+    }
+
+    @Test
+    public void testIncomingConnectedInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testAllowedIncomingConnectionInDisconnected() {
+        mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        verify(mNativeInterface, times(0)).connectA2dpSink(mDevice);
+    }
+
+    @Test
+    public void testForbiddenIncomingConnectionInDisconnected() {
+        mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mNativeInterface, times(1)).disconnectA2dpSink(mDevice);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testUnknownIncomingConnectionInDisconnected() {
+        mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        verify(mNativeInterface, times(0)).connectA2dpSink(mDevice);
+    }
+
+    @Test
+    public void testIncomingDisconnectInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+    }
+
+    @Test
+    public void testIncomingDisconnectingInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        verify(mService, times(0)).removeStateMachine(mStateMachine);
+    }
+
+    @Test
+    public void testIncomingConnectingInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testUnhandledMessageInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        mStateMachine.sendMessage(UNHANDLED_MESSAGE);
+        mStateMachine.sendMessage(UNHANDLED_MESSAGE, 0 /* arbitrary payload */);
+    }
+
+    /**********************************************************************************************
+     * CONNECTING STATE TESTS                                                                     *
+     *********************************************************************************************/
+
+    @Test
+    public void testConnectedInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testConnectingInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    @Test
+    public void testDisconnectingInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    @Test
+    public void testDisconnectedInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testConnectionTimeoutInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        verify(mService, timeout(CONNECT_TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testAudioStateChangeInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendAudioConfigChangedEvent(44, 1);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        assertThat(mStateMachine.getAudioConfig()).isNull();
+    }
+
+    @Test
+    public void testConnectInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        mStateMachine.connect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    @Test
+    public void testDisconnectInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        mStateMachine.disconnect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    /**********************************************************************************************
+     * CONNECTED STATE TESTS                                                                      *
+     *********************************************************************************************/
+
+    @Test
+    public void testConnectInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        mStateMachine.connect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testDisconnectInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        mStateMachine.disconnect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mNativeInterface, times(1)).disconnectA2dpSink(mDevice);
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testAudioStateChangeInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendAudioConfigChangedEvent(44, 1);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        BluetoothAudioConfig expected =
+                new BluetoothAudioConfig(44, 1, AudioFormat.ENCODING_PCM_16BIT);
+        BluetoothAudioConfig config = mStateMachine.getAudioConfig();
+        assertThat(config).isEqualTo(expected);
+    }
+
+    @Test
+    public void testConnectedInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testConnectingInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testDisconnectingInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testDisconnectedInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    /**********************************************************************************************
+     * OTHER TESTS                                                                                *
+     *********************************************************************************************/
+
+    @Test
+    public void testDump() {
+        StringBuilder sb = new StringBuilder();
+        mStateMachine.dump(sb);
+        assertThat(sb.toString()).isNotNull();
+    }
+}
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
index 1a70021..9f3bd45 100644
--- a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.bluetooth.a2dpsink;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.*;
 
 import android.content.Context;
@@ -30,6 +32,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
 
 import org.junit.Assume;
 import org.junit.Before;
@@ -46,10 +49,10 @@
     private A2dpSinkStreamHandler mStreamHandler;
     private Context mTargetContext;
 
-    @Mock private Context mMockContext;
-
     @Mock private A2dpSinkService mMockA2dpSink;
 
+    @Mock private A2dpSinkNativeInterface mMockNativeInterface;
+
     @Mock private AudioManager mMockAudioManager;
 
     @Mock private Resources mMockResources;
@@ -70,20 +73,21 @@
         mHandlerThread = new HandlerThread("A2dpSinkStreamHandlerTest");
         mHandlerThread.start();
 
-        when(mMockContext.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mMockAudioManager);
-        when(mMockContext.getSystemServiceName(AudioManager.class))
+        when(mMockA2dpSink.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mMockAudioManager);
+        when(mMockA2dpSink.getSystemServiceName(AudioManager.class))
                 .thenReturn(Context.AUDIO_SERVICE);
-        when(mMockContext.getResources()).thenReturn(mMockResources);
+        when(mMockA2dpSink.getResources()).thenReturn(mMockResources);
         when(mMockResources.getInteger(anyInt())).thenReturn(DUCK_PERCENT);
         when(mMockAudioManager.requestAudioFocus(any())).thenReturn(
                 AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
-        when(mMockAudioManager.abandonAudioFocus(any())).thenReturn(AudioManager.AUDIOFOCUS_GAIN);
-        doNothing().when(mMockA2dpSink).informAudioTrackGainNative(anyFloat());
-        when(mMockContext.getMainLooper()).thenReturn(mHandlerThread.getLooper());
-        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockAudioManager.abandonAudioFocus(any())).thenReturn(
+                AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+        when(mMockAudioManager.generateAudioSessionId()).thenReturn(0);
+        when(mMockA2dpSink.getMainLooper()).thenReturn(mHandlerThread.getLooper());
+        when(mMockA2dpSink.getPackageManager()).thenReturn(mMockPackageManager);
         when(mMockPackageManager.hasSystemFeature(any())).thenReturn(false);
 
-        mStreamHandler = spy(new A2dpSinkStreamHandler(mMockA2dpSink, mMockContext));
+        mStreamHandler = spy(new A2dpSinkStreamHandler(mMockA2dpSink, mMockNativeInterface));
     }
 
     @Test
@@ -92,8 +96,9 @@
         mStreamHandler.handleMessage(
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_STR_START));
         verify(mMockAudioManager, times(0)).requestAudioFocus(any());
-        verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
-        verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+        verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+        verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+        assertThat(mStreamHandler.isPlaying()).isFalse();
     }
 
     @Test
@@ -102,17 +107,19 @@
         mStreamHandler.handleMessage(
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_STR_STOP));
         verify(mMockAudioManager, times(0)).requestAudioFocus(any());
-        verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
-        verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+        verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+        verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+        assertThat(mStreamHandler.isPlaying()).isFalse();
     }
 
     @Test
     public void testSnkPlay() {
-        // Play was pressed locally, expect streaming to start.
+        // Play was pressed locally, expect streaming to start soon.
         mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SNK_PLAY));
         verify(mMockAudioManager, times(1)).requestAudioFocus(any());
-        verify(mMockA2dpSink, times(1)).informAudioFocusStateNative(1);
-        verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(1.0f);
+        verify(mMockNativeInterface, times(1)).informAudioFocusState(1);
+        verify(mMockNativeInterface, times(1)).informAudioTrackGain(1.0f);
+        assertThat(mStreamHandler.isPlaying()).isFalse();
     }
 
     @Test
@@ -120,8 +127,9 @@
         // Pause was pressed locally, expect streaming to stop.
         mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SNK_PAUSE));
         verify(mMockAudioManager, times(0)).requestAudioFocus(any());
-        verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
-        verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+        verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+        verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+        assertThat(mStreamHandler.isPlaying()).isFalse();
     }
 
     @Test
@@ -131,7 +139,8 @@
         mStreamHandler.handleMessage(
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.DISCONNECT));
         verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
-        verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0);
+        verify(mMockNativeInterface, times(0)).informAudioFocusState(0);
+        assertThat(mStreamHandler.isPlaying()).isFalse();
     }
 
     @Test
@@ -139,8 +148,9 @@
         // Play was pressed remotely, expect no streaming due to lack of audio focus.
         mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY));
         verify(mMockAudioManager, times(0)).requestAudioFocus(any());
-        verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
-        verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+        verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+        verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+        assertThat(mStreamHandler.isPlaying()).isFalse();
     }
 
     @Test
@@ -149,8 +159,9 @@
         when(mMockPackageManager.hasSystemFeature(any())).thenReturn(true);
         mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY));
         verify(mMockAudioManager, times(1)).requestAudioFocus(any());
-        verify(mMockA2dpSink, times(1)).informAudioFocusStateNative(1);
-        verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(1.0f);
+        verify(mMockNativeInterface, times(1)).informAudioFocusState(1);
+        verify(mMockNativeInterface, times(1)).informAudioTrackGain(1.0f);
+        assertThat(mStreamHandler.isPlaying()).isTrue();
     }
 
     @Test
@@ -158,8 +169,9 @@
         // Play was pressed locally, expect streaming to start.
         mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY));
         verify(mMockAudioManager, times(0)).requestAudioFocus(any());
-        verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
-        verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+        verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+        verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+        assertThat(mStreamHandler.isPlaying()).isFalse();
     }
 
     @Test
@@ -170,8 +182,11 @@
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
                         AudioManager.AUDIOFOCUS_GAIN));
         verify(mMockAudioManager, times(1)).requestAudioFocus(any());
-        verify(mMockA2dpSink, times(2)).informAudioFocusStateNative(1);
-        verify(mMockA2dpSink, times(2)).informAudioTrackGainNative(1.0f);
+        verify(mMockNativeInterface, times(2)).informAudioFocusState(1);
+        verify(mMockNativeInterface, times(2)).informAudioTrackGain(1.0f);
+
+        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+        assertThat(mStreamHandler.getFocusState()).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);
     }
 
     @Test
@@ -181,7 +196,11 @@
         mStreamHandler.handleMessage(
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
                         AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
-        verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(DUCK_PERCENT / 100.0f);
+        verify(mMockNativeInterface, times(1)).informAudioTrackGain(DUCK_PERCENT / 100.0f);
+
+        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+        assertThat(mStreamHandler.getFocusState()).isEqualTo(
+                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
     }
 
     @Test
@@ -192,8 +211,12 @@
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
                         AudioManager.AUDIOFOCUS_LOSS_TRANSIENT));
         verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
-        verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0);
-        verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(0);
+        verify(mMockNativeInterface, times(0)).informAudioFocusState(0);
+        verify(mMockNativeInterface, times(1)).informAudioTrackGain(0);
+
+        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+        assertThat(mStreamHandler.getFocusState()).isEqualTo(
+                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
     }
 
     @Test
@@ -204,8 +227,8 @@
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
                         AudioManager.AUDIOFOCUS_LOSS_TRANSIENT));
         verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
-        verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0);
-        verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(0);
+        verify(mMockNativeInterface, times(0)).informAudioFocusState(0);
+        verify(mMockNativeInterface, times(1)).informAudioTrackGain(0);
         mStreamHandler.handleMessage(
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.REQUEST_FOCUS, true));
         verify(mMockAudioManager, times(2)).requestAudioFocus(any());
@@ -224,9 +247,12 @@
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
                         AudioManager.AUDIOFOCUS_GAIN));
         verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
-        verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0);
-        verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(0);
-        verify(mMockA2dpSink, times(2)).informAudioTrackGainNative(1.0f);
+        verify(mMockNativeInterface, times(0)).informAudioFocusState(0);
+        verify(mMockNativeInterface, times(1)).informAudioTrackGain(0);
+        verify(mMockNativeInterface, times(2)).informAudioTrackGain(1.0f);
+
+        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+        assertThat(mStreamHandler.getFocusState()).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);
     }
 
     @Test
@@ -237,6 +263,10 @@
                 mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
                         AudioManager.AUDIOFOCUS_LOSS));
         verify(mMockAudioManager, times(1)).abandonAudioFocus(any());
-        verify(mMockA2dpSink, times(1)).informAudioFocusStateNative(0);
+        verify(mMockNativeInterface, times(1)).informAudioFocusState(0);
+
+        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+        assertThat(mStreamHandler.getFocusState()).isEqualTo(AudioManager.AUDIOFOCUS_NONE);
+        assertThat(mStreamHandler.isPlaying()).isFalse();
     }
 }
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/StackEventTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/StackEventTest.java
new file mode 100644
index 0000000..ac346d3
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/StackEventTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package com.android.bluetooth.a2dpsink;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StackEventTest {
+    private Context mTargetContext = null;
+    private BluetoothAdapter mAdapter = null;
+    private BluetoothDevice mDevice = null;
+    private static final String TEST_ADDRESS = "11:11:11:11:11:11";
+
+    @Before
+    public void setUp() throws Exception {
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        Assume.assumeTrue("Ignore test when A2DP Sink is not enabled",
+                mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink));
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        assertThat(mAdapter).isNotNull();
+        mDevice = mAdapter.getRemoteDevice(TEST_ADDRESS);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mTargetContext = null;
+        mAdapter = null;
+        mDevice = null;
+    }
+
+    @Test
+    public void testCreateConnectionStateChangedDisconnectedEvent() {
+        testConnectionStateChangedBase(StackEvent.CONNECTION_STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testCreateConnectionStateChangedConnectingEvent() {
+        testConnectionStateChangedBase(StackEvent.CONNECTION_STATE_CONNECTING);
+    }
+
+    @Test
+    public void testCreateConnectionStateChangedConnectedEvent() {
+        testConnectionStateChangedBase(StackEvent.CONNECTION_STATE_CONNECTED);
+    }
+
+    @Test
+    public void testCreateConnectionStateChangedDisconnectingEvent() {
+        testConnectionStateChangedBase(StackEvent.CONNECTION_STATE_DISCONNECTING);
+    }
+
+    private void testConnectionStateChangedBase(int state) {
+        StackEvent event = StackEvent.connectionStateChanged(mDevice, state);
+        assertThat(event).isNotNull();
+        assertThat(event.mType).isEqualTo(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+        assertThat(event.mDevice).isEqualTo(mDevice);
+        assertThat(event.mState).isEqualTo(state);
+        assertThat(event.toString()).isNotNull();
+    }
+
+    @Test
+    public void testCreateAudioStateStoppedEvent() {
+        testAudioStateChangedBase(StackEvent.AUDIO_STATE_STOPPED);
+    }
+
+    @Test
+    public void testCreateAudioStateStartedvent() {
+        testAudioStateChangedBase(StackEvent.AUDIO_STATE_STARTED);
+    }
+
+    @Test
+    public void testCreateAudioStateRemoteSuspendEvent() {
+        testAudioStateChangedBase(StackEvent.AUDIO_STATE_REMOTE_SUSPEND);
+    }
+
+    private void testAudioStateChangedBase(int state) {
+        StackEvent event = StackEvent.audioStateChanged(mDevice, state);
+        assertThat(event).isNotNull();
+        assertThat(event.mType).isEqualTo(StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED);
+        assertThat(event.mDevice).isEqualTo(mDevice);
+        assertThat(event.mState).isEqualTo(state);
+        assertThat(event.toString()).isNotNull();
+    }
+
+    @Test
+    public void testCreateAudioConfigurationChangedEvent() {
+        int sampleRate = 44000;
+        int channelCount = 1;
+        StackEvent event = StackEvent.audioConfigChanged(mDevice, sampleRate, channelCount);
+        assertThat(event).isNotNull();
+        assertThat(event.mType).isEqualTo(StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED);
+        assertThat(event.mDevice).isEqualTo(mDevice);
+        assertThat(event.mSampleRate).isEqualTo(sampleRate);
+        assertThat(event.mChannelCount).isEqualTo(channelCount);
+        assertThat(event.toString()).isNotNull();
+    }
+}
diff --git a/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
index 90c3953..c0132aa 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -1070,7 +1070,7 @@
         device.put("migrated", false);
         assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
                 CoreMatchers.not(-1));
-        // Migrate database from 106 to 107
+        // Migrate database from 107 to 108
         db.close();
         db = testHelper.runMigrationsAndValidate(DB_NAME, 108, true,
                 MetadataDatabase.MIGRATION_107_108);
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java
index 99a9f34..130f457 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java
@@ -137,9 +137,9 @@
     public void testListenForPhoneState_ServiceAndSignalStrength() {
         BluetoothDevice device1 = TestUtils.getTestDevice(mAdapter, 1);
         mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
         verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
     }
 
     /**
@@ -150,9 +150,9 @@
     public void testListenForPhoneState_ServiceAndSignalStrengthUpdateTurnOffSignalStrengh() {
         BluetoothDevice device1 = TestUtils.getTestDevice(mAdapter, 1);
         mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
         verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
         mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE);
         verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
         verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE));
@@ -165,9 +165,9 @@
     public void testListenForPhoneState_ServiceAndSignalStrengthUpdateTurnOffAll() {
         BluetoothDevice device1 = TestUtils.getTestDevice(mAdapter, 1);
         mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
         verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
         mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_NONE);
         verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
     }
@@ -183,12 +183,12 @@
         BluetoothDevice device2 = TestUtils.getTestDevice(mAdapter, 2);
         // Enabling updates from first device should trigger subscription
         mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
         verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
         // Enabling updates from second device should not trigger the same subscription
         mHeadsetPhoneState.listenForPhoneState(device2, PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
         // Disabling updates from first device should not cancel subscription
         mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_NONE);
         // Disabling updates from second device should cancel subscription
@@ -211,15 +211,15 @@
         verifyNoMoreInteractions(mTelephonyManager);
         // Partially enabling updates from second device should trigger partial subscription
         mHeadsetPhoneState.listenForPhoneState(device2,
-                PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+                PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
         verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
         verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
         // Partially disabling updates from first device should not cancel all subscription
         mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_NONE);
         verify(mTelephonyManager, times(2)).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
         verify(mTelephonyManager).listen(
-                any(), eq(PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+                any(), eq(PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
         // Partially disabling updates from second device should cancel subscription
         mHeadsetPhoneState.listenForPhoneState(device2, PhoneStateListener.LISTEN_NONE);
         verify(mTelephonyManager, times(3)).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
index 7ff5f1c..3ebcc05 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
@@ -954,8 +954,7 @@
         mHeadsetService.startVoiceRecognition(deviceA);
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(deviceA,
                 HeadsetHalConstants.AT_RESPONSE_OK, 0);
-        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
-                .setParameters("A2dpSuspended=true");
+        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(deviceA);
         verifyNoMoreInteractions(mNativeInterface);
     }
@@ -1008,8 +1007,7 @@
         // We still continue on the initiating HF
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(deviceA,
                 HeadsetHalConstants.AT_RESPONSE_OK, 0);
-        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
-                .setParameters("A2dpSuspended=true");
+        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(deviceA);
         verifyNoMoreInteractions(mNativeInterface);
     }
@@ -1084,8 +1082,7 @@
         verify(mNativeInterface).setActiveDevice(deviceA);
         Assert.assertEquals(deviceA, mHeadsetService.getActiveDevice());
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).startVoiceRecognition(deviceA);
-        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
-                .setParameters("A2dpSuspended=true");
+        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(deviceA);
         waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, deviceA,
                 BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
@@ -1133,8 +1130,7 @@
         Assert.assertTrue(mHeadsetService.startVoiceRecognition(device));
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(device,
                 HeadsetHalConstants.AT_RESPONSE_OK, 0);
-        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
-                .setParameters("A2dpSuspended=true");
+        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(device);
         waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
                 BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
@@ -1151,8 +1147,7 @@
         Assert.assertNotNull(device);
         Assert.assertTrue(mHeadsetService.startVoiceRecognition(device));
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).startVoiceRecognition(device);
-        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
-                .setParameters("A2dpSuspended=true");
+        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
         verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(device);
         waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
                 BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
index 1a4e877..d1888bb 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
@@ -706,7 +706,7 @@
                 headsetCallState.mType, headsetCallState.mName, mAdapter.getAttributionSource());
         TestUtils.waitForLooperToFinishScheduledTask(
                 mHeadsetService.getStateMachinesThreadLooper());
-        verify(mAudioManager, never()).setParameters("A2dpSuspended=true");
+        verify(mAudioManager, never()).setA2dpSuspended(true);
         HeadsetTestUtils.verifyPhoneStateChangeSetters(mPhoneState, headsetCallState,
                 ASYNC_CALL_TIMEOUT_MILLIS);
     }
@@ -765,7 +765,7 @@
                 mHeadsetService.getStateMachinesThreadLooper());
 
         // Should not ask Audio HAL to suspend A2DP without active device
-        verify(mAudioManager, never()).setParameters("A2dpSuspended=true");
+        verify(mAudioManager, never()).setA2dpSuspended(true);
         // Make sure we notify device about this change
         verify(mStateMachines.get(mCurrentDevice)).sendMessage(
                 HeadsetStateMachine.CALL_STATE_CHANGED, headsetCallState);
@@ -783,7 +783,7 @@
         TestUtils.waitForLooperToFinishScheduledTask(
                 mHeadsetService.getStateMachinesThreadLooper());
         // Ask Audio HAL to suspend A2DP
-        verify(mAudioManager).setParameters("A2dpSuspended=true");
+        verify(mAudioManager).setA2dpSuspended(true);
         // Make sure state is updated
         verify(mStateMachines.get(mCurrentDevice)).sendMessage(
                 HeadsetStateMachine.CALL_STATE_CHANGED, headsetCallState);
@@ -847,8 +847,7 @@
                 headsetCallState.mNumHeld, headsetCallState.mCallState, headsetCallState.mNumber,
                 headsetCallState.mType, headsetCallState.mName, mAdapter.getAttributionSource());
         // Ask Audio HAL to suspend A2DP
-        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
-                .setParameters("A2dpSuspended=true");
+        verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
         // Make sure we notify devices about this change
         for (BluetoothDevice device : connectedDevices) {
             verify(mStateMachines.get(device)).sendMessage(HeadsetStateMachine.CALL_STATE_CHANGED,
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
index 34d228d..f927a61 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
@@ -17,6 +17,7 @@
 package com.android.bluetooth.hfp;
 
 import static android.Manifest.permission.BLUETOOTH_CONNECT;
+
 import static org.mockito.Mockito.*;
 
 import android.bluetooth.BluetoothAdapter;
@@ -69,6 +70,7 @@
     private static final int CONNECT_TIMEOUT_TEST_WAIT_MILLIS = CONNECT_TIMEOUT_TEST_MILLIS * 3 / 2;
     private static final int ASYNC_CALL_TIMEOUT_MILLIS = 250;
     private static final String TEST_PHONE_NUMBER = "1234567890";
+    private static final int MAX_RETRY_DISCONNECT_AUDIO = 3;
     private Context mTargetContext;
     private BluetoothAdapter mAdapter;
     private HandlerThread mHandlerThread;
@@ -742,23 +744,50 @@
     }
 
     /**
-     * Test state transition from AudioDisconnecting to Connected state via
-     * CONNECT_TIMEOUT message
+     * Test state transition from AudioDisconnecting to AudioOn state via CONNECT_TIMEOUT message
+     * until retry count is reached, then test transition to Disconnecting state.
      */
     @Test
-    public void testStateTransition_AudioDisconnectingToConnected_Timeout() {
+    public void testStateTransition_AudioDisconnectingToAudioOnAndDisconnecting_Timeout() {
         int numBroadcastsSent = setUpAudioDisconnectingState();
         // Wait for connection to timeout
         numBroadcastsSent++;
-        verify(mHeadsetService, timeout(CONNECT_TIMEOUT_TEST_WAIT_MILLIS).times(
-                numBroadcastsSent)).sendBroadcastAsUser(mIntentArgument.capture(),
-                eq(UserHandle.ALL), eq(BLUETOOTH_CONNECT),
-                any(Bundle.class));
-        HeadsetTestUtils.verifyAudioStateBroadcast(mTestDevice,
-                BluetoothHeadset.STATE_AUDIO_DISCONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTED,
-                mIntentArgument.getValue());
-        Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
-                IsInstanceOf.instanceOf(HeadsetStateMachine.Connected.class));
+        for (int i = 0; i <= MAX_RETRY_DISCONNECT_AUDIO; i++) {
+            if (i > 0) { // Skip first AUDIO_DISCONNECTING init as it was setup before the loop
+                mHeadsetStateMachine.sendMessage(HeadsetStateMachine.DISCONNECT_AUDIO, mTestDevice);
+                // No new broadcast due to lack of AUDIO_DISCONNECTING intent variable
+                verify(mHeadsetService, after(ASYNC_CALL_TIMEOUT_MILLIS)
+                        .times(numBroadcastsSent)).sendBroadcastAsUser(
+                        any(Intent.class), eq(UserHandle.ALL), eq(BLUETOOTH_CONNECT),
+                        any(Bundle.class));
+                Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
+                        IsInstanceOf.instanceOf(HeadsetStateMachine.AudioDisconnecting.class));
+                if (i == MAX_RETRY_DISCONNECT_AUDIO) {
+                    // Increment twice numBroadcastsSent as DISCONNECT message is added on max retry
+                    numBroadcastsSent += 2;
+                } else {
+                    numBroadcastsSent++;
+                }
+            }
+            verify(mHeadsetService, timeout(CONNECT_TIMEOUT_TEST_WAIT_MILLIS).times(
+                    numBroadcastsSent)).sendBroadcastAsUser(mIntentArgument.capture(),
+                    eq(UserHandle.ALL), eq(BLUETOOTH_CONNECT), any(Bundle.class));
+            if (i < MAX_RETRY_DISCONNECT_AUDIO) { // Test if state is AudioOn before max retry
+                HeadsetTestUtils.verifyAudioStateBroadcast(mTestDevice,
+                        BluetoothHeadset.STATE_AUDIO_CONNECTED,
+                        BluetoothHeadset.STATE_AUDIO_CONNECTED,
+                        mIntentArgument.getValue());
+                Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
+                        IsInstanceOf.instanceOf(HeadsetStateMachine.AudioOn.class));
+            } else { // Max retry count reached, test Disconnecting state
+                HeadsetTestUtils.verifyConnectionStateBroadcast(mTestDevice,
+                        BluetoothHeadset.STATE_DISCONNECTING,
+                        BluetoothHeadset.STATE_CONNECTED,
+                        mIntentArgument.getValue());
+                Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
+                        IsInstanceOf.instanceOf(HeadsetStateMachine.Disconnecting.class));
+            }
+        }
     }
 
     /**
@@ -867,7 +896,7 @@
     public void testAtBiaEvent_initialSubscriptionWithUpdates() {
         setUpConnectedState();
         verify(mPhoneState).listenForPhoneState(mTestDevice, PhoneStateListener.LISTEN_SERVICE_STATE
-                | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+                | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
         mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
                 new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_BIA,
                         new HeadsetAgIndicatorEnableState(true, true, false, false), mTestDevice));
@@ -877,7 +906,7 @@
                 new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_BIA,
                         new HeadsetAgIndicatorEnableState(false, true, true, false), mTestDevice));
         verify(mPhoneState, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).listenForPhoneState(mTestDevice,
-                PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+                PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
         mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
                 new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_BIA,
                         new HeadsetAgIndicatorEnableState(false, true, false, false), mTestDevice));
diff --git a/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
index cb49e02..f7cb5c0 100644
--- a/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
@@ -83,6 +83,8 @@
                 mAudioManager);
         when(mHeadsetClientService.getResources()).thenReturn(mMockHfpResources);
         when(mMockHfpResources.getBoolean(R.bool.hfp_clcc_poll_during_call)).thenReturn(true);
+        when(mMockHfpResources.getInteger(R.integer.hfp_clcc_poll_interval_during_call))
+                .thenReturn(2000);
         mNativeInterface = spy(NativeInterface.getInstance());
 
         // This line must be called to make sure relevant objects are initialized properly
diff --git a/tests/unit/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionServiceTest.java b/tests/unit/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionServiceTest.java
index c6d383f..8c4be97 100644
--- a/tests/unit/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionServiceTest.java
@@ -17,6 +17,7 @@
 package com.android.bluetooth.hfpclient.connserv;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -270,7 +271,16 @@
         mServiceRule.startService(createServiceIntent());
         InstrumentationRegistry.getTargetContext().sendBroadcast(
                 createDeviceConnectedIntent(device));
-        assertThat(buildLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+
+        buildLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        long startTime = System.currentTimeMillis();
+        while (mHfpClientConnectionService.findBlockForDevice(device) == null) {
+            if (System.currentTimeMillis() - startTime > TIMEOUT_MS) {
+                assertWithMessage(
+                        "Timeout waiting for block to be added to HfpClientConnectionService")
+                        .fail();
+            }
+        }
     }
 
     private HfpClientDeviceBlock.Factory createDeviceBlockFactoryForTest(
diff --git a/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java b/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
index 94c2468..43ddfcc 100644
--- a/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
@@ -21,9 +21,14 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.eq;
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -54,7 +59,10 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeoutException;
@@ -62,14 +70,21 @@
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class LeAudioServiceTest {
+    private static final int ASYNC_CALL_TIMEOUT_MILLIS = 250;
+    private static final int TIMEOUT_MS = 1000;
+    private static final int MAX_LE_AUDIO_CONNECTIONS = 5;
+    private static final int LE_AUDIO_GROUP_ID_INVALID = -1;
+
     private BluetoothAdapter mAdapter;
     private Context mTargetContext;
     private LeAudioService mService;
     private BluetoothDevice mLeftDevice;
     private BluetoothDevice mRightDevice;
     private BluetoothDevice mSingleDevice;
+    private HashSet<BluetoothDevice> mBondedDevices = new HashSet<>();
     private HashMap<BluetoothDevice, LinkedBlockingQueue<Intent>> mDeviceQueueMap;
-    private static final int TIMEOUT_MS = 1000;
+    private LinkedBlockingQueue<Intent> mGroupIntentQueue = new LinkedBlockingQueue<>();
+    private int testGroupId = 1;
 
     private BroadcastReceiver mLeAudioIntentReceiver;
 
@@ -87,23 +102,37 @@
         MockitoAnnotations.initMocks(this);
 
         TestUtils.setAdapterService(mAdapterService);
+        doReturn(MAX_LE_AUDIO_CONNECTIONS).when(mAdapterService).getMaxConnectedAudioDevices();
+        doReturn(new ParcelUuid[]{BluetoothUuid.LE_AUDIO}).when(mAdapterService)
+                .getRemoteUuids(any(BluetoothDevice.class));
         doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
         doReturn(true, false).when(mAdapterService).isStartedProfile(anyString());
 
         mAdapter = BluetoothAdapter.getDefaultAdapter();
+        // Mock methods in AdapterService
+        doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
+                mAdapterService).getBondedDevices();
 
         startService();
         mService.mLeAudioNativeInterface = mNativeInterface;
+        mService.mAudioManager = mAudioManager;
 
         // Override the timeout value to speed up the test
         LeAudioStateMachine.sConnectTimeoutMs = TIMEOUT_MS;    // 1s
 
+        mGroupIntentQueue = new LinkedBlockingQueue<>();
+
         // Set up the Connection State Changed receiver
         IntentFilter filter = new IntentFilter();
         filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+        filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONF_CHANGED);
+        filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED);
         mLeAudioIntentReceiver = new LeAudioIntentReceiver();
         mTargetContext.registerReceiver(mLeAudioIntentReceiver, filter);
 
+        doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
+                mAdapterService).getBondedDevices();
+
         // Get a device for testing
         mLeftDevice = TestUtils.getTestDevice(mAdapter, 0);
         mRightDevice = TestUtils.getTestDevice(mAdapter, 1);
@@ -120,6 +149,8 @@
 
     @After
     public void tearDown() throws Exception {
+        mBondedDevices.clear();
+        mGroupIntentQueue.clear();
         stopService();
         mTargetContext.unregisterReceiver(mLeAudioIntentReceiver);
         mDeviceQueueMap.clear();
@@ -155,6 +186,28 @@
                             + e.getMessage()).fail();
                 }
             }
+            if (BluetoothLeAudio.ACTION_LE_AUDIO_CONF_CHANGED.equals(intent.getAction())) {
+                try {
+                    BluetoothDevice device = intent.getParcelableExtra(
+                            BluetoothDevice.EXTRA_DEVICE);
+                    assertThat(device).isNotNull();
+                    LinkedBlockingQueue<Intent> queue = mDeviceQueueMap.get(device);
+                    assertThat(queue).isNotNull();
+                    queue.put(intent);
+                } catch (InterruptedException e) {
+                    assertWithMessage("Cannot add Le Audio Intent to the Connection State queue: "
+                            + e.getMessage()).fail();
+                }
+            }
+
+            if (BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED.equals(intent.getAction())) {
+                try {
+                    mGroupIntentQueue.put(intent);
+                } catch (InterruptedException e) {
+                    assertWithMessage("Cannot add Le Audio Intent to the Connection State queue: "
+                            + e.getMessage()).fail();
+                }
+            }
         }
     }
 
@@ -191,12 +244,6 @@
                 assertThat(mService.stop()).isTrue();
             }
         });
-        // Try to restart the service. Note: must be done on the main thread
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
-            public void run() {
-                assertThat(mService.start()).isTrue();
-            }
-        });
     }
 
     /**
@@ -714,4 +761,269 @@
         Intent intent = TestUtils.waitForNoIntent(timeoutMs, mDeviceQueueMap.get(device));
         assertThat(intent).isNull();
     }
+
+    /**
+     * Test setting connection policy
+     */
+    @Test
+    public void testSetConnectionPolicy() {
+        doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+        doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
+        doReturn(true).when(mDatabaseManager).setProfileConnectionPolicy(any(BluetoothDevice.class),
+                anyInt(), anyInt());
+        when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
+                .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+
+        assertThat(mService.setConnectionPolicy(mSingleDevice,
+                BluetoothProfile.CONNECTION_POLICY_ALLOWED)).isTrue();
+
+        // Verify the connection state broadcast, and that we are in Connecting state
+        verifyConnectionStateIntent(TIMEOUT_MS, mSingleDevice, BluetoothProfile.STATE_CONNECTING,
+                BluetoothProfile.STATE_DISCONNECTED);
+        assertThat(BluetoothProfile.STATE_CONNECTING)
+                .isEqualTo(mService.getConnectionState(mSingleDevice));
+
+        LeAudioStackEvent connCompletedEvent;
+        // Send a message to trigger connection completed
+        connCompletedEvent = new LeAudioStackEvent(
+                LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+        connCompletedEvent.device = mSingleDevice;
+        connCompletedEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_CONNECTED;
+        mService.messageFromNative(connCompletedEvent);
+
+        // Verify the connection state broadcast, and that we are in Connected state
+        verifyConnectionStateIntent(TIMEOUT_MS, mSingleDevice, BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.STATE_CONNECTING);
+        assertThat(BluetoothProfile.STATE_CONNECTED)
+                .isEqualTo(mService.getConnectionState(mSingleDevice));
+
+        // Set connection policy to forbidden
+        assertThat(mService.setConnectionPolicy(mSingleDevice,
+                BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)).isTrue();
+
+        // Verify the connection state broadcast, and that we are in Connecting state
+        verifyConnectionStateIntent(TIMEOUT_MS, mSingleDevice, BluetoothProfile.STATE_DISCONNECTING,
+                BluetoothProfile.STATE_CONNECTED);
+        assertThat(BluetoothProfile.STATE_DISCONNECTING)
+                .isEqualTo(mService.getConnectionState(mSingleDevice));
+
+        // Send a message to trigger disconnection completed
+        connCompletedEvent = new LeAudioStackEvent(
+                LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+        connCompletedEvent.device = mSingleDevice;
+        connCompletedEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED;
+        mService.messageFromNative(connCompletedEvent);
+
+        // Verify the connection state broadcast, and that we are in Disconnected state
+        verifyConnectionStateIntent(TIMEOUT_MS, mSingleDevice, BluetoothProfile.STATE_DISCONNECTED,
+                BluetoothProfile.STATE_DISCONNECTING);
+        assertThat(BluetoothProfile.STATE_DISCONNECTED)
+                .isEqualTo(mService.getConnectionState(mSingleDevice));
+    }
+
+    /**
+     *  Helper function to connect Test device
+     *
+     *  @param device test device
+     */
+    private void connectTestDevice(BluetoothDevice device, int GroupId) {
+        List<BluetoothDevice> prevConnectedDevices = mService.getConnectedDevices();
+
+        when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO))
+                .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+        // Send a connect request
+        assertWithMessage("Connect failed").that(mService.connect(device)).isTrue();
+
+        // Make device bonded
+        mBondedDevices.add(device);
+
+        // Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
+        // 250ms for processing two messages should be way more than enough. Anything that breaks
+        // this indicate some breakage in other part of Android OS
+
+        verifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
+                BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED);
+        assertThat(BluetoothProfile.STATE_CONNECTING)
+                .isEqualTo(mService.getConnectionState(device));
+
+        // Use connected event to indicate that device is connected
+        LeAudioStackEvent connCompletedEvent =
+                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+        connCompletedEvent.device = device;
+        connCompletedEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_CONNECTED;
+        mService.messageFromNative(connCompletedEvent);
+
+        verifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
+                BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING);
+
+        assertThat(BluetoothProfile.STATE_CONNECTED)
+                .isEqualTo(mService.getConnectionState(device));
+
+        LeAudioStackEvent nodeGroupAdded =
+                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_NODE_STATUS_CHANGED);
+        nodeGroupAdded.device = device;
+        nodeGroupAdded.valueInt1 = GroupId;
+        nodeGroupAdded.valueInt2 = LeAudioStackEvent.GROUP_NODE_ADDED;
+        mService.messageFromNative(nodeGroupAdded);
+
+        // Verify that the device is in the list of connected devices
+        assertThat(mService.getConnectedDevices().contains(device)).isTrue();
+        // Verify the list of previously connected devices
+        for (BluetoothDevice prevDevice : prevConnectedDevices) {
+                assertThat(mService.getConnectedDevices().contains(prevDevice)).isTrue();
+        }
+   }
+
+    /**
+     * Test matching connection state devices.
+     */
+    @Test
+    public void testGetDevicesMatchingConnectionState() {
+        // Update the device priority so okToConnect() returns true
+        doReturn(new ParcelUuid[]{BluetoothUuid.LE_AUDIO}).when(mAdapterService)
+                .getRemoteUuids(any(BluetoothDevice.class));
+        doReturn(new BluetoothDevice[]{mSingleDevice}).when(mAdapterService).getBondedDevices();
+        when(mDatabaseManager
+                .getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
+                .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+        doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
+
+        connectTestDevice(mSingleDevice, testGroupId);
+    }
+
+    /**
+     * Test adding node
+     */
+    @Test
+    public void testGroupAddRemoveNode() {
+        int groupId = 1;
+
+        doReturn(true).when(mNativeInterface).groupAddNode(groupId, mSingleDevice);
+        doReturn(true).when(mNativeInterface).groupRemoveNode(groupId, mSingleDevice);
+
+        assertThat(mService.groupAddNode(groupId, mSingleDevice)).isTrue();
+        assertThat(mService.groupRemoveNode(groupId, mSingleDevice)).isTrue();
+    }
+
+    /**
+     * Test setting active device group
+     */
+    @Test
+    public void testSetActiveDeviceGroup() {
+        int groupId = 1;
+
+        // Not connected device
+        assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
+
+        // Connected device
+        doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+        connectTestDevice(mSingleDevice, testGroupId);
+        assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
+
+        // no active device
+        assertThat(mService.setActiveDevice(null)).isTrue();
+    }
+
+    /**
+     * Test getting active device
+     */
+    @Test
+    public void testGetActiveDevices() {
+        int groupId = 1;
+        int nodeStatus = LeAudioStackEvent.GROUP_NODE_ADDED;
+
+        // No active device
+        assertThat(mService.getActiveDevices().isEmpty()).isTrue();
+
+        // Single active device
+        doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+        connectTestDevice(mSingleDevice, testGroupId);
+
+        // Add device to group
+        LeAudioStackEvent nodeStatusChangedEvent =
+                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_NODE_STATUS_CHANGED);
+        nodeStatusChangedEvent.device = mSingleDevice;
+        nodeStatusChangedEvent.valueInt1 = groupId;
+        nodeStatusChangedEvent.valueInt2 = nodeStatus;
+        mService.messageFromNative(nodeStatusChangedEvent);
+
+        assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
+        assertThat(mService.getActiveDevices().contains(mSingleDevice)).isTrue();
+    }
+
+    /**
+     * Test native interface audio configuration changed message handling
+     */
+    @Test
+    public void testMessageFromNativeAudioConfChanged() {
+        int direction = 1;
+        int groupId = 2;
+        int snkAudioLocation = 3;
+        int srcAudioLocation = 4;
+        int availableContexts = 5;
+        int eventType = LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED;
+        String action = BluetoothLeAudio.ACTION_LE_AUDIO_CONF_CHANGED;
+
+        // Add device to group
+        LeAudioStackEvent audioConfChangedEvent = new LeAudioStackEvent(eventType);
+        audioConfChangedEvent.device = mSingleDevice;
+        audioConfChangedEvent.valueInt1 = direction;
+        audioConfChangedEvent.valueInt2 = groupId;
+        audioConfChangedEvent.valueInt3 = snkAudioLocation;
+        audioConfChangedEvent.valueInt4 = srcAudioLocation;
+        audioConfChangedEvent.valueInt5 = availableContexts;
+        mService.messageFromNative(audioConfChangedEvent);
+
+        Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mDeviceQueueMap.get(mSingleDevice));
+        assertThat(intent).isNotNull();
+        assertThat(action).isEqualTo(intent.getAction());
+        assertThat(mSingleDevice)
+                .isEqualTo(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+        assertThat(groupId)
+                .isEqualTo(intent.getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID, -groupId));
+        assertThat(direction)
+                .isEqualTo(intent
+                .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_DIRECTION, -direction));
+        assertThat(snkAudioLocation)
+                .isEqualTo(intent
+                .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_SINK_LOCATION, -snkAudioLocation));
+        assertThat(srcAudioLocation)
+                .isEqualTo(intent
+                .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_SOURCE_LOCATION, srcAudioLocation));
+        assertThat(availableContexts)
+                .isEqualTo(intent
+                .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_AVAILABLE_CONTEXTS, availableContexts));
+    }
+
+    private void sendEventAndVerifyIntentForGroupStatusChanged(int groupId, int groupStatus) {
+        int eventType = LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED;
+        String action = BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED;
+
+        LeAudioStackEvent groupStatusChangedEvent = new LeAudioStackEvent(eventType);
+        groupStatusChangedEvent.valueInt1 = groupId;
+        groupStatusChangedEvent.valueInt2 = groupStatus;
+        mService.messageFromNative(groupStatusChangedEvent);
+
+        Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mGroupIntentQueue);
+        assertThat(intent).isNotNull();
+        assertThat(action).isEqualTo(intent.getAction());
+        assertThat(groupId)
+                .isEqualTo(intent.getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID, -groupId));
+        assertThat(groupStatus)
+                .isEqualTo(intent
+                .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_STATUS, -groupStatus));
+    }
+
+    /**
+     * Test native interface group status message handling
+     */
+    @Test
+    public void testMessageFromNativeGroupStatusChanged() {
+        doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+        connectTestDevice(mSingleDevice, testGroupId);
+
+        sendEventAndVerifyIntentForGroupStatusChanged(testGroupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE);
+        sendEventAndVerifyIntentForGroupStatusChanged(testGroupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE);
+    }
 }