Revert "Bluetooth updateability: Move BluetoothPhoneService out ..."

Revert "Updateability: Remove IBluetoothHeadsetPhone aidl"

Revert submission 1311861-BluetoothInCallService

Reason for revert: Bug: 176883407
Reverted Changes:
Ie3a5ceda5:UpdateAbility: Implement BluetoothInCallService an...
I2e3bc64eb:Updateability: Remove IBluetoothHeadsetPhone aidl
If26bab4ad:Bluetooth updateability: Move BluetoothPhoneServic...

Change-Id: Id9649e78d447a6c360860bcdde3069bb9b09c89f
diff --git a/src/com/android/server/telecom/BluetoothHeadsetProxy.java b/src/com/android/server/telecom/BluetoothHeadsetProxy.java
index e4eed87..a43b3cd 100644
--- a/src/com/android/server/telecom/BluetoothHeadsetProxy.java
+++ b/src/com/android/server/telecom/BluetoothHeadsetProxy.java
@@ -36,6 +36,19 @@
         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();
     }
diff --git a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
new file mode 100644
index 0000000..f2ea950
--- /dev/null
+++ b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
@@ -0,0 +1,932 @@
+/*
+ * Copyright (C) 2014 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.server.telecom;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothHeadsetPhone;
+import android.content.BroadcastReceiver;
+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.os.RemoteException;
+import android.telecom.Connection;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.VideoProfile;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.CallsManager.CallsManagerListener;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Bluetooth headset manager for Telecom. This class shares the call state with the bluetooth device
+ * and accepts call-related commands to perform on behalf of the BT device.
+ */
+public class BluetoothPhoneServiceImpl {
+
+    public interface BluetoothPhoneServiceImplFactory {
+        BluetoothPhoneServiceImpl makeBluetoothPhoneServiceImpl(Context context,
+                TelecomSystem.SyncRoot lock, CallsManager callsManager,
+                PhoneAccountRegistrar phoneAccountRegistrar);
+    }
+
+    private static final String TAG = "BluetoothPhoneService";
+
+    // 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 call 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 Call mOldHeldCall = null;
+    private boolean mIsDisconnectedTonePlaying = false;
+
+    /**
+     * Binder implementation of IBluetoothHeadsetPhone. Implements the command interface that the
+     * bluetooth headset code uses to control call.
+     */
+    @VisibleForTesting
+    public final IBluetoothHeadsetPhone.Stub mBinder = new IBluetoothHeadsetPhone.Stub() {
+        @Override
+        public boolean answerCall() throws RemoteException {
+            synchronized (mLock) {
+                enforceModifyPermission();
+                Log.startSession("BPSI.aC");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    Log.i(TAG, "BT - answering call");
+                    Call call = mCallsManager.getRingingOrSimulatedRingingCall();
+                    if (call != null) {
+                        mCallsManager.answerCall(call, VideoProfile.STATE_AUDIO_ONLY);
+                        return true;
+                    }
+                    return false;
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+
+            }
+        }
+
+        @Override
+        public boolean hangupCall() throws RemoteException {
+            synchronized (mLock) {
+                enforceModifyPermission();
+                Log.startSession("BPSI.hC");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    Log.i(TAG, "BT - hanging up call");
+                    Call call = mCallsManager.getForegroundCall();
+                    if (call != null) {
+                        mCallsManager.disconnectCall(call);
+                        return true;
+                    }
+                    return false;
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+        }
+
+        @Override
+        public boolean sendDtmf(int dtmf) throws RemoteException {
+            synchronized (mLock) {
+                enforceModifyPermission();
+                Log.startSession("BPSI.sD");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    Log.i(TAG, "BT - sendDtmf %c", Log.DEBUG ? dtmf : '.');
+                    Call call = mCallsManager.getForegroundCall();
+                    if (call != null) {
+                        // TODO: Consider making this a queue instead of starting/stopping
+                        // in quick succession.
+                        mCallsManager.playDtmfTone(call, (char) dtmf);
+                        mCallsManager.stopDtmfTone(call);
+                        return true;
+                    }
+                    return false;
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+        }
+
+        @Override
+        public String getNetworkOperator() throws RemoteException {
+            synchronized (mLock) {
+                enforceModifyPermission();
+                Log.startSession("BPSI.gNO");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    Log.i(TAG, "getNetworkOperator");
+                    PhoneAccount account = getBestPhoneAccount();
+                    if (account != null && account.getLabel() != null) {
+                        return account.getLabel().toString();
+                    } else {
+                        // Finally, just get the network name from telephony.
+                        return mContext.getSystemService(TelephonyManager.class)
+                                .getNetworkOperatorName();
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+        }
+
+        @Override
+        public String getSubscriberNumber() throws RemoteException {
+            synchronized (mLock) {
+                enforceModifyPermission();
+                Log.startSession("BPSI.gSN");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    Log.i(TAG, "getSubscriberNumber");
+                    String address = null;
+                    PhoneAccount account = getBestPhoneAccount();
+                    if (account != null) {
+                        Uri addressUri = account.getAddress();
+                        if (addressUri != null) {
+                            address = addressUri.getSchemeSpecificPart();
+                        }
+                    }
+                    if (TextUtils.isEmpty(address)) {
+                        address = mContext.getSystemService(TelephonyManager.class)
+                                .getLine1Number();
+                        if (address == null) address = "";
+                    }
+                    return address;
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+        }
+
+        @Override
+        public boolean listCurrentCalls() throws RemoteException {
+            synchronized (mLock) {
+                enforceModifyPermission();
+                Log.startSession("BPSI.lCC");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    // 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;
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+        }
+
+        @Override
+        public boolean queryPhoneState() throws RemoteException {
+            synchronized (mLock) {
+                enforceModifyPermission();
+                Log.startSession("BPSI.qPS");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    Log.i(TAG, "queryPhoneState");
+                    updateHeadsetWithCallState(true /* force */);
+                    return true;
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+        }
+
+        @Override
+        public boolean processChld(int chld) throws RemoteException {
+            synchronized (mLock) {
+                enforceModifyPermission();
+                Log.startSession("BPSI.pC");
+                long token = Binder.clearCallingIdentity();
+                try {
+                    Log.i(TAG, "processChld %d", chld);
+                    return BluetoothPhoneServiceImpl.this.processChld(chld);
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                    Log.endSession();
+                }
+            }
+        }
+
+        @Override
+        public void updateBtHandsfreeAfterRadioTechnologyChange() throws RemoteException {
+            Log.d(TAG, "RAT change - deprecated");
+            // deprecated
+        }
+
+        @Override
+        public void cdmaSetSecondCallState(boolean state) throws RemoteException {
+            Log.d(TAG, "cdma 1 - deprecated");
+            // deprecated
+        }
+
+        @Override
+        public void cdmaSwapSecondCallState() throws RemoteException {
+            Log.d(TAG, "cdma 2 - deprecated");
+            // deprecated
+        }
+    };
+
+    /**
+     * Listens to call changes from the CallsManager and calls into methods to update the bluetooth
+     * headset with the new states.
+     */
+    @VisibleForTesting
+    public CallsManagerListener mCallsManagerListener = new CallsManagerListenerBase() {
+        @Override
+        public void onCallAdded(Call call) {
+            if (call.isExternalCall()) {
+                return;
+            }
+            updateHeadsetWithCallState(false /* force */);
+        }
+
+        @Override
+        public void onCallRemoved(Call call) {
+            if (call.isExternalCall()) {
+                return;
+            }
+            mClccIndexMap.remove(call);
+            updateHeadsetWithCallState(false /* force */);
+        }
+
+        /**
+         * Where a call which was external becomes a regular call, or a regular call becomes
+         * external, treat as an add or remove, respectively.
+         *
+         * @param call The call.
+         * @param isExternalCall {@code True} if the call became external, {@code false} otherwise.
+         */
+        @Override
+        public void onExternalCallChanged(Call call, boolean isExternalCall) {
+            if (isExternalCall) {
+                onCallRemoved(call);
+            } else {
+                onCallAdded(call);
+            }
+        }
+
+        @Override
+        public void onCallStateChanged(Call call, int oldState, int newState) {
+            if (call.isExternalCall()) {
+                return;
+            }
+            // If a call 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 call later transitions to DIALING/DISCONNECTED we will then send out the
+            // aggregated update.
+            if (oldState == CallState.ACTIVE && newState == CallState.ON_HOLD) {
+                for (Call otherCall : mCallsManager.getCalls()) {
+                    if (otherCall.getState() == CallState.CONNECTING) {
+                        return;
+                    }
+                }
+            }
+
+            // To have an active call and another dialing at the same time is an invalid BT
+            // state. We can assume that the active call will be automatically held which will
+            // send another update at which point we will be in the right state.
+            if (mCallsManager.getActiveCall() != null
+                    && oldState == CallState.CONNECTING &&
+                    (newState == CallState.DIALING || newState == CallState.PULLING)) {
+                return;
+            }
+            updateHeadsetWithCallState(false /* force */);
+        }
+
+        @Override
+        public void onIsConferencedChanged(Call call) {
+            if (call.isExternalCall()) {
+                return;
+            }
+            /*
+             * Filter certain onIsConferencedChanged callbacks. Unfortunately this needs to be done
+             * because conference change events are not atomic and multiple callbacks get fired
+             * when two calls are conferenced together. This confuses updateHeadsetWithCallState
+             * if it runs in the middle of two calls being conferenced and can cause spurious and
+             * incorrect headset state updates. One of the scenarios is described below for CDMA
+             * conference calls.
+             *
+             * 1) Call 1 and Call 2 are being merged into conference Call 3.
+             * 2) Call 1 has its parent set to Call 3, but Call 2 does not have a parent yet.
+             * 3) updateHeadsetWithCallState now thinks that there are two active calls (Call 2 and
+             * Call 3) when there is actually only one active call (Call 3).
+             */
+            if (call.getParentCall() != null) {
+                // If this call is newly conferenced, ignore the callback. We only care about the
+                // one sent for the parent conference call.
+                Log.d(this, "Ignoring onIsConferenceChanged from child call with new parent");
+                return;
+            }
+            if (call.getChildCalls().size() == 1) {
+                // If this is a parent call with only one child, ignore the callback as well since
+                // the minimum number of child calls to start a conference call is 2. We expect
+                // this to be called again when the parent call has another child call added.
+                Log.d(this, "Ignoring onIsConferenceChanged from parent with only one child call");
+                return;
+            }
+            updateHeadsetWithCallState(false /* force */);
+        }
+
+        @Override
+        public void onDisconnectedTonePlaying(boolean isTonePlaying) {
+            mIsDisconnectedTonePlaying = isTonePlaying;
+            updateHeadsetWithCallState(false /* force */);
+        }
+    };
+
+    /**
+     * Listens to connections and disconnections of bluetooth headsets.  We need to save the current
+     * bluetooth headset so that we know where to send call updates.
+     */
+    @VisibleForTesting
+    public BluetoothProfile.ServiceListener mProfileListener =
+            new BluetoothProfile.ServiceListener() {
+                @Override
+                public void onServiceConnected(int profile, BluetoothProfile proxy) {
+                    synchronized (mLock) {
+                        setBluetoothHeadset(new BluetoothHeadsetProxy((BluetoothHeadset) proxy));
+                        updateHeadsetWithCallState(true /* force */);
+                    }
+                }
+
+                @Override
+                public void onServiceDisconnected(int profile) {
+                    synchronized (mLock) {
+                        mBluetoothHeadset = null;
+                    }
+                }
+            };
+
+    /**
+     * Receives events for global state changes of the bluetooth adapter.
+     */
+    @VisibleForTesting
+    public final BroadcastReceiver mBluetoothAdapterReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            synchronized (mLock) {
+                int state = intent
+                        .getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+                Log.d(TAG, "Bluetooth Adapter state: %d", state);
+                if (state == BluetoothAdapter.STATE_ON) {
+                    try {
+                        mBinder.queryPhoneState();
+                    } catch (RemoteException e) {
+                        // Remote exception not expected
+                    }
+                }
+            }
+        }
+    };
+
+    private BluetoothAdapterProxy mBluetoothAdapter;
+    private BluetoothHeadsetProxy mBluetoothHeadset;
+
+    // A map from Calls to indexes used to identify calls for CLCC (C* List Current Calls).
+    private Map<Call, Integer> mClccIndexMap = new HashMap<>();
+
+    private boolean mHeadsetUpdatedRecently = false;
+
+    private final Context mContext;
+    private final TelecomSystem.SyncRoot mLock;
+    private final CallsManager mCallsManager;
+    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+
+    public IBinder getBinder() {
+        return mBinder;
+    }
+
+    public BluetoothPhoneServiceImpl(
+            Context context,
+            TelecomSystem.SyncRoot lock,
+            CallsManager callsManager,
+            BluetoothAdapterProxy bluetoothAdapter,
+            PhoneAccountRegistrar phoneAccountRegistrar) {
+        Log.d(this, "onCreate");
+
+        mContext = context;
+        mLock = lock;
+        mCallsManager = callsManager;
+        mPhoneAccountRegistrar = phoneAccountRegistrar;
+
+        mBluetoothAdapter = bluetoothAdapter;
+        if (mBluetoothAdapter == null) {
+            Log.d(this, "BluetoothPhoneService shutting down, no BT Adapter found.");
+            return;
+        }
+        mBluetoothAdapter.getProfileProxy(context, mProfileListener, BluetoothProfile.HEADSET);
+
+        IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+        context.registerReceiver(mBluetoothAdapterReceiver, intentFilter);
+
+        mCallsManager.addListener(mCallsManagerListener);
+        updateHeadsetWithCallState(false /* force */);
+    }
+
+    @VisibleForTesting
+    public void setBluetoothHeadset(BluetoothHeadsetProxy bluetoothHeadset) {
+        mBluetoothHeadset = bluetoothHeadset;
+    }
+
+    private boolean processChld(int chld) {
+        Call activeCall = mCallsManager.getActiveCall();
+        Call ringingCall = mCallsManager.getRingingOrSimulatedRingingCall();
+        Call heldCall = mCallsManager.getHeldCall();
+
+        // TODO: Keeping as Log.i for now.  Move to Log.d after L release if BT proves stable.
+        Log.i(TAG, "Active: %s\nRinging: %s\nHeld: %s", activeCall, ringingCall, heldCall);
+
+        if (chld == CHLD_TYPE_RELEASEHELD) {
+            if (ringingCall != null) {
+                mCallsManager.rejectCall(ringingCall, false, null);
+                return true;
+            } else if (heldCall != null) {
+                mCallsManager.disconnectCall(heldCall);
+                return true;
+            }
+        } else if (chld == CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD) {
+            if (activeCall == null && ringingCall == null && heldCall == null)
+                return false;
+            if (activeCall != null) {
+                mCallsManager.disconnectCall(activeCall);
+                if (ringingCall != null) {
+                    mCallsManager.answerCall(ringingCall, VideoProfile.STATE_AUDIO_ONLY);
+                }
+                return true;
+            }
+            if (ringingCall != null) {
+                mCallsManager.answerCall(ringingCall, ringingCall.getVideoState());
+            } else if (heldCall != null) {
+                mCallsManager.unholdCall(heldCall);
+            }
+            return true;
+        } else if (chld == CHLD_TYPE_HOLDACTIVE_ACCEPTHELD) {
+            if (activeCall != null && 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 (ringingCall != null) {
+                mCallsManager.answerCall(ringingCall, VideoProfile.STATE_AUDIO_ONLY);
+                return true;
+            } else if (heldCall != null) {
+                // CallsManager will hold any active calls when unhold() is called on a
+                // currently-held call.
+                mCallsManager.unholdCall(heldCall);
+                return true;
+            } else if (activeCall != null && activeCall.can(Connection.CAPABILITY_HOLD)) {
+                mCallsManager.holdCall(activeCall);
+                return true;
+            }
+        } else if (chld == CHLD_TYPE_ADDHELDTOCONF) {
+            if (activeCall != null) {
+                if (activeCall.can(Connection.CAPABILITY_MERGE_CONFERENCE)) {
+                    activeCall.mergeConference();
+                    return true;
+                } else {
+                    List<Call> conferenceable = activeCall.getConferenceableCalls();
+                    if (!conferenceable.isEmpty()) {
+                        mCallsManager.conference(activeCall, conferenceable.get(0));
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private void enforceModifyPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE, null);
+    }
+
+    private void sendListOfCalls(boolean shouldLog) {
+        Collection<Call> mCalls = mCallsManager.getCalls();
+        for (Call call : mCalls) {
+            // We don't send the parent conference call 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();
+    }
+
+    /**
+     * Sends a single clcc (C* List Current Calls) event for the specified call.
+     */
+    private void sendClccForCall(Call call, boolean shouldLog) {
+        boolean isForeground = mCallsManager.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;
+        }
+
+        Call conferenceCall = call.getParentCall();
+        if (conferenceCall != null) {
+            isPartOfConference = true;
+
+            // Run some alternative states for Conference-level merge/swap support.
+            // Basically, if call 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 call and
+            // that the conference itself has a notion of the current "active" child call.
+            Call activeChild = conferenceCall.getConferenceLevelActiveCall();
+            if (state == CALL_STATE_ACTIVE && activeChild != null) {
+                // 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() == CallState.ON_HOLD &&
+                    conferenceCall.can(Connection.CAPABILITY_MANAGE_CONFERENCE)) {
+                // If the parent IMS CEP conference call is on hold, we should mark this call 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 call without conference event package
+            // support.  The call 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(this, "sending clcc for call %d, %d, %d, %b, %s, %d",
+                    index, direction, state, isPartOfConference, Log.piiHandle(address),
+                    addressType);
+        }
+
+        if (mBluetoothHeadset != null) {
+            mBluetoothHeadset.clccResponse(
+                    index, direction, state, 0, isPartOfConference, address, addressType);
+        }
+    }
+
+    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);
+        }
+    }
+
+    /**
+     * 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(Call 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;
+    }
+
+    /**
+     * Sends an update of the current call 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) {
+        Call activeCall = mCallsManager.getActiveCall();
+        Call ringingCall = mCallsManager.getRingingOrSimulatedRingingCall();
+        Call heldCall = mCallsManager.getHeldCall();
+
+        int bluetoothCallState = getBluetoothCallStateForUpdate();
+
+        String ringingAddress = null;
+        int ringingAddressType = DEFAULT_RINGING_ADDRESS_TYPE;
+        String ringingName = null;
+        if (ringingCall != null && 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.getName();
+            }
+        }
+        if (ringingAddress == null) {
+            ringingAddress = "";
+        }
+
+        int numActiveCalls = activeCall == null ? 0 : 1;
+        int numHeldCalls = mCallsManager.getNumHeldCalls();
+        int numChildrenOfActiveCall = activeCall == null ? 0 : activeCall.getChildCalls().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 call within the conference
+        // (namely CDMA calls) we need to expose that as a held call in order for the BT device
+        // to show "swap" and "merge" functionality.
+        boolean ignoreHeldCallChange = false;
+        if (activeCall != null && 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
+                // call 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 (Call childCall : activeCall.getChildCalls()) {
+                // Held call 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 call change.
+                if (mOldHeldCall == childCall) {
+                    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 call 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 %s, " +
+                        "numHeld %s, " +
+                        "callState %s, " +
+                        "ringing number %s, " +
+                        "ringing type %s, " +
+                        "ringing name %s",
+                        mNumActiveCalls,
+                        mNumHeldCalls,
+                        CALL_STATE_DIALING,
+                        Log.pii(mRingingAddress),
+                        mRingingAddressType,
+                        Log.pii(ringingName));
+                mBluetoothHeadset.phoneStateChanged(
+                        mNumActiveCalls,
+                        mNumHeldCalls,
+                        CALL_STATE_DIALING,
+                        mRingingAddress,
+                        mRingingAddressType,
+                        ringingName);
+            }
+
+            Log.i(TAG, "updateHeadsetWithCallState " +
+                    "numActive %s, " +
+                    "numHeld %s, " +
+                    "callState %s, " +
+                    "ringing number %s, " +
+                    "ringing type %s, " +
+                    "ringing name %s",
+                    mNumActiveCalls,
+                    mNumHeldCalls,
+                    mBluetoothCallState,
+                    Log.pii(mRingingAddress),
+                    mRingingAddressType,
+                    Log.pii(ringingName));
+
+            mBluetoothHeadset.phoneStateChanged(
+                    mNumActiveCalls,
+                    mNumHeldCalls,
+                    mBluetoothCallState,
+                    mRingingAddress,
+                    mRingingAddressType,
+                    ringingName);
+
+            mHeadsetUpdatedRecently = true;
+        }
+    }
+
+    private int getBluetoothCallStateForUpdate() {
+        Call ringingCall = mCallsManager.getRingingOrSimulatedRingingCall();
+        Call dialingCall = mCallsManager.getOutgoingCall();
+        boolean hasOnlyDisconnectedCalls = mCallsManager.hasOnlyDisconnectedCalls();
+
+        //
+        // !! WARNING !!
+        // You will note that CALL_STATE_WAITING, CALL_STATE_HELD, and CALL_STATE_ACTIVE are not
+        // used in this version of the call 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 (ringingCall != null && !ringingCall.isSilentRingingRequested()) {
+            bluetoothCallState = CALL_STATE_INCOMING;
+        } else if (dialingCall != null) {
+            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(Call call, boolean isForeground) {
+        switch (call.getState()) {
+            case CallState.NEW:
+            case CallState.ABORTED:
+            case CallState.DISCONNECTED:
+            case CallState.AUDIO_PROCESSING:
+                return CALL_STATE_IDLE;
+
+            case CallState.ACTIVE:
+                return CALL_STATE_ACTIVE;
+
+            case CallState.CONNECTING:
+            case CallState.SELECT_PHONE_ACCOUNT:
+            case CallState.DIALING:
+            case CallState.PULLING:
+                // Yes, this is correctly returning ALERTING.
+                // "Dialing" for BT means that we have sent information to the service provider
+                // to place the call but there is no confirmation that the call 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 CallState.ON_HOLD:
+                return CALL_STATE_HELD;
+
+            case CallState.RINGING:
+            case CallState.ANSWERED:
+            case CallState.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;
+    }
+
+    /**
+     * 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.
+     */
+    private PhoneAccount getBestPhoneAccount() {
+        if (mPhoneAccountRegistrar == null) {
+            return null;
+        }
+
+        Call call = mCallsManager.getForegroundCall();
+
+        PhoneAccount account = null;
+        if (call != null) {
+            // First try to get the network name of the foreground call.
+            account = mPhoneAccountRegistrar.getPhoneAccountOfCurrentUser(
+                    call.getTargetPhoneAccount());
+        }
+
+        if (account == null) {
+            // Second, Try to get the label for the default Phone Account.
+            account = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+                    mPhoneAccountRegistrar.getOutgoingPhoneAccountForSchemeOfCurrentUser(
+                            PhoneAccount.SCHEME_TEL));
+        }
+        return account;
+    }
+}
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 8928e76..4842796 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -26,6 +26,7 @@
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
+import com.android.server.telecom.BluetoothPhoneServiceImpl.BluetoothPhoneServiceImplFactory;
 import com.android.server.telecom.CallAudioManager.AudioServiceFactory;
 import com.android.server.telecom.DefaultDialerCache.DefaultDialerManagerAdapter;
 import com.android.server.telecom.ui.ToastFactory;
@@ -114,6 +115,7 @@
     private final CallsManager mCallsManager;
     private final RespondViaSmsManager mRespondViaSmsManager;
     private final Context mContext;
+    private final BluetoothPhoneServiceImpl mBluetoothPhoneServiceImpl;
     private final CallIntentProcessor mCallIntentProcessor;
     private final TelecomBroadcastIntentProcessor mTelecomBroadcastIntentProcessor;
     private final TelecomServiceImpl mTelecomServiceImpl;
@@ -190,6 +192,8 @@
             ProximitySensorManagerFactory proximitySensorManagerFactory,
             InCallWakeLockControllerFactory inCallWakeLockControllerFactory,
             AudioServiceFactory audioServiceFactory,
+            BluetoothPhoneServiceImplFactory
+                    bluetoothPhoneServiceImplFactory,
             ConnectionServiceFocusManager.ConnectionServiceFocusManagerFactory
                     connectionServiceFocusManagerFactory,
             Timeouts.Adapter timeoutsAdapter,
@@ -346,6 +350,8 @@
             mCallsManager.onUserSwitch(currentUserHandle);
         }
 
+        mBluetoothPhoneServiceImpl = bluetoothPhoneServiceImplFactory.makeBluetoothPhoneServiceImpl(
+                mContext, mLock, mCallsManager, mPhoneAccountRegistrar);
         mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager, defaultDialerCache);
         mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor(
                 mContext, mCallsManager);
@@ -383,6 +389,10 @@
         return mCallsManager;
     }
 
+    public BluetoothPhoneServiceImpl getBluetoothPhoneServiceImpl() {
+        return mBluetoothPhoneServiceImpl;
+    }
+
     public CallIntentProcessor getCallIntentProcessor() {
         return mCallIntentProcessor;
     }
diff --git a/src/com/android/server/telecom/components/BluetoothPhoneService.java b/src/com/android/server/telecom/components/BluetoothPhoneService.java
new file mode 100644
index 0000000..c5e195c
--- /dev/null
+++ b/src/com/android/server/telecom/components/BluetoothPhoneService.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 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.server.telecom.components;
+
+import com.android.server.telecom.TelecomSystem;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Bluetooth headset manager for Telecom. This class shares the call state with the bluetooth device
+ * and accepts call-related commands to perform on behalf of the BT device.
+ */
+public final class BluetoothPhoneService extends Service implements TelecomSystem.Component {
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        synchronized (getTelecomSystem().getLock()) {
+            return getTelecomSystem().getBluetoothPhoneServiceImpl().getBinder();
+        }
+    }
+
+    @Override
+    public TelecomSystem getTelecomSystem() {
+        return TelecomSystem.getInstance();
+    }
+}
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 9ad0da4..271264d 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -18,6 +18,7 @@
 
 import android.app.Service;
 import android.app.role.RoleManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.content.Intent;
 import android.media.IAudioService;
@@ -34,6 +35,8 @@
 import com.android.internal.telecom.ITelecomLoader;
 import com.android.internal.telecom.ITelecomService;
 import com.android.server.telecom.AsyncRingtonePlayer;
+import com.android.server.telecom.BluetoothAdapterProxy;
+import com.android.server.telecom.BluetoothPhoneServiceImpl;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.CallerInfoAsyncQueryFactory;
@@ -169,6 +172,17 @@
                                             ServiceManager.getService(Context.AUDIO_SERVICE));
                                 }
                             },
