Merge changes from topic "avrcp-target-cover-art"

* changes:
  Add tests for the AVRCP Target Cover Art feature
  Add the AVRCP Target 1.6 cover art feature
  Get the AVRCP Version that the user has requested
  Add image member to Metadata object
  Add BIP Client status setting API to JNI interface
  Add the BIP Server registration API to the JNI interface
  Create a builder for audio_util Metadata and migrate off bundles
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 158504b..34a4583 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -26,6 +26,7 @@
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
     <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED"/>
     <uses-permission android:name="android.permission.BLUETOOTH_MAP"/>
+    <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
     <uses-permission android:name="android.permission.DUMP"/>
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
@@ -424,5 +425,19 @@
             <meta-data android:name="android.accounts.AccountAuthenticator"
                  android:resource="@xml/authenticator"/>
         </service>
+        <service
+            android:name=".telephony.BluetoothInCallService"
+            android:permission="android.permission.BIND_INCALL_SERVICE"
+            android:process="@string/process"
+            android:enabled="@bool/profile_supported_hfp_incallservice"
+            android:exported="true">
+            <meta-data android:name="android.telecom.IN_CALL_SERVICE_RINGING"
+                       android:value="true" />
+            <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
+                       android:value="true" />
+            <intent-filter>
+              <action android:name="android.telecom.InCallService"/>
+            </intent-filter>
+         </service>
     </application>
 </manifest>
diff --git a/jni/com_android_bluetooth.h b/jni/com_android_bluetooth.h
index 11fe06a..03337fb 100644
--- a/jni/com_android_bluetooth.h
+++ b/jni/com_android_bluetooth.h
@@ -157,6 +157,8 @@
 int register_com_android_bluetooth_btservice_BluetoothKeystore(JNIEnv* env);
 
 int register_com_android_bluetooth_btservice_activity_attribution(JNIEnv* env);
+
+int register_com_android_bluetooth_le_audio(JNIEnv* env);
 }  // namespace android
 
 #endif /* COM_ANDROID_BLUETOOTH_H */
diff --git a/jni/com_android_bluetooth_btservice_AdapterService.cpp b/jni/com_android_bluetooth_btservice_AdapterService.cpp
index b19bfb9..42d0ce7 100644
--- a/jni/com_android_bluetooth_btservice_AdapterService.cpp
+++ b/jni/com_android_bluetooth_btservice_AdapterService.cpp
@@ -1486,5 +1486,11 @@
     return JNI_ERR;
   }
 
+  status = android::register_com_android_bluetooth_le_audio(e);
+  if (status < 0) {
+    ALOGE("jni le_audio registration failure: %d", status);
+    return JNI_ERR;
+  }
+
   return JNI_VERSION_1_6;
 }
diff --git a/jni/com_android_bluetooth_gatt.cpp b/jni/com_android_bluetooth_gatt.cpp
index c09c440..be415d3 100644
--- a/jni/com_android_bluetooth_gatt.cpp
+++ b/jni/com_android_bluetooth_gatt.cpp
@@ -863,6 +863,96 @@
   }
 };
 
+class JniScanningCallbacks : ScanningCallbacks {
+ public:
+  static ScanningCallbacks* GetInstance() {
+    static ScanningCallbacks* instance = new JniScanningCallbacks();
+    return instance;
+  }
+
+  void OnScannerRegistered(const Uuid app_uuid, uint8_t scannerId,
+                           uint8_t status) {
+    CallbackEnv sCallbackEnv(__func__);
+    if (!sCallbackEnv.valid()) return;
+    sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onScannerRegistered,
+                                 status, scannerId, UUID_PARAMS(app_uuid));
+  }
+
+  void OnScanResult(uint16_t event_type, uint8_t addr_type, RawAddress* bda,
+                    uint8_t primary_phy, uint8_t secondary_phy,
+                    uint8_t advertising_sid, int8_t tx_power, int8_t rssi,
+                    uint16_t periodic_adv_int, std::vector<uint8_t> adv_data) {
+    CallbackEnv sCallbackEnv(__func__);
+    if (!sCallbackEnv.valid()) return;
+
+    ScopedLocalRef<jstring> address(sCallbackEnv.get(),
+                                    bdaddr2newjstr(sCallbackEnv.get(), bda));
+    ScopedLocalRef<jbyteArray> jb(sCallbackEnv.get(),
+                                  sCallbackEnv->NewByteArray(adv_data.size()));
+    sCallbackEnv->SetByteArrayRegion(jb.get(), 0, adv_data.size(),
+                                     (jbyte*)adv_data.data());
+
+    sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onScanResult, event_type,
+                                 addr_type, address.get(), primary_phy,
+                                 secondary_phy, advertising_sid, tx_power, rssi,
+                                 periodic_adv_int, jb.get());
+  }
+
+  void OnTrackAdvFoundLost(btgatt_track_adv_info_t* p_adv_track_info) {
+    CallbackEnv sCallbackEnv(__func__);
+    if (!sCallbackEnv.valid()) return;
+
+    ScopedLocalRef<jstring> address(
+        sCallbackEnv.get(),
+        bdaddr2newjstr(sCallbackEnv.get(), &p_adv_track_info->bd_addr));
+
+    ScopedLocalRef<jbyteArray> jb_adv_pkt(
+        sCallbackEnv.get(),
+        sCallbackEnv->NewByteArray(p_adv_track_info->adv_pkt_len));
+    ScopedLocalRef<jbyteArray> jb_scan_rsp(
+        sCallbackEnv.get(),
+        sCallbackEnv->NewByteArray(p_adv_track_info->scan_rsp_len));
+
+    sCallbackEnv->SetByteArrayRegion(jb_adv_pkt.get(), 0,
+                                     p_adv_track_info->adv_pkt_len,
+                                     (jbyte*)p_adv_track_info->p_adv_pkt_data);
+
+    sCallbackEnv->SetByteArrayRegion(jb_scan_rsp.get(), 0,
+                                     p_adv_track_info->scan_rsp_len,
+                                     (jbyte*)p_adv_track_info->p_scan_rsp_data);
+
+    ScopedLocalRef<jobject> trackadv_obj(
+        sCallbackEnv.get(),
+        sCallbackEnv->CallObjectMethod(
+            mCallbacksObj, method_createOnTrackAdvFoundLostObject,
+            p_adv_track_info->client_if, p_adv_track_info->adv_pkt_len,
+            jb_adv_pkt.get(), p_adv_track_info->scan_rsp_len, jb_scan_rsp.get(),
+            p_adv_track_info->filt_index, p_adv_track_info->advertiser_state,
+            p_adv_track_info->advertiser_info_present, address.get(),
+            p_adv_track_info->addr_type, p_adv_track_info->tx_power,
+            p_adv_track_info->rssi_value, p_adv_track_info->time_stamp));
+
+    if (NULL != trackadv_obj.get()) {
+      sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onTrackAdvFoundLost,
+                                   trackadv_obj.get());
+    }
+  }
+
+  void OnBatchScanReports(int client_if, int status, int report_format,
+                          int num_records, std::vector<uint8_t> data) {
+    CallbackEnv sCallbackEnv(__func__);
+    if (!sCallbackEnv.valid()) return;
+    ScopedLocalRef<jbyteArray> jb(sCallbackEnv.get(),
+                                  sCallbackEnv->NewByteArray(data.size()));
+    sCallbackEnv->SetByteArrayRegion(jb.get(), 0, data.size(),
+                                     (jbyte*)data.data());
+
+    sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onBatchScanReports,
+                                 status, client_if, report_format, num_records,
+                                 jb.get());
+  }
+};
+
 /**
  * Native function definitions
  */
@@ -1015,6 +1105,7 @@
 
   sGattIf->advertiser->RegisterCallbacks(
       JniAdvertisingCallbacks::GetInstance());
+  sGattIf->scanner->RegisterCallbacks(JniScanningCallbacks::GetInstance());
 
   mCallbacksObj = env->NewGlobalRef(object);
 }
