Merge "Remove redundant UUID parsing in GattService"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 158504b..51b547d 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=".hfp.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_gatt.cpp b/jni/com_android_bluetooth_gatt.cpp
index 6aec75a..be415d3 100644
--- a/jni/com_android_bluetooth_gatt.cpp
+++ b/jni/com_android_bluetooth_gatt.cpp
@@ -1136,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,
@@ -1657,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,
@@ -2334,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},
@@ -2373,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/res/values/config.xml b/res/values/config.xml
index 9a1c5b1..484253d 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/btservice/AdapterState.java b/src/com/android/bluetooth/btservice/AdapterState.java
index 8596627..570239b 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.hfp.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 91ea464..04a9874 100644
--- a/src/com/android/bluetooth/gatt/GattService.java
+++ b/src/com/android/bluetooth/gatt/GattService.java
@@ -442,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
@@ -715,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
@@ -2302,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) {
@@ -2967,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) {
@@ -3351,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);
 
@@ -3401,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/hfp/BluetoothCall.java b/src/com/android/bluetooth/hfp/BluetoothCall.java
new file mode 100644
index 0000000..81c3fc1
--- /dev/null
+++ b/src/com/android/bluetooth/hfp/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.hfp;
+
+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/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/BluetoothInCallService.java b/src/com/android/bluetooth/hfp/BluetoothInCallService.java
new file mode 100644
index 0000000..7444ffc
--- /dev/null
+++ b/src/com/android/bluetooth/hfp/BluetoothInCallService.java
@@ -0,0 +1,1089 @@
+/*
+ * 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.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 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/src/com/android/bluetooth/hfp/HeadsetService.java b/src/com/android/bluetooth/hfp/HeadsetService.java
index 6031a93..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();
diff --git a/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java b/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
index 52ca1bc..a618019 100644
--- a/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
+++ b/src/com/android/bluetooth/hfp/HeadsetSystemInterface.java
@@ -20,19 +20,15 @@
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
-import android.bluetooth.IBluetoothHeadsetPhone;
 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 +48,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 +60,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 +104,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 +149,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 +174,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 +195,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 +211,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 +227,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 +243,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 +260,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 +274,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/tests/unit/src/com/android/bluetooth/hfp/BluetoothInCallServiceTest.java b/tests/unit/src/com/android/bluetooth/hfp/BluetoothInCallServiceTest.java
new file mode 100644
index 0000000..d3db9c6
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/hfp/BluetoothInCallServiceTest.java
@@ -0,0 +1,1186 @@
+/*
+ * 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 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 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.hfp.BluetoothCall.class);
+        String uuid = UUID.randomUUID().toString();
+        when(call.getTelecomCallId()).thenReturn(uuid);
+        return call;
+    }
+}
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);