+                            new BluetoothPhoneServiceImpl.BluetoothPhoneServiceImplFactory() {
+                                @Override
+                                public BluetoothPhoneServiceImpl makeBluetoothPhoneServiceImpl(
+                                        Context context, TelecomSystem.SyncRoot lock,
+                                        CallsManager callsManager,
+                                        PhoneAccountRegistrar phoneAccountRegistrar) {
+                                    return new BluetoothPhoneServiceImpl(context, lock,
+                                            callsManager, new BluetoothAdapterProxy(),
+                                            phoneAccountRegistrar);
+                                }
+                            },
                             ConnectionServiceFocusManager::new,
                             new Timeouts.Adapter(),
                             new AsyncRingtonePlayer(),
@@ -193,6 +207,9 @@
                             new ContactsAsyncHelper.Factory(),
                             internalServiceRetriever.getDeviceIdleController()));
         }
+        if (BluetoothAdapter.getDefaultAdapter() != null) {
+            context.startService(new Intent(context, BluetoothPhoneService.class));
+        }
     }
 
     @Override
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java b/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
new file mode 100644
index 0000000..532cc7e
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
@@ -0,0 +1,1136 @@
+/*
+ * Copyright (C) 2015 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.server.telecom.tests;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Binder;
+import android.telecom.Connection;
+import android.telecom.GatewayInfo;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.BluetoothAdapterProxy;
+import com.android.server.telecom.BluetoothHeadsetProxy;
+import com.android.server.telecom.BluetoothPhoneServiceImpl;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.PhoneAccountRegistrar;
+import com.android.server.telecom.TelecomSystem;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyChar;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+
+@RunWith(JUnit4.class)
+public class BluetoothPhoneServiceTest extends TelecomTestCase {
+
+    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;
+
+    // match up with BluetoothPhoneServiceImpl
+    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 BluetoothPhoneServiceImpl mBluetoothPhoneService;
+    private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {
+    };
+
+    @Mock CallsManager mMockCallsManager;
+    @Mock PhoneAccountRegistrar mMockPhoneAccountRegistrar;
+    @Mock BluetoothHeadsetProxy mMockBluetoothHeadset;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+
+        // 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.
+        doNothing().when(mMockCallsManager).addListener(any(
+                CallsManager.CallsManagerListener.class));
+        doReturn(null).when(mMockCallsManager).getActiveCall();
+        doReturn(null).when(mMockCallsManager).getRingingOrSimulatedRingingCall();
+        doReturn(null).when(mMockCallsManager).getHeldCall();
+        doReturn(null).when(mMockCallsManager).getOutgoingCall();
+        doReturn(0).when(mMockCallsManager).getNumHeldCalls();
+        doReturn(false).when(mMockCallsManager).hasOnlyDisconnectedCalls();
+        mBluetoothPhoneService = new BluetoothPhoneServiceImpl(mContext, mLock, mMockCallsManager,
+                mock(BluetoothAdapterProxy.class), mMockPhoneAccountRegistrar);
+
+        // Bring in test Bluetooth Headset
+        mBluetoothPhoneService.setBluetoothHeadset(mMockBluetoothHeadset);
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+
+        mBluetoothPhoneService = null;
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void testHeadsetAnswerCall() throws Exception {
+        Call mockCall = createRingingCall();
+
+        boolean callAnswered = mBluetoothPhoneService.mBinder.answerCall();
+
+        verify(mMockCallsManager).answerCall(eq(mockCall), any(int.class));
+        assertEquals(callAnswered, true);
+    }
+
+    @SmallTest
+    @Test
+    public void testHeadsetAnswerCallNull() throws Exception {
+        when(mMockCallsManager.getRingingOrSimulatedRingingCall()).thenReturn(null);
+
+        boolean callAnswered = mBluetoothPhoneService.mBinder.answerCall();
+
+        verify(mMockCallsManager,never()).answerCall(any(Call.class), any(int.class));
+        assertEquals(callAnswered, false);
+    }
+
+    @SmallTest
+    @Test
+    public void testHeadsetHangupCall() throws Exception {
+        Call mockCall = createForegroundCall();
+
+        boolean callHungup = mBluetoothPhoneService.mBinder.hangupCall();
+
+        verify(mMockCallsManager).disconnectCall(eq(mockCall));
+        assertEquals(callHungup, true);
+    }
+
+    @SmallTest
+    @Test
+    public void testHeadsetHangupCallNull() throws Exception {
+        when(mMockCallsManager.getForegroundCall()).thenReturn(null);
+
+        boolean callHungup = mBluetoothPhoneService.mBinder.hangupCall();
+
+        verify(mMockCallsManager,never()).disconnectCall(any(Call.class));
+        assertEquals(callHungup, false);
+    }
+
+    @SmallTest
+    @Test
+    public void testHeadsetSendDTMF() throws Exception {
+        Call mockCall = createForegroundCall();
+
+        boolean sentDtmf = mBluetoothPhoneService.mBinder.sendDtmf(TEST_DTMF_TONE);
+
+        verify(mMockCallsManager).playDtmfTone(eq(mockCall), eq((char) TEST_DTMF_TONE));
+        verify(mMockCallsManager).stopDtmfTone(eq(mockCall));
+        assertEquals(sentDtmf, true);
+    }
+
+    @SmallTest
+    @Test
+    public void testHeadsetSendDTMFNull() throws Exception {
+        when(mMockCallsManager.getForegroundCall()).thenReturn(null);
+
+        boolean sentDtmf = mBluetoothPhoneService.mBinder.sendDtmf(TEST_DTMF_TONE);
+
+        verify(mMockCallsManager,never()).playDtmfTone(any(Call.class), anyChar());
+        verify(mMockCallsManager,never()).stopDtmfTone(any(Call.class));
+        assertEquals(sentDtmf, false);
+    }
+
+    @SmallTest
+    @Test
+    public void testGetNetworkOperator() throws Exception {
+        Call mockCall = createForegroundCall();
+        PhoneAccount fakePhoneAccount = makeQuickAccount("id0", TEST_ACCOUNT_INDEX);
+        when(mMockPhoneAccountRegistrar.getPhoneAccountOfCurrentUser(
+                nullable(PhoneAccountHandle.class))).thenReturn(fakePhoneAccount);
+
+        String networkOperator = mBluetoothPhoneService.mBinder.getNetworkOperator();
+
+        assertEquals(networkOperator, "label0");
+    }
+
+    @SmallTest
+    @Test
+    public void testGetNetworkOperatorNoPhoneAccount() throws Exception {
+        when(mMockCallsManager.getForegroundCall()).thenReturn(null);
+
+        String networkOperator = mBluetoothPhoneService.mBinder.getNetworkOperator();
+
+        assertEquals(networkOperator, "label1");
+    }
+
+    @SmallTest
+    @Test
+    public void testGetSubscriberNumber() throws Exception {
+        Call mockCall = createForegroundCall();
+        PhoneAccount fakePhoneAccount = makeQuickAccount("id0", TEST_ACCOUNT_INDEX);
+        when(mMockPhoneAccountRegistrar.getPhoneAccountOfCurrentUser(
+                nullable(PhoneAccountHandle.class))).thenReturn(fakePhoneAccount);
+
+        String subscriberNumber = mBluetoothPhoneService.mBinder.getSubscriberNumber();
+
+        assertEquals(subscriberNumber, TEST_ACCOUNT_ADDRESS + TEST_ACCOUNT_INDEX);
+    }
+
+    @SmallTest
+    @Test
+    public void testGetSubscriberNumberFallbackToTelephony() throws Exception {
+        Call mockCall = createForegroundCall();
+        String fakeNumber = "8675309";
+        when(mMockPhoneAccountRegistrar.getPhoneAccountOfCurrentUser(
+                nullable(PhoneAccountHandle.class))).thenReturn(null);
+        when(mMockPhoneAccountRegistrar.getPhoneAccountUnchecked(
+                nullable(PhoneAccountHandle.class))).thenReturn(null);
+        when(mComponentContextFixture.getTelephonyManager().getLine1Number())
+                .thenReturn(fakeNumber);
+
+        String subscriberNumber = mBluetoothPhoneService.mBinder.getSubscriberNumber();
+
+        assertEquals(subscriberNumber, fakeNumber);
+    }
+
+    @MediumTest
+    @Test
+    public void testListCurrentCallsOneCall() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        Call activeCall = createActiveCall();
+        when(activeCall.getState()).thenReturn(CallState.ACTIVE);
+        calls.add(activeCall);
+        when(activeCall.isConference()).thenReturn(false);
+        when(activeCall.getHandle()).thenReturn(Uri.parse("tel:555-000"));
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+
+        mBluetoothPhoneService.mBinder.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);
+    }
+
+    @MediumTest
+    @Test
+    public void testListCurrentCallsSilentRinging() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        Call silentRingingCall = createActiveCall();
+        when(silentRingingCall.getState()).thenReturn(CallState.RINGING);
+        when(silentRingingCall.isSilentRingingRequested()).thenReturn(true);
+        calls.add(silentRingingCall);
+        when(silentRingingCall.isConference()).thenReturn(false);
+        when(silentRingingCall.getHandle()).thenReturn(Uri.parse("tel:555-000"));
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        when(mMockCallsManager.getRingingOrSimulatedRingingCall()).thenReturn(silentRingingCall);
+
+        mBluetoothPhoneService.mBinder.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);
+    }
+
+    @MediumTest
+    @Test
+    public void testConferenceInProgressCDMA() throws Exception {
+        // If two calls are being conferenced and updateHeadsetWithCallState runs while this is
+        // still occuring, it will look like there is an active and held call still while we are
+        // transitioning into a conference.
+        // Call has been put into a CDMA "conference" with one call on hold.
+        ArrayList<Call> calls = new ArrayList<>();
+        Call parentCall = createActiveCall();
+        final Call confCall1 = mock(Call.class);
+        final Call confCall2 = createHeldCall();
+        calls.add(parentCall);
+        calls.add(confCall1);
+        calls.add(confCall2);
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        when(confCall1.getState()).thenReturn(CallState.ACTIVE);
+        when(confCall2.getState()).thenReturn(CallState.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);
+        when(parentCall.getConferenceLevelActiveCall()).thenReturn(confCall1);
+        when(parentCall.isConference()).thenReturn(true);
+        when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+            add(confCall1);
+            add(confCall2);
+        }});
+        //Add links from child calls to parent
+        when(confCall1.getParentCall()).thenReturn(parentCall);
+        when(confCall2.getParentCall()).thenReturn(parentCall);
+
+        mBluetoothPhoneService.mBinder.queryPhoneState();
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+        when(parentCall.wasConferencePreviouslyMerged()).thenReturn(true);
+        mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(parentCall);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+        when(mMockCallsManager.getHeldCall()).thenReturn(null);
+        // Spurious call to onIsConferencedChanged.
+        mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(parentCall);
+        // Make sure the call 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));
+    }
+
+    @MediumTest
+    @Test
+    public void testListCurrentCallsCdmaHold() throws Exception {
+        // Call has been put into a CDMA "conference" with one call on hold.
+        ArrayList<Call> calls = new ArrayList<>();
+        Call parentCall = createActiveCall();
+        final Call foregroundCall = mock(Call.class);
+        final Call heldCall = createHeldCall();
+        calls.add(parentCall);
+        calls.add(foregroundCall);
+        calls.add(heldCall);
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        when(foregroundCall.getState()).thenReturn(CallState.ACTIVE);
+        when(heldCall.getState()).thenReturn(CallState.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);
+        when(parentCall.getConferenceLevelActiveCall()).thenReturn(foregroundCall);
+        when(parentCall.isConference()).thenReturn(true);
+        when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+            add(foregroundCall);
+            add(heldCall);
+        }});
+        //Add links from child calls to parent
+        when(foregroundCall.getParentCall()).thenReturn(parentCall);
+        when(heldCall.getParentCall()).thenReturn(parentCall);
+
+        mBluetoothPhoneService.mBinder.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);
+    }
+
+    @MediumTest
+    @Test
+    public void testListCurrentCallsCdmaConference() throws Exception {
+        // Call is in a true CDMA conference
+        ArrayList<Call> calls = new ArrayList<>();
+        Call parentCall = createActiveCall();
+        final Call confCall1 = mock(Call.class);
+        final Call confCall2 = createHeldCall();
+        calls.add(parentCall);
+        calls.add(confCall1);
+        calls.add(confCall2);
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        when(confCall1.getState()).thenReturn(CallState.ACTIVE);
+        when(confCall2.getState()).thenReturn(CallState.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);
+        when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+            add(confCall1);
+            add(confCall2);
+        }});
+        //Add links from child calls to parent
+        when(confCall1.getParentCall()).thenReturn(parentCall);
+        when(confCall2.getParentCall()).thenReturn(parentCall);
+
+        mBluetoothPhoneService.mBinder.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);
+    }
+
+    @MediumTest
+    @Test
+    public void testWaitingCallClccResponse() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        // This test does not define a value for getForegroundCall(), so this ringing call will
+        // be treated as if it is a waiting call when listCurrentCalls() is invoked.
+        Call waitingCall = createRingingCall();
+        calls.add(waitingCall);
+        when(waitingCall.isIncoming()).thenReturn(true);
+        when(waitingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+                Uri.parse("tel:555-0000")));
+        when(waitingCall.getState()).thenReturn(CallState.RINGING);
+        when(waitingCall.isConference()).thenReturn(false);
+
+        mBluetoothPhoneService.mBinder.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());
+    }
+
+    @MediumTest
+    @Test
+    public void testNewCallClccResponse() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        Call newCall = createForegroundCall();
+        calls.add(newCall);
+        when(newCall.getState()).thenReturn(CallState.NEW);
+        when(newCall.isConference()).thenReturn(false);
+
+        mBluetoothPhoneService.mBinder.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());
+    }
+
+    @MediumTest
+    @Test
+    public void testRingingCallClccResponse() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        Call ringingCall = createForegroundCall();
+        calls.add(ringingCall);
+        when(ringingCall.getState()).thenReturn(CallState.RINGING);
+        when(ringingCall.isIncoming()).thenReturn(true);
+        when(ringingCall.isConference()).thenReturn(false);
+        when(ringingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+                Uri.parse("tel:555-0000")));
+
+        mBluetoothPhoneService.mBinder.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());
+    }
+
+    @MediumTest
+    @Test
+    public void testCallClccCache() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        Call ringingCall = createForegroundCall();
+        calls.add(ringingCall);
+        when(ringingCall.getState()).thenReturn(CallState.RINGING);
+        when(ringingCall.isIncoming()).thenReturn(true);
+        when(ringingCall.isConference()).thenReturn(false);
+        when(ringingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+                Uri.parse("tel:5550000")));
+
+        mBluetoothPhoneService.mBinder.listCurrentCalls();
+        verify(mMockBluetoothHeadset).clccResponse(1, 1, CALL_STATE_INCOMING, 0, false,
+                "5550000", PhoneNumberUtils.TOA_Unknown);
+
+        // Test Caching of old call indicies in clcc
+        when(ringingCall.getState()).thenReturn(CallState.ACTIVE);
+        Call newHoldingCall = createHeldCall();
+        calls.add(0, newHoldingCall);
+        when(newHoldingCall.getState()).thenReturn(CallState.ON_HOLD);
+        when(newHoldingCall.isIncoming()).thenReturn(true);
+        when(newHoldingCall.isConference()).thenReturn(false);
+        when(newHoldingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+                Uri.parse("tel:555-0001")));
+
+        mBluetoothPhoneService.mBinder.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);
+    }
+
+    @MediumTest
+    @Test
+    public void testAlertingCallClccResponse() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        Call dialingCall = createForegroundCall();
+        calls.add(dialingCall);
+        when(dialingCall.getState()).thenReturn(CallState.DIALING);
+        when(dialingCall.isIncoming()).thenReturn(false);
+        when(dialingCall.isConference()).thenReturn(false);
+        when(dialingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+                Uri.parse("tel:555-0000")));
+
+        mBluetoothPhoneService.mBinder.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());
+    }
+
+    @MediumTest
+    @Test
+    public void testHoldingCallClccResponse() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+        Call dialingCall = createForegroundCall();
+        calls.add(dialingCall);
+        when(dialingCall.getState()).thenReturn(CallState.DIALING);
+        when(dialingCall.isIncoming()).thenReturn(false);
+        when(dialingCall.isConference()).thenReturn(false);
+        when(dialingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+                Uri.parse("tel:555-0000")));
+        Call holdingCall = createHeldCall();
+        calls.add(holdingCall);
+        when(holdingCall.getState()).thenReturn(CallState.ON_HOLD);
+        when(holdingCall.isIncoming()).thenReturn(true);
+        when(holdingCall.isConference()).thenReturn(false);
+        when(holdingCall.getGatewayInfo()).thenReturn(new GatewayInfo(null, null,
+                Uri.parse("tel:555-0001")));
+
+        mBluetoothPhoneService.mBinder.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());
+    }
+
+    @MediumTest
+    @Test
+    public void testListCurrentCallsImsConference() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        Call parentCall = createActiveCall();
+        calls.add(parentCall);
+        addCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+        when(parentCall.isConference()).thenReturn(true);
+        when(parentCall.getState()).thenReturn(CallState.ACTIVE);
+        when(parentCall.isIncoming()).thenReturn(true);
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+
+        mBluetoothPhoneService.mBinder.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);
+    }
+
+    @MediumTest
+    @Test
+    public void testListCurrentCallsHeldImsCepConference() throws Exception {
+        ArrayList<Call> calls = new ArrayList<>();
+        Call parentCall = createHeldCall();
+        Call childCall1 = createActiveCall();
+        Call childCall2 = createActiveCall();
+        calls.add(parentCall);
+        calls.add(childCall1);
+        calls.add(childCall2);
+        addCallCapability(parentCall, Connection.CAPABILITY_MANAGE_CONFERENCE);
+        when(childCall1.getParentCall()).thenReturn(parentCall);
+        when(childCall2.getParentCall()).thenReturn(parentCall);
+
+        when(parentCall.isConference()).thenReturn(true);
+        when(parentCall.getState()).thenReturn(CallState.ON_HOLD);
+        when(childCall1.getState()).thenReturn(CallState.ACTIVE);
+        when(childCall2.getState()).thenReturn(CallState.ACTIVE);
+
+        when(parentCall.isIncoming()).thenReturn(true);
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+
+        mBluetoothPhoneService.mBinder.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);
+    }
+
+    @MediumTest
+    @Test
+    public void testQueryPhoneState() throws Exception {
+        Call ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:5550000"));
+
+        mBluetoothPhoneService.mBinder.queryPhoneState();
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("5550000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testCDMAConferenceQueryState() throws Exception {
+        Call parentConfCall = createActiveCall();
+        final Call confCall1 = mock(Call.class);
+        final Call confCall2 = mock(Call.class);
+        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);
+        when(parentConfCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+            add(confCall1);
+            add(confCall2);
+        }});
+
+        mBluetoothPhoneService.mBinder.queryPhoneState();
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldTypeReleaseHeldRinging() throws Exception {
+        Call ringingCall = createRingingCall();
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.processChld(CHLD_TYPE_RELEASEHELD);
+
+        verify(mMockCallsManager).rejectCall(eq(ringingCall), eq(false), nullable(String.class));
+        assertEquals(didProcess, true);
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldTypeReleaseHeldHold() throws Exception {
+        Call onHoldCall = createHeldCall();
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.processChld(CHLD_TYPE_RELEASEHELD);
+
+        verify(mMockCallsManager).disconnectCall(eq(onHoldCall));
+        assertEquals(didProcess, true);
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldReleaseActiveRinging() throws Exception {
+        Call activeCall = createActiveCall();
+        Call ringingCall = createRingingCall();
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+                CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD);
+
+        verify(mMockCallsManager).disconnectCall(eq(activeCall));
+        verify(mMockCallsManager).answerCall(eq(ringingCall), any(int.class));
+        assertEquals(didProcess, true);
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldReleaseActiveHold() throws Exception {
+        Call activeCall = createActiveCall();
+        Call heldCall = createHeldCall();
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+                CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD);
+
+        verify(mMockCallsManager).disconnectCall(eq(activeCall));
+        // Call unhold will occur as part of CallsManager auto-unholding the background call on its
+        // own.
+        assertEquals(didProcess, true);
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldHoldActiveRinging() throws Exception {
+        Call ringingCall = createRingingCall();
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+                CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+        verify(mMockCallsManager).answerCall(eq(ringingCall), any(int.class));
+        assertEquals(didProcess, true);
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldHoldActiveUnhold() throws Exception {
+        Call heldCall = createHeldCall();
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+                CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+        verify(mMockCallsManager).unholdCall(eq(heldCall));
+        assertEquals(didProcess, true);
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldHoldActiveHold() throws Exception {
+        Call activeCall = createActiveCall();
+        addCallCapability(activeCall, Connection.CAPABILITY_HOLD);
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.processChld(
+                CHLD_TYPE_HOLDACTIVE_ACCEPTHELD);
+
+        verify(mMockCallsManager).holdCall(eq(activeCall));
+        assertEquals(didProcess, true);
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldAddHeldToConfHolding() throws Exception {
+        Call activeCall = createActiveCall();
+        addCallCapability(activeCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.processChld(CHLD_TYPE_ADDHELDTOCONF);
+
+        verify(activeCall).mergeConference();
+        assertEquals(didProcess, true);
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldAddHeldToConf() throws Exception {
+        Call activeCall = createActiveCall();
+        removeCallCapability(activeCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+        Call conferenceableCall = mock(Call.class);
+        ArrayList<Call> conferenceableCalls = new ArrayList<>();
+        conferenceableCalls.add(conferenceableCall);
+        when(activeCall.getConferenceableCalls()).thenReturn(conferenceableCalls);
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.processChld(CHLD_TYPE_ADDHELDTOCONF);
+
+        verify(mMockCallsManager).conference(activeCall, conferenceableCall);
+        assertEquals(didProcess, true);
+    }
+
+    @MediumTest
+    @Test
+    public void testProcessChldHoldActiveSwapConference() throws Exception {
+        // Create an active CDMA Call with a call on hold and simulate a swapConference().
+        Call parentCall = createActiveCall();
+        final Call foregroundCall = mock(Call.class);
+        final Call 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);
+        when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+            add(foregroundCall);
+            add(heldCall);
+        }});
+
+        boolean didProcess = mBluetoothPhoneService.mBinder.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));
+        assertEquals(didProcess, true);
+    }
+
+    // Testing the CallsManager Listener Functionality on Bluetooth
+    @MediumTest
+    @Test
+    public void testOnCallAddedRinging() throws Exception {
+        Call ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555000"));
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallAdded(ringingCall);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("555000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testSilentRingingCallState() throws Exception {
+        Call ringingCall = createRingingCall();
+        when(ringingCall.isSilentRingingRequested()).thenReturn(true);
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555000"));
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallAdded(ringingCall);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallAddedCdmaActiveHold() throws Exception {
+        // Call has been put into a CDMA "conference" with one call on hold.
+        Call parentCall = createActiveCall();
+        final Call foregroundCall = mock(Call.class);
+        final Call heldCall = createHeldCall();
+        addCallCapability(parentCall, Connection.CAPABILITY_MERGE_CONFERENCE);
+        removeCallCapability(parentCall, Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN);
+        when(parentCall.isConference()).thenReturn(true);
+        when(parentCall.getChildCalls()).thenReturn(new LinkedList<Call>() {{
+            add(foregroundCall);
+            add(heldCall);
+        }});
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallAdded(parentCall);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallRemoved() throws Exception {
+        Call activeCall = createActiveCall();
+        mBluetoothPhoneService.mCallsManagerListener.onCallAdded(activeCall);
+        doReturn(null).when(mMockCallsManager).getActiveCall();
+        mBluetoothPhoneService.mCallsManagerListener.onCallRemoved(activeCall);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallStateChangedConnectingCall() throws Exception {
+        Call activeCall = mock(Call.class);
+        Call connectingCall = mock(Call.class);
+        when(connectingCall.getState()).thenReturn(CallState.CONNECTING);
+        ArrayList<Call> calls = new ArrayList<>();
+        calls.add(connectingCall);
+        calls.add(activeCall);
+        when(mMockCallsManager.getCalls()).thenReturn(calls);
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(activeCall,
+                CallState.ACTIVE, CallState.ON_HOLD);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallAddedAudioProcessing() throws Exception {
+        Call call = mock(Call.class);
+        when(call.getState()).thenReturn(CallState.AUDIO_PROCESSING);
+        mBluetoothPhoneService.mCallsManagerListener.onCallAdded(call);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallStateChangedRingingToAudioProcessing() throws Exception {
+        Call ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555000"));
+
+        mBluetoothPhoneService.mCallsManagerListener.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(CallState.AUDIO_PROCESSING);
+        when(mMockCallsManager.getRingingOrSimulatedRingingCall()).thenReturn(null);
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(ringingCall,
+                CallState.RINGING, CallState.AUDIO_PROCESSING);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallStateChangedAudioProcessingToSimulatedRinging() throws Exception {
+        Call ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(ringingCall,
+                CallState.AUDIO_PROCESSING, CallState.SIMULATED_RINGING);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("555-0000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallStateChangedAudioProcessingToActive() throws Exception {
+        Call activeCall = createActiveCall();
+        when(activeCall.getState()).thenReturn(CallState.ACTIVE);
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(activeCall,
+                CallState.AUDIO_PROCESSING, CallState.ACTIVE);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallStateChangedDialing() throws Exception {
+        Call activeCall = createActiveCall();
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(activeCall,
+                CallState.CONNECTING, CallState.DIALING);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallStateChangedAlerting() throws Exception {
+        Call outgoingCall = createOutgoingCall();
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(outgoingCall,
+                CallState.NEW, CallState.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));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallStateChangedDisconnected() throws Exception {
+        Call disconnectedCall = createDisconnectedCall();
+        doReturn(true).when(mMockCallsManager).hasOnlyDisconnectedCalls();
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(disconnectedCall,
+                CallState.DISCONNECTING, CallState.DISCONNECTED);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_DISCONNECTED),
+                eq(""), eq(128), nullable(String.class));
+
+        doReturn(false).when(mMockCallsManager).hasOnlyDisconnectedCalls();
+        mBluetoothPhoneService.mCallsManagerListener.onDisconnectedTonePlaying(true);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_DISCONNECTED),
+                eq(""), eq(128), nullable(String.class));
+
+        mBluetoothPhoneService.mCallsManagerListener.onDisconnectedTonePlaying(false);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallStateChanged() throws Exception {
+        Call ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+        mBluetoothPhoneService.mCallsManagerListener.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(mMockCallsManager).getRingingOrSimulatedRingingCall();
+        when(mMockCallsManager.getActiveCall()).thenReturn(ringingCall);
+
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(ringingCall,
+                CallState.RINGING, CallState.ACTIVE);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnCallStateChangedGSMSwap() throws Exception {
+        Call heldCall = createHeldCall();
+        when(heldCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
+        doReturn(2).when(mMockCallsManager).getNumHeldCalls();
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(heldCall,
+                CallState.ACTIVE, CallState.ON_HOLD);
+
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(eq(0), eq(2), eq(CALL_STATE_HELD),
+                eq("5550000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testOnIsConferencedChanged() 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.
+        Call parentCall = createActiveCall();
+        Call activeCall = mock(Call.class);
+        Call heldCall = createHeldCall();
+        when(activeCall.getParentCall()).thenReturn(parentCall);
+        when(heldCall.getParentCall()).thenReturn(parentCall);
+        ArrayList<Call> calls = new ArrayList<>();
+        calls.add(activeCall);
+        when(parentCall.getChildCalls()).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);
+
+        // Be sure that onIsConferencedChanged rejects spurious changes during set up of
+        // CDMA "conference"
+        mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(activeCall);
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+        mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(heldCall);
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+        mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(parentCall);
+        verify(mMockBluetoothHeadset, never()).phoneStateChanged(anyInt(), anyInt(), anyInt(),
+                anyString(), anyInt(), nullable(String.class));
+
+        calls.add(heldCall);
+        mBluetoothPhoneService.mCallsManagerListener.onIsConferencedChanged(parentCall);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(1), eq(1), eq(CALL_STATE_IDLE),
+                eq(""), eq(128), nullable(String.class));
+    }
+
+    @MediumTest
+    @Test
+    public void testBluetoothAdapterReceiver() throws Exception {
+        Call ringingCall = createRingingCall();
+        when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:5550000"));
+
+        Intent intent = new Intent();
+        intent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_ON);
+        mBluetoothPhoneService.mBluetoothAdapterReceiver.onReceive(mContext, intent);
+
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_INCOMING),
+                eq("5550000"), eq(PhoneNumberUtils.TOA_Unknown), nullable(String.class));
+    }
+
+    private void addCallCapability(Call call, int capability) {
+        when(call.can(capability)).thenReturn(true);
+    }
+
+    private void removeCallCapability(Call call, int capability) {
+        when(call.can(capability)).thenReturn(false);
+    }
+
+    private Call createActiveCall() {
+        Call call = mock(Call.class);
+        when(mMockCallsManager.getActiveCall()).thenReturn(call);
+        return call;
+    }
+
+    private Call createRingingCall() {
+        Call call = mock(Call.class);
+        when(mMockCallsManager.getRingingOrSimulatedRingingCall()).thenReturn(call);
+        return call;
+    }
+
+    private Call createHeldCall() {
+        Call call = mock(Call.class);
+        when(mMockCallsManager.getHeldCall()).thenReturn(call);
+        return call;
+    }
+
+    private Call createOutgoingCall() {
+        Call call = mock(Call.class);
+        when(mMockCallsManager.getOutgoingCall()).thenReturn(call);
+        return call;
+    }
+
+    private Call createDisconnectedCall() {
+        Call call = mock(Call.class);
+        when(mMockCallsManager.getFirstCallWithState(CallState.DISCONNECTED)).thenReturn(call);
+        return call;
+    }
+
+    private Call createForegroundCall() {
+        Call call = mock(Call.class);
+        when(mMockCallsManager.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)
+                .setIcon(Icon.createWithResource(
+                        "com.android.server.telecom.tests", R.drawable.stat_sys_phone_call))
+                .setShortDescription("desc" + idx)
+                .setIsEnabled(true)
+                .build();
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 5c1cdc4..e8234fc 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -69,6 +69,7 @@
 
 import com.android.internal.telecom.IInCallAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
+import com.android.server.telecom.BluetoothPhoneServiceImpl;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
@@ -200,6 +201,7 @@
     @Mock HeadsetMediaButton mHeadsetMediaButton;
     @Mock ProximitySensorManager mProximitySensorManager;
     @Mock InCallWakeLockController mInCallWakeLockController;
+    @Mock BluetoothPhoneServiceImpl mBluetoothPhoneServiceImpl;
     @Mock AsyncRingtonePlayer mAsyncRingtonePlayer;
     @Mock IncomingCallNotifier mIncomingCallNotifier;
     @Mock ClockProxy mClockProxy;
@@ -483,6 +485,7 @@
                 proximitySensorManagerFactory,
                 inCallWakeLockControllerFactory,
                 () -> mAudioService,
+                (context, lock, callsManager, phoneAccountRegistrar) -> mBluetoothPhoneServiceImpl,
                 mConnServFMFactory,
                 mTimeoutsAdapter,
                 mAsyncRingtonePlayer,