@@ -1045,11 +1136,11 @@
 }
 
 static void gattClientRegisterAppNative(JNIEnv* env, jobject object,
-                                        jlong app_uuid_lsb,
-                                        jlong app_uuid_msb) {
+                                        jlong app_uuid_lsb, jlong app_uuid_msb,
+                                        jboolean eatt_support) {
   if (!sGattIf) return;
   Uuid uuid = from_java_uuid(app_uuid_msb, app_uuid_lsb);
-  sGattIf->client->register_client(uuid);
+  sGattIf->client->register_client(uuid, eatt_support);
 }
 
 static void gattClientUnregisterAppNative(JNIEnv* env, jobject object,
@@ -1072,7 +1163,7 @@
 
   Uuid uuid = from_java_uuid(app_uuid_msb, app_uuid_lsb);
   sGattIf->scanner->RegisterScanner(
-      base::Bind(&btgattc_register_scanner_cb, uuid));
+      uuid, base::Bind(&btgattc_register_scanner_cb, uuid));
 }
 
 static void unregisterScannerNative(JNIEnv* env, jobject object,
@@ -1566,11 +1657,11 @@
  * Native server functions
  */
 static void gattServerRegisterAppNative(JNIEnv* env, jobject object,
-                                        jlong app_uuid_lsb,
-                                        jlong app_uuid_msb) {
+                                        jlong app_uuid_lsb, jlong app_uuid_msb,
+                                        jboolean eatt_support) {
   if (!sGattIf) return;
   Uuid uuid = from_java_uuid(app_uuid_msb, app_uuid_lsb);
-  sGattIf->server->register_server(uuid);
+  sGattIf->server->register_server(uuid, eatt_support);
 }
 
 static void gattServerUnregisterAppNative(JNIEnv* env, jobject object,
@@ -2243,7 +2334,7 @@
     {"cleanupNative", "()V", (void*)cleanupNative},
     {"gattClientGetDeviceTypeNative", "(Ljava/lang/String;)I",
      (void*)gattClientGetDeviceTypeNative},
-    {"gattClientRegisterAppNative", "(JJ)V",
+    {"gattClientRegisterAppNative", "(JJZ)V",
      (void*)gattClientRegisterAppNative},
     {"gattClientUnregisterAppNative", "(I)V",
      (void*)gattClientUnregisterAppNative},
@@ -2282,7 +2373,7 @@
      (void*)gattClientConfigureMTUNative},
     {"gattConnectionParameterUpdateNative", "(ILjava/lang/String;IIIIII)V",
      (void*)gattConnectionParameterUpdateNative},
-    {"gattServerRegisterAppNative", "(JJ)V",
+    {"gattServerRegisterAppNative", "(JJZ)V",
      (void*)gattServerRegisterAppNative},
     {"gattServerUnregisterAppNative", "(I)V",
      (void*)gattServerUnregisterAppNative},
diff --git a/jni/com_android_bluetooth_hfp.cpp b/jni/com_android_bluetooth_hfp.cpp
index 9918c87..813bdd0 100644
--- a/jni/com_android_bluetooth_hfp.cpp
+++ b/jni/com_android_bluetooth_hfp.cpp
@@ -593,6 +593,44 @@
   return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
 }
 
+static jboolean isNoiseReductionSupportedNative(JNIEnv* env, jobject object,
+                                                jbyteArray address) {
+  std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+  if (!sBluetoothHfpInterface) {
+    ALOGW("%s: sBluetoothHfpInterface is null", __func__);
+    return JNI_FALSE;
+  }
+  jbyte* addr = env->GetByteArrayElements(address, nullptr);
+  if (!addr) {
+    ALOGE("%s: failed to get device address", __func__);
+    jniThrowIOException(env, EINVAL);
+    return JNI_FALSE;
+  }
+  bt_status_t status =
+      sBluetoothHfpInterface->isNoiseReductionSupported((RawAddress*)addr);
+  env->ReleaseByteArrayElements(address, addr, 0);
+  return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean isVoiceRecognitionSupportedNative(JNIEnv* env, jobject object,
+                                                  jbyteArray address) {
+  std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+  if (!sBluetoothHfpInterface) {
+    ALOGW("%s: sBluetoothHfpInterface is null", __func__);
+    return JNI_FALSE;
+  }
+  jbyte* addr = env->GetByteArrayElements(address, nullptr);
+  if (!addr) {
+    ALOGE("%s: failed to get device address", __func__);
+    jniThrowIOException(env, EINVAL);
+    return JNI_FALSE;
+  }
+  bt_status_t status =
+      sBluetoothHfpInterface->isVoiceRecognitionSupported((RawAddress*)addr);
+  env->ReleaseByteArrayElements(address, addr, 0);
+  return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
 static jboolean startVoiceRecognitionNative(JNIEnv* env, jobject object,
                                             jbyteArray address) {
   std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
@@ -924,6 +962,10 @@
     {"disconnectHfpNative", "([B)Z", (void*)disconnectHfpNative},
     {"connectAudioNative", "([B)Z", (void*)connectAudioNative},
     {"disconnectAudioNative", "([B)Z", (void*)disconnectAudioNative},
+    {"isNoiseReductionSupportedNative", "([B)Z",
+     (void*)isNoiseReductionSupportedNative},
+    {"isVoiceRecognitionSupportedNative", "([B)Z",
+     (void*)isVoiceRecognitionSupportedNative},
     {"startVoiceRecognitionNative", "([B)Z",
      (void*)startVoiceRecognitionNative},
     {"stopVoiceRecognitionNative", "([B)Z", (void*)stopVoiceRecognitionNative},
diff --git a/jni/com_android_bluetooth_le_audio.cpp b/jni/com_android_bluetooth_le_audio.cpp
new file mode 100644
index 0000000..a8f5dbe
--- /dev/null
+++ b/jni/com_android_bluetooth_le_audio.cpp
@@ -0,0 +1,280 @@
+/*   Copyright 2019 HIMSA II K/S - www.himsa.com
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+#define LOG_TAG "BluetoothLeAudioServiceJni"
+
+#define LOG_NDEBUG 0
+
+#include <hardware/bluetooth.h>
+
+#include <array>
+#include <optional>
+#include <shared_mutex>
+
+#include "com_android_bluetooth.h"
+#include "hardware/bt_le_audio.h"
+
+using bluetooth::le_audio::ConnectionState;
+using bluetooth::le_audio::GroupStatus;
+using bluetooth::le_audio::LeAudioClientCallbacks;
+using bluetooth::le_audio::LeAudioClientInterface;
+
+namespace android {
+static jmethodID method_onConnectionStateChanged;
+static jmethodID method_onGroupStatus;
+static jmethodID method_onAudioConf;
+static jmethodID method_onSetMemberAvailable;
+
+static LeAudioClientInterface* sLeAudioClientInterface = nullptr;
+static std::shared_timed_mutex interface_mutex;
+
+static jobject mCallbacksObj = nullptr;
+static std::shared_timed_mutex callbacks_mutex;
+
+class LeAudioClientCallbacksImpl : public LeAudioClientCallbacks {
+ public:
+  ~LeAudioClientCallbacksImpl() = default;
+
+  void OnConnectionState(ConnectionState state,
+                         const RawAddress& bd_addr) override {
+    LOG(INFO) << __func__ << ", state:" << int(state);
+
+    std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+    CallbackEnv sCallbackEnv(__func__);
+    if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+    ScopedLocalRef<jbyteArray> addr(
+        sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+    if (!addr.get()) {
+      LOG(ERROR) << "Failed to new jbyteArray bd addr for connection state";
+      return;
+    }
+
+    sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+                                     (jbyte*)&bd_addr);
+    sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onConnectionStateChanged,
+                                 (jint)state, addr.get());
+  }
+
+  void OnGroupStatus(uint8_t group_id, GroupStatus group_status,
+                     uint8_t group_flags) override {
+    LOG(INFO) << __func__;
+
+    std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+    CallbackEnv sCallbackEnv(__func__);
+    if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+    sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onGroupStatus,
+                                 (jint)group_id, (jint)group_status,
+                                 (jint)group_flags);
+  }
+
+  void OnAudioConf(const RawAddress& bd_addr, uint8_t direction,
+                   uint8_t group_id, uint32_t sink_audio_location,
+                   uint32_t source_audio_location) override {
+    LOG(INFO) << __func__;
+
+    std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+    CallbackEnv sCallbackEnv(__func__);
+    if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+    ScopedLocalRef<jbyteArray> addr(
+        sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+    if (!addr.get()) {
+      LOG(ERROR) << "Failed to new jbyteArray bd addr for group status";
+      return;
+    }
+
+    sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+                                     (jbyte*)&bd_addr);
+    sCallbackEnv->CallVoidMethod(
+        mCallbacksObj, method_onAudioConf, (jint)direction, (jint)group_id,
+        (jint)sink_audio_location, (jint)source_audio_location, addr.get());
+  }
+
+  void OnSetMemberAvailable(const RawAddress& bd_addr,
+                            uint8_t group_id) override {
+    LOG(INFO) << __func__ << ", group id:" << int(group_id);
+
+    std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+    CallbackEnv sCallbackEnv(__func__);
+    if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+    ScopedLocalRef<jbyteArray> addr(
+        sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+    if (!addr.get()) {
+      LOG(ERROR) << "Failed to new jbyteArray bd addr for connection state";
+      return;
+    }
+
+    sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+                                     (jbyte*)&bd_addr);
+    sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onSetMemberAvailable,
+                                 addr.get(), (jint)group_id);
+  }
+};
+
+static LeAudioClientCallbacksImpl sLeAudioClientCallbacks;
+
+static void classInitNative(JNIEnv* env, jclass clazz) {
+  method_onGroupStatus = env->GetMethodID(clazz, "onGroupStatus", "(III)V");
+  method_onAudioConf = env->GetMethodID(clazz, "onAudioConf", "(IIII[B)V");
+  method_onConnectionStateChanged =
+      env->GetMethodID(clazz, "onConnectionStateChanged", "(I[B)V");
+  method_onSetMemberAvailable =
+      env->GetMethodID(clazz, "onSetMemberAvailable", "([BI)V");
+}
+
+static void initNative(JNIEnv* env, jobject object) {
+  std::unique_lock<std::shared_timed_mutex> interface_lock(interface_mutex);
+  std::unique_lock<std::shared_timed_mutex> callbacks_lock(callbacks_mutex);
+
+  const bt_interface_t* btInf = getBluetoothInterface();
+  if (btInf == nullptr) {
+    LOG(ERROR) << "Bluetooth module is not loaded";
+    return;
+  }
+
+  if (mCallbacksObj != nullptr) {
+    LOG(INFO) << "Cleaning up LeAudio callback object";
+    env->DeleteGlobalRef(mCallbacksObj);
+    mCallbacksObj = nullptr;
+  }
+
+  if ((mCallbacksObj = env->NewGlobalRef(object)) == nullptr) {
+    LOG(ERROR) << "Failed to allocate Global Ref for LeAudio Callbacks";
+    return;
+  }
+
+  sLeAudioClientInterface =
+      (LeAudioClientInterface*)btInf->get_profile_interface(
+          BT_PROFILE_LE_AUDIO_ID);
+  if (sLeAudioClientInterface == nullptr) {
+    LOG(ERROR) << "Failed to get Bluetooth LeAudio Interface";
+    return;
+  }
+
+  sLeAudioClientInterface->Initialize(&sLeAudioClientCallbacks);
+}
+
+static void cleanupNative(JNIEnv* env, jobject object) {
+  std::unique_lock<std::shared_timed_mutex> interface_lock(interface_mutex);
+  std::unique_lock<std::shared_timed_mutex> callbacks_lock(callbacks_mutex);
+
+  const bt_interface_t* btInf = getBluetoothInterface();
+  if (btInf == nullptr) {
+    LOG(ERROR) << "Bluetooth module is not loaded";
+    return;
+  }
+
+  if (sLeAudioClientInterface != nullptr) {
+    sLeAudioClientInterface->Cleanup();
+    sLeAudioClientInterface = nullptr;
+  }
+
+  if (mCallbacksObj != nullptr) {
+    env->DeleteGlobalRef(mCallbacksObj);
+    mCallbacksObj = nullptr;
+  }
+}
+
+static jboolean connectLeAudioNative(JNIEnv* env, jobject object,
+                                     jbyteArray address) {
+  LOG(INFO) << __func__;
+  std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+  if (!sLeAudioClientInterface) return JNI_FALSE;
+
+  jbyte* addr = env->GetByteArrayElements(address, nullptr);
+  if (!addr) {
+    jniThrowIOException(env, EINVAL);
+    return JNI_FALSE;
+  }
+
+  RawAddress* tmpraw = (RawAddress*)addr;
+  sLeAudioClientInterface->Connect(*tmpraw);
+  env->ReleaseByteArrayElements(address, addr, 0);
+  return JNI_TRUE;
+}
+
+static jboolean disconnectLeAudioNative(JNIEnv* env, jobject object,
+                                        jbyteArray address) {
+  LOG(INFO) << __func__;
+  std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+  if (!sLeAudioClientInterface) return JNI_FALSE;
+
+  jbyte* addr = env->GetByteArrayElements(address, nullptr);
+  if (!addr) {
+    jniThrowIOException(env, EINVAL);
+    return JNI_FALSE;
+  }
+
+  RawAddress* tmpraw = (RawAddress*)addr;
+  sLeAudioClientInterface->Disconnect(*tmpraw);
+  env->ReleaseByteArrayElements(address, addr, 0);
+  return JNI_TRUE;
+}
+
+static void groupStreamNative(JNIEnv* env, jobject object, jint group_id,
+                              jint content_type) {
+  LOG(INFO) << __func__;
+
+  if (!sLeAudioClientInterface) {
+    LOG(ERROR) << __func__ << ": Failed to get the Bluetooth LeAudio Interface";
+    return;
+  }
+
+  sLeAudioClientInterface->GroupStream(group_id, content_type);
+}
+
+static void groupSuspendNative(JNIEnv* env, jobject object, jint group_id) {
+  LOG(INFO) << __func__;
+
+  if (!sLeAudioClientInterface) {
+    LOG(ERROR) << __func__ << ": Failed to get the Bluetooth LeAudio Interface";
+    return;
+  }
+
+  sLeAudioClientInterface->GroupSuspend(group_id);
+}
+
+static void groupStopNative(JNIEnv* env, jobject object, jint group_id) {
+  LOG(INFO) << __func__;
+
+  if (!sLeAudioClientInterface) {
+    LOG(ERROR) << __func__ << ": Failed to get the Bluetooth LeAudio Interface";
+    return;
+  }
+
+  sLeAudioClientInterface->GroupStop(group_id);
+}
+
+static JNINativeMethod sMethods[] = {
+    {"classInitNative", "()V", (void*)classInitNative},
+    {"initNative", "()V", (void*)initNative},
+    {"cleanupNative", "()V", (void*)cleanupNative},
+    {"connectLeAudioNative", "([B)Z", (void*)connectLeAudioNative},
+    {"disconnectLeAudioNative", "([B)Z", (void*)disconnectLeAudioNative},
+    {"groupStreamNative", "(II)V", (void*)groupStreamNative},
+    {"groupSuspendNative", "(I)V", (void*)groupSuspendNative},
+    {"groupStopNative", "(I)V", (void*)groupStopNative},
+};
+
+int register_com_android_bluetooth_le_audio(JNIEnv* env) {
+  return jniRegisterNativeMethods(
+      env, "com/android/bluetooth/le_audio/LeAudioNativeInterface", sMethods,
+      NELEM(sMethods));
+}
+}  // namespace android
diff --git a/jni/com_android_bluetooth_sdp.cpp b/jni/com_android_bluetooth_sdp.cpp
old mode 100644
new mode 100755
index 90e3fc1..bba109a
--- a/jni/com_android_bluetooth_sdp.cpp
+++ b/jni/com_android_bluetooth_sdp.cpp
@@ -31,6 +31,7 @@
 static const Uuid UUID_MAP_MAS = Uuid::From16Bit(0x1132);
 static const Uuid UUID_MAP_MNS = Uuid::From16Bit(0x1133);
 static const Uuid UUID_SAP = Uuid::From16Bit(0x112D);
+static const Uuid UUID_DIP = Uuid::From16Bit(0x1200);
 
 namespace android {
 static jmethodID method_sdpRecordFoundCallback;
@@ -39,6 +40,7 @@
 static jmethodID method_sdpPseRecordFoundCallback;
 static jmethodID method_sdpOppOpsRecordFoundCallback;
 static jmethodID method_sdpSapsRecordFoundCallback;
+static jmethodID method_sdpDipRecordFoundCallback;
 
 static const btsdp_interface_t* sBluetoothSdpInterface = NULL;
 
@@ -96,6 +98,9 @@
   /* SAP Server record */
   method_sdpSapsRecordFoundCallback = env->GetMethodID(
       clazz, "sdpSapsRecordFoundCallback", "(I[B[BIILjava/lang/String;Z)V");
+  /* DIP record */
+  method_sdpDipRecordFoundCallback = env->GetMethodID(
+      clazz, "sdpDipRecordFoundCallback", "(I[B[BIIIIIZZ)V");
 }
 
 static jboolean sdpSearchNative(JNIEnv* env, jobject obj, jbyteArray address,
@@ -214,6 +219,17 @@
           addr.get(), uuid.get(), (jint)record->mas.hdr.rfcomm_channel_number,
           (jint)record->mas.hdr.profile_version, service_name.get(),
           more_results);
+    } else if (uuid_in == UUID_DIP) {
+      ALOGD("%s, Get UUID_DIP", __func__);
+      sCallbackEnv->CallVoidMethod(
+          sCallbacksObj, method_sdpDipRecordFoundCallback, (jint)status,
+          addr.get(), uuid.get(), (jint)record->dip.spec_id,
+          (jint)record->dip.vendor,
+          (jint)record->dip.vendor_id_source,
+          (jint)record->dip.product,
+          (jint)record->dip.version,
+          record->dip.primary_record,
+          more_results);
     } else {
       // we don't have a wrapper for this uuid, send as raw data
       jint record_data_size = record->hdr.user1_ptr_len;
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 43af307..66ad1f1 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -111,12 +111,10 @@
     <string name="inbound_noti_title" msgid="4143352641953027595">"Partage Bluetooth : fichiers reçus"</string>
     <plurals name="noti_caption_unsuccessful" formatted="false" msgid="2020750076679526122">
       <item quantity="one">Échec de <xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g> élément.</item>
-      <item quantity="many"><xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g> unsuccessful.</item>
       <item quantity="other">Échec de <xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g> éléments.</item>
     </plurals>
     <plurals name="noti_caption_success" formatted="false" msgid="1572472450257645181">
       <item quantity="one">Réussite de <xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g> élément, %2$s</item>
-      <item quantity="many"><xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g> successful, %2$s</item>
       <item quantity="other">Réussite de <xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g> éléments, %2$s</item>
     </plurals>
     <string name="transfer_menu_clear_all" msgid="790017462957873132">"Effacer la liste"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index a89375a..03e41d3 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -111,12 +111,10 @@
     <string name="inbound_noti_title" msgid="4143352641953027595">"Partage Bluetooth : fichiers reçus"</string>
     <plurals name="noti_caption_unsuccessful" formatted="false" msgid="2020750076679526122">
       <item quantity="one">Échec de <xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g> élément</item>
-      <item quantity="many"><xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g> unsuccessful.</item>
       <item quantity="other">Échec de <xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g> éléments</item>
     </plurals>
     <plurals name="noti_caption_success" formatted="false" msgid="1572472450257645181">
       <item quantity="one">Réussite de <xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g> élément, %2$s</item>
-      <item quantity="many"><xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g> successful, %2$s</item>
       <item quantity="other">Réussite de <xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g> éléments, %2$s</item>
     </plurals>
     <string name="transfer_menu_clear_all" msgid="790017462957873132">"Effacer la liste"</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index f9ef20a..ba35912 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -17,6 +17,7 @@
     <bool name="profile_supported_a2dp_sink">false</bool>
     <bool name="profile_supported_hs_hfp">true</bool>
     <bool name="profile_supported_hfpclient">false</bool>
+    <bool name="profile_supported_hfp_incallservice">true</bool>
     <bool name="profile_supported_hid_host">true</bool>
     <bool name="profile_supported_opp">true</bool>
     <bool name="profile_supported_pan">true</bool>
diff --git a/src/com/android/bluetooth/ObexServerSockets.java b/src/com/android/bluetooth/ObexServerSockets.java
index 789f12f..7bfd3fe 100644
--- a/src/com/android/bluetooth/ObexServerSockets.java
+++ b/src/com/android/bluetooth/ObexServerSockets.java
@@ -143,6 +143,10 @@
             } catch (IOException e) {
                 Log.e(STAG, "Error create ServerSockets ", e);
                 initSocketOK = false;
+            } catch (SecurityException e) {
+                Log.e(STAG, "Error create ServerSockets ", e);
+                initSocketOK = false;
+                break;
             }
             if (!initSocketOK) {
                 // Need to break out of this loop if BT is being turned off.
diff --git a/src/com/android/bluetooth/btservice/AdapterService.java b/src/com/android/bluetooth/btservice/AdapterService.java
index caaf51b..dbcfd58 100644
--- a/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/src/com/android/bluetooth/btservice/AdapterService.java
@@ -3099,11 +3099,14 @@
     // Boolean flags
     private static final String GD_CORE_FLAG = "INIT_gd_core";
     private static final String GD_ADVERTISING_FLAG = "INIT_gd_advertising";
+    private static final String GD_SCANNING_FLAG = "INIT_gd_scanning";
     private static final String GD_HCI_FLAG = "INIT_gd_hci";
     private static final String GD_CONTROLLER_FLAG = "INIT_gd_controller";
     private static final String GD_ACL_FLAG = "INIT_gd_acl";
     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";
+
     /**
      * Logging flags logic (only applies to DEBUG and VERBOSE levels):
      * if LOG_TAG in LOGGING_DEBUG_DISABLED_FOR_TAGS_FLAG:
@@ -3133,6 +3136,9 @@
         if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_ADVERTISING_FLAG, false)) {
             initFlags.add(String.format("%s=%s", GD_ADVERTISING_FLAG, "true"));
         }
+        if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_SCANNING_FLAG, false)) {
+            initFlags.add(String.format("%s=%s", GD_SCANNING_FLAG, "true"));
+        }
         if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_HCI_FLAG, false)) {
             initFlags.add(String.format("%s=%s", GD_HCI_FLAG, "true"));
         }
@@ -3148,6 +3154,9 @@
         if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_RUST_FLAG, false)) {
             initFlags.add(String.format("%s=%s", GD_RUST_FLAG, "true"));
         }
+        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,
                 LOGGING_DEBUG_ENABLED_FOR_ALL_FLAG, false)) {
             initFlags.add(String.format("%s=%s", LOGGING_DEBUG_ENABLED_FOR_ALL_FLAG, "true"));
diff --git a/src/com/android/bluetooth/btservice/AdapterState.java b/src/com/android/bluetooth/btservice/AdapterState.java
index 8596627..811a03c 100644
--- a/src/com/android/bluetooth/btservice/AdapterState.java
+++ b/src/com/android/bluetooth/btservice/AdapterState.java
@@ -17,9 +17,13 @@
 package com.android.bluetooth.btservice;
 
 import android.bluetooth.BluetoothAdapter;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
 import android.os.Message;
 import android.util.Log;
 
+import com.android.bluetooth.R;
+import com.android.bluetooth.telephony.BluetoothInCallService;
 import com.android.bluetooth.statemachine.State;
 import com.android.bluetooth.statemachine.StateMachine;
 
@@ -75,6 +79,10 @@
     static final int BREDR_START_TIMEOUT_DELAY = 4000;
     static final int BREDR_STOP_TIMEOUT_DELAY = 4000;
 
+    static final ComponentName BLUETOOTH_INCALLSERVICE_COMPONENT
+            = new ComponentName(R.class.getPackage().getName(),
+            BluetoothInCallService.class.getCanonicalName());
+
     private AdapterService mAdapterService;
     private TurningOnState mTurningOnState = new TurningOnState();
     private TurningBleOnState mTurningBleOnState = new TurningBleOnState();
@@ -223,6 +231,24 @@
         }
 
         @Override
+        public void enter() {
+            super.enter();
+            mAdapterService.getPackageManager().setComponentEnabledSetting(
+                    BLUETOOTH_INCALLSERVICE_COMPONENT,
+                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+                    PackageManager.DONT_KILL_APP);
+        }
+
+        @Override
+        public void exit() {
+            mAdapterService.getPackageManager().setComponentEnabledSetting(
+                    BLUETOOTH_INCALLSERVICE_COMPONENT,
+                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                    PackageManager.DONT_KILL_APP);
+            super.exit();
+        }
+
+        @Override
         public boolean processMessage(Message msg) {
             switch (msg.what) {
                 case USER_TURN_OFF:
diff --git a/src/com/android/bluetooth/gatt/GattService.java b/src/com/android/bluetooth/gatt/GattService.java
index 81ec766..04a9874 100644
--- a/src/com/android/bluetooth/gatt/GattService.java
+++ b/src/com/android/bluetooth/gatt/GattService.java
@@ -89,6 +89,8 @@
     private static final boolean DBG = GattServiceConfig.DBG;
     private static final boolean VDBG = GattServiceConfig.VDBG;
     private static final String TAG = GattServiceConfig.TAG_PREFIX + "GattService";
+    private static final String UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb";
+    private static final String UUID_ZERO_PAD = "00000000";
 
     static final int SCAN_FILTER_ENABLED = 1;
     static final int SCAN_FILTER_MODIFIED = 2;
@@ -440,12 +442,12 @@
         }
 
         @Override
-        public void registerClient(ParcelUuid uuid, IBluetoothGattCallback callback) {
+        public void registerClient(ParcelUuid uuid, IBluetoothGattCallback callback, boolean eatt_support) {
             GattService service = getService();
             if (service == null) {
                 return;
             }
-            service.registerClient(uuid.getUuid(), callback);
+            service.registerClient(uuid.getUuid(), callback, eatt_support);
         }
 
         @Override
@@ -713,12 +715,13 @@
         }
 
         @Override
-        public void registerServer(ParcelUuid uuid, IBluetoothGattServerCallback callback) {
+        public void registerServer(ParcelUuid uuid, IBluetoothGattServerCallback callback,
+                                   boolean eatt_support) {
             GattService service = getService();
             if (service == null) {
                 return;
             }
-            service.registerServer(uuid.getUuid(), callback);
+            service.registerServer(uuid.getUuid(), callback, eatt_support);
         }
 
         @Override
@@ -1025,27 +1028,10 @@
                     + Integer.toHexString(advertisingSid) + ", txPower=" + txPower + ", rssi="
                     + rssi + ", periodicAdvInt=0x" + Integer.toHexString(periodicAdvInt));
         }
-        List<UUID> remoteUuids = parseUuids(advData);
 
         byte[] legacyAdvData = Arrays.copyOfRange(advData, 0, 62);
 
         for (ScanClient client : mScanManager.getRegularScanQueue()) {
-            if (client.uuids.length > 0) {
-                int matches = 0;
-                for (UUID search : client.uuids) {
-                    for (UUID remote : remoteUuids) {
-                        if (remote.equals(search)) {
-                            ++matches;
-                            break; // Only count 1st match in case of duplicates
-                        }
-                    }
-                }
-
-                if (matches < client.uuids.length) {
-                    continue;
-                }
-            }
-
             ScannerMap.App app = mScannerMap.getById(client.scannerId);
             if (app == null) {
                 continue;
@@ -2317,14 +2303,14 @@
      * GATT Service functions - CLIENT
      *************************************************************************/
 
-    void registerClient(UUID uuid, IBluetoothGattCallback callback) {
+    void registerClient(UUID uuid, IBluetoothGattCallback callback, boolean eatt_support) {
         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
 
         if (DBG) {
             Log.d(TAG, "registerClient() - UUID=" + uuid);
         }
         mClientMap.add(uuid, null, callback, null, this);
-        gattClientRegisterAppNative(uuid.getLeastSignificantBits(), uuid.getMostSignificantBits());
+        gattClientRegisterAppNative(uuid.getLeastSignificantBits(), uuid.getMostSignificantBits(), eatt_support);
     }
 
     void unregisterClient(int clientIf) {
@@ -2982,14 +2968,14 @@
      * GATT Service functions - SERVER
      *************************************************************************/
 
-    void registerServer(UUID uuid, IBluetoothGattServerCallback callback) {
+    void registerServer(UUID uuid, IBluetoothGattServerCallback callback, boolean eatt_support) {
         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
 
         if (DBG) {
             Log.d(TAG, "registerServer() - UUID=" + uuid);
         }
         mServerMap.add(uuid, null, callback, null, this);
-        gattServerRegisterAppNative(uuid.getLeastSignificantBits(), uuid.getMostSignificantBits());
+        gattServerRegisterAppNative(uuid.getLeastSignificantBits(), uuid.getMostSignificantBits(), eatt_support);
     }
 
     void unregisterServer(int serverIf) {
@@ -3276,38 +3262,6 @@
         }
     }
 
-    private List<UUID> parseUuids(byte[] advData) {
-        List<UUID> uuids = new ArrayList<UUID>();
-
-        int offset = 0;
-        while (offset < (advData.length - 2)) {
-            int len = Byte.toUnsignedInt(advData[offset++]);
-            if (len == 0) {
-                break;
-            }
-
-            int type = advData[offset++];
-            switch (type) {
-                case 0x02: // Partial list of 16-bit UUIDs
-                case 0x03: // Complete list of 16-bit UUIDs
-                    while (len > 1) {
-                        int uuid16 = advData[offset++];
-                        uuid16 += (advData[offset++] << 8);
-                        len -= 2;
-                        uuids.add(UUID.fromString(
-                                String.format("%08x-0000-1000-8000-00805f9b34fb", uuid16)));
-                    }
-                    break;
-
-                default:
-                    offset += (len - 1);
-                    break;
-            }
-        }
-
-        return uuids;
-    }
-
     void dumpRegisterId(StringBuilder sb) {
         sb.append("  Scanner:\n");
         for (Integer appId : mScannerMap.getAllAppsIds()) {
@@ -3398,7 +3352,7 @@
 
     private native int gattClientGetDeviceTypeNative(String address);
 
-    private native void gattClientRegisterAppNative(long appUuidLsb, long appUuidMsb);
+    private native void gattClientRegisterAppNative(long appUuidLsb, long appUuidMsb, boolean eatt_support);
 
     private native void gattClientUnregisterAppNative(int clientIf);
 
@@ -3448,7 +3402,7 @@
             int minInterval, int maxInterval, int latency, int timeout, int minConnectionEventLen,
             int maxConnectionEventLen);
 
-    private native void gattServerRegisterAppNative(long appUuidLsb, long appUuidMsb);
+    private native void gattServerRegisterAppNative(long appUuidLsb, long appUuidMsb, boolean eatt_support);
 
     private native void gattServerUnregisterAppNative(int serverIf);
 
diff --git a/src/com/android/bluetooth/gatt/ScanClient.java b/src/com/android/bluetooth/gatt/ScanClient.java
index 9f17cc0..ade4992 100644
--- a/src/com/android/bluetooth/gatt/ScanClient.java
+++ b/src/com/android/bluetooth/gatt/ScanClient.java
@@ -24,7 +24,6 @@
 
 import java.util.List;
 import java.util.Objects;
-import java.util.UUID;
 
 /**
  * Helper class identifying a client that has requested LE scan results.
@@ -33,7 +32,6 @@
  */
 /* package */class ScanClient {
     public int scannerId;
-    public UUID[] uuids;
     public ScanSettings settings;
     public ScanSettings passiveSettings;
     public int appUid;
@@ -56,26 +54,16 @@
             new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
 
     ScanClient(int scannerId) {
-        this(scannerId, new UUID[0], DEFAULT_SCAN_SETTINGS, null, null);
-    }
-
-    ScanClient(int scannerId, UUID[] uuids) {
-        this(scannerId, uuids, DEFAULT_SCAN_SETTINGS, null, null);
+        this(scannerId, DEFAULT_SCAN_SETTINGS, null, null);
     }
 
     ScanClient(int scannerId, ScanSettings settings, List<ScanFilter> filters) {
-        this(scannerId, new UUID[0], settings, filters, null);
+        this(scannerId, settings, filters, null);
     }
 
     ScanClient(int scannerId, ScanSettings settings, List<ScanFilter> filters,
             List<List<ResultStorageDescriptor>> storages) {
-        this(scannerId, new UUID[0], settings, filters, storages);
-    }
-
-    private ScanClient(int scannerId, UUID[] uuids, ScanSettings settings, List<ScanFilter> filters,
-            List<List<ResultStorageDescriptor>> storages) {
         this.scannerId = scannerId;
-        this.uuids = uuids;
         this.settings = settings;
         this.passiveSettings = null;
         this.filters = filters;
diff --git a/src/com/android/bluetooth/hfp/BluetoothHeadsetProxy.java b/src/com/android/bluetooth/hfp/BluetoothHeadsetProxy.java
new file mode 100644
index 0000000..824955d
--- /dev/null
+++ b/src/com/android/bluetooth/hfp/BluetoothHeadsetProxy.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.hfp;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+
+import java.util.List;
+
+/**
+ * A proxy class that facilitates testing of the BluetoothInCallService class.
+ *
+ * This is necessary due to the "final" attribute of the BluetoothHeadset class. In order to
+ * test the correct functioning of the BluetoothInCallService class, the final class must be put
+ * into a container that can be mocked correctly.
+ */
+public class BluetoothHeadsetProxy {
+
+    private BluetoothHeadset mBluetoothHeadset;
+
+    public BluetoothHeadsetProxy(BluetoothHeadset headset) {
+        mBluetoothHeadset = headset;
+    }
+
+    public void clccResponse(int index, int direction, int status, int mode, boolean mpty,
+            String number, int type) {
+
+        mBluetoothHeadset.clccResponse(index, direction, status, mode, mpty, number, type);
+    }
+
+    public void phoneStateChanged(int numActive, int numHeld, int callState, String number,
+            int type, String name) {
+
+        mBluetoothHeadset.phoneStateChanged(numActive, numHeld, callState, number, type,
+                name);
+    }
+
+    public List<BluetoothDevice> getConnectedDevices() {
+        return mBluetoothHeadset.getConnectedDevices();
+    }
+
+    public int getConnectionState(BluetoothDevice device) {
+        return mBluetoothHeadset.getConnectionState(device);
+    }
+
+    public int getAudioState(BluetoothDevice device) {
+        return mBluetoothHeadset.getAudioState(device);
+    }
+
+    public boolean connectAudio() {
+        return mBluetoothHeadset.connectAudio();
+    }
+
+    public boolean setActiveDevice(BluetoothDevice device) {
+        return mBluetoothHeadset.setActiveDevice(device);
+    }
+
+    public BluetoothDevice getActiveDevice() {
+        return mBluetoothHeadset.getActiveDevice();
+    }
+
+    public boolean isAudioOn() {
+        return mBluetoothHeadset.isAudioOn();
+    }
+
+    public boolean disconnectAudio() {
+        return mBluetoothHeadset.disconnectAudio();
+    }
+
+    public boolean isInbandRingingEnabled() {
+        return mBluetoothHeadset.isInbandRingingEnabled();
+    }
+}
diff --git a/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java b/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java
index 29c571d..79fe01f 100644
--- a/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java
+++ b/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java
@@ -298,6 +298,27 @@
     }
 
     /**
+     * Checks whether the device support echo cancellation and/or noise reduction via the AT+BRSF
+     * bitmask
+     *
+     * @param device target headset
+     * @return true if the device support echo cancellation or noise reduction, false otherwise
+     */
+    public boolean isNoiseReductionSupported(BluetoothDevice device) {
+        return isNoiseReductionSupportedNative(Utils.getByteAddress(device));
+    }
+
+    /**
+     * Checks whether the device supports voice recognition via the AT+BRSF bitmask
+     *
+     * @param device target headset
+     * @return true if the device supports voice recognition, false otherwise
+     */
+    public boolean isVoiceRecognitionSupported(BluetoothDevice device) {
+        return isVoiceRecognitionSupportedNative(Utils.getByteAddress(device));
+    }
+
+    /**
      * Start voice recognition
      *
      * @param device target headset
@@ -475,6 +496,10 @@
 
     private native boolean disconnectAudioNative(byte[] address);
 
+    private native boolean isNoiseReductionSupportedNative(byte[] address);
+
+    private native boolean isVoiceRecognitionSupportedNative(byte[] address);
+
     private native boolean startVoiceRecognitionNative(byte[] address);
 
     private native boolean stopVoiceRecognitionNative(byte[] address);
diff --git a/src/com/android/bluetooth/hfp/HeadsetService.java b/src/com/android/bluetooth/hfp/HeadsetService.java
index 101dacf..7c08f15 100644
--- a/src/com/android/bluetooth/hfp/HeadsetService.java
+++ b/src/com/android/bluetooth/hfp/HeadsetService.java
@@ -153,7 +153,6 @@
         mStateMachinesThread.start();
         // Step 3: Initialize system interface
         mSystemInterface = HeadsetObjectsFactory.getInstance().makeSystemInterface(this);
-        mSystemInterface.init();
         // Step 4: Initialize native interface
         mMaxHeadsetConnections = mAdapterService.getMaxConnectedAudioDevices();
         mNativeInterface = HeadsetObjectsFactory.getInstance().getNativeInterface();
@@ -525,6 +524,26 @@
         }
 
         @Override
+        public boolean isNoiseReductionSupported(BluetoothDevice device) {
+            HeadsetService service = getService();
+            if (service == null) {
+                return false;
+            }
+            enforceBluetoothPermission(service);
+            return service.isNoiseReductionSupported(device);
+        }
+
+        @Override
+        public boolean isVoiceRecognitionSupported(BluetoothDevice device) {
+            HeadsetService service = getService();
+            if (service == null) {
+                return false;
+            }
+            enforceBluetoothPermission(service);
+            return service.isVoiceRecognitionSupported(device);
+        }
+
+        @Override
         public boolean startVoiceRecognition(BluetoothDevice device) {
             HeadsetService service = getService();
             if (service == null) {
@@ -895,6 +914,14 @@
                 .getProfileConnectionPolicy(device, BluetoothProfile.HEADSET);
     }
 
+    boolean isNoiseReductionSupported(BluetoothDevice device) {
+        return mNativeInterface.isNoiseReductionSupported(device);
+    }
+
+    boolean isVoiceRecognitionSupported(BluetoothDevice device) {
+        return mNativeInterface.isVoiceRecognitionSupported(device);
+    }
+
     boolean startVoiceRecognition(BluetoothDevice device) {
         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
         Log.i(TAG, "startVoiceRecognition: device=" + device + ", " + Utils.getUidPidString());
diff --git a/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java b/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
index 52ca1bc..df30a3f 100644
--- a/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
+++ b/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
@@ -20,19 +20,16 @@
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
-import android.bluetooth.IBluetoothHeadsetPhone;
+import com.android.bluetooth.telephony.BluetoothInCallService;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.media.AudioManager;
-import android.os.IBinder;
 import android.os.PowerManager;
-import android.os.RemoteException;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -52,28 +49,6 @@
     private final AudioManager mAudioManager;
     private final HeadsetPhoneState mHeadsetPhoneState;
     private PowerManager.WakeLock mVoiceRecognitionWakeLock;
-    private volatile IBluetoothHeadsetPhone mPhoneProxy;
-    private final ServiceConnection mPhoneProxyConnection = new ServiceConnection() {
-        @Override
-        public void onServiceConnected(ComponentName className, IBinder service) {
-            if (DBG) {
-                Log.d(TAG, "Proxy object connected");
-            }
-            synchronized (HeadsetSystemInterface.this) {
-                mPhoneProxy = IBluetoothHeadsetPhone.Stub.asInterface(service);
-            }
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName className) {
-            if (DBG) {
-                Log.d(TAG, "Proxy object disconnected");
-            }
-            synchronized (HeadsetSystemInterface.this) {
-                mPhoneProxy = null;
-            }
-        }
-    };
 
     HeadsetSystemInterface(HeadsetService headsetService) {
         if (headsetService == null) {
@@ -86,21 +61,11 @@
         mVoiceRecognitionWakeLock =
                 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG + ":VoiceRecognition");
         mVoiceRecognitionWakeLock.setReferenceCounted(false);
-        mHeadsetPhoneState = new HeadsetPhoneState(mHeadsetService);
+        mHeadsetPhoneState = new com.android.bluetooth.hfp.HeadsetPhoneState(mHeadsetService);
     }
 
-    /**
-     * Initialize this system interface
-     */
-    public synchronized void init() {
-        // Bind to Telecom phone proxy service
-        Intent intent = new Intent(IBluetoothHeadsetPhone.class.getName());
-        intent.setComponent(resolveSystemService(mHeadsetService.getPackageManager(), 0, intent));
-        if (intent.getComponent() == null || !mHeadsetService.bindService(intent,
-                mPhoneProxyConnection, 0)) {
-            // Crash the stack if cannot bind to Telecom
-            Log.wtf(TAG, "Could not bind to IBluetoothHeadsetPhone Service, intent=" + intent);
-        }
+    private BluetoothInCallService getBluetoothInCallServiceInstance() {
+        return BluetoothInCallService.getInstance();
     }
 
     /**
@@ -140,14 +105,6 @@
      * Stop this system interface
      */
     public synchronized void stop() {
-        if (mPhoneProxy != null) {
-            if (DBG) {
-                Log.d(TAG, "Unbinding phone proxy");
-            }
-            mPhoneProxy = null;
-            // Synchronization should make sure unbind can be successful
-            mHeadsetService.unbindService(mPhoneProxyConnection);
-        }
         mHeadsetPhoneState.cleanup();
     }
 
@@ -193,14 +150,10 @@
             Log.w(TAG, "answerCall device is null");
             return;
         }
-
-        if (mPhoneProxy != null) {
-            try {
-                mHeadsetService.setActiveDevice(device);
-                mPhoneProxy.answerCall();
-            } catch (RemoteException e) {
-                Log.e(TAG, Log.getStackTraceString(new Throwable()));
-            }
+        BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
+        if (bluetoothInCallService != null) {
+            mHeadsetService.setActiveDevice(device);
+            bluetoothInCallService.answerCall();
         } else {
             Log.e(TAG, "Handsfree phone proxy null for answering call");
         }
@@ -222,12 +175,9 @@
         if (mHeadsetService.isVirtualCallStarted()) {
             mHeadsetService.stopScoUsingVirtualVoiceCall();
         } else {
-            if (mPhoneProxy != null) {
-                try {
-                    mPhoneProxy.hangupCall();
-                } catch (RemoteException e) {
-                    Log.e(TAG, Log.getStackTraceString(new Throwable()));
-                }
+            BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
+            if (bluetoothInCallService != null) {
+                bluetoothInCallService.hangupCall();
             } else {
                 Log.e(TAG, "Handsfree phone proxy null for hanging up call");
             }
@@ -246,12 +196,9 @@
             Log.w(TAG, "sendDtmf device is null");
             return false;
         }
-        if (mPhoneProxy != null) {
-            try {
-                return mPhoneProxy.sendDtmf(dtmf);
-            } catch (RemoteException e) {
-                Log.e(TAG, Log.getStackTraceString(new Throwable()));
-            }
+        BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
+        if (bluetoothInCallService != null) {
+            return bluetoothInCallService.sendDtmf(dtmf);
         } else {
             Log.e(TAG, "Handsfree phone proxy null for sending DTMF");
         }
@@ -265,12 +212,9 @@
      */
     @VisibleForTesting
     public boolean processChld(int chld) {
-        if (mPhoneProxy != null) {
-            try {
-                return mPhoneProxy.processChld(chld);
-            } catch (RemoteException e) {
-                Log.e(TAG, Log.getStackTraceString(new Throwable()));
-            }
+        BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
+        if (bluetoothInCallService != null) {
+            return bluetoothInCallService.processChld(chld);
         } else {
             Log.e(TAG, "Handsfree phone proxy null for sending DTMF");
         }
@@ -284,19 +228,13 @@
      */
     @VisibleForTesting
     public String getNetworkOperator() {
-        final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy;
-        if (phoneProxy == null) {
-            Log.e(TAG, "getNetworkOperator() failed: mPhoneProxy is null");
+        BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
+        if (bluetoothInCallService == null) {
+            Log.e(TAG, "getNetworkOperator() failed: mBluetoothInCallService is null");
             return null;
         }
-        try {
-            // Should never return null
-            return mPhoneProxy.getNetworkOperator();
-        } catch (RemoteException exception) {
-            Log.e(TAG, "getNetworkOperator() failed: " + exception.getMessage());
-            exception.printStackTrace();
-            return null;
-        }
+        // Should never return null
+        return bluetoothInCallService.getNetworkOperator();
     }
 
     /**
@@ -306,18 +244,12 @@
      */
     @VisibleForTesting
     public String getSubscriberNumber() {
-        final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy;
-        if (phoneProxy == null) {
-            Log.e(TAG, "getSubscriberNumber() failed: mPhoneProxy is null");
+        BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
+        if (bluetoothInCallService == null) {
+            Log.e(TAG, "getSubscriberNumber() failed: mBluetoothInCallService is null");
             return null;
         }
-        try {
-            return mPhoneProxy.getSubscriberNumber();
-        } catch (RemoteException exception) {
-            Log.e(TAG, "getSubscriberNumber() failed: " + exception.getMessage());
-            exception.printStackTrace();
-            return null;
-        }
+        return bluetoothInCallService.getSubscriberNumber();
     }
 
 
@@ -329,18 +261,12 @@
      */
     @VisibleForTesting
     public boolean listCurrentCalls() {
-        final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy;
-        if (phoneProxy == null) {
-            Log.e(TAG, "listCurrentCalls() failed: mPhoneProxy is null");
+        BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
+        if (bluetoothInCallService == null) {
+            Log.e(TAG, "listCurrentCalls() failed: mBluetoothInCallService is null");
             return false;
         }
-        try {
-            return mPhoneProxy.listCurrentCalls();
-        } catch (RemoteException exception) {
-            Log.e(TAG, "listCurrentCalls() failed: " + exception.getMessage());
-            exception.printStackTrace();
-            return false;
-        }
+        return bluetoothInCallService.listCurrentCalls();
     }
 
     /**
@@ -349,13 +275,9 @@
      */
     @VisibleForTesting
     public void queryPhoneState() {
-        final IBluetoothHeadsetPhone phoneProxy = mPhoneProxy;
-        if (phoneProxy != null) {
-            try {
-                mPhoneProxy.queryPhoneState();
-            } catch (RemoteException e) {
-                Log.e(TAG, Log.getStackTraceString(new Throwable()));
-            }
+        BluetoothInCallService bluetoothInCallService = getBluetoothInCallServiceInstance();
+        if (bluetoothInCallService != null) {
+            bluetoothInCallService.queryPhoneState();
         } else {
             Log.e(TAG, "Handsfree phone proxy null for query phone state");
         }
diff --git a/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java b/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
new file mode 100644
index 0000000..4aa8a53
--- /dev/null
+++ b/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+/*
+ * Defines the native interface that is used by state machine/service to
+ * send or receive messages from the native stack. This file is registered
+ * for the native methods in the corresponding JNI C++ file.
+ */
+package com.android.bluetooth.le_audio;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * LeAudio Native Interface to/from JNI.
+ */
+public class LeAudioNativeInterface {
+    private static final String TAG = "LeAudioNativeInterface";
+    private static final boolean DBG = true;
+    private BluetoothAdapter mAdapter;
+
+    @GuardedBy("INSTANCE_LOCK")
+    private static LeAudioNativeInterface sInstance;
+    private static final Object INSTANCE_LOCK = new Object();
+
+    static {
+        classInitNative();
+    }
+
+    private LeAudioNativeInterface() {
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        if (mAdapter == null) {
+            Log.wtfStack(TAG, "No Bluetooth Adapter Available");
+        }
+    }
+
+    /**
+     * Get singleton instance.
+     */
+    public static LeAudioNativeInterface getInstance() {
+        synchronized (INSTANCE_LOCK) {
+            if (sInstance == null) {
+                sInstance = new LeAudioNativeInterface();
+            }
+            return sInstance;
+        }
+    }
+
+    private byte[] getByteAddress(BluetoothDevice device) {
+        if (device == null) {
+            return Utils.getBytesFromAddress("00:00:00:00:00:00");
+        }
+        return Utils.getBytesFromAddress(device.getAddress());
+    }
+
+    private void sendMessageToService(LeAudioStackEvent event) {
+        LeAudioService service = LeAudioService.getLeAudioService();
+        if (service != null) {
+            service.messageFromNative(event);
+        } else {
+            Log.e(TAG, "Event ignored, service not available: " + event);
+        }
+    }
+
+    private BluetoothDevice getDevice(byte[] address) {
+        return mAdapter.getRemoteDevice(address);
+    }
+
+    // Callbacks from the native stack back into the Java framework.
+    // All callbacks are routed via the Service which will disambiguate which
+    // state machine the message should be routed to.
+    private void onConnectionStateChanged(int state, byte[] address) {
+        LeAudioStackEvent event =
+                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+        event.device = getDevice(address);
+        event.valueInt1 = state;
+
+        if (DBG) {
+            Log.d(TAG, "onConnectionStateChanged: " + event);
+        }
+        sendMessageToService(event);
+    }
+
+    // Callbacks from the native stack back into the Java framework.
+    // All callbacks are routed via the Service which will disambiguate which
+    // state machine the message should be routed to.
+    private void onSetMemberAvailable(byte[] address, int groupId) {
+        LeAudioStackEvent event =
+                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_SET_MEMBER_AVAILABLE);
+        event.device = getDevice(address);
+        event.valueInt1 = groupId;
+        if (DBG) {
+            Log.d(TAG, "onSetMemberAvailable: " + event);
+        }
+        sendMessageToService(event);
+    }
+
+    private void onGroupStatus(int groupId, int groupStatus, int groupFlags) {
+        LeAudioStackEvent event =
+                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED);
+        event.valueInt1 = groupId;
+        event.valueInt2 = groupStatus;
+        event.valueInt3 = groupFlags;
+        event.device = null;
+
+        if (DBG) {
+            Log.d(TAG, "onGroupStatus: " + event);
+        }
+        sendMessageToService(event);
+    }
+
+    private void onAudioConf(int direction, int groupId, int sinkAudioLocation,
+                             int sourceAudioLocation, byte[] address) {
+        LeAudioStackEvent event =
+                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
+        event.valueInt1 = direction;
+        event.valueInt2 = groupId;
+        event.valueInt3 = sinkAudioLocation;
+        event.valueInt4 = sourceAudioLocation;
+        event.device = getDevice(address);
+
+        if (DBG) {
+            Log.d(TAG, "onAudioConf: " + event);
+        }
+        sendMessageToService(event);
+    }
+
+    /**
+     * Initializes the native interface.
+     *
+     * priorities to configure.
+     */
+    public void init() {
+        initNative();
+    }
+
+    /**
+     * Cleanup the native interface.
+     */
+    public void cleanup() {
+        cleanupNative();
+    }
+
+    /**
+     * Initiates LeAudio connection to a remote device.
+     *
+     * @param device the remote device
+     * @return true on success, otherwise false.
+     */
+    public boolean connectLeAudio(BluetoothDevice device) {
+        return connectLeAudioNative(getByteAddress(device));
+    }
+
+    /**
+     * Disconnects LeAudio from a remote device.
+     *
+     * @param device the remote device
+     * @return true on success, otherwise false.
+     */
+    public boolean disconnectLeAudio(BluetoothDevice device) {
+        return disconnectLeAudioNative(getByteAddress(device));
+    }
+
+    /**
+     * Enable content streaming.
+     * @param groupId group identifier
+     * @param contentType type of content to stream
+     */
+    public void groupStream(int groupId, int contentType) {
+        groupStreamNative(groupId, contentType);
+    }
+
+    /**
+     * Suspend content streaming.
+     * @param groupId  group identifier
+     */
+    public void groupSuspend(int groupId) {
+        groupSuspendNative(groupId);
+    }
+
+    /**
+     * Stop all content streaming.
+     * @param groupId  group identifier
+     * TODO: Maybe we should use also pass the content type argument
+     */
+    public void groupStop(int groupId) {
+        groupStopNative(groupId);
+    }
+
+    // Native methods that call into the JNI interface
+    private static native void classInitNative();
+    private native void initNative();
+    private native void cleanupNative();
+    private native boolean connectLeAudioNative(byte[] address);
+    private native boolean disconnectLeAudioNative(byte[] address);
+    private native void groupStreamNative(int groupId, int contentType);
+    private native void groupSuspendNative(int groupId);
+    private native void groupStopNative(int groupId);
+}
diff --git a/src/com/android/bluetooth/le_audio/LeAudioService.java b/src/com/android/bluetooth/le_audio/LeAudioService.java
index 2bf11dc..4cd0fbb 100644
--- a/src/com/android/bluetooth/le_audio/LeAudioService.java
+++ b/src/com/android/bluetooth/le_audio/LeAudioService.java
@@ -20,9 +20,17 @@
 import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID;
 
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
 import android.bluetooth.IBluetoothLeAudio;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
 import android.os.HandlerThread;
+import android.os.ParcelUuid;
 import android.util.Log;
 
 import com.android.bluetooth.Utils;
@@ -30,10 +38,14 @@
 import com.android.bluetooth.btservice.ProfileService;
 import com.android.bluetooth.btservice.storage.DatabaseManager;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Provides Bluetooth LeAudio profile, as a service in the Bluetooth application.
@@ -43,11 +55,26 @@
     private static final boolean DBG = true;
     private static final String TAG = "LeAudioService";
 
+    // Upper limit of all LeAudio devices: Bonded or Connected
+    private static final int MAX_LE_AUDIO_STATE_MACHINES = 10;
     private static LeAudioService sLeAudioService;
 
     private AdapterService mAdapterService;
     private DatabaseManager mDatabaseManager;
     private HandlerThread mStateMachinesThread;
+    private BluetoothDevice mPreviousAudioDevice;
+
+    LeAudioNativeInterface mLeAudioNativeInterface;
+    AudioManager mAudioManager;
+
+    private final Map<BluetoothDevice, LeAudioStateMachine> mStateMachines = new HashMap<>();
+
+    private final Map<BluetoothDevice, Integer> mDeviceGroupIdMap = new ConcurrentHashMap<>();
+    private final Map<Integer, Boolean> mGroupIdConnectedMap = new HashMap<>();
+    private int mActiveDeviceGroupId = LE_AUDIO_GROUP_ID_INVALID;
+
+    private BroadcastReceiver mBondStateChangedReceiver;
+    private BroadcastReceiver mConnectionStateChangedReceiver;
 
     @Override
     protected IProfileServiceBinder initBinder() {
@@ -66,16 +93,39 @@
             throw new IllegalStateException("start() called twice");
         }
 
-        // Get AdapterService, AudioManager. None of them can be null.
         mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
                 "AdapterService cannot be null when LeAudioService starts");
+        mLeAudioNativeInterface = Objects.requireNonNull(LeAudioNativeInterface.getInstance(),
+                "LeAudioNativeInterface cannot be null when LeAudioService starts");
         mDatabaseManager = Objects.requireNonNull(mAdapterService.getDatabase(),
                 "DatabaseManager cannot be null when A2dpService starts");
+        mAudioManager = getSystemService(AudioManager.class);
+        Objects.requireNonNull(mAudioManager,
+                "AudioManager cannot be null when LeAudioService starts");
+
+        // Start handler thread for state machines
+        mStateMachines.clear();
+        mStateMachinesThread = new HandlerThread("LeAudioService.StateMachines");
+        mStateMachinesThread.start();
+
+        mDeviceGroupIdMap.clear();
+        mGroupIdConnectedMap.clear();
+
+        // Setup broadcast receivers
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+        mBondStateChangedReceiver = new BondStateChangedReceiver();
+        registerReceiver(mBondStateChangedReceiver, filter);
+        filter = new IntentFilter();
+        filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+        mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
+        registerReceiver(mConnectionStateChangedReceiver, filter);
 
         // Mark service as started
         setLeAudioService(this);
 
-        setActiveDevice(null);
+        mLeAudioNativeInterface.init();
+
         return true;
     }
 
@@ -87,11 +137,37 @@
             return true;
         }
 
-        setActiveDevice(null);
+        // Cleanup native interfaces
+        mLeAudioNativeInterface.cleanup();
+        mLeAudioNativeInterface = null;
 
         // Set the service and BLE devices as inactive
         setLeAudioService(null);
 
+        // Unregister broadcast receivers
+        unregisterReceiver(mBondStateChangedReceiver);
+        mBondStateChangedReceiver = null;
+        unregisterReceiver(mConnectionStateChangedReceiver);
+        mConnectionStateChangedReceiver = null;
+
+        // Destroy state machines and stop handler thread
+        synchronized (mStateMachines) {
+            for (LeAudioStateMachine sm : mStateMachines.values()) {
+                sm.doQuit();
+                sm.cleanup();
+            }
+            mStateMachines.clear();
+        }
+
+        mDeviceGroupIdMap.clear();
+        mGroupIdConnectedMap.clear();
+
+        if (mStateMachinesThread != null) {
+            mStateMachinesThread.quitSafely();
+            mStateMachinesThread = null;
+        }
+
+        mAudioManager = null;
         mAdapterService = null;
         return true;
     }
@@ -130,9 +206,50 @@
             Log.e(TAG, "Cannot connect to " + device + " : CONNECTION_POLICY_FORBIDDEN");
             return false;
         }
+        ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
+        if (!ArrayUtils.contains(featureUuids, BluetoothUuid.LE_AUDIO)) {
+            Log.e(TAG, "Cannot connect to " + device + " : Remote does not have LE_AUDIO UUID");
+            return false;
+        }
 
-        //TODO: implement
-        return false;
+        int groupId = getGroupId(device);
+
+        //TODO: disconnect active device if it's not in groupId
+
+        if (DBG) {
+            Log.d(TAG, "connect(): " + device + "group id: " + groupId);
+        }
+
+        synchronized (mStateMachines) {
+            LeAudioStateMachine sm = getOrCreateStateMachine(device);
+            if (sm == null) {
+                Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
+                return false;
+            }
+            sm.sendMessage(LeAudioStateMachine.CONNECT, groupId);
+        }
+
+        // Connect other devices from this group
+        if (groupId != LE_AUDIO_GROUP_ID_INVALID) {
+            for (BluetoothDevice storedDevice : mDeviceGroupIdMap.keySet()) {
+                if (device.equals(storedDevice)) {
+                    continue;
+                }
+                if (getGroupId(storedDevice) != groupId) {
+                    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);
+                }
+            }
+        }
+        return true;
     }
 
     /**
@@ -147,27 +264,104 @@
             Log.d(TAG, "disconnect(): " + device);
         }
 
-        //TODO: implement
-        return false;
+        // Disconnect this device
+        synchronized (mStateMachines) {
+            LeAudioStateMachine sm = mStateMachines.get(device);
+            if (sm == null) {
+                Log.e(TAG, "Ignored disconnect request for " + device
+                        + " : no state machine");
+                return false;
+            }
+            sm.sendMessage(LeAudioStateMachine.DISCONNECT);
+        }
+
+        // Disconnect other devices from this group
+        int groupId = getGroupId(device);
+        if (groupId != LE_AUDIO_GROUP_ID_INVALID) {
+            for (BluetoothDevice storedDevice : mDeviceGroupIdMap.keySet()) {
+                if (device.equals(storedDevice)) {
+                    continue;
+                }
+                if (getGroupId(storedDevice) != groupId) {
+                    continue;
+                }
+                synchronized (mStateMachines) {
+                    LeAudioStateMachine sm = mStateMachines.get(storedDevice);
+                    if (sm == null) {
+                        Log.e(TAG, "Ignored disconnect request for " + storedDevice
+                                + " : no state machine");
+                        continue;
+                    }
+                    sm.sendMessage(LeAudioStateMachine.DISCONNECT);
+                }
+            }
+        }
+        return true;
     }
 
     List<BluetoothDevice> getConnectedDevices() {
         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-        List<BluetoothDevice> devices = new ArrayList<>();
-        return devices;
+        synchronized (mStateMachines) {
+            List<BluetoothDevice> devices = new ArrayList<>();
+            for (LeAudioStateMachine sm : mStateMachines.values()) {
+                if (sm.isConnected()) {
+                    devices.add(sm.getDevice());
+                }
+            }
+            return devices;
+        }
     }
 
     List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
         ArrayList<BluetoothDevice> devices = new ArrayList<>();
-        return devices;
+        if (states == null) {
+            return devices;
+        }
+        final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
+        if (bondedDevices == null) {
+            return devices;
+        }
+        synchronized (mStateMachines) {
+            for (BluetoothDevice device : bondedDevices) {
+                final ParcelUuid[] featureUuids = device.getUuids();
+                if (!ArrayUtils.contains(featureUuids, BluetoothUuid.LE_AUDIO)) {
+                    continue;
+                }
+                int connectionState = BluetoothProfile.STATE_DISCONNECTED;
+                LeAudioStateMachine sm = mStateMachines.get(device);
+                if (sm != null) {
+                    connectionState = sm.getConnectionState();
+                }
+                for (int state : states) {
+                    if (connectionState == state) {
+                        devices.add(device);
+                        break;
+                    }
+                }
+            }
+            return devices;
+        }
     }
 
+    /**
+     * Get the current connection state of the profile
+     *
+     * @param device is the remote bluetooth device
+     * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected,
+     * {@link BluetoothProfile#STATE_CONNECTING} if this profile is being connected,
+     * {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or
+     * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
+     */
     public int getConnectionState(BluetoothDevice device) {
         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
-        //TODO: implement
-        return BluetoothProfile.STATE_DISCONNECTED;
+        synchronized (mStateMachines) {
+            LeAudioStateMachine sm = mStateMachines.get(device);
+            if (sm == null) {
+                return BluetoothProfile.STATE_DISCONNECTED;
+            }
+            return sm.getConnectionState();
+        }
     }
 
     /**
@@ -182,7 +376,7 @@
             Log.d(TAG, "setActiveDevice:" + device);
         }
 
-        return true;
+        return false;
     }
 
     /**
@@ -198,6 +392,240 @@
         return activeDevices;
     }
 
+    void messageFromNative(LeAudioStackEvent stackEvent) {
+        Log.d(TAG, "Message from native: " + stackEvent);
+        BluetoothDevice device = stackEvent.device;
+
+        if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
+        // Some events require device state machine
+            synchronized (mStateMachines) {
+                LeAudioStateMachine sm = mStateMachines.get(device);
+                if (sm == null) {
+                    switch (stackEvent.valueInt1) {
+                        case LeAudioStackEvent.CONNECTION_STATE_CONNECTED:
+                        case LeAudioStackEvent.CONNECTION_STATE_CONNECTING:
+                            sm = getOrCreateStateMachine(device);
+                            break;
+                        default:
+                            break;
+                    }
+                }
+
+                if (sm == null) {
+                    Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent);
+                    return;
+                }
+
+                sm.sendMessage(LeAudioStateMachine.STACK_EVENT, stackEvent);
+                return;
+            }
+        }
+
+        // Some events do not require device state machine
+        if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED) {
+            int group_id = stackEvent.valueInt1;
+            int group_status = stackEvent.valueInt2;
+            int group_flags = stackEvent.valueInt3;
+
+            // TODO: Handle Stream events
+            switch (group_status) {
+                case LeAudioStackEvent.GROUP_STATUS_IDLE:
+                case LeAudioStackEvent.GROUP_STATUS_RECONFIGURED:
+                case LeAudioStackEvent.GROUP_STATUS_DESTROYED:
+                case LeAudioStackEvent.GROUP_STATUS_SUSPENDED:
+                    setActiveDevice(null);
+                    // TODO: Get all devices with a group and Unassign? or they are already unasigned
+                    // This event may come after removing all the nodes from a certain group but only that.
+                    break;
+                case LeAudioStackEvent.GROUP_STATUS_STREAMING:
+                    BluetoothDevice streaming_device = getConnectedPeerDevices(group_id).get(0);
+                    setActiveDevice(streaming_device);
+                    break;
+                default:
+                    break;
+            }
+
+        }
+    }
+
+    private LeAudioStateMachine getOrCreateStateMachine(BluetoothDevice device) {
+        if (device == null) {
+            Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
+            return null;
+        }
+        synchronized (mStateMachines) {
+            LeAudioStateMachine sm = mStateMachines.get(device);
+            if (sm != null) {
+                return sm;
+            }
+            // Limit the maximum number of state machines to avoid DoS attack
+            if (mStateMachines.size() >= MAX_LE_AUDIO_STATE_MACHINES) {
+                Log.e(TAG, "Maximum number of LeAudio state machines reached: "
+                        + MAX_LE_AUDIO_STATE_MACHINES);
+                return null;
+            }
+            if (DBG) {
+                Log.d(TAG, "Creating a new state machine for " + device);
+            }
+            sm = LeAudioStateMachine.make(device, this,
+                    mLeAudioNativeInterface, mStateMachinesThread.getLooper());
+            mStateMachines.put(device, sm);
+            return sm;
+        }
+    }
+
+    // Remove state machine if the bonding for a device is removed
+    private class BondStateChangedReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
+                return;
+            }
+            int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
+                                           BluetoothDevice.ERROR);
+            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+            Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
+            bondStateChanged(device, state);
+        }
+    }
+
+    /**
+     * Process a change in the bonding state for a device.
+     *
+     * @param device the device whose bonding state has changed
+     * @param bondState the new bond state for the device. Possible values are:
+     * {@link BluetoothDevice#BOND_NONE},
+     * {@link BluetoothDevice#BOND_BONDING},
+     * {@link BluetoothDevice#BOND_BONDED}.
+     */
+    @VisibleForTesting
+    void bondStateChanged(BluetoothDevice device, int bondState) {
+        if (DBG) {
+            Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
+        }
+        // Remove state machine if the bonding for a device is removed
+        if (bondState != BluetoothDevice.BOND_NONE) {
+            return;
+        }
+        mDeviceGroupIdMap.remove(device);
+        synchronized (mStateMachines) {
+            LeAudioStateMachine sm = mStateMachines.get(device);
+            if (sm == null) {
+                return;
+            }
+            if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
+                return;
+            }
+            removeStateMachine(device);
+        }
+    }
+
+    private void removeStateMachine(BluetoothDevice device) {
+        synchronized (mStateMachines) {
+            LeAudioStateMachine sm = mStateMachines.get(device);
+            if (sm == null) {
+                Log.w(TAG, "removeStateMachine: device " + device
+                        + " does not have a state machine");
+                return;
+            }
+            Log.i(TAG, "removeStateMachine: removing state machine for device: " + device);
+            sm.doQuit();
+            sm.cleanup();
+            mStateMachines.remove(device);
+        }
+    }
+
+    private List<BluetoothDevice> getConnectedPeerDevices(int groupId) {
+        List<BluetoothDevice> result = new ArrayList<>();
+        for (BluetoothDevice peerDevice : getConnectedDevices()) {
+            if (getGroupId(peerDevice) == groupId) {
+                result.add(peerDevice);
+            }
+        }
+        return result;
+    }
+
+    @VisibleForTesting
+    synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
+                                                     int toState) {
+        if ((device == null) || (fromState == toState)) {
+            Log.e(TAG, "connectionStateChanged: unexpected invocation. device=" + device
+                    + " fromState=" + fromState + " toState=" + toState);
+            return;
+        }
+        if (toState == BluetoothProfile.STATE_CONNECTED) {
+            int myGroupId = getGroupId(device);
+            if (myGroupId == LE_AUDIO_GROUP_ID_INVALID
+                    || getConnectedPeerDevices(myGroupId).size() == 1) {
+                // Log LE Audio connection event if we are the first device in a set
+                // Or when the GroupId has not been found
+                // MetricsLogger.logProfileConnectionEvent(
+                //         BluetoothMetricsProto.ProfileId.LE_AUDIO);
+            }
+            if (!mGroupIdConnectedMap.getOrDefault(myGroupId, false)) {
+                mGroupIdConnectedMap.put(myGroupId, true);
+            }
+        }
+        if (fromState == BluetoothProfile.STATE_CONNECTED && getConnectedDevices().isEmpty()) {
+            setActiveDevice(null);
+            int myGroupId = getGroupId(device);
+            mGroupIdConnectedMap.put(myGroupId, false);
+        }
+        // Check if the device is disconnected - if unbond, remove the state machine
+        if (toState == BluetoothProfile.STATE_DISCONNECTED) {
+            int bondState = mAdapterService.getBondState(device);
+            if (bondState == BluetoothDevice.BOND_NONE) {
+                if (DBG) {
+                    Log.d(TAG, device + " is unbond. Remove state machine");
+                }
+                removeStateMachine(device);
+            }
+        }
+    }
+
+    private class ConnectionStateChangedReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (!BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
+                return;
+            }
+            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+            int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+            int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+            connectionStateChanged(device, fromState, toState);
+        }
+    }
+
+   /**
+     * Check whether can connect to a peer device.
+     * The check considers a number of factors during the evaluation.
+     *
+     * @param device the peer device to connect to
+     * @return true if connection is allowed, otherwise false
+     */
+    public boolean okToConnect(BluetoothDevice device) {
+        // Check if this is an incoming connection in Quiet mode.
+        if (mAdapterService.isQuietModeEnabled()) {
+            Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled");
+            return false;
+        }
+        // Check connectionPolicy and accept or reject the connection.
+        int connectionPolicy = getConnectionPolicy(device);
+        int bondState = mAdapterService.getBondState(device);
+        // Allow this connection only if the device is bonded. Any attempt to connect while
+        // bonding would potentially lead to an unauthorized connection.
+        if (bondState != BluetoothDevice.BOND_BONDED) {
+            Log.w(TAG, "okToConnect: return false, bondState=" + bondState);
+            return false;
+        } else if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN
+                && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
+            // Otherwise, reject the connection if connectionPolicy is not valid.
+            Log.w(TAG, "okToConnect: return false, connectionPolicy=" + connectionPolicy);
+            return false;
+        }
+        return true;
+    }
+
     /**
      * Set connection policy of the profile and connects it if connectionPolicy is
      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is
diff --git a/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java b/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java
new file mode 100644
index 0000000..d451b35
--- /dev/null
+++ b/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.le_audio;
+
+import android.bluetooth.BluetoothDevice;
+
+/**
+ * Stack event sent via a callback from JNI to Java, or generated
+ * internally by the LeAudio State Machine.
+ */
+public class LeAudioStackEvent {
+    // Event types for STACK_EVENT message (coming from native in bt_le_audio.h)
+    private static final int EVENT_TYPE_NONE = 0;
+    public static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
+    public static final int EVENT_TYPE_GROUP_STATUS_CHANGED = 2;
+    public static final int EVENT_TYPE_AUDIO_CONF_CHANGED = 4;
+    public static final int EVENT_TYPE_SET_MEMBER_AVAILABLE = 5;
+    // -------- DO NOT PUT ANY NEW UNICAST EVENTS BELOW THIS LINE-------------
+    public static final int EVENT_TYPE_UNICAST_MAX = 7;
+
+    // Do not modify without updating the HAL bt_le_audio.h files.
+    // Match up with GroupStatus enum of bt_le_audio.h
+    static final int CONNECTION_STATE_DISCONNECTED = 0;
+    static final int CONNECTION_STATE_CONNECTING = 1;
+    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_STREAMING = 1;
+    static final int GROUP_STATUS_SUSPENDED = 2;
+    static final int GROUP_STATUS_RECONFIGURED = 3;
+    static final int GROUP_STATUS_DESTROYED = 4;
+
+    public int type = EVENT_TYPE_NONE;
+    public BluetoothDevice device;
+    public int valueInt1 = 0;
+    public int valueInt2 = 0;
+    public int valueInt3 = 0;
+    public int valueInt4 = 0;
+
+    LeAudioStackEvent(int type) {
+        this.type = type;
+    }
+
+    @Override
+    public String toString() {
+        // event dump
+        StringBuilder result = new StringBuilder();
+        result.append("LeAudioStackEvent {type:" + eventTypeToString(type));
+        result.append(", device:" + device);
+        result.append(", value1:" + eventTypeValue1ToString(type, valueInt1));
+        result.append(", value2:" + eventTypeValue2ToString(type, valueInt2));
+        result.append(", value3:" + eventTypeValue3ToString(type, valueInt3));
+        result.append(", value4:" + eventTypeValue4ToString(type, valueInt4));
+        result.append("}");
+        return result.toString();
+    }
+
+    private static String eventTypeToString(int type) {
+        switch (type) {
+            case EVENT_TYPE_NONE:
+                return "EVENT_TYPE_NONE";
+            case EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                return "EVENT_TYPE_CONNECTION_STATE_CHANGED";
+            case EVENT_TYPE_GROUP_STATUS_CHANGED:
+                return "EVENT_TYPE_GROUP_STATUS_CHANGED";
+            case EVENT_TYPE_AUDIO_CONF_CHANGED:
+                return "EVENT_TYPE_AUDIO_CONF_CHANGED";
+            case EVENT_TYPE_SET_MEMBER_AVAILABLE:
+                return "EVENT_TYPE_SET_MEMBER_AVAILABLE";
+            default:
+                return "EVENT_TYPE_UNKNOWN:" + type;
+        }
+    }
+
+    private static String eventTypeValue1ToString(int type, int value) {
+        switch (type) {
+            case EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                switch (value) {
+                    case CONNECTION_STATE_DISCONNECTED:
+                        return  "CONNECTION_STATE_DISCONNECTED";
+                    case CONNECTION_STATE_CONNECTING:
+                        return  "CONNECTION_STATE_CONNECTING";
+                    case CONNECTION_STATE_CONNECTED:
+                        return  "CONNECTION_STATE_CONNECTED";
+                    case CONNECTION_STATE_DISCONNECTING:
+                        return  "CONNECTION_STATE_DISCONNECTING";
+                    default:
+                        return "UNKNOWN";
+                }
+            case EVENT_TYPE_GROUP_STATUS_CHANGED:
+                // same as EVENT_TYPE_GROUP_STATUS_CHANGED
+            case EVENT_TYPE_SET_MEMBER_AVAILABLE:
+                // same as EVENT_TYPE_GROUP_STATUS_CHANGED
+            case EVENT_TYPE_AUDIO_CONF_CHANGED:
+                // FIXME: It should have proper direction names here
+                return "{direction:" + value + "}";
+            default:
+                break;
+        }
+        return Integer.toString(value);
+    }
+
+    private static String eventTypeValue2ToString(int type, int value) {
+        switch (type) {
+            case EVENT_TYPE_GROUP_STATUS_CHANGED:
+                switch (value) {
+                    case GROUP_STATUS_IDLE:
+                        return "GROUP_STATUS_IDLE";
+                    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;
+                }
+                break;
+            case EVENT_TYPE_AUDIO_CONF_CHANGED:
+                return "{group_id:" + Integer.toString(value) + "}";
+            default:
+                break;
+        }
+        return Integer.toString(value);
+    }
+
+    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 + "}";
+            default:
+                break;
+        }
+        return Integer.toString(value);
+    }
+
+    private static String eventTypeValue4ToString(int type, int value) {
+        switch (type) {
+            case EVENT_TYPE_AUDIO_CONF_CHANGED:
+                // FIXME: It should have proper location names here
+                return "{src_audio_loc:" + value + "}";
+            default:
+                break;
+        }
+        return Integer.toString(value);
+    }
+}
diff --git a/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java b/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java
new file mode 100644
index 0000000..f1db26b
--- /dev/null
+++ b/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java
@@ -0,0 +1,567 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.
+ */
+
+/**
+ * Bluetooth LeAudio StateMachine. There is one instance per remote device's ASE.
+ *  - "Disconnected" and "Connected" are steady states.
+ *  - "Connecting" and "Disconnecting" are transient states until the
+ *     connection / disconnection is completed.
+ *
+ *
+ *                        (Disconnected)
+ *                           |       ^
+ *                   CONNECT |       | DISCONNECTED
+ *                           V       |
+ *                 (Connecting)<--->(Disconnecting)
+ *                           |       ^
+ *                 CONNECTED |       | DISCONNECT
+ *                           V       |
+ *                          (Connected)
+ * NOTES:
+ *  - If state machine is in "Connecting" state and the remote device sends
+ *    DISCONNECT request, the state machine transitions to "Disconnecting" state.
+ *  - Similarly, if the state machine is in "Disconnecting" state and the remote device
+ *    sends CONNECT request, the state machine transitions to "Connecting" state.
+ *
+ *                    DISCONNECT
+ *    (Connecting) ---------------> (Disconnecting)
+ *                 <---------------
+ *                      CONNECT
+ *
+ */
+
+package com.android.bluetooth.le_audio;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothProfile;
+import android.content.Intent;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+final class LeAudioStateMachine extends StateMachine {
+    private static final boolean DBG = false;
+    private static final String TAG = "LeAudioStateMachine";
+
+    static final int CONNECT = 1;
+    static final int DISCONNECT = 2;
+    @VisibleForTesting
+    static final int STACK_EVENT = 101;
+    private static final int CONNECT_TIMEOUT = 201;
+
+    @VisibleForTesting
+    static int sConnectTimeoutMs = 30000;        // 30s
+
+    private Disconnected mDisconnected;
+    private Connecting mConnecting;
+    private Disconnecting mDisconnecting;
+    private Connected mConnected;
+
+    private int mLastConnectionState = -1;
+
+    private LeAudioService mService;
+    private LeAudioNativeInterface mNativeInterface;
+
+    private final BluetoothDevice mDevice;
+
+    LeAudioStateMachine(BluetoothDevice device, LeAudioService svc,
+            LeAudioNativeInterface nativeInterface, Looper looper) {
+        super(TAG, looper);
+        mDevice = device;
+        mService = svc;
+        mNativeInterface = nativeInterface;
+
+        mDisconnected = new Disconnected();
+        mConnecting = new Connecting();
+        mDisconnecting = new Disconnecting();
+        mConnected = new Connected();
+
+        addState(mDisconnected);
+        addState(mConnecting);
+        addState(mDisconnecting);
+        addState(mConnected);
+
+        setInitialState(mDisconnected);
+    }
+
+    static LeAudioStateMachine make(BluetoothDevice device, LeAudioService svc,
+            LeAudioNativeInterface nativeInterface, Looper looper) {
+        Log.i(TAG, "make for device");
+        LeAudioStateMachine LeAudioSm = new LeAudioStateMachine(device, svc, nativeInterface, looper);
+        LeAudioSm.start();
+        return LeAudioSm;
+    }
+
+    public void doQuit() {
+        log("doQuit for device " + mDevice);
+        quitNow();
+    }
+
+    public void cleanup() {
+        log("cleanup for device " + mDevice);
+    }
+
+    @VisibleForTesting
+    class Disconnected extends State {
+        @Override
+        public void enter() {
+            Log.i(TAG, "Enter Disconnected(" + mDevice + "): " + messageWhatToString(
+                    getCurrentMessage().what));
+
+            removeDeferredMessages(DISCONNECT);
+
+            if (mLastConnectionState != -1) {
+                // Don't broadcast during startup
+                broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTED,
+                        mLastConnectionState);
+            }
+        }
+
+        @Override
+        public void exit() {
+            log("Exit Disconnected(" + mDevice + "): " + messageWhatToString(
+                    getCurrentMessage().what));
+            mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED;
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            log("Disconnected process message(" + mDevice + "): " + messageWhatToString(
+                    message.what));
+
+            switch (message.what) {
+                case CONNECT:
+                    int groupId = message.arg1;
+                    log("Connecting to " + mDevice + " group " + groupId);
+                    if (!mNativeInterface.connectLeAudio(mDevice)) {
+                        Log.e(TAG, "Disconnected: error connecting to " + mDevice);
+                        break;
+                    }
+                    if (mService.okToConnect(mDevice)) {
+                        transitionTo(mConnecting);
+                    } else {
+                        // Reject the request and stay in Disconnected state
+                        Log.w(TAG, "Outgoing LeAudio Connecting request rejected: " + mDevice);
+                    }
+                    break;
+                case DISCONNECT:
+                    Log.d(TAG, "Disconnected: " + mDevice);
+                    mNativeInterface.disconnectLeAudio(mDevice);
+                    break;
+                case STACK_EVENT:
+                    LeAudioStackEvent event = (LeAudioStackEvent) message.obj;
+                    if (DBG) {
+                        Log.d(TAG, "Disconnected: stack event: " + event);
+                    }
+                    if (!mDevice.equals(event.device)) {
+                        Log.wtfStack(TAG, "Device(" + mDevice + "): event mismatch: " + event);
+                    }
+                    switch (event.type) {
+                        case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                            processConnectionEvent(event.valueInt1, event.valueInt2);
+                            break;
+                        default:
+                            Log.e(TAG, "Disconnected: ignoring stack event: " + event);
+                            break;
+                    }
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+
+        // in Disconnected state
+        private void processConnectionEvent(int state, int groupId) {
+            switch (state) {
+                case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
+                    Log.w(TAG, "Ignore LeAudio DISCONNECTED event: " + mDevice);
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_CONNECTING:
+                    if (mService.okToConnect(mDevice)) {
+                        Log.i(TAG, "Incoming LeAudio Connecting request accepted: " + mDevice);
+                        transitionTo(mConnecting);
+                    } else {
+                        // Reject the connection and stay in Disconnected state itself
+                        Log.w(TAG, "Incoming LeAudio Connecting request rejected: " + mDevice);
+                        mNativeInterface.disconnectLeAudio(mDevice);
+                    }
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_CONNECTED:
+                    Log.w(TAG, "LeAudio Connected from Disconnected state: " + mDevice);
+                    if (mService.okToConnect(mDevice)) {
+                        Log.i(TAG, "Incoming LeAudio Connected request accepted: " + mDevice);
+                        transitionTo(mConnected);
+                    } else {
+                        // Reject the connection and stay in Disconnected state itself
+                        Log.w(TAG, "Incoming LeAudio Connected request rejected: " + mDevice);
+                        mNativeInterface.disconnectLeAudio(mDevice);
+                    }
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTING:
+                    Log.w(TAG, "Ignore LeAudio DISCONNECTING event: " + mDevice);
+                    break;
+                default:
+                    Log.e(TAG, "Incorrect state: " + state + " device: " + mDevice);
+                    break;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    class Connecting extends State {
+        @Override
+        public void enter() {
+            Log.i(TAG, "Enter Connecting(" + mDevice + "): "
+                    + messageWhatToString(getCurrentMessage().what));
+            sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+            broadcastConnectionState(BluetoothProfile.STATE_CONNECTING, mLastConnectionState);
+        }
+
+        @Override
+        public void exit() {
+            log("Exit Connecting(" + mDevice + "): "
+                    + messageWhatToString(getCurrentMessage().what));
+            mLastConnectionState = BluetoothProfile.STATE_CONNECTING;
+            removeMessages(CONNECT_TIMEOUT);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            log("Connecting process message(" + mDevice + "): "
+                    + messageWhatToString(message.what));
+
+            switch (message.what) {
+                case CONNECT:
+                    deferMessage(message);
+                    break;
+                case CONNECT_TIMEOUT:
+                    Log.w(TAG, "Connecting connection timeout: " + mDevice);
+                    mNativeInterface.disconnectLeAudio(mDevice);
+                    LeAudioStackEvent disconnectEvent =
+                            new LeAudioStackEvent(
+                                    LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+                    disconnectEvent.device = mDevice;
+                    disconnectEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED;
+                    sendMessage(STACK_EVENT, disconnectEvent);
+                    break;
+                case DISCONNECT:
+                    log("Connecting: connection canceled to " + mDevice);
+                    mNativeInterface.disconnectLeAudio(mDevice);
+                    transitionTo(mDisconnected);
+                    break;
+                case STACK_EVENT:
+                    LeAudioStackEvent event = (LeAudioStackEvent) message.obj;
+                    log("Connecting: stack event: " + event);
+                    if (!mDevice.equals(event.device)) {
+                        Log.wtfStack(TAG, "Device(" + mDevice + "): event mismatch: " + event);
+                    }
+                    switch (event.type) {
+                        case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                            processConnectionEvent(event.valueInt1, event.valueInt2);
+                            break;
+                        default:
+                            Log.e(TAG, "Connecting: ignoring stack event: " + event);
+                            break;
+                    }
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+
+        // in Connecting state
+        private void processConnectionEvent(int state, int groupId) {
+            switch (state) {
+                case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
+                    Log.w(TAG, "Connecting device disconnected: " + mDevice);
+                    transitionTo(mDisconnected);
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_CONNECTED:
+                    transitionTo(mConnected);
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_CONNECTING:
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTING:
+                    Log.w(TAG, "Connecting interrupted: device is disconnecting: " + mDevice);
+                    transitionTo(mDisconnecting);
+                    break;
+                default:
+                    Log.e(TAG, "Incorrect state: " + state);
+                    break;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    class Disconnecting extends State {
+        @Override
+        public void enter() {
+            Log.i(TAG, "Enter Disconnecting(" + mDevice + "): "
+                    + messageWhatToString(getCurrentMessage().what));
+            sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+            broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTING, mLastConnectionState);
+        }
+
+        @Override
+        public void exit() {
+            log("Exit Disconnecting(" + mDevice + "): "
+                    + messageWhatToString(getCurrentMessage().what));
+            mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING;
+            removeMessages(CONNECT_TIMEOUT);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            log("Disconnecting process message(" + mDevice + "): "
+                    + messageWhatToString(message.what));
+
+            switch (message.what) {
+                case CONNECT:
+                    deferMessage(message);
+                    break;
+                case CONNECT_TIMEOUT: {
+                    Log.w(TAG, "Disconnecting connection timeout: " + mDevice);
+                    mNativeInterface.disconnectLeAudio(mDevice);
+                    LeAudioStackEvent disconnectEvent =
+                            new LeAudioStackEvent(
+                                    LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+                    disconnectEvent.device = mDevice;
+                    disconnectEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED;
+                    sendMessage(STACK_EVENT, disconnectEvent);
+                    break;
+                }
+                case DISCONNECT:
+                    deferMessage(message);
+                    break;
+                case STACK_EVENT:
+                    LeAudioStackEvent event = (LeAudioStackEvent) message.obj;
+                    log("Disconnecting: stack event: " + event);
+                    if (!mDevice.equals(event.device)) {
+                        Log.wtfStack(TAG, "Device(" + mDevice + "): event mismatch: " + event);
+                    }
+                    switch (event.type) {
+                        case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                            processConnectionEvent(event.valueInt1, event.valueInt2);
+                            break;
+                        default:
+                            Log.e(TAG, "Disconnecting: ignoring stack event: " + event);
+                            break;
+                    }
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+
+        // in Disconnecting state
+        private void processConnectionEvent(int state, int groupId) {
+            switch (state) {
+                case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
+                    Log.i(TAG, "Disconnected: " + mDevice);
+                    transitionTo(mDisconnected);
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_CONNECTED:
+                    if (mService.okToConnect(mDevice)) {
+                        Log.w(TAG, "Disconnecting interrupted: device is connected: " + mDevice);
+                        transitionTo(mConnected);
+                    } else {
+                        // Reject the connection and stay in Disconnecting state
+                        Log.w(TAG, "Incoming LeAudio Connected request rejected: " + mDevice);
+                        mNativeInterface.disconnectLeAudio(mDevice);
+                    }
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_CONNECTING:
+                    if (mService.okToConnect(mDevice)) {
+                        Log.i(TAG, "Disconnecting interrupted: try to reconnect: " + mDevice);
+                        transitionTo(mConnecting);
+                    } else {
+                        // Reject the connection and stay in Disconnecting state
+                        Log.w(TAG, "Incoming LeAudio Connecting request rejected: " + mDevice);
+                        mNativeInterface.disconnectLeAudio(mDevice);
+                    }
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTING:
+                    break;
+                default:
+                    Log.e(TAG, "Incorrect state: " + state);
+                    break;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    class Connected extends State {
+        @Override
+        public void enter() {
+            Log.i(TAG, "Enter Connected(" + mDevice + "): "
+                    + messageWhatToString(getCurrentMessage().what));
+            removeDeferredMessages(CONNECT);
+            broadcastConnectionState(BluetoothProfile.STATE_CONNECTED, mLastConnectionState);
+        }
+
+        @Override
+        public void exit() {
+            log("Exit Connected(" + mDevice + "): "
+                    + messageWhatToString(getCurrentMessage().what));
+            mLastConnectionState = BluetoothProfile.STATE_CONNECTED;
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            log("Connected process message(" + mDevice + "): "
+                    + messageWhatToString(message.what));
+
+            switch (message.what) {
+                case CONNECT:
+                    Log.w(TAG, "Connected: CONNECT ignored: " + mDevice);
+                    break;
+                case DISCONNECT:
+                    log("Disconnecting from " + mDevice);
+                    if (!mNativeInterface.disconnectLeAudio(mDevice)) {
+                        // If error in the native stack, transition directly to Disconnected state.
+                        Log.e(TAG, "Connected: error disconnecting from " + mDevice);
+                        transitionTo(mDisconnected);
+                        break;
+                    }
+                    transitionTo(mDisconnecting);
+                    break;
+                case STACK_EVENT:
+                    LeAudioStackEvent event = (LeAudioStackEvent) message.obj;
+                    log("Connected: stack event: " + event);
+                    if (!mDevice.equals(event.device)) {
+                        Log.wtfStack(TAG, "Device(" + mDevice + "): event mismatch: " + event);
+                    }
+                    switch (event.type) {
+                        case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+                            processConnectionEvent(event.valueInt1, event.valueInt2);
+                            break;
+                        default:
+                            Log.e(TAG, "Connected: ignoring stack event: " + event);
+                            break;
+                    }
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+
+        // in Connected state
+        private void processConnectionEvent(int state, int groupId) {
+            switch (state) {
+                case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
+                    Log.i(TAG, "Disconnected from " + mDevice);
+                    transitionTo(mDisconnected);
+                    break;
+                case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTING:
+                    Log.i(TAG, "Disconnecting from " + mDevice);
+                    transitionTo(mDisconnecting);
+                    break;
+                default:
+                    Log.e(TAG, "Connection State Device: " + mDevice + " bad state: " + state);
+                    break;
+            }
+        }
+    }
+
+    int getConnectionState() {
+        String currentState = getCurrentState().getName();
+        switch (currentState) {
+            case "Disconnected":
+                return BluetoothProfile.STATE_DISCONNECTED;
+            case "Connecting":
+                return BluetoothProfile.STATE_CONNECTING;
+            case "Connected":
+                return BluetoothProfile.STATE_CONNECTED;
+            case "Disconnecting":
+                return BluetoothProfile.STATE_DISCONNECTING;
+            default:
+                Log.e(TAG, "Bad currentState: " + currentState);
+                return BluetoothProfile.STATE_DISCONNECTED;
+        }
+    }
+
+    BluetoothDevice getDevice() {
+        return mDevice;
+    }
+
+    synchronized boolean isConnected() {
+        return getCurrentState() == mConnected;
+    }
+
+    // This method does not check for error condition (newState == prevState)
+    private void broadcastConnectionState(int newState, int prevState) {
+        log("Connection state " + mDevice + ": " + profileStateToString(prevState)
+                    + "->" + profileStateToString(newState));
+
+        Intent intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
+        intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
+        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+                        | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+        mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
+    }
+
+    private static String messageWhatToString(int what) {
+        switch (what) {
+            case CONNECT:
+                return "CONNECT";
+            case DISCONNECT:
+                return "DISCONNECT";
+            case STACK_EVENT:
+                return "STACK_EVENT";
+            case CONNECT_TIMEOUT:
+                return "CONNECT_TIMEOUT";
+            default:
+                break;
+        }
+        return Integer.toString(what);
+    }
+
+    private static String profileStateToString(int state) {
+        switch (state) {
+            case BluetoothProfile.STATE_DISCONNECTED:
+                return "DISCONNECTED";
+            case BluetoothProfile.STATE_CONNECTING:
+                return "CONNECTING";
+            case BluetoothProfile.STATE_CONNECTED:
+                return "CONNECTED";
+            case BluetoothProfile.STATE_DISCONNECTING:
+                return "DISCONNECTING";
+            default:
+                break;
+        }
+        return Integer.toString(state);
+    }
+
+    @Override
+    protected void log(String msg) {
+        if (DBG) {
+            super.log(msg);
+        }
+    }
+}
diff --git a/src/com/android/bluetooth/sdp/SdpManager.java b/src/com/android/bluetooth/sdp/SdpManager.java
index d5e3770..775965e 100644
--- a/src/com/android/bluetooth/sdp/SdpManager.java
+++ b/src/com/android/bluetooth/sdp/SdpManager.java
@@ -15,6 +15,7 @@
 package com.android.bluetooth.sdp;
 
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.SdpDipRecord;
 import android.bluetooth.SdpMasRecord;
 import android.bluetooth.SdpMnsRecord;
 import android.bluetooth.SdpOppOpsRecord;
@@ -365,6 +366,39 @@
         }
     }
 
+    void sdpDipRecordFoundCallback(int status, byte[] address,
+            byte[] uuid,  int specificationId,
+            int vendorId, int vendorIdSource,
+            int productId, int version,
+            boolean primaryRecord,
+            boolean moreResults) {
+        synchronized(TRACKER_LOCK) {
+            SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid);
+            SdpDipRecord sdpRecord = null;
+            if (inst == null) {
+              Log.e(TAG, "sdpDipRecordFoundCallback: Search instance is NULL");
+              return;
+            }
+            inst.setStatus(status);
+            if (D) {
+                Log.d(TAG, "sdpDipRecordFoundCallback: status " + status);
+            }
+            if (status == AbstractionLayer.BT_STATUS_SUCCESS) {
+                sdpRecord = new SdpDipRecord(specificationId,
+                        vendorId, vendorIdSource,
+                        productId, version,
+                        primaryRecord);
+            }
+            if (D) {
+                Log.d(TAG, "UUID: " + Arrays.toString(uuid));
+            }
+            if (D) {
+                Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString());
+            }
+            sendSdpIntent(inst, sdpRecord, moreResults);
+        }
+    }
+
     /* TODO: Test or remove! */
     void sdpRecordFoundCallback(int status, byte[] address, byte[] uuid, int sizeRecord,
             byte[] record) {
diff --git a/src/com/android/bluetooth/telephony/BluetoothCall.java b/src/com/android/bluetooth/telephony/BluetoothCall.java
new file mode 100644
index 0000000..85a0b9a
--- /dev/null
+++ b/src/com/android/bluetooth/telephony/BluetoothCall.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.telephony;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.telecom.Call;
+import android.telecom.GatewayInfo;
+import android.telecom.InCallService;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A proxy class of android.telecom.Call that
+ * 1) facilitates testing of the BluetoothInCallService class; We can't mock the final class
+ * Call directly;
+ * 2) Some helper functions, to let Call have same methods as com.android.server.telecom.Call
+ *
+ * This is necessary due to the "final" attribute of the Call class. In order to
+ * test the correct functioning of the BluetoothInCallService class, the final class must be put
+ * into a container that can be mocked correctly.
+ */
+@VisibleForTesting
+public class BluetoothCall {
+
+    private Call mCall;
+
+    public Call getCall() {
+        return mCall;
+    }
+
+    public void setCall(Call call) {
+        mCall = call;
+    }
+
+    public BluetoothCall(Call call) {
+        mCall = call;
+    }
+
+    public String getRemainingPostDialSequence() {
+        return mCall.getRemainingPostDialSequence();
+    }
+
+    public void answer(int videoState) {
+        mCall.answer(videoState);
+    }
+
+    public void deflect(Uri address) {
+        mCall.deflect(address);
+    }
+
+    public void reject(boolean rejectWithMessage, String textMessage) {
+        mCall.reject(rejectWithMessage, textMessage);
+    }
+
+    public void disconnect() {
+        mCall.disconnect();
+    }
+
+    public void hold() {
+        mCall.hold();
+    }
+
+    public void unhold() {
+        mCall.unhold();
+    }
+
+    public void enterBackgroundAudioProcessing() {
+        mCall.enterBackgroundAudioProcessing();
+    }
+
+    public void exitBackgroundAudioProcessing(boolean shouldRing) {
+        mCall.exitBackgroundAudioProcessing(shouldRing);
+    }
+
+    public void playDtmfTone(char digit) {
+        mCall.playDtmfTone(digit);
+    }
+
+    public void stopDtmfTone() {
+        mCall.stopDtmfTone();
+    }
+
+    public void postDialContinue(boolean proceed) {
+        mCall.postDialContinue(proceed);
+    }
+
+    public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
+        mCall.phoneAccountSelected(accountHandle, setDefault);
+    }
+
+    public void conference(BluetoothCall callToConferenceWith) {
+        if (callToConferenceWith != null) {
+            mCall.conference(callToConferenceWith.getCall());
+        }
+    }
+
+    public void splitFromConference() {
+        mCall.splitFromConference();
+    }
+
+    public void mergeConference() {
+        mCall.mergeConference();
+    }
+
+    public void swapConference() {
+        mCall.swapConference();
+    }
+
+    public void pullExternalCall() {
+        mCall.pullExternalCall();
+    }
+
+    public void sendCallEvent(String event, Bundle extras) {
+        mCall.sendCallEvent(event, extras);
+    }
+
+    public void sendRttRequest() {
+        mCall.sendRttRequest();
+    }
+
+    public void respondToRttRequest(int id, boolean accept) {
+        mCall.respondToRttRequest(id, accept);
+    }
+
+    public void handoverTo(PhoneAccountHandle toHandle, int videoState, Bundle extras) {
+        mCall.handoverTo(toHandle, videoState, extras);
+    }
+
+    public void stopRtt() {
+        mCall.stopRtt();
+    }
+
+    public void putExtras(Bundle extras) {
+        mCall.putExtras(extras);
+    }
+
+    public void putExtra(String key, boolean value) {
+        mCall.putExtra(key, value);
+    }
+
+    public void putExtra(String key, int value) {
+        mCall.putExtra(key, value);
+    }
+
+    public void putExtra(String key, String value) {
+        mCall.putExtra(key, value);
+    }
+
+    public void removeExtras(List<String> keys) {
+        mCall.removeExtras(keys);
+    }
+
+    public void removeExtras(String... keys) {
+        mCall.removeExtras(keys);
+    }
+
+    public String getParentId() {
+        Call parent = mCall.getParent();
+        if (parent != null) {
+            return parent.getDetails().getTelecomCallId();
+        }
+        return null;
+    }
+
+    public List<String> getChildrenIds() {
+        return getIds(mCall.getChildren());
+    }
+
+    public List<String> getConferenceableCalls() {
+        return getIds(mCall.getConferenceableCalls());
+    }
+
+    public int getState() {
+        return mCall.getState();
+    }
+
+    public List<String> getCannedTextResponses() {
+        return mCall.getCannedTextResponses();
+    }
+
+    public InCallService.VideoCall getVideoCall() {
+        return mCall.getVideoCall();
+    }
+
+    public Call.Details getDetails() {
+        return mCall.getDetails();
+    }
+
+    public Call.RttCall getRttCall() {
+        return mCall.getRttCall();
+    }
+
+    public boolean isRttActive() {
+        return mCall.isRttActive();
+    }
+
+    public void registerCallback(Call.Callback callback) {
+        mCall.registerCallback(callback);
+    }
+
+    public void registerCallback(Call.Callback callback, Handler handler) {
+        mCall.registerCallback(callback, handler);
+    }
+
+    public void unregisterCallback(Call.Callback callback) {
+        mCall.unregisterCallback(callback);
+    }
+
+    public String toString() {
+        String string = mCall.toString();
+        return string == null ? "" : string;
+    }
+
+    public void addListener(Call.Listener listener) {
+        mCall.addListener(listener);
+    }
+
+    public void removeListener(Call.Listener listener) {
+        mCall.removeListener(listener);
+    }
+
+    public String getGenericConferenceActiveChildCallId() {
+        return mCall.getGenericConferenceActiveChildCall().getDetails().getTelecomCallId();
+    }
+
+    public String getContactDisplayName() {
+        return mCall.getDetails().getContactDisplayName();
+    }
+
+    public PhoneAccountHandle getAccountHandle() {
+        return mCall.getDetails().getAccountHandle();
+    }
+
+    public int getVideoState() {
+        return mCall.getDetails().getVideoState();
+    }
+
+    public String getCallerDisplayName() {
+        return mCall.getDetails().getCallerDisplayName();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null) {
+            return getCall() == null;
+        }
+        return o instanceof BluetoothCall && getCall() == ((BluetoothCall) o).getCall();
+    }
+
+    // helper functions
+    public boolean isSilentRingingRequested() {
+        return getDetails().getExtras() != null
+                && getDetails().getExtras().getBoolean(Call.EXTRA_SILENT_RINGING_REQUESTED);
+    }
+
+    public boolean isConference() {
+        return getDetails().hasProperty(Call.Details.PROPERTY_CONFERENCE);
+    }
+
+    public boolean can(int capability) {
+        return getDetails().can(capability);
+    }
+
+    public Uri getHandle() {
+        return getDetails().getHandle();
+    }
+
+    public GatewayInfo getGatewayInfo() {
+        return getDetails().getGatewayInfo();
+    }
+
+    public boolean isIncoming() {
+        return getDetails().getCallDirection() == Call.Details.DIRECTION_INCOMING;
+    }
+
+    public boolean isExternalCall() {
+        return getDetails().hasProperty(Call.Details.PROPERTY_IS_EXTERNAL_CALL);
+    }
+
+    public String getTelecomCallId() {
+        return getDetails().getTelecomCallId();
+    }
+
+    public boolean wasConferencePreviouslyMerged() {
+        return can(Call.Details.CAPABILITY_SWAP_CONFERENCE) &&
+                !can(Call.Details.CAPABILITY_MERGE_CONFERENCE);
+    }
+
+    public static List<String> getIds(List<Call> calls) {
+        List<String> result = new ArrayList<>();
+        for (Call call : calls) {
+            if (call != null) {
+                result.add(call.getDetails().getTelecomCallId());
+            }
+        }
+        return result;
+    }
+}
diff --git a/src/com/android/bluetooth/telephony/BluetoothInCallService.java b/src/com/android/bluetooth/telephony/BluetoothInCallService.java
new file mode 100644
index 0000000..bcd7079
--- /dev/null
+++ b/src/com/android/bluetooth/telephony/BluetoothInCallService.java
@@ -0,0 +1,1091 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.telephony;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.Connection;
+import android.telecom.InCallService;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.bluetooth.hfp.BluetoothHeadsetProxy;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Used to receive updates about calls from the Telecom component. This service is bound to Telecom
+ * while there exist calls which potentially require UI. This includes ringing (incoming), dialing
+ * (outgoing), and active calls. When the last BluetoothCall is disconnected, Telecom will unbind
+ * to the service triggering InCallActivity (via CallList) to finish soon after.
+ */
+public class BluetoothInCallService extends InCallService {
+
+    private static final String TAG = "BluetoothInCallService";
+
+    // match up with bthf_call_state_t of bt_hf.h
+    private static final int CALL_STATE_ACTIVE = 0;
+    private static final int CALL_STATE_HELD = 1;
+    private static final int CALL_STATE_DIALING = 2;
+    private static final int CALL_STATE_ALERTING = 3;
+    private static final int CALL_STATE_INCOMING = 4;
+    private static final int CALL_STATE_WAITING = 5;
+    private static final int CALL_STATE_IDLE = 6;
+    private static final int CALL_STATE_DISCONNECTED = 7;
+
+    // match up with bthf_call_state_t of bt_hf.h
+    // Terminate all held or set UDUB("busy") to a waiting call
+    private static final int CHLD_TYPE_RELEASEHELD = 0;
+    // Terminate all active calls and accepts a waiting/held call
+    private static final int CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD = 1;
+    // Hold all active calls and accepts a waiting/held call
+    private static final int CHLD_TYPE_HOLDACTIVE_ACCEPTHELD = 2;
+    // Add all held calls to a conference
+    private static final int CHLD_TYPE_ADDHELDTOCONF = 3;
+
+    // Indicates that no BluetoothCall is ringing
+    private static final int DEFAULT_RINGING_ADDRESS_TYPE = 128;
+
+    private int mNumActiveCalls = 0;
+    private int mNumHeldCalls = 0;
+    private int mNumChildrenOfActiveCall = 0;
+    private int mBluetoothCallState = CALL_STATE_IDLE;
+    private String mRingingAddress = "";
+    private int mRingingAddressType = DEFAULT_RINGING_ADDRESS_TYPE;
+    private BluetoothCall mOldHeldCall = null;
+    private boolean mHeadsetUpdatedRecently = false;
+    private boolean mIsDisconnectedTonePlaying = false;
+
+    private static final Object LOCK = new Object();
+    private BluetoothHeadsetProxy mBluetoothHeadset;
+
+    @VisibleForTesting
+    public TelephonyManager mTelephonyManager;
+
+    @VisibleForTesting
+    public TelecomManager mTelecomManager;
+
+    @VisibleForTesting
+    public final HashMap<String, CallStateCallback> mCallbacks = new HashMap<>();
+
+    @VisibleForTesting
+    public final HashMap<String, BluetoothCall> mBluetoothCallHashMap = new HashMap<>();
+
+    // A map from Calls to indexes used to identify calls for CLCC (C* List Current Calls).
+    private final Map<BluetoothCall, Integer> mClccIndexMap = new HashMap<>();
+
+    private static BluetoothInCallService sInstance;
+
+    public CallInfo mCallInfo = new CallInfo();
+
+    /**
+     * Listens to connections and disconnections of bluetooth headsets.  We need to save the current
+     * bluetooth headset so that we know where to send BluetoothCall updates.
+     */
+    @VisibleForTesting
+    public BluetoothProfile.ServiceListener mProfileListener =
+            new BluetoothProfile.ServiceListener() {
+                @Override
+                public void onServiceConnected(int profile, BluetoothProfile proxy) {
+                    synchronized (LOCK) {
+                        setBluetoothHeadset(new BluetoothHeadsetProxy((BluetoothHeadset) proxy));
+                        updateHeadsetWithCallState(true /* force */);
+                    }
+                }
+
+                @Override
+                public void onServiceDisconnected(int profile) {
+                    synchronized (LOCK) {
+                        setBluetoothHeadset(null);
+                    }
+                }
+            };
+
+    public class BluetoothAdapterReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            synchronized (LOCK) {
+                if (intent.getAction() != BluetoothAdapter.ACTION_STATE_CHANGED) {
+                    Log.w(TAG, "BluetoothAdapterReceiver: Intent action " + intent.getAction());
+                    return;
+                }
+                int state = intent
+                        .getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+                Log.d(TAG, "Bluetooth Adapter state: " + state);
+                if (state == BluetoothAdapter.STATE_ON) {
+                    queryPhoneState();
+                }
+            }
+        }
+    };
+
+    /**
+     * Receives events for global state changes of the bluetooth adapter.
+     */
+    // TODO: The code is moved from Telecom stack. Since we're running in the BT process itself,
+    // we may be able to simplify this in a future patch.
+    @VisibleForTesting
+    public BluetoothAdapterReceiver mBluetoothAdapterReceiver;
+
+    @VisibleForTesting
+    public class CallStateCallback extends Call.Callback {
+        public int mLastState;
+
+        public CallStateCallback(int initialState) {
+            mLastState = initialState;
+        }
+
+        public int getLastState() {
+            return mLastState;
+        }
+
+        public void onStateChanged(BluetoothCall call, int state) {
+            if (mCallInfo.isNullCall(call)) {
+                return;
+            }
+            if (call.isExternalCall()) {
+                return;
+            }
+
+            // If a BluetoothCall is being put on hold because of a new connecting call, ignore the
+            // CONNECTING since the BT state update needs to send out the numHeld = 1 + dialing
+            // state atomically.
+            // When the BluetoothCall later transitions to DIALING/DISCONNECTED we will then
+            // send out the aggregated update.
+            if (getLastState() == Call.STATE_ACTIVE && state == Call.STATE_HOLDING) {
+                for (BluetoothCall otherCall : mCallInfo.getBluetoothCalls()) {
+                    if (otherCall.getState() == Call.STATE_CONNECTING) {
+                        mLastState = state;
+                        return;
+                    }
+                }
+            }
+
+            // To have an active BluetoothCall and another dialing at the same time is an invalid BT
+            // state. We can assume that the active BluetoothCall will be automatically held
+            // which will send another update at which point we will be in the right state.
+            BluetoothCall activeCall = mCallInfo.getActiveCall();
+            if (!mCallInfo.isNullCall(activeCall)
+                    && getLastState() == Call.STATE_CONNECTING
+                    && (state == Call.STATE_DIALING || state == Call.STATE_PULLING_CALL)) {
+                mLastState = state;
+                return;
+            }
+            mLastState = state;
+            updateHeadsetWithCallState(false /* force */);
+        }
+
+        @Override
+        public void onStateChanged(Call call, int state) {
+            super.onStateChanged(call, state);
+            onStateChanged(getBluetoothCallById(call.getDetails().getTelecomCallId()), state);
+        }
+
+        public void onDetailsChanged(BluetoothCall call, Call.Details details) {
+            if (mCallInfo.isNullCall(call)) {
+                return;
+            }
+            if (call.isExternalCall()) {
+                onCallRemoved(call);
+            } else {
+                onCallAdded(call);
+            }
+        }
+
+        @Override
+        public void onDetailsChanged(Call call, Call.Details details) {
+            super.onDetailsChanged(call, details);
+            onDetailsChanged(getBluetoothCallById(call.getDetails().getTelecomCallId()), details);
+        }
+
+        public void onParentChanged(BluetoothCall call) {
+            if (call.isExternalCall()) {
+                return;
+            }
+            if (call.getParentId() != null) {
+                // If this BluetoothCall is newly conferenced, ignore the callback.
+                // We only care about the one sent for the parent conference call.
+                Log.d(TAG,
+                        "Ignoring onIsConferenceChanged from child BluetoothCall with new parent");
+                return;
+            }
+            updateHeadsetWithCallState(false /* force */);
+        }
+
+        @Override
+        public void onParentChanged(Call call, Call parent) {
+            super.onParentChanged(call, parent);
+            onParentChanged(
+                    getBluetoothCallById(call.getDetails().getTelecomCallId()));
+        }
+
+        public void onChildrenChanged(BluetoothCall call, List<BluetoothCall> children) {
+            if (call.isExternalCall()) {
+                return;
+            }
+            if (call.getChildrenIds().size() == 1) {
+                // If this is a parent BluetoothCall with only one child,
+                // ignore the callback as well since the minimum number of child calls to
+                // start a conference BluetoothCall is 2. We expect this to be called again
+                // when the parent BluetoothCall has another child BluetoothCall added.
+                Log.d(TAG,
+                        "Ignoring onIsConferenceChanged from parent with only one child call");
+                return;
+            }
+            updateHeadsetWithCallState(false /* force */);
+        }
+
+        @Override
+        public void onChildrenChanged(Call call, List<Call> children) {
+            super.onChildrenChanged(call, children);
+            onChildrenChanged(
+                    getBluetoothCallById(call.getDetails().getTelecomCallId()),
+                    getBluetoothCallsByIds(BluetoothCall.getIds(children)));
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Log.i(TAG, "onBind. Intent: " + intent);
+        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
+            Log.i(TAG, "Bluetooth is off");
+            ComponentName componentName
+                    = new ComponentName(getPackageName(), this.getClass().getName());
+            getPackageManager().setComponentEnabledSetting(
+                    componentName,
+                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+                    PackageManager.DONT_KILL_APP);
+            return null;
+        }
+        IBinder binder = super.onBind(intent);
+        mTelephonyManager = getSystemService(TelephonyManager.class);
+        mTelecomManager = getSystemService(TelecomManager.class);
+        return binder;
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        Log.i(TAG, "onUnbind. Intent: " + intent);
+        return super.onUnbind(intent);
+    }
+
+    public BluetoothInCallService() {
+        Log.i(TAG, "BluetoothInCallService is created");
+        BluetoothAdapter.getDefaultAdapter()
+                .getProfileProxy(this, mProfileListener, BluetoothProfile.HEADSET);
+        sInstance = this;
+    }
+
+    public static BluetoothInCallService getInstance() {
+        return sInstance;
+    }
+
+    protected void enforceModifyPermission() {
+        enforceCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE, null);
+    }
+
+    public boolean answerCall() {
+        synchronized (LOCK) {
+            enforceModifyPermission();
+            Log.i(TAG, "BT - answering call");
+            BluetoothCall call = mCallInfo.getRingingOrSimulatedRingingCall();
+            if (mCallInfo.isNullCall(call)) {
+                return false;
+            }
+            call.answer(VideoProfile.STATE_AUDIO_ONLY);
+            return true;
+        }
+    }
+
+    public boolean hangupCall() {
+        synchronized (LOCK) {
+            enforceModifyPermission();
+            Log.i(TAG, "BT - hanging up call");
+            BluetoothCall call = mCallInfo.getForegroundCall();
+            if (mCallInfo.isNullCall(call)) {
+                return false;
+            }
+            call.disconnect();
+            return true;
+        }
+    }
+
+    public boolean sendDtmf(int dtmf) {
+        synchronized (LOCK) {
+            enforceModifyPermission();
+            Log.i(TAG, "BT - sendDtmf " + dtmf);
+            BluetoothCall call = mCallInfo.getForegroundCall();
+            if (mCallInfo.isNullCall(call)) {
+                return false;
+            }
+            // TODO: Consider making this a queue instead of starting/stopping
+            // in quick succession.
+            call.playDtmfTone((char) dtmf);
+            call.stopDtmfTone();
+            return true;
+        }
+    }
+
+    public String getNetworkOperator()  {
+        synchronized (LOCK) {
+            enforceModifyPermission();
+            Log.i(TAG, "getNetworkOperator");
+            PhoneAccount account = mCallInfo.getBestPhoneAccount();
+            if (account != null && account.getLabel() != null) {
+                return account.getLabel().toString();
+            }
+            // Finally, just get the network name from telephony.
+            return mTelephonyManager.getNetworkOperatorName();
+        }
+    }
+
+    public String getSubscriberNumber() {
+        synchronized (LOCK) {
+            enforceModifyPermission();
+            Log.i(TAG, "getSubscriberNumber");
+            String address = null;
+            PhoneAccount account = mCallInfo.getBestPhoneAccount();
+            if (account != null) {
+                Uri addressUri = account.getAddress();
+                if (addressUri != null) {
+                    address = addressUri.getSchemeSpecificPart();
+                }
+            }
+            if (TextUtils.isEmpty(address)) {
+                address = mTelephonyManager.getLine1Number();
+                if (address == null) address = "";
+            }
+            return address;
+        }
+    }
+
+    public boolean listCurrentCalls() {
+        synchronized (LOCK) {
+            enforceModifyPermission();
+            // only log if it is after we recently updated the headset state or else it can
+            // clog the android log since this can be queried every second.
+            boolean logQuery = mHeadsetUpdatedRecently;
+            mHeadsetUpdatedRecently = false;
+
+            if (logQuery) {
+                Log.i(TAG, "listcurrentCalls");
+            }
+
+            sendListOfCalls(logQuery);
+            return true;
+        }
+    }
+
+    public boolean queryPhoneState() {
+        synchronized (LOCK) {
+            enforceModifyPermission();
+            Log.i(TAG, "queryPhoneState");
+            updateHeadsetWithCallState(true);
+            return true;
+        }
+    }
+
+    public boolean processChld(int chld) {
+        synchronized (LOCK) {
+            enforceModifyPermission();
+            long token = Binder.clearCallingIdentity();
+            Log.i(TAG, "processChld " + chld);
+            return _processChld(chld);
+        }
+    }
+
+    public void onCallAdded(BluetoothCall call) {
+        if (call.isExternalCall()) {
+            return;
+        }
+        if (!mBluetoothCallHashMap.containsKey(call.getTelecomCallId())) {
+            Log.d(TAG, "onCallAdded");
+            CallStateCallback callback = new CallStateCallback(call.getState());
+            mCallbacks.put(call.getTelecomCallId(), callback);
+            call.registerCallback(callback);
+
+            mBluetoothCallHashMap.put(call.getTelecomCallId(), call);
+            updateHeadsetWithCallState(false /* force */);
+        }
+    }
+
+    @Override
+    public void onCallAdded(Call call) {
+        super.onCallAdded(call);
+        onCallAdded(new BluetoothCall(call));
+    }
+
+    public void onCallRemoved(BluetoothCall call) {
+        if (call.isExternalCall()) {
+            return;
+        }
+        Log.d(TAG, "onCallRemoved");
+        CallStateCallback callback = getCallback(call);
+        if (callback != null) {
+            call.unregisterCallback(callback);
+        }
+
+        if (mBluetoothCallHashMap.containsKey(call.getTelecomCallId())) {
+            mBluetoothCallHashMap.remove(call.getTelecomCallId());
+        }
+
+        mClccIndexMap.remove(call);
+        updateHeadsetWithCallState(false /* force */);
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        super.onCallRemoved(call);
+        BluetoothCall bluetoothCall = getBluetoothCallById(call.getDetails().getTelecomCallId());
+        if (bluetoothCall == null) {
+            Log.w(TAG, "onCallRemoved, BluetoothCall is removed before registered");
+            return;
+        }
+        onCallRemoved(bluetoothCall);
+    }
+
+    @Override
+    public void onCallAudioStateChanged(CallAudioState audioState) {
+        super.onCallAudioStateChanged(audioState);
+        Log.d(TAG, "onCallAudioStateChanged, audioState == " + audioState);
+    }
+
+
+    @Override
+    public void onCreate() {
+        Log.d(TAG, "onCreate");
+        super.onCreate();
+        mBluetoothAdapterReceiver = new BluetoothAdapterReceiver();
+        IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+        registerReceiver(mBluetoothAdapterReceiver, intentFilter);
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.d(TAG, "onDestroy");
+        if (mBluetoothAdapterReceiver != null) {
+            unregisterReceiver(mBluetoothAdapterReceiver);
+            mBluetoothAdapterReceiver = null;
+        }
+        super.onDestroy();
+    }
+
+    private void sendListOfCalls(boolean shouldLog) {
+        Collection<BluetoothCall> calls = mCallInfo.getBluetoothCalls();
+        for (BluetoothCall call : calls) {
+            // We don't send the parent conference BluetoothCall to the bluetooth device.
+            // We do, however want to send conferences that have no children to the bluetooth
+            // device (e.g. IMS Conference).
+            if (!call.isConference()
+                    || (call.isConference()
+                            && call.can(Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN))) {
+                sendClccForCall(call, shouldLog);
+            }
+        }
+        sendClccEndMarker();
+    }
+
+    private void sendClccEndMarker() {
+        // End marker is recognized with an index value of 0. All other parameters are ignored.
+        if (mBluetoothHeadset != null) {
+            mBluetoothHeadset.clccResponse(0 /* index */, 0, 0, 0, false, null, 0);
+        }
+    }
+
+    /**
+     * Sends a single clcc (C* List Current Calls) event for the specified call.
+     */
+    private void sendClccForCall(BluetoothCall call, boolean shouldLog) {
+        boolean isForeground = mCallInfo.getForegroundCall() == call;
+        int state = getBtCallState(call, isForeground);
+        boolean isPartOfConference = false;
+        boolean isConferenceWithNoChildren = call.isConference()
+                && call.can(Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+
+        if (state == CALL_STATE_IDLE) {
+            return;
+        }
+
+        BluetoothCall conferenceCall = getBluetoothCallById(call.getParentId());
+        if (!mCallInfo.isNullCall(conferenceCall)) {
+            isPartOfConference = true;
+
+            // Run some alternative states for Conference-level merge/swap support.
+            // Basically, if BluetoothCall supports swapping or merging at the conference-level,
+            // then we need to expose the calls as having distinct states
+            // (ACTIVE vs CAPABILITY_HOLD) or
+            // the functionality won't show up on the bluetooth device.
+
+            // Before doing any special logic, ensure that we are dealing with an
+            // ACTIVE BluetoothCall and that the conference itself has a notion of
+            // the current "active" child call.
+            BluetoothCall activeChild = getBluetoothCallById(
+                    conferenceCall.getGenericConferenceActiveChildCallId());
+            if (state == CALL_STATE_ACTIVE && !mCallInfo.isNullCall(activeChild)) {
+                // Reevaluate state if we can MERGE or if we can SWAP without previously having
+                // MERGED.
+                boolean shouldReevaluateState =
+                        conferenceCall.can(Connection.CAPABILITY_MERGE_CONFERENCE)
+                                || (conferenceCall.can(Connection.CAPABILITY_SWAP_CONFERENCE)
+                                        && !conferenceCall.wasConferencePreviouslyMerged());
+
+                if (shouldReevaluateState) {
+                    isPartOfConference = false;
+                    if (call == activeChild) {
+                        state = CALL_STATE_ACTIVE;
+                    } else {
+                        // At this point we know there is an "active" child and we know that it is
+                        // not this call, so set it to HELD instead.
+                        state = CALL_STATE_HELD;
+                    }
+                }
+            }
+            if (conferenceCall.getState() == Call.STATE_HOLDING
+                    && conferenceCall.can(Connection.CAPABILITY_MANAGE_CONFERENCE)) {
+                // If the parent IMS CEP conference BluetoothCall is on hold, we should mark
+                // this BluetoothCall as being on hold regardless of what the other
+                // children are doing.
+                state = CALL_STATE_HELD;
+            }
+        } else if (isConferenceWithNoChildren) {
+            // Handle the special case of an IMS conference BluetoothCall without conference
+            // event package support.
+            // The BluetoothCall will be marked as a conference, but the conference will not have
+            // child calls where conference event packages are not used by the carrier.
+            isPartOfConference = true;
+        }
+
+        int index = getIndexForCall(call);
+        int direction = call.isIncoming() ? 1 : 0;
+        final Uri addressUri;
+        if (call.getGatewayInfo() != null) {
+            addressUri = call.getGatewayInfo().getOriginalAddress();
+        } else {
+            addressUri = call.getHandle();
+        }
+
+        String address = addressUri == null ? null : addressUri.getSchemeSpecificPart();
+        if (address != null) {
+            address = PhoneNumberUtils.stripSeparators(address);
+        }
+
+        int addressType = address == null ? -1 : PhoneNumberUtils.toaFromString(address);
+
+        if (shouldLog) {
+            Log.i(TAG, "sending clcc for BluetoothCall "
+                            + index + ", "
+                            + direction + ", "
+                            + state + ", "
+                            + isPartOfConference + ", "
+                            + addressType);
+        }
+
+        if (mBluetoothHeadset != null) {
+            mBluetoothHeadset.clccResponse(
+                    index, direction, state, 0, isPartOfConference, address, addressType);
+        }
+    }
+
+    /**
+     * Returns the caches index for the specified call.  If no such index exists, then an index is
+     * given (smallest number starting from 1 that isn't already taken).
+     */
+    private int getIndexForCall(BluetoothCall call) {
+        if (mClccIndexMap.containsKey(call)) {
+            return mClccIndexMap.get(call);
+        }
+
+        int i = 1;  // Indexes for bluetooth clcc are 1-based.
+        while (mClccIndexMap.containsValue(i)) {
+            i++;
+        }
+
+        // NOTE: Indexes are removed in {@link #onCallRemoved}.
+        mClccIndexMap.put(call, i);
+        return i;
+    }
+
+    private boolean _processChld(int chld) {
+        BluetoothCall activeCall = mCallInfo.getActiveCall();
+        BluetoothCall ringingCall = mCallInfo.getRingingOrSimulatedRingingCall();
+        if (ringingCall == null) {
+            Log.i(TAG, "asdf ringingCall null");
+        } else {
+            Log.i(TAG, "asdf ringingCall not null " + ringingCall.hashCode());
+        }
+
+        BluetoothCall heldCall = mCallInfo.getHeldCall();
+
+        Log.i(TAG, "Active: " + activeCall
+                + " Ringing: " + ringingCall
+                + " Held: " + heldCall);
+        Log.i(TAG, "asdf chld " + chld);
+
+        if (chld == CHLD_TYPE_RELEASEHELD) {
+            Log.i(TAG, "asdf CHLD_TYPE_RELEASEHELD");
+            if (!mCallInfo.isNullCall(ringingCall)) {
+                Log.i(TAG, "asdf reject " + ringingCall.hashCode());
+                ringingCall.reject(false, null);
+                return true;
+            } else if (!mCallInfo.isNullCall(heldCall)) {
+                heldCall.disconnect();
+                return true;
+            }
+        } else if (chld == CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD) {
+            if (mCallInfo.isNullCall(activeCall)
+                    && mCallInfo.isNullCall(ringingCall)
+                    && mCallInfo.isNullCall(heldCall)) {
+                return false;
+            }
+            if (!mCallInfo.isNullCall(activeCall)) {
+                activeCall.disconnect();
+                if (!mCallInfo.isNullCall(ringingCall)) {
+                    ringingCall.answer(VideoProfile.STATE_AUDIO_ONLY);
+                }
+                return true;
+            }
+            if (!mCallInfo.isNullCall(ringingCall)) {
+                ringingCall.answer(ringingCall.getVideoState());
+            } else if (!mCallInfo.isNullCall(heldCall)) {
+                heldCall.unhold();
+            }
+            return true;
+        } else if (chld == CHLD_TYPE_HOLDACTIVE_ACCEPTHELD) {
+            if (!mCallInfo.isNullCall(activeCall)
+                    && activeCall.can(Connection.CAPABILITY_SWAP_CONFERENCE)) {
+                activeCall.swapConference();
+                Log.i(TAG, "CDMA calls in conference swapped, updating headset");
+                updateHeadsetWithCallState(true /* force */);
+                return true;
+            } else if (!mCallInfo.isNullCall(ringingCall)) {
+                ringingCall.answer(VideoProfile.STATE_AUDIO_ONLY);
+                return true;
+            } else if (!mCallInfo.isNullCall(heldCall)) {
+                // CallsManager will hold any active calls when unhold() is called on a
+                // currently-held call.
+                heldCall.unhold();
+                return true;
+            } else if (!mCallInfo.isNullCall(activeCall)
+                    && activeCall.can(Connection.CAPABILITY_HOLD)) {
+                activeCall.hold();
+                return true;
+            }
+        } else if (chld == CHLD_TYPE_ADDHELDTOCONF) {
+            if (!mCallInfo.isNullCall(activeCall)) {
+                if (activeCall.can(Connection.CAPABILITY_MERGE_CONFERENCE)) {
+                    activeCall.mergeConference();
+                    return true;
+                } else {
+                    List<BluetoothCall> conferenceable = getBluetoothCallsByIds(
+                            activeCall.getConferenceableCalls());
+                    if (!conferenceable.isEmpty()) {
+                        activeCall.conference(conferenceable.get(0));
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Sends an update of the current BluetoothCall state to the current Headset.
+     *
+     * @param force {@code true} if the headset state should be sent regardless if no changes to
+     * the state have occurred, {@code false} if the state should only be sent if the state
+     * has changed.
+     */
+    private void updateHeadsetWithCallState(boolean force) {
+        BluetoothCall activeCall = mCallInfo.getActiveCall();
+        BluetoothCall ringingCall = mCallInfo.getRingingOrSimulatedRingingCall();
+        BluetoothCall heldCall = mCallInfo.getHeldCall();
+
+        int bluetoothCallState = getBluetoothCallStateForUpdate();
+
+        String ringingAddress = null;
+        int ringingAddressType = DEFAULT_RINGING_ADDRESS_TYPE;
+        String ringingName = null;
+        if (!mCallInfo.isNullCall(ringingCall) && ringingCall.getHandle() != null
+                && !ringingCall.isSilentRingingRequested()) {
+            ringingAddress = ringingCall.getHandle().getSchemeSpecificPart();
+            if (ringingAddress != null) {
+                ringingAddressType = PhoneNumberUtils.toaFromString(ringingAddress);
+            }
+            ringingName = ringingCall.getCallerDisplayName();
+            if (TextUtils.isEmpty(ringingName)) {
+                ringingName = ringingCall.getContactDisplayName();
+            }
+        }
+        if (ringingAddress == null) {
+            ringingAddress = "";
+        }
+
+        int numActiveCalls = mCallInfo.isNullCall(activeCall) ? 0 : 1;
+        int numHeldCalls = mCallInfo.getNumHeldCalls();
+        int numChildrenOfActiveCall =
+                mCallInfo.isNullCall(activeCall) ? 0 : activeCall.getChildrenIds().size();
+
+        // Intermediate state for GSM calls which are in the process of being swapped.
+        // TODO: Should we be hardcoding this value to 2 or should we check if all top level calls
+        //       are held?
+        boolean callsPendingSwitch = (numHeldCalls == 2);
+
+        // For conference calls which support swapping the active BluetoothCall within the
+        // conference (namely CDMA calls) we need to expose that as a held BluetoothCall
+        // in order for the BT device to show "swap" and "merge" functionality.
+        boolean ignoreHeldCallChange = false;
+        if (!mCallInfo.isNullCall(activeCall) && activeCall.isConference()
+                && !activeCall.can(Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN)) {
+            if (activeCall.can(Connection.CAPABILITY_SWAP_CONFERENCE)) {
+                // Indicate that BT device should show SWAP command by indicating that there is a
+                // BluetoothCall on hold, but only if the conference wasn't previously merged.
+                numHeldCalls = activeCall.wasConferencePreviouslyMerged() ? 0 : 1;
+            } else if (activeCall.can(Connection.CAPABILITY_MERGE_CONFERENCE)) {
+                numHeldCalls = 1;  // Merge is available, so expose via numHeldCalls.
+            }
+
+            for (String id : activeCall.getChildrenIds()) {
+                // Held BluetoothCall has changed due to it being combined into a CDMA conference.
+                // Keep track of this and ignore any future update since it doesn't really count
+                // as a BluetoothCall change.
+                if (mOldHeldCall != null && mOldHeldCall.getTelecomCallId() == id) {
+                    ignoreHeldCallChange = true;
+                    break;
+                }
+            }
+        }
+
+        if (mBluetoothHeadset != null
+                && (force
+                    || (!callsPendingSwitch
+                        && (numActiveCalls != mNumActiveCalls
+                            || numChildrenOfActiveCall != mNumChildrenOfActiveCall
+                            || numHeldCalls != mNumHeldCalls
+                            || bluetoothCallState != mBluetoothCallState
+                            || !TextUtils.equals(ringingAddress, mRingingAddress)
+                            || ringingAddressType != mRingingAddressType
+                            || (heldCall != mOldHeldCall && !ignoreHeldCallChange))))) {
+
+            // If the BluetoothCall is transitioning into the alerting state, send DIALING first.
+            // Some devices expect to see a DIALING state prior to seeing an ALERTING state
+            // so we need to send it first.
+            boolean sendDialingFirst = mBluetoothCallState != bluetoothCallState
+                    && bluetoothCallState == CALL_STATE_ALERTING;
+
+            mOldHeldCall = heldCall;
+            mNumActiveCalls = numActiveCalls;
+            mNumChildrenOfActiveCall = numChildrenOfActiveCall;
+            mNumHeldCalls = numHeldCalls;
+            mBluetoothCallState = bluetoothCallState;
+            mRingingAddress = ringingAddress;
+            mRingingAddressType = ringingAddressType;
+
+            if (sendDialingFirst) {
+                // Log in full to make logs easier to debug.
+                Log.i(TAG, "updateHeadsetWithCallState "
+                                + "numActive " + mNumActiveCalls + ", "
+                                + "numHeld " + mNumHeldCalls + ", "
+                                + "callState " + CALL_STATE_DIALING + ", "
+                                + "ringing type " + mRingingAddressType);
+                mBluetoothHeadset.phoneStateChanged(
+                        mNumActiveCalls,
+                        mNumHeldCalls,
+                        CALL_STATE_DIALING,
+                        mRingingAddress,
+                        mRingingAddressType,
+                        ringingName);
+            }
+
+            Log.i(TAG, "updateHeadsetWithCallState "
+                    + "numActive " + mNumActiveCalls + ", "
+                    + "numHeld " + mNumHeldCalls + ", "
+                    + "callState " + mBluetoothCallState + ", "
+                    + "ringing type " + mRingingAddressType);
+
+            mBluetoothHeadset.phoneStateChanged(
+                    mNumActiveCalls,
+                    mNumHeldCalls,
+                    mBluetoothCallState,
+                    mRingingAddress,
+                    mRingingAddressType,
+                    ringingName);
+
+            mHeadsetUpdatedRecently = true;
+        }
+    }
+
+    private int getBluetoothCallStateForUpdate() {
+        BluetoothCall ringingCall = mCallInfo.getRingingOrSimulatedRingingCall();
+        BluetoothCall dialingCall = mCallInfo.getOutgoingCall();
+        boolean hasOnlyDisconnectedCalls = mCallInfo.hasOnlyDisconnectedCalls();
+
+        //
+        // !! WARNING !!
+        // You will note that CALL_STATE_WAITING, CALL_STATE_HELD, and CALL_STATE_ACTIVE are not
+        // used in this version of the BluetoothCall state mappings.  This is on purpose.
+        // phone_state_change() in btif_hf.c is not written to handle these states. Only with the
+        // listCalls*() method are WAITING and ACTIVE used.
+        // Using the unsupported states here caused problems with inconsistent state in some
+        // bluetooth devices (like not getting out of ringing state after answering a call).
+        //
+        int bluetoothCallState = CALL_STATE_IDLE;
+        if (!mCallInfo.isNullCall(ringingCall) && !ringingCall.isSilentRingingRequested()) {
+            bluetoothCallState = CALL_STATE_INCOMING;
+        } else if (!mCallInfo.isNullCall(dialingCall)) {
+            bluetoothCallState = CALL_STATE_ALERTING;
+        } else if (hasOnlyDisconnectedCalls || mIsDisconnectedTonePlaying) {
+            // Keep the DISCONNECTED state until the disconnect tone's playback is done
+            bluetoothCallState = CALL_STATE_DISCONNECTED;
+        }
+        return bluetoothCallState;
+    }
+
+    private int getBtCallState(BluetoothCall call, boolean isForeground) {
+        switch (call.getState()) {
+            case Call.STATE_NEW:
+            case Call.STATE_DISCONNECTED:
+            case Call.STATE_AUDIO_PROCESSING:
+                return CALL_STATE_IDLE;
+
+            case Call.STATE_ACTIVE:
+                return CALL_STATE_ACTIVE;
+
+            case Call.STATE_CONNECTING:
+            case Call.STATE_SELECT_PHONE_ACCOUNT:
+            case Call.STATE_DIALING:
+            case Call.STATE_PULLING_CALL:
+                // Yes, this is correctly returning ALERTING.
+                // "Dialing" for BT means that we have sent information to the service provider
+                // to place the BluetoothCall but there is no confirmation that the BluetoothCall
+                // is going through. When there finally is confirmation, the ringback is
+                // played which is referred to as an "alert" tone, thus, ALERTING.
+                // TODO: We should consider using the ALERTING terms in Telecom because that
+                // seems to be more industry-standard.
+                return CALL_STATE_ALERTING;
+
+            case Call.STATE_HOLDING:
+                return CALL_STATE_HELD;
+
+            case Call.STATE_RINGING:
+            case Call.STATE_SIMULATED_RINGING:
+                if (call.isSilentRingingRequested()) {
+                    return CALL_STATE_IDLE;
+                } else if (isForeground) {
+                    return CALL_STATE_INCOMING;
+                } else {
+                    return CALL_STATE_WAITING;
+                }
+        }
+        return CALL_STATE_IDLE;
+    }
+
+    @VisibleForTesting
+    public CallStateCallback getCallback(BluetoothCall call) {
+        return mCallbacks.get(call.getTelecomCallId());
+    }
+
+    @VisibleForTesting
+    public void setBluetoothHeadset(BluetoothHeadsetProxy bluetoothHeadset) {
+        mBluetoothHeadset = bluetoothHeadset;
+    }
+
+    @VisibleForTesting
+    public BluetoothCall getBluetoothCallById(String id) {
+        if (mBluetoothCallHashMap.containsKey(id)) {
+            return mBluetoothCallHashMap.get(id);
+        }
+        return null;
+    }
+
+    @VisibleForTesting
+    public List<BluetoothCall> getBluetoothCallsByIds(List<String> ids) {
+        List<BluetoothCall> calls = new ArrayList<>();
+        for (String id : ids) {
+            BluetoothCall call = getBluetoothCallById(id);
+            if (!mCallInfo.isNullCall(call)) {
+                calls.add(call);
+            }
+        }
+        return calls;
+    }
+
+    // extract call information functions out into this part, so we can mock it in testing
+    @VisibleForTesting
+    public class CallInfo {
+
+        public BluetoothCall getForegroundCall() {
+            LinkedHashSet<Integer> states = new LinkedHashSet<Integer>();
+            BluetoothCall foregroundCall;
+
+            states.add(Call.STATE_CONNECTING);
+            foregroundCall = getCallByStates(states);
+            if (!mCallInfo.isNullCall(foregroundCall)) {
+                return foregroundCall;
+            }
+
+            states.clear();
+            states.add(Call.STATE_ACTIVE);
+            states.add(Call.STATE_DIALING);
+            states.add(Call.STATE_PULLING_CALL);
+            foregroundCall = getCallByStates(states);
+            if (!mCallInfo.isNullCall(foregroundCall)) {
+                return foregroundCall;
+            }
+
+            states.clear();
+            states.add(Call.STATE_RINGING);
+            foregroundCall = getCallByStates(states);
+            if (!mCallInfo.isNullCall(foregroundCall)) {
+                return foregroundCall;
+            }
+
+            return null;
+        }
+
+        public BluetoothCall getCallByStates(LinkedHashSet<Integer> states) {
+            List<BluetoothCall> calls = getBluetoothCalls();
+            for (BluetoothCall call : calls) {
+                if (states.contains(call.getState())) {
+                    return call;
+                }
+            }
+            return null;
+        }
+
+        public BluetoothCall getCallByState(int state) {
+            List<BluetoothCall> calls = getBluetoothCalls();
+            for (BluetoothCall call : calls) {
+                if (state == call.getState()) {
+                    return call;
+                }
+            }
+            return null;
+        }
+
+        public int getNumHeldCalls() {
+            int number = 0;
+            List<BluetoothCall> calls = getBluetoothCalls();
+            for (BluetoothCall call : calls) {
+                if (call.getState() == Call.STATE_HOLDING) {
+                    number++;
+                }
+            }
+            return number;
+        }
+
+        public boolean hasOnlyDisconnectedCalls() {
+            List<BluetoothCall> calls = getBluetoothCalls();
+            if (calls.size() == 0) {
+                return false;
+            }
+            for (BluetoothCall call : calls) {
+                if (call.getState() != Call.STATE_DISCONNECTED) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        public List<BluetoothCall> getBluetoothCalls() {
+            return getBluetoothCallsByIds(BluetoothCall.getIds(getCalls()));
+        }
+
+        public BluetoothCall getOutgoingCall() {
+            LinkedHashSet<Integer> states = new LinkedHashSet<Integer>();
+            states.add(Call.STATE_CONNECTING);
+            states.add(Call.STATE_DIALING);
+            states.add(Call.STATE_PULLING_CALL);
+            return getCallByStates(states);
+        }
+
+        public BluetoothCall getRingingOrSimulatedRingingCall() {
+            LinkedHashSet<Integer> states = new LinkedHashSet<Integer>();
+            states.add(Call.STATE_RINGING);
+            states.add(Call.STATE_SIMULATED_RINGING);
+            return getCallByStates(states);
+        }
+
+        public BluetoothCall getActiveCall() {
+            return getCallByState(Call.STATE_ACTIVE);
+        }
+
+        public BluetoothCall getHeldCall() {
+            return getCallByState(Call.STATE_HOLDING);
+        }
+
+        /**
+         * Returns the best phone account to use for the given state of all calls.
+         * First, tries to return the phone account for the foreground call, second the default
+         * phone account for PhoneAccount.SCHEME_TEL.
+         */
+        public PhoneAccount getBestPhoneAccount() {
+            BluetoothCall call = getForegroundCall();
+
+            PhoneAccount account = null;
+            if (!mCallInfo.isNullCall(call)) {
+                PhoneAccountHandle handle = call.getAccountHandle();
+                if (handle != null) {
+                    // First try to get the network name of the foreground call.
+                    account = mTelecomManager.getPhoneAccount(handle);
+                }
+            }
+
+            if (account == null) {
+                // Second, Try to get the label for the default Phone Account.
+                List<PhoneAccountHandle> handles =
+                        mTelecomManager.getPhoneAccountsSupportingScheme(PhoneAccount.SCHEME_TEL);
+                while (handles.iterator().hasNext()) {
+                    account = mTelecomManager.getPhoneAccount(handles.iterator().next());
+                    if (account != null) {
+                        return account;
+                    }
+                }
+            }
+            return null;
+        }
+
+        public boolean isNullCall(BluetoothCall call) {
+            return call == null || call.getCall() == null;
+        }
+    };
+};
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
old mode 100644
new mode 100755
index 07b5a2f..499d204
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -22,6 +22,7 @@
         "androidx.room_room-migration",
         "androidx.room_room-runtime",
         "androidx.room_room-testing",
+        "truth-prebuilt",
     ],
 
     asset_dirs: ["src/com/android/bluetooth/btservice/storage/schemas"],
diff --git a/tests/unit/src/com/android/bluetooth/StateMachineTest.java b/tests/unit/src/com/android/bluetooth/StateMachineTest.java
index 5cf2255..31f97bb 100644
--- a/tests/unit/src/com/android/bluetooth/StateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/StateMachineTest.java
@@ -85,13 +85,13 @@
     /**
      * Tests {@link StateMachine#toString()}.
      */
-    class StateMachineToStringTest extends StateMachine {
+    static class StateMachineToStringTest extends StateMachine {
         StateMachineToStringTest(String name) {
             super(name);
         }
     }
 
-    class ExampleState extends State {
+    static class ExampleState extends State {
         String mName;
 
         ExampleState(String name) {
@@ -195,7 +195,7 @@
             }
         }
 
-        private StateMachineQuitTest mThisSm;
+        private final StateMachineQuitTest mThisSm;
         private S1 mS1 = new S1();
     }
 
@@ -298,7 +298,7 @@
             }
         }
 
-        private StateMachineQuitNowTest mThisSm;
+        private final StateMachineQuitNowTest mThisSm;
         private S1 mS1 = new S1();
     }
 
@@ -402,14 +402,14 @@
             }
         }
 
-        private StateMachineQuitNowAfterStartTest mThisSm;
+        private final StateMachineQuitNowAfterStartTest mThisSm;
         private S1 mS1 = new S1();
     }
 
     /**
      * Test enter/exit can use transitionTo
      */
-    class StateMachineEnterExitTransitionToTest extends StateMachine {
+    static class StateMachineEnterExitTransitionToTest extends StateMachine {
 
         StateMachineEnterExitTransitionToTest(String name) {
             super(name);
@@ -502,7 +502,7 @@
             }
         }
 
-        private StateMachineEnterExitTransitionToTest mThisSm;
+        private final StateMachineEnterExitTransitionToTest mThisSm;
         private S1 mS1 = new S1();
         private S2 mS2 = new S2();
         private S3 mS3 = new S3();
@@ -597,7 +597,7 @@
     /**
      * Tests that ProcessedMessage works as a circular buffer.
      */
-    class StateMachine0 extends StateMachine {
+    static class StateMachine0 extends StateMachine {
         StateMachine0(String name) {
             super(name);
             mThisSm = this;
@@ -628,7 +628,7 @@
             }
         }
 
-        private StateMachine0 mThisSm;
+        private final StateMachine0 mThisSm;
         private S1 mS1 = new S1();
     }
 
@@ -684,7 +684,7 @@
      * in state mS1. With the first message it transitions to
      * itself which causes it to be exited and reentered.
      */
-    class StateMachine1 extends StateMachine {
+    static class StateMachine1 extends StateMachine {
         StateMachine1(String name) {
             super(name);
             mThisSm = this;
@@ -729,7 +729,7 @@
             }
         }
 
-        private StateMachine1 mThisSm;
+        private final StateMachine1 mThisSm;
         private S1 mS1 = new S1();
 
         private int mEnterCount;
@@ -784,7 +784,7 @@
      * mS2 then receives both of the deferred messages first TEST_CMD_1 and
      * then TEST_CMD_2.
      */
-    class StateMachine2 extends StateMachine {
+    static class StateMachine2 extends StateMachine {
         StateMachine2(String name) {
             super(name);
             mThisSm = this;
@@ -835,7 +835,7 @@
             }
         }
 
-        private StateMachine2 mThisSm;
+        private final StateMachine2 mThisSm;
         private S1 mS1 = new S1();
         private S2 mS2 = new S2();
 
@@ -891,7 +891,7 @@
      * Test that unhandled messages in a child are handled by the parent.
      * When TEST_CMD_2 is received.
      */
-    class StateMachine3 extends StateMachine {
+    static class StateMachine3 extends StateMachine {
         StateMachine3(String name) {
             super(name);
             mThisSm = this;
@@ -932,7 +932,7 @@
             }
         }
 
-        private StateMachine3 mThisSm;
+        private final StateMachine3 mThisSm;
         private ParentState mParentState = new ParentState();
         private ChildState mChildState = new ChildState();
     }
@@ -977,7 +977,7 @@
      * with transition from child 1 to child 2 and child 2
      * lets the parent handle the messages.
      */
-    class StateMachine4 extends StateMachine {
+    static class StateMachine4 extends StateMachine {
         StateMachine4(String name) {
             super(name);
             mThisSm = this;
@@ -1027,7 +1027,7 @@
             }
         }
 
-        private StateMachine4 mThisSm;
+        private final StateMachine4 mThisSm;
         private ParentState mParentState = new ParentState();
         private ChildState1 mChildState1 = new ChildState1();
         private ChildState2 mChildState2 = new ChildState2();
@@ -1073,7 +1073,7 @@
      * Test transition from one child to another of a "complex"
      * hierarchy with two parents and multiple children.
      */
-    class StateMachine5 extends StateMachine {
+    static class StateMachine5 extends StateMachine {
         StateMachine5(String name) {
             super(name);
             mThisSm = this;
@@ -1303,7 +1303,7 @@
             }
         }
 
-        private StateMachine5 mThisSm;
+        private final StateMachine5 mThisSm;
         private ParentState1 mParentState1 = new ParentState1();
         private ChildState1 mChildState1 = new ChildState1();
         private ChildState2 mChildState2 = new ChildState2();
@@ -1408,7 +1408,7 @@
      * after construction and before any other messages arrive and that
      * sendMessageDelayed works.
      */
-    class StateMachine6 extends StateMachine {
+    static class StateMachine6 extends StateMachine {
         StateMachine6(String name) {
             super(name);
             mThisSm = this;
@@ -1446,7 +1446,7 @@
             }
         }
 
-        private StateMachine6 mThisSm;
+        private final StateMachine6 mThisSm;
         private S1 mS1 = new S1();
 
         private long mArrivalTimeMsg1;
@@ -1492,7 +1492,7 @@
      * Test that enter is invoked immediately after exit. This validates
      * that enter can be used to send a watch dog message for its state.
      */
-    class StateMachine7 extends StateMachine {
+    static class StateMachine7 extends StateMachine {
         private final int SM7_DELAY_TIME = 250;
 
         StateMachine7(String name) {
@@ -1551,7 +1551,7 @@
             }
         }
 
-        private StateMachine7 mThisSm;
+        private final StateMachine7 mThisSm;
         private S1 mS1 = new S1();
         private S2 mS2 = new S2();
 
@@ -1597,7 +1597,7 @@
     /**
      * Test unhandledMessage.
      */
-    class StateMachineUnhandledMessage extends StateMachine {
+    static class StateMachineUnhandledMessage extends StateMachine {
         StateMachineUnhandledMessage(String name) {
             super(name);
             mThisSm = this;
@@ -1631,7 +1631,7 @@
             }
         }
 
-        private StateMachineUnhandledMessage mThisSm;
+        private final StateMachineUnhandledMessage mThisSm;
         private int mUnhandledMessageCount;
         private S1 mS1 = new S1();
     }
@@ -1671,7 +1671,7 @@
      * will be used to notify testStateMachineSharedThread that the test is
      * complete.
      */
-    class StateMachineSharedThread extends StateMachine {
+    static class StateMachineSharedThread extends StateMachine {
         StateMachineSharedThread(String name, Looper looper, int maxCount) {
             super(name, looper);
             mMaxCount = maxCount;
diff --git a/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java b/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
index 6882b50..2f7a84e 100644
--- a/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
@@ -89,5 +89,4 @@
         });
         Assert.assertEquals(99700000000L, timestampNanos);
     }
-
 }
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
index 2dd5356..dbbf389 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
@@ -185,7 +185,6 @@
         doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
                 mAdapterService).getBondedDevices();
         // Mock system interface
-        doNothing().when(mSystemInterface).init();
         doNothing().when(mSystemInterface).stop();
         when(mSystemInterface.getHeadsetPhoneState()).thenReturn(mPhoneState);
         when(mSystemInterface.getAudioManager()).thenReturn(mAudioManager);
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
index cf3458c..b3deb3a 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
@@ -116,7 +116,6 @@
             return keys.toArray(new BluetoothDevice[keys.size()]);
         }).when(mAdapterService).getBondedDevices();
         // Mock system interface
-        doNothing().when(mSystemInterface).init();
         doNothing().when(mSystemInterface).stop();
         when(mSystemInterface.getHeadsetPhoneState()).thenReturn(mPhoneState);
         when(mSystemInterface.getAudioManager()).thenReturn(mAudioManager);
diff --git a/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentObserverTest.java b/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentObserverTest.java
index cca340e..dcb3edf 100644
--- a/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentObserverTest.java
+++ b/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentObserverTest.java
@@ -46,7 +46,7 @@
 public class BluetoothMapContentObserverTest {
     private Context mTargetContext;
 
-    class ExceptionTestProvider extends MockContentProvider {
+    static class ExceptionTestProvider extends MockContentProvider {
         public ExceptionTestProvider(Context context) {
             super(context);
         }
diff --git a/tests/unit/src/com/android/bluetooth/sdp/DipTest.java b/tests/unit/src/com/android/bluetooth/sdp/DipTest.java
new file mode 100755
index 0000000..e56a988
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/sdp/DipTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.sdp;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.SdpDipRecord;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Looper;
+import android.os.ParcelUuid;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.sdp.SdpManager;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AbstractionLayer;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.Utils;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DipTest {
+    private BluetoothAdapter mAdapter;
+    private Context mTargetContext;
+    private SdpManager mSdpManager;
+    private BluetoothDevice mTestDevice;
+
+    private ArgumentCaptor<Intent> mIntentArgument = ArgumentCaptor.forClass(Intent.class);
+    private ArgumentCaptor<String> mStringArgument = ArgumentCaptor.forClass(String.class);
+
+    @Mock private AdapterService mAdapterService = null;
+
+    @Before
+    public void setUp() throws Exception {
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        // Set up mocks and test assets
+        MockitoAnnotations.initMocks(this);
+
+        TestUtils.setAdapterService(mAdapterService);
+
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        mSdpManager = SdpManager.init(mAdapterService);
+
+        // Get a device for testing
+        mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    private void verifyDipSdpRecordIntent(ArgumentCaptor<Intent> intentArgument,
+            int status, BluetoothDevice device,
+            byte[] uuid,  int specificationId,
+            int vendorId, int vendorIdSource,
+            int productId, int version,
+            boolean primaryRecord) {
+        Intent intent = intentArgument.getValue();
+
+        assertThat(intent).isNotEqualTo(null);
+        assertThat(intent.getAction()).isEqualTo(BluetoothDevice.ACTION_SDP_RECORD);
+        assertThat(device).isEqualTo(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+        assertThat(Utils.byteArrayToUuid(uuid)[0]).isEqualTo(intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID));
+        assertThat(status).isEqualTo(intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1));
+
+        SdpDipRecord record = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
+        assertThat(record).isNotEqualTo(null);
+        assertThat(specificationId).isEqualTo(record.getSpecificationId());
+        assertThat(vendorId).isEqualTo(record.getVendorId());
+        assertThat(vendorIdSource).isEqualTo(record.getVendorIdSource());
+        assertThat(productId).isEqualTo(record.getProductId());
+        assertThat(version).isEqualTo(record.getVersion());
+        assertThat(primaryRecord).isEqualTo(record.getPrimaryRecord());
+    }
+
+    /**
+     * Test that an outgoing connection/disconnection succeeds
+     */
+    @Test
+    @SmallTest
+    public void testDipCallbackSuccess() {
+        // DIP uuid in bytes
+        byte[] uuid = {0, 0, 18, 0, 0, 0, 16, 0, -128, 0, 0, -128, 95, -101, 52, -5};
+        int specificationId = 0x0103;
+        int vendorId = 0x18d1;
+        int vendorIdSource = 1;
+        int productId = 0x1234;
+        int version = 0x0100;
+        boolean primaryRecord = true;
+        boolean moreResults = false;
+
+        mSdpManager.sdpSearch(mTestDevice, BluetoothUuid.DIP);
+        mSdpManager.sdpDipRecordFoundCallback(AbstractionLayer.BT_STATUS_SUCCESS,
+                Utils.getByteAddress(mTestDevice), uuid, specificationId,
+                vendorId, vendorIdSource, productId, version, primaryRecord, moreResults);
+        verify(mAdapterService).sendBroadcast(mIntentArgument.capture(), mStringArgument.capture());
+        verifyDipSdpRecordIntent(mIntentArgument, AbstractionLayer.BT_STATUS_SUCCESS, mTestDevice,
+                uuid, specificationId, vendorId, vendorIdSource, productId, version, primaryRecord);
+    }
+}
diff --git a/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java b/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java
new file mode 100644
index 0000000..878ad8b
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java
@@ -0,0 +1,1188 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.telephony;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.telecom.Call;
+import android.telecom.Connection;
+import android.telecom.GatewayInfo;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.hfp.BluetoothHeadsetProxy;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for {@link BluetoothInCallService}
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothInCallServiceTest {
+
+    private static final int TEST_DTMF_TONE = 0;
+    private static final String TEST_ACCOUNT_ADDRESS = "//foo.com/";
+    private static final int TEST_ACCOUNT_INDEX = 0;
+
+    private static final int CALL_STATE_ACTIVE = 0;
+    private static final int CALL_STATE_HELD = 1;
+    private static final int CALL_STATE_DIALING = 2;
+    private static final int CALL_STATE_ALERTING = 3;
+    private static final int CALL_STATE_INCOMING = 4;
+    private static final int CALL_STATE_WAITING = 5;
+    private static final int CALL_STATE_IDLE = 6;
+    private static final int CALL_STATE_DISCONNECTED = 7;
+    // Terminate all held or set UDUB("busy") to a waiting call
+    private static final int CHLD_TYPE_RELEASEHELD = 0;
+    // Terminate all active calls and accepts a waiting/held call
+    private static final int CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD = 1;
+    // Hold all active calls and accepts a waiting/held call
+    private static final int CHLD_TYPE_HOLDACTIVE_ACCEPTHELD = 2;
+    // Add all held calls to a conference
+    private static final int CHLD_TYPE_ADDHELDTOCONF = 3;
+
+    private TestableBluetoothInCallService mBluetoothInCallService;
+    @Rule public final ServiceTestRule mServiceRule
+            = ServiceTestRule.withTimeout(1, TimeUnit.SECONDS);
+
+    @Mock private BluetoothHeadsetProxy mMockBluetoothHeadset;
+    @Mock private BluetoothInCallService.CallInfo mMockCallInfo;
+    @Mock private TelephonyManager mMockTelephonyManager;
+
+    public class TestableBluetoothInCallService extends BluetoothInCallService {
+        @Override
+        public IBinder onBind(Intent intent) {
+            IBinder binder = super.onBind(intent);
+            IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+            registerReceiver(mBluetoothAdapterReceiver, intentFilter);
+            mTelephonyManager = getSystemService(TelephonyManager.class);
+            mTelecomManager = getSystemService(TelecomManager.class);
+            return binder;
+        }
+        @Override
+        protected void enforceModifyPermission() {}
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        // Create the service Intent.
+        Intent serviceIntent =
+                new Intent(ApplicationProvider.getApplicationContext(),
+                        TestableBluetoothInCallService.class);
+        // Bind the service
+        mServiceRule.bindService(serviceIntent);
+
+        // Ensure initialization does not actually try to access any of the CallsManager fields.
+        // This also works to return null if it is not overwritten later in the test.
+        doReturn(null).when(mMockCallInfo).getActiveCall();
+        doReturn(null).when(mMockCallInfo)
+                .getRingingOrSimulatedRingingCall();
+        doReturn(null).when(mMockCallInfo).getHeldCall();
+        doReturn(null).when(mMockCallInfo).getOutgoingCall();
+        doReturn(0).when(mMockCallInfo).getNumHeldCalls();
+        doReturn(false).when(mMockCallInfo).hasOnlyDisconnectedCalls();
+        doReturn(true).when(mMockCallInfo).isNullCall(null);
+        doReturn(false).when(mMockCallInfo).isNullCall(notNull());
+
+        mBluetoothInCallService = new TestableBluetoothInCallService();
+        mBluetoothInCallService.setBluetoothHeadset(mMockBluetoothHeadset);
+        mBluetoothInCallService.mCallInfo = mMockCallInfo;
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mServiceRule.unbindService();
+        mBluetoothInCallService = null;
+    }
+
+    @Test
+    public void testHeadsetAnswerCall() throws Exception {
+        BluetoothCall mockCall = createRingingCall();
+
+        boolean callAnswered = mBluetoothInCallService.answerCall();
+        verify(mockCall).answer(any(int.class));
+
+        Assert.assertTrue(callAnswered);
+    }
+
+    @Test
+    public void testHeadsetAnswerCallNull() throws Exception {
+        when(mMockCallInfo.getRingingOrSimulatedRingingCall()).thenReturn(null);
+
+        boolean callAnswered = mBluetoothInCallService.answerCall();
+        Assert.assertFalse(callAnswered);
+    }
+
+    @Test
+    public void testHeadsetHangupCall() throws Exception {
+        BluetoothCall mockCall = createForegroundCall();
+
+        boolean callHungup = mBluetoothInCallService.hangupCall();
+
+        verify(mockCall).disconnect();
+        Assert.assertTrue(callHungup);
+    }
+
+    @Test
+    public void testHeadsetHangupCallNull() throws Exception {
+        when(mMockCallInfo.getForegroundCall()).thenReturn(null);
+
+        boolean callHungup = mBluetoothInCallService.hangupCall();
+        Assert.assertFalse(callHungup);
+    }
+
+    @Test
+    public void testHeadsetSendDTMF() throws Exception {
+        BluetoothCall mockCall = createForegroundCall();
+
+        boolean sentDtmf = mBluetoothInCallService.sendDtmf(TEST_DTMF_TONE);
+
+        verify(mockCall).playDtmfTone(eq((char) TEST_DTMF_TONE));
+        verify(mockCall).stopDtmfTone();
+        Assert.assertTrue(sentDtmf);
+    }
+
+    @Test
+    public void testHeadsetSendDTMFNull() throws Exception {
+        when(mMockCallInfo.getForegroundCall()).thenReturn(null);
+
+        boolean sentDtmf = mBluetoothInCallService.sendDtmf(TEST_DTMF_TONE);
+        Assert.assertFalse(sentDtmf);
+    }
+
+    @Test
+    public void testGetNetworkOperator() throws Exception {
+        PhoneAccount fakePhoneAccount = makeQuickAccount("id0", TEST_ACCOUNT_INDEX);
+        when(mMockCallInfo.getBestPhoneAccount()).thenReturn(fakePhoneAccount);
+
+        String networkOperator = mBluetoothInCallService.getNetworkOperator();
+        Assert.assertEquals(networkOperator, "label0");
+    }
+
+    @Test
+    public void testGetNetworkOperatorNoPhoneAccount() throws Exception {
+        when(mMockCallInfo.getForegroundCall()).thenReturn(null);
+        when(mMockTelephonyManager.getNetworkOperatorName()).thenReturn("label1");
+        mBluetoothInCallService.mTelephonyManager = mMockTelephonyManager;
+
+        String networkOperator = mBluetoothInCallService.getNetworkOperator();
+        Assert.assertEquals(networkOperator, "label1");
+    }
+
+    @Test
+    public void testGetSubscriberNumber() throws Exception {
+        PhoneAccount fakePhoneAccount = makeQuickAccount("id0", TEST_ACCOUNT_INDEX);
+        when(mMockCallInfo.getBestPhoneAccount()).thenReturn(fakePhoneAccount);
+
+        String subscriberNumber = mBluetoothInCallService.getSubscriberNumber();
+        Assert.assertEquals(subscriberNumber, TEST_ACCOUNT_ADDRESS + TEST_ACCOUNT_INDEX);
+    }
+
+    @Test
+    public void testGetSubscriberNumberFallbackToTelephony() throws Exception {
+        String fakeNumber = "8675309";
+        when(mMockCallInfo.getBestPhoneAccount()).thenReturn(null);
+        when(mMockTelephonyManager.getLine1Number())
+                .thenReturn(fakeNumber);
+        mBluetoothInCallService.mTelephonyManager = mMockTelephonyManager;
+
+        String subscriberNumber = mBluetoothInCallService.getSubscriberNumber();
+        Assert.assertEquals(subscriberNumber, fakeNumber);
+    }
+
+    @Test
+    public void testListCurrentCallsOneCall() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        BluetoothCall activeCall = createActiveCall();
+        when(activeCall.getState()).thenReturn(Call.STATE_ACTIVE);
+        calls.add(activeCall);
+        mBluetoothInCallService.onCallAdded(activeCall);
+        when(activeCall.isConference()).thenReturn(false);
+        when(activeCall.getHandle()).thenReturn(Uri.parse("tel:555-000"));
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+
+        verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(0), eq(0), eq(0), eq(false),
+                eq("555000"), eq(PhoneNumberUtils.TOA_Unknown));
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+    }
+
+    @Test
+    public void testListCurrentCallsSilentRinging() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        BluetoothCall silentRingingCall = createActiveCall();
+        when(silentRingingCall.getState()).thenReturn(Call.STATE_RINGING);
+        when(silentRingingCall.isSilentRingingRequested()).thenReturn(true);
+        calls.add(silentRingingCall);
+        mBluetoothInCallService.onCallAdded(silentRingingCall);
+
+        when(silentRingingCall.isConference()).thenReturn(false);
+        when(silentRingingCall.getHandle()).thenReturn(Uri.parse("tel:555-000"));
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        when(mMockCallInfo.getRingingOrSimulatedRingingCall()).thenReturn(silentRingingCall);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+
+        verify(mMockBluetoothHeadset, never()).clccResponse(eq(1), eq(0), eq(0), eq(0), eq(false),
+                eq("555000"), eq(PhoneNumberUtils.TOA_Unknown));
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+    }
+
+    @Test
+    public void testConferenceInProgressCDMA() throws Exception {
+        // If two calls are being conferenced and updateHeadsetWithCallState runs while this is
+        // still occurring, it will look like there is an active and held BluetoothCall still while
+        // we are transitioning into a conference.
+        // BluetoothCall has been put into a CDMA "conference" with one BluetoothCall on hold.
+        ArrayList<BluetoothCall>   calls = new ArrayList<>();
+        BluetoothCall parentCall = createActiveCall();
+        final BluetoothCall confCall1 = getMockCall();
+        final BluetoothCall confCall2 = createHeldCall();
+        calls.add(parentCall);
+        calls.add(confCall1);
+        calls.add(confCall2);
+        mBluetoothInCallService.onCallAdded(parentCall);
+        mBluetoothInCallService.onCallAdded(confCall1);
+        mBluetoothInCallService.onCallAdded(confCall2);
+
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        when(confCall1.getState()).thenReturn(Call.STATE_ACTIVE);
+        when(confCall2.getState()).thenReturn(Call.STATE_ACTIVE);
+        when(confCall1.isIncoming()).thenReturn(false);
+        when(confCall2.isIncoming()).thenReturn(true);
+        when(confCall1.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0000")));
+        when(confCall2.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0001")));
+        addCallCapability(parentCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+        addCallCapability(parentCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+        removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+        String confCall1Id = confCall1.getTelecomCallId();
+        when(parentCall.getGenericConferenceActiveChildCallId())
+                .thenReturn(confCall1Id);
+        when(parentCall.isConference()).thenReturn(true);
+        List<String> childrenIds = new LinkedList<String>(){{
+            add(confCall1.getTelecomCallId());
+            add(confCall2.getTelecomCallId());
+        }};
+        when(parentCall.getChildrenIds()).thenReturn(childrenIds);
+        //Add links from child calls to parent
+        String parentId = parentCall.getTelecomCallId();
+        when(confCall1.getParentId()).thenReturn(parentId);
+        when(confCall2.getParentId()).thenReturn(parentId);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.queryPhoneState();
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+
+        when(parentCall.wasConferencePreviouslyMerged()).thenReturn(true);
+        List<BluetoothCall> children =
+                mBluetoothInCallService.getBluetoothCallsByIds(parentCall.getChildrenIds());
+        mBluetoothInCallService.getCallback(parentCall)
+                .onChildrenChanged(parentCall, children);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+
+        when(mMockCallInfo.getHeldCall()).thenReturn(null);
+        // Spurious BluetoothCall to onIsConferencedChanged.
+        mBluetoothInCallService.getCallback(parentCall)
+                .onChildrenChanged(parentCall, children);
+        // Make sure the BluetoothCall has only occurred collectively 2 times (not on the third)
+        verify(mMockBluetoothHeadset, times(2)).phoneStateChanged(any(int.class),
+                any(int.class), any(int.class), nullable(String.class), any(int.class),
+                nullable(String.class));
+    }
+
+    @Test
+    public void testListCurrentCallsCdmaHold() throws Exception {
+        // BluetoothCall has been put into a CDMA "conference" with one BluetoothCall on hold.
+        List<BluetoothCall> calls = new ArrayList<BluetoothCall>();
+        BluetoothCall parentCall = createActiveCall();
+        final BluetoothCall foregroundCall = getMockCall();
+        final BluetoothCall heldCall = createHeldCall();
+        calls.add(parentCall);
+        calls.add(foregroundCall);
+        calls.add(heldCall);
+        mBluetoothInCallService.onCallAdded(parentCall);
+        mBluetoothInCallService.onCallAdded(foregroundCall);
+        mBluetoothInCallService.onCallAdded(heldCall);
+
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        when(foregroundCall.getState()).thenReturn(Call.STATE_ACTIVE);
+        when(heldCall.getState()).thenReturn(Call.STATE_ACTIVE);
+        when(foregroundCall.isIncoming()).thenReturn(false);
+        when(heldCall.isIncoming()).thenReturn(true);
+        when(foregroundCall.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0000")));
+        when(heldCall.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0001")));
+        addCallCapability(parentCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+        addCallCapability(parentCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+        removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+
+        String foregroundCallId = foregroundCall.getTelecomCallId();
+        when(parentCall.getGenericConferenceActiveChildCallId()).thenReturn(foregroundCallId);
+        when(parentCall.isConference()).thenReturn(true);
+        List<String> childrenIds = new LinkedList<String>(){{
+            add(foregroundCall.getTelecomCallId());
+            add(heldCall.getTelecomCallId());
+        }};
+        when(parentCall.getChildrenIds()).thenReturn(childrenIds);
+        //Add links from child calls to parent
+        String parentId = parentCall.getTelecomCallId();
+        when(foregroundCall.getParentId()).thenReturn(parentId);
+        when(heldCall.getParentId()).thenReturn(parentId);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+
+        verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(0), eq(CALL_STATE_ACTIVE), eq(0),
+                eq(false), eq("5550000"), eq(PhoneNumberUtils.TOA_Unknown));
+        verify(mMockBluetoothHeadset).clccResponse(eq(2), eq(1), eq(CALL_STATE_HELD), eq(0),
+                eq(false), eq("5550001"), eq(PhoneNumberUtils.TOA_Unknown));
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+    }
+
+    @Test
+    public void testListCurrentCallsCdmaConference() throws Exception {
+        // BluetoothCall is in a true CDMA conference
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        BluetoothCall parentCall = createActiveCall();
+        final BluetoothCall confCall1 = getMockCall();
+        final BluetoothCall confCall2 = createHeldCall();
+        calls.add(parentCall);
+        calls.add(confCall1);
+        calls.add(confCall2);
+        mBluetoothInCallService.onCallAdded(parentCall);
+        mBluetoothInCallService.onCallAdded(confCall1);
+        mBluetoothInCallService.onCallAdded(confCall2);
+
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        when(confCall1.getState()).thenReturn(Call.STATE_ACTIVE);
+        when(confCall2.getState()).thenReturn(Call.STATE_ACTIVE);
+        when(confCall1.isIncoming()).thenReturn(false);
+        when(confCall2.isIncoming()).thenReturn(true);
+        when(confCall1.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0000")));
+        when(confCall2.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0001")));
+        removeCallCapability(parentCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+        removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+        when(parentCall.wasConferencePreviouslyMerged()).thenReturn(true);
+        //when(parentCall.getConferenceLevelActiveCall()).thenReturn(confCall1);
+        when(parentCall.isConference()).thenReturn(true);
+        List<String> childrenIds = new LinkedList<String>(){{
+            add(confCall1.getTelecomCallId());
+            add(confCall2.getTelecomCallId());
+        }};
+        when(parentCall.getChildrenIds()).thenReturn(childrenIds);
+        //Add links from child calls to parent
+        String parentId = parentCall.getTelecomCallId();
+        when(confCall1.getParentId()).thenReturn(parentId);
+        when(confCall2.getParentId()).thenReturn(parentId);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+
+        verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(0), eq(CALL_STATE_ACTIVE), eq(0),
+                eq(true), eq("5550000"), eq(PhoneNumberUtils.TOA_Unknown));
+        verify(mMockBluetoothHeadset).clccResponse(eq(2), eq(1), eq(CALL_STATE_ACTIVE), eq(0),
+                eq(true), eq("5550001"), eq(PhoneNumberUtils.TOA_Unknown));
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+    }
+
+    @Test
+    public void testWaitingCallClccResponse() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        // This test does not define a value for getForegroundCall(), so this ringing
+        // BluetoothCall will be treated as if it is a waiting BluetoothCall
+        // when listCurrentCalls() is invoked.
+        BluetoothCall waitingCall = createRingingCall();
+        calls.add(waitingCall);
+        mBluetoothInCallService.onCallAdded(waitingCall);
+
+        when(waitingCall.isIncoming()).thenReturn(true);
+        when(waitingCall.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0000")));
+        when(waitingCall.getState()).thenReturn(Call.STATE_RINGING);
+        when(waitingCall.isConference()).thenReturn(false);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+        verify(mMockBluetoothHeadset).clccResponse(1, 1, CALL_STATE_WAITING, 0, false,
+                "5550000", PhoneNumberUtils.TOA_Unknown);
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+        verify(mMockBluetoothHeadset, times(2)).clccResponse(anyInt(),
+                anyInt(), anyInt(), anyInt(), anyBoolean(), nullable(String.class), anyInt());
+    }
+
+    @Test
+    public void testNewCallClccResponse() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        BluetoothCall newCall = createForegroundCall();
+        calls.add(newCall);
+        mBluetoothInCallService.onCallAdded(newCall);
+
+        when(newCall.getState()).thenReturn(Call.STATE_NEW);
+        when(newCall.isConference()).thenReturn(false);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+        verify(mMockBluetoothHeadset, times(1)).clccResponse(anyInt(),
+                anyInt(), anyInt(), anyInt(), anyBoolean(), nullable(String.class), anyInt());
+    }
+
+    @Test
+    public void testRingingCallClccResponse() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        BluetoothCall ringingCall = createForegroundCall();
+        calls.add(ringingCall);
+        mBluetoothInCallService.onCallAdded(ringingCall);
+
+        when(ringingCall.getState()).thenReturn(Call.STATE_RINGING);
+        when(ringingCall.isIncoming()).thenReturn(true);
+        when(ringingCall.isConference()).thenReturn(false);
+        when(ringingCall.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0000")));
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+        verify(mMockBluetoothHeadset).clccResponse(1, 1, CALL_STATE_INCOMING, 0, false,
+                "5550000", PhoneNumberUtils.TOA_Unknown);
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+        verify(mMockBluetoothHeadset, times(2)).clccResponse(anyInt(),
+                anyInt(), anyInt(), anyInt(), anyBoolean(), nullable(String.class), anyInt());
+    }
+
+    @Test
+    public void testCallClccCache() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        BluetoothCall ringingCall = createForegroundCall();
+        calls.add(ringingCall);
+        mBluetoothInCallService.onCallAdded(ringingCall);
+
+        when(ringingCall.getState()).thenReturn(Call.STATE_RINGING);
+        when(ringingCall.isIncoming()).thenReturn(true);
+        when(ringingCall.isConference()).thenReturn(false);
+        when(ringingCall.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:5550000")));
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+        verify(mMockBluetoothHeadset).clccResponse(1, 1, CALL_STATE_INCOMING, 0, false,
+                "5550000", PhoneNumberUtils.TOA_Unknown);
+
+        // Test Caching of old BluetoothCall indices in clcc
+        when(ringingCall.getState()).thenReturn(Call.STATE_ACTIVE);
+        BluetoothCall newHoldingCall = createHeldCall();
+        calls.add(0, newHoldingCall);
+        mBluetoothInCallService.onCallAdded(newHoldingCall);
+
+        when(newHoldingCall.getState()).thenReturn(Call.STATE_HOLDING);
+        when(newHoldingCall.isIncoming()).thenReturn(true);
+        when(newHoldingCall.isConference()).thenReturn(false);
+        when(newHoldingCall.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0001")));
+
+        mBluetoothInCallService.listCurrentCalls();
+        verify(mMockBluetoothHeadset).clccResponse(1, 1, CALL_STATE_ACTIVE, 0, false,
+                "5550000", PhoneNumberUtils.TOA_Unknown);
+        verify(mMockBluetoothHeadset).clccResponse(2, 1, CALL_STATE_HELD, 0, false,
+                "5550001", PhoneNumberUtils.TOA_Unknown);
+        verify(mMockBluetoothHeadset, times(2)).clccResponse(0, 0, 0, 0, false, null, 0);
+    }
+
+    @Test
+    public void testAlertingCallClccResponse() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        BluetoothCall dialingCall = createForegroundCall();
+        calls.add(dialingCall);
+        mBluetoothInCallService.onCallAdded(dialingCall);
+
+        when(dialingCall.getState()).thenReturn(Call.STATE_DIALING);
+        when(dialingCall.isIncoming()).thenReturn(false);
+        when(dialingCall.isConference()).thenReturn(false);
+        when(dialingCall.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0000")));
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+        verify(mMockBluetoothHeadset).clccResponse(1, 0, CALL_STATE_ALERTING, 0, false,
+                "5550000", PhoneNumberUtils.TOA_Unknown);
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+        verify(mMockBluetoothHeadset, times(2)).clccResponse(anyInt(),
+                anyInt(), anyInt(), anyInt(), anyBoolean(), nullable(String.class), anyInt());
+    }
+
+    @Test
+    public void testHoldingCallClccResponse() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+        BluetoothCall dialingCall = createForegroundCall();
+        calls.add(dialingCall);
+        mBluetoothInCallService.onCallAdded(dialingCall);
+
+        when(dialingCall.getState()).thenReturn(Call.STATE_DIALING);
+        when(dialingCall.isIncoming()).thenReturn(false);
+        when(dialingCall.isConference()).thenReturn(false);
+        when(dialingCall.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0000")));
+        BluetoothCall holdingCall = createHeldCall();
+        calls.add(holdingCall);
+        mBluetoothInCallService.onCallAdded(holdingCall);
+
+        when(holdingCall.getState()).thenReturn(Call.STATE_HOLDING);
+        when(holdingCall.isIncoming()).thenReturn(true);
+        when(holdingCall.isConference()).thenReturn(false);
+        when(holdingCall.getGatewayInfo()).thenReturn(
+                new GatewayInfo(null, null, Uri.parse("tel:555-0001")));
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+        verify(mMockBluetoothHeadset).clccResponse(1, 0, CALL_STATE_ALERTING, 0, false,
+                "5550000", PhoneNumberUtils.TOA_Unknown);
+        verify(mMockBluetoothHeadset).clccResponse(2, 1, CALL_STATE_HELD, 0, false,
+                "5550001", PhoneNumberUtils.TOA_Unknown);
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+        verify(mMockBluetoothHeadset, times(3)).clccResponse(anyInt(),
+                anyInt(), anyInt(), anyInt(), anyBoolean(), nullable(String.class), anyInt());
+    }
+
+    @Test
+    public void testListCurrentCallsImsConference() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        BluetoothCall parentCall = createActiveCall();
+        calls.add(parentCall);
+        mBluetoothInCallService.onCallAdded(parentCall);
+
+        addCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+        when(parentCall.isConference()).thenReturn(true);
+        when(parentCall.getState()).thenReturn(Call.STATE_ACTIVE);
+        when(parentCall.isIncoming()).thenReturn(true);
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+
+        verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(1), eq(CALL_STATE_ACTIVE), eq(0),
+                eq(true), (String) isNull(), eq(-1));
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+    }
+
+    @Test
+    public void testListCurrentCallsHeldImsCepConference() throws Exception {
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        BluetoothCall parentCall = createHeldCall();
+        BluetoothCall childCall1 = createActiveCall();
+        BluetoothCall childCall2 = createActiveCall();
+        calls.add(parentCall);
+        calls.add(childCall1);
+        calls.add(childCall2);
+        mBluetoothInCallService.onCallAdded(parentCall);
+        mBluetoothInCallService.onCallAdded(childCall1);
+        mBluetoothInCallService.onCallAdded(childCall2);
+
+        addCallCapability(parentCall, Connection.CAPABILITY_MANAGE_CONFERENCE);
+        String parentId = parentCall.getTelecomCallId();
+        when(childCall1.getParentId()).thenReturn(parentId);
+        when(childCall2.getParentId()).thenReturn(parentId);
+
+        when(parentCall.isConference()).thenReturn(true);
+        when(parentCall.getState()).thenReturn(Call.STATE_HOLDING);
+        when(childCall1.getState()).thenReturn(Call.STATE_ACTIVE);
+        when(childCall2.getState()).thenReturn(Call.STATE_ACTIVE);
+
+        when(parentCall.isIncoming()).thenReturn(true);
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.listCurrentCalls();
+
+        verify(mMockBluetoothHeadset).clccResponse(eq(1), eq(0), eq(CALL_STATE_HELD), eq(0),
+                eq(true), (String) isNull(), eq(-1));
+        verify(mMockBluetoothHeadset).clccResponse(eq(2), eq(0), eq(CALL_STATE_HELD), eq(0),
+                eq(true), (String) isNull(), eq(-1));
+        verify(mMockBluetoothHeadset).clccResponse(0, 0, 0, 0, false, null, 0);
+    }
+
+    @Test
+    public void testQueryPhoneState() throws Exception {
+        BluetoothCall ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:5550000"));
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.queryPhoneState();
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("5550000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    @Test
+    public void testCDMAConferenceQueryState() throws Exception {
+        BluetoothCall parentConfCall = createActiveCall();
+        final BluetoothCall confCall1 = getMockCall();
+        final BluetoothCall confCall2 = getMockCall();
+        mBluetoothInCallService.onCallAdded(confCall1);
+        mBluetoothInCallService.onCallAdded(confCall2);
+        when(parentConfCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+        addCallCapability(parentConfCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+        removeCallCapability(parentConfCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+        when(parentConfCall.wasConferencePreviouslyMerged()).thenReturn(true);
+        when(parentConfCall.isConference()).thenReturn(true);
+        List<String> childrenIds = new LinkedList<String>(){{
+            add(confCall1.getTelecomCallId());
+            add(confCall2.getTelecomCallId());
+        }};
+        when(parentConfCall.getChildrenIds()).thenReturn(childrenIds);
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.queryPhoneState();
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @Test
+    public void testProcessChldTypeReleaseHeldRinging() throws Exception {
+        BluetoothCall ringingCall = createRingingCall();
+        Log.i("BluetoothInCallService", "asdf start " + Integer.toString(ringingCall.hashCode()));
+
+        boolean didProcess = mBluetoothInCallService.processChld(CHLD_TYPE_RELEASEHELD);
+
+        verify(ringingCall).reject(eq(false), nullable(String.class));
+        Assert.assertTrue(didProcess);
+    }
+
+    @Test
+    public void testProcessChldTypeReleaseHeldHold() throws Exception {
+        BluetoothCall onHoldCall = createHeldCall();
+        boolean didProcess = mBluetoothInCallService.processChld(CHLD_TYPE_RELEASEHELD);
+
+        verify(onHoldCall).disconnect();
+        Assert.assertTrue(didProcess);
+    }
+
+    @Test
+    public void testProcessChldReleaseActiveRinging() throws Exception {
+        BluetoothCall activeCall = createActiveCall();
+        BluetoothCall ringingCall = createRingingCall();
+
+        boolean didProcess = mBluetoothInCallService.processChld(
+                CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD);
+
+        verify(activeCall).disconnect();
+        verify(ringingCall).answer(any(int.class));
+        Assert.assertTrue(didProcess);
+    }
+
+    @Test
+    public void testProcessChldReleaseActiveHold() throws Exception {
+        BluetoothCall activeCall = createActiveCall();
+        BluetoothCall heldCall = createHeldCall();
+
+        boolean didProcess = mBluetoothInCallService.processChld(
+                CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD);
+
+        verify(activeCall).disconnect();
+        // BluetoothCall unhold will occur as part of CallsManager auto-unholding
+        // the background BluetoothCall on its own.
+        Assert.assertTrue(didProcess);
+    }
+
+    @Test
+    public void testProcessChldHoldActiveRinging() throws Exception {
+        BluetoothCall ringingCall = createRingingCall();
+
+        boolean didProcess = mBluetoothInCallService.processChld(
+                CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+        verify(ringingCall).answer(any(int.class));
+        Assert.assertTrue(didProcess);
+    }
+
+    @Test
+    public void testProcessChldHoldActiveUnhold() throws Exception {
+        BluetoothCall heldCall = createHeldCall();
+
+        boolean didProcess = mBluetoothInCallService.processChld(
+                CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+        verify(heldCall).unhold();
+        Assert.assertTrue(didProcess);
+    }
+
+    @Test
+    public void testProcessChldHoldActiveHold() throws Exception {
+        BluetoothCall activeCall = createActiveCall();
+        addCallCapability(activeCall, Connection.CAPABILITY_HOLD);
+
+        boolean didProcess = mBluetoothInCallService.processChld(
+                CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+        verify(activeCall).hold();
+        Assert.assertTrue(didProcess);
+    }
+
+    @Test
+    public void testProcessChldAddHeldToConfHolding() throws Exception {
+        BluetoothCall activeCall = createActiveCall();
+        addCallCapability(activeCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+
+        boolean didProcess = mBluetoothInCallService.processChld(CHLD_TYPE_ADDHELDTOCONF);
+
+        verify(activeCall).mergeConference();
+        Assert.assertTrue(didProcess);
+    }
+
+    @Test
+    public void testProcessChldAddHeldToConf() throws Exception {
+        BluetoothCall activeCall = createActiveCall();
+        removeCallCapability(activeCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+        BluetoothCall conferenceableCall = getMockCall();
+        ArrayList<String> conferenceableCalls = new ArrayList<>();
+        conferenceableCalls.add(conferenceableCall.getTelecomCallId());
+        mBluetoothInCallService.onCallAdded(conferenceableCall);
+
+        when(activeCall.getConferenceableCalls()).thenReturn(conferenceableCalls);
+
+        boolean didProcess = mBluetoothInCallService.processChld(CHLD_TYPE_ADDHELDTOCONF);
+
+        verify(activeCall).conference(conferenceableCall);
+        Assert.assertTrue(didProcess);
+    }
+
+    @Test
+    public void testProcessChldHoldActiveSwapConference() throws Exception {
+        // Create an active CDMA BluetoothCall with a BluetoothCall on hold
+        // and simulate a swapConference().
+        BluetoothCall parentCall = createActiveCall();
+        final BluetoothCall foregroundCall = getMockCall();
+        final BluetoothCall heldCall = createHeldCall();
+        addCallCapability(parentCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+        removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+        when(parentCall.isConference()).thenReturn(true);
+        when(parentCall.wasConferencePreviouslyMerged()).thenReturn(false);
+        List<String> childrenIds = new LinkedList<String>(){{
+            add(foregroundCall.getTelecomCallId());
+            add(heldCall.getTelecomCallId());
+        }};
+        when(parentCall.getChildrenIds()).thenReturn(childrenIds);
+
+        clearInvocations(mMockBluetoothHeadset);
+        boolean didProcess = mBluetoothInCallService.processChld(
+                CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+        verify(parentCall).swapConference();
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE), eq(""),
+                eq(128), nullable(String.class));
+        Assert.assertTrue(didProcess);
+    }
+
+    // Testing the CallsManager Listener Functionality on Bluetooth
+    @Test
+    public void testOnCallAddedRinging() throws Exception {
+        BluetoothCall ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555000"));
+
+        mBluetoothInCallService.onCallAdded(ringingCall);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("555000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    @Test
+    public void testSilentRingingCallState() throws Exception {
+        BluetoothCall ringingCall = createRingingCall();
+        when(ringingCall.isSilentRingingRequested()).thenReturn(true);
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555000"));
+
+        mBluetoothInCallService.onCallAdded(ringingCall);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallAddedCdmaActiveHold() throws Exception {
+        // BluetoothCall has been put into a CDMA "conference" with one BluetoothCall on hold.
+        BluetoothCall parentCall = createActiveCall();
+        final BluetoothCall foregroundCall = getMockCall();
+        final BluetoothCall heldCall = createHeldCall();
+        addCallCapability(parentCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+        removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+        when(parentCall.isConference()).thenReturn(true);
+        List<String> childrenIds = new LinkedList<String>(){{
+            add(foregroundCall.getTelecomCallId());
+            add(heldCall.getTelecomCallId());
+        }};
+        when(parentCall.getChildrenIds()).thenReturn(childrenIds);
+
+        mBluetoothInCallService.onCallAdded(parentCall);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallRemoved() throws Exception {
+        BluetoothCall activeCall = createActiveCall();
+        mBluetoothInCallService.onCallAdded(activeCall);
+        doReturn(null).when(mMockCallInfo).getActiveCall();
+        mBluetoothInCallService.onCallRemoved(activeCall);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallStateChangedConnectingCall() throws Exception {
+        BluetoothCall activeCall = getMockCall();
+        BluetoothCall connectingCall = getMockCall();
+        when(connectingCall.getState()).thenReturn(Call.STATE_CONNECTING);
+        ArrayList<BluetoothCall> calls = new ArrayList<>();
+        calls.add(connectingCall);
+        calls.add(activeCall);
+        mBluetoothInCallService.onCallAdded(connectingCall);
+        mBluetoothInCallService.onCallAdded(activeCall);
+        when(mMockCallInfo.getBluetoothCalls()).thenReturn(calls);
+
+        mBluetoothInCallService.getCallback(activeCall)
+                .onStateChanged(activeCall, Call.STATE_HOLDING);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallAddedAudioProcessing() throws Exception {
+        BluetoothCall call = getMockCall();
+        when(call.getState()).thenReturn(Call.STATE_AUDIO_PROCESSING);
+        mBluetoothInCallService.onCallAdded(call);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallStateChangedRingingToAudioProcessing() throws Exception {
+        BluetoothCall ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555000"));
+
+        mBluetoothInCallService.onCallAdded(ringingCall);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("555000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+
+        when(ringingCall.getState()).thenReturn(Call.STATE_AUDIO_PROCESSING);
+        when(mMockCallInfo.getRingingOrSimulatedRingingCall()).thenReturn(null);
+
+        mBluetoothInCallService.getCallback(ringingCall)
+                .onStateChanged(ringingCall, Call.STATE_AUDIO_PROCESSING);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallStateChangedAudioProcessingToSimulatedRinging() throws Exception {
+        BluetoothCall ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+        mBluetoothInCallService.onCallAdded(ringingCall);
+        mBluetoothInCallService.getCallback(ringingCall)
+                .onStateChanged(ringingCall, Call.STATE_SIMULATED_RINGING);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("555-0000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallStateChangedAudioProcessingToActive() throws Exception {
+        BluetoothCall activeCall = createActiveCall();
+        when(activeCall.getState()).thenReturn(Call.STATE_ACTIVE);
+        mBluetoothInCallService.onCallAdded(activeCall);
+        mBluetoothInCallService.getCallback(activeCall)
+                .onStateChanged(activeCall, Call.STATE_ACTIVE);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallStateChangedDialing() throws Exception {
+        BluetoothCall activeCall = createActiveCall();
+
+        // make "mLastState" STATE_CONNECTING
+        BluetoothInCallService.CallStateCallback callback =
+                mBluetoothInCallService.new CallStateCallback(Call.STATE_CONNECTING);
+        mBluetoothInCallService.mCallbacks.put(
+                activeCall.getTelecomCallId(), callback);
+
+        mBluetoothInCallService.mCallbacks.get(activeCall.getTelecomCallId())
+                .onStateChanged(activeCall, Call.STATE_DIALING);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallStateChangedAlerting() throws Exception {
+        BluetoothCall outgoingCall = createOutgoingCall();
+        mBluetoothInCallService.onCallAdded(outgoingCall);
+        mBluetoothInCallService.getCallback(outgoingCall)
+                .onStateChanged(outgoingCall, Call.STATE_DIALING);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_DIALING),
+                eq(""), eq(128), nullable(String.class));
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_ALERTING),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallStateChangedDisconnected() throws Exception {
+        BluetoothCall disconnectedCall = createDisconnectedCall();
+        doReturn(true).when(mMockCallInfo).hasOnlyDisconnectedCalls();
+        mBluetoothInCallService.onCallAdded(disconnectedCall);
+        mBluetoothInCallService.getCallback(disconnectedCall)
+                .onStateChanged(disconnectedCall, Call.STATE_DISCONNECTED);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_DISCONNECTED),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallStateChanged() throws Exception {
+        BluetoothCall ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+        mBluetoothInCallService.onCallAdded(ringingCall);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("555-0000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+
+        //Switch to active
+        doReturn(null).when(mMockCallInfo).getRingingOrSimulatedRingingCall();
+        when(mMockCallInfo.getActiveCall()).thenReturn(ringingCall);
+
+        mBluetoothInCallService.getCallback(ringingCall)
+                .onStateChanged(ringingCall, Call.STATE_ACTIVE);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @Test
+    public void testOnCallStateChangedGSMSwap() throws Exception {
+        BluetoothCall heldCall = createHeldCall();
+        when(heldCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+        mBluetoothInCallService.onCallAdded(heldCall);
+        doReturn(2).when(mMockCallInfo).getNumHeldCalls();
+
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.getCallback(heldCall)
+                .onStateChanged(heldCall, Call.STATE_HOLDING);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(eq(0), eq(2), eq(CALL_STATE_HELD),
+                eq("5550000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    @Test
+    public void testOnParentOnChildrenChanged() throws Exception {
+        // Start with two calls that are being merged into a CDMA conference call. The
+        // onIsConferencedChanged method will be called multiple times during the call. Make sure
+        // that the bluetooth phone state is updated properly.
+        BluetoothCall parentCall = createActiveCall();
+        BluetoothCall activeCall = getMockCall();
+        BluetoothCall heldCall = createHeldCall();
+        mBluetoothInCallService.onCallAdded(parentCall);
+        mBluetoothInCallService.onCallAdded(activeCall);
+        mBluetoothInCallService.onCallAdded(heldCall);
+        String parentId = parentCall.getTelecomCallId();
+        when(activeCall.getParentId()).thenReturn(parentId);
+        when(heldCall.getParentId()).thenReturn(parentId);
+
+        ArrayList<String> calls = new ArrayList<>();
+        calls.add(activeCall.getTelecomCallId());
+
+        when(parentCall.getChildrenIds()).thenReturn(calls);
+        when(parentCall.isConference()).thenReturn(true);
+
+        removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+        addCallCapability(parentCall, Connection.CAPABILITY_SWAP_CONFERENCE);
+        when(parentCall.wasConferencePreviouslyMerged()).thenReturn(false);
+
+        clearInvocations(mMockBluetoothHeadset);
+        // Be sure that onIsConferencedChanged rejects spurious changes during set up of
+        // CDMA "conference"
+        mBluetoothInCallService.getCallback(activeCall).onParentChanged(activeCall);
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+
+        mBluetoothInCallService.getCallback(heldCall).onParentChanged(heldCall);
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+
+        mBluetoothInCallService.getCallback(parentCall)
+                .onChildrenChanged(
+                        parentCall,
+                        mBluetoothInCallService.getBluetoothCallsByIds(calls));
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+
+        calls.add(heldCall.getTelecomCallId());
+        mBluetoothInCallService.onCallAdded(heldCall);
+        mBluetoothInCallService.getCallback(parentCall)
+                .onChildrenChanged(
+                        parentCall,
+                        mBluetoothInCallService.getBluetoothCallsByIds(calls));
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @Test
+    public void testBluetoothAdapterReceiver() throws Exception {
+        BluetoothCall ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:5550000"));
+
+        Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED);
+        intent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_ON);
+        clearInvocations(mMockBluetoothHeadset);
+        mBluetoothInCallService.mBluetoothAdapterReceiver
+                = mBluetoothInCallService.new BluetoothAdapterReceiver();
+        mBluetoothInCallService.mBluetoothAdapterReceiver
+                .onReceive(mBluetoothInCallService, intent);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("5550000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    private void addCallCapability(BluetoothCall call, int capability) {
+        when(call.can(capability)).thenReturn(true);
+    }
+
+    private void removeCallCapability(BluetoothCall call, int capability) {
+        when(call.can(capability)).thenReturn(false);
+    }
+
+    private BluetoothCall createActiveCall() {
+        BluetoothCall call = getMockCall();
+        when(mMockCallInfo.getActiveCall()).thenReturn(call);
+        return call;
+    }
+
+    private BluetoothCall createRingingCall() {
+        BluetoothCall call = getMockCall();
+        Log.i("BluetoothInCallService", "asdf creaete " + Integer.toString(call.hashCode()));
+        when(mMockCallInfo.getRingingOrSimulatedRingingCall()).thenReturn(call);
+        return call;
+    }
+
+    private BluetoothCall createHeldCall() {
+        BluetoothCall call = getMockCall();
+        when(mMockCallInfo.getHeldCall()).thenReturn(call);
+        return call;
+    }
+
+    private BluetoothCall createOutgoingCall() {
+        BluetoothCall call = getMockCall();
+        when(mMockCallInfo.getOutgoingCall()).thenReturn(call);
+        return call;
+    }
+
+    private BluetoothCall createDisconnectedCall() {
+        BluetoothCall call = getMockCall();
+        when(mMockCallInfo.getCallByState(Call.STATE_DISCONNECTED)).thenReturn(call);
+        return call;
+    }
+
+    private BluetoothCall createForegroundCall() {
+        BluetoothCall call = getMockCall();
+        when(mMockCallInfo.getForegroundCall()).thenReturn(call);
+        return call;
+    }
+
+    private static ComponentName makeQuickConnectionServiceComponentName() {
+        return new ComponentName("com.android.server.telecom.tests",
+                "com.android.server.telecom.tests.MockConnectionService");
+    }
+
+    private static PhoneAccountHandle makeQuickAccountHandle(String id) {
+        return new PhoneAccountHandle(makeQuickConnectionServiceComponentName(), id,
+                Binder.getCallingUserHandle());
+    }
+
+    private PhoneAccount.Builder makeQuickAccountBuilder(String id, int idx) {
+        return new PhoneAccount.Builder(makeQuickAccountHandle(id), "label" + idx);
+    }
+
+    private PhoneAccount makeQuickAccount(String id, int idx) {
+        return makeQuickAccountBuilder(id, idx)
+                .setAddress(Uri.parse(TEST_ACCOUNT_ADDRESS + idx))
+                .setSubscriptionAddress(Uri.parse("tel:555-000" + idx))
+                .setCapabilities(idx)
+                .setShortDescription("desc" + idx)
+                .setIsEnabled(true)
+                .build();
+    }
+
+    private BluetoothCall getMockCall() {
+        BluetoothCall call = mock(com.android.bluetooth.telephony.BluetoothCall.class);
+        String uuid = UUID.randomUUID().toString();
+        when(call.getTelecomCallId()).thenReturn(uuid);
+        return call;
+    }
+}