Move BluetoothPhoneService to telecom.

BluetoothPhoneService needs to be in telecom to be aware of all types of
calls.  While in telephony, there's no way for it to know about
third-party sourced calls.

Additionally, conference calls for CDMA are no longer functional in the
telephony layer and needs to move to telecom to support BT commands on
CDMA calls.

Bug: 17475562
Change-Id: I443431291a3b0120d92b52dba2acca17a4c0c983
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index dfad570..50cdcb1 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -212,6 +212,5 @@
             android:exported="false">
         </receiver>
 
-
     </application>
 </manifest>
diff --git a/src/com/android/server/telecom/BluetoothPhoneService.java b/src/com/android/server/telecom/BluetoothPhoneService.java
new file mode 100644
index 0000000..efac3bf
--- /dev/null
+++ b/src/com/android/server/telecom/BluetoothPhoneService.java
@@ -0,0 +1,567 @@
+/*
+ * 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.app.Service;
+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.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.server.telecom.CallsManager.CallsManagerListener;
+
+import java.util.List;
+
+/**
+ * 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 {
+    /**
+     * Request object for performing synchronous requests to the main thread.
+     */
+    private static class MainThreadRequest {
+        Object result;
+        int param;
+
+        MainThreadRequest(int param) {
+            this.param = param;
+        }
+
+        void setResult(Object value) {
+            result = value;
+            synchronized (this) {
+                notifyAll();
+            }
+        }
+    }
+
+    private static final String TAG = "BluetoothPhoneService";
+
+    private static final int MSG_ANSWER_CALL = 1;
+    private static final int MSG_HANGUP_CALL = 2;
+    private static final int MSG_SEND_DTMF = 3;
+    private static final int MSG_PROCESS_CHLD = 4;
+    private static final int MSG_GET_NETWORK_OPERATOR = 5;
+    private static final int MSG_LIST_CURRENT_CALLS = 6;
+    private static final int MSG_QUERY_PHONE_STATE = 7;
+    private static final int MSG_GET_SUBSCRIBER_NUMBER = 8;
+
+    // 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;
+
+    // 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;
+
+    /**
+     * Binder implementation of IBluetoothHeadsetPhone. Implements the command interface that the
+     * bluetooth headset code uses to control call.
+     */
+    private final IBluetoothHeadsetPhone.Stub mBinder = new IBluetoothHeadsetPhone.Stub() {
+        @Override
+        public boolean answerCall() throws RemoteException {
+            enforceModifyPermission();
+            Log.i(TAG, "BT - answering call");
+            return sendSynchronousRequest(MSG_ANSWER_CALL);
+        }
+
+        @Override
+        public boolean hangupCall() throws RemoteException {
+            enforceModifyPermission();
+            Log.i(TAG, "BT - hanging up call");
+            return sendSynchronousRequest(MSG_HANGUP_CALL);
+        }
+
+        @Override
+        public boolean sendDtmf(int dtmf) throws RemoteException {
+            enforceModifyPermission();
+            Log.i(TAG, "BT - sendDtmf %c", Log.DEBUG ? dtmf : '.');
+            return sendSynchronousRequest(MSG_SEND_DTMF, dtmf);
+        }
+
+        @Override
+        public String getNetworkOperator() throws RemoteException {
+            Log.i(TAG, "getNetworkOperator");
+            enforceModifyPermission();
+            return sendSynchronousRequest(MSG_GET_NETWORK_OPERATOR);
+        }
+
+        @Override
+        public String getSubscriberNumber() throws RemoteException {
+            Log.i(TAG, "getSubscriberNumber");
+            enforceModifyPermission();
+            return sendSynchronousRequest(MSG_GET_SUBSCRIBER_NUMBER);
+        }
+
+        @Override
+        public boolean listCurrentCalls() throws RemoteException {
+            Log.i(TAG, "listcurrentCalls");
+            enforceModifyPermission();
+            return sendSynchronousRequest(MSG_LIST_CURRENT_CALLS);
+        }
+
+        @Override
+        public boolean queryPhoneState() throws RemoteException {
+            Log.i(TAG, "queryPhoneState");
+            enforceModifyPermission();
+            return sendSynchronousRequest(MSG_QUERY_PHONE_STATE);
+        }
+
+        @Override
+        public boolean processChld(int chld) throws RemoteException {
+            Log.i(TAG, "processChld %d", chld);
+            enforceModifyPermission();
+            return sendSynchronousRequest(MSG_PROCESS_CHLD, chld);
+        }
+
+        @Override
+        public void updateBtHandsfreeAfterRadioTechnologyChange() throws RemoteException {
+            Log.d(TAG, "RAT change");
+            // deprecated
+        }
+
+        @Override
+        public void cdmaSetSecondCallState(boolean state) throws RemoteException {
+            Log.d(TAG, "cdma 1");
+            // deprecated
+        }
+
+        @Override
+        public void cdmaSwapSecondCallState() throws RemoteException {
+            Log.d(TAG, "cdma 2");
+            // deprecated
+        }
+    };
+
+    /**
+     * Main-thread handler for BT commands.  Since telecom logic runs on a single thread, commands
+     * that are sent to it from the headset need to be moved over to the main thread before
+     * executing. This handler exists for that reason.
+     */
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            MainThreadRequest request = msg.obj instanceof MainThreadRequest ?
+                    (MainThreadRequest) msg.obj : null;
+            CallsManager callsManager = getCallsManager();
+            Call call = null;
+
+            Log.d(TAG, "handleMessage(%d) w/ param %s",
+                    msg.what, request == null ? null : request.param);
+
+            switch (msg.what) {
+                case MSG_ANSWER_CALL:
+                    try {
+                        call = callsManager.getRingingCall();
+                        if (call != null) {
+                            getCallsManager().answerCall(call, 0);
+                        }
+                    } finally {
+                        request.setResult(call != null);
+                    }
+                    break;
+
+                case MSG_HANGUP_CALL:
+                    try {
+                        call = callsManager.getForegroundCall();
+                        if (call != null) {
+                            callsManager.disconnectCall(call);
+                        }
+                    } finally {
+                        request.setResult(call != null);
+                    }
+                    break;
+
+                case MSG_SEND_DTMF:
+                    try {
+                        call = callsManager.getForegroundCall();
+                        if (call != null) {
+                            // TODO: Consider making this a queue instead of starting/stopping
+                            // in quick succession.
+                            callsManager.playDtmfTone(call, (char) request.param);
+                            callsManager.stopDtmfTone(call);
+                        }
+                    } finally {
+                        request.setResult(call != null);
+                    }
+                    break;
+
+                case MSG_PROCESS_CHLD:
+                    Boolean result = false;
+                    try {
+                        result = processChld(request.param);
+                    } finally {
+                        request.setResult(result);
+                    }
+                    break;
+
+                case MSG_GET_SUBSCRIBER_NUMBER:
+                    String address = null;
+                    try {
+                        PhoneAccount account = getBestPhoneAccount();
+                        if (account != null) {
+                            Uri addressUri = account.getAddress();
+                            if (addressUri != null) {
+                                address = addressUri.getSchemeSpecificPart();
+                            }
+                        }
+
+                        if (TextUtils.isEmpty(address)) {
+                            address = TelephonyManager.from(BluetoothPhoneService.this)
+                                    .getLine1Number();
+                        }
+                    } finally {
+                        request.setResult(address);
+                    }
+                    break;
+
+                case MSG_GET_NETWORK_OPERATOR:
+                    String label = null;
+                    try {
+                        PhoneAccount account = getBestPhoneAccount();
+                        if (account != null) {
+                            label = account.getLabel().toString();
+                        } else {
+                            // Finally, just get the network name from telephony.
+                            label = TelephonyManager.from(BluetoothPhoneService.this)
+                                    .getNetworkOperatorName();
+                        }
+                    } finally {
+                        request.setResult(label);
+                    }
+                    break;
+
+                case MSG_LIST_CURRENT_CALLS:
+                    // TODO - Add current calls.
+                    request.setResult(true);
+                    break;
+
+                case MSG_QUERY_PHONE_STATE:
+                    try {
+                        updateHeadsetWithCallState();
+                    } finally {
+                        if (request != null) {
+                            request.setResult(true);
+                        }
+                    }
+                    break;
+            }
+        }
+    };
+
+    /**
+     * Listens to call changes from the CallsManager and calls into methods to update the bluetooth
+     * headset with the new states.
+     */
+    private CallsManagerListener mCallsManagerListener = new CallsManagerListenerBase() {
+        @Override
+        public void onCallAdded(Call call) {
+            updateHeadsetWithCallState();
+        }
+
+        @Override
+        public void onCallRemoved(Call call) {
+            updateHeadsetWithCallState();
+        }
+
+        @Override
+        public void onCallStateChanged(Call call, int oldState, int newState) {
+            updateHeadsetWithCallState();
+        }
+
+        @Override
+        public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+            updateHeadsetWithCallState();
+        }
+
+        @Override
+        public void onIsConferencedChanged(Call call) {
+            updateHeadsetWithCallState();
+        }
+    };
+
+    /**
+     * 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.
+     */
+    private BluetoothProfile.ServiceListener mProfileListener =
+            new BluetoothProfile.ServiceListener() {
+        @Override
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            mBluetoothHeadset = (BluetoothHeadset) proxy;
+        }
+
+        @Override
+        public void onServiceDisconnected(int profile) {
+            mBluetoothHeadset = null;
+        }
+    };
+
+    /**
+     * Receives events for global state changes of the bluetooth adapter.
+     */
+    private final BroadcastReceiver mBluetoothAdapterReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+            Log.d(TAG, "Bluetooth Adapter state: %d", state);
+            if (state == BluetoothAdapter.STATE_ON) {
+                mHandler.sendEmptyMessage(MSG_QUERY_PHONE_STATE);
+            }
+        }
+    };
+
+    private BluetoothAdapter mBluetoothAdapter;
+    private BluetoothHeadset mBluetoothHeadset;
+
+    public BluetoothPhoneService() {
+        Log.v(TAG, "Constructor");
+    }
+
+    public static final void start(Context context) {
+        if (BluetoothAdapter.getDefaultAdapter() != null) {
+            context.startService(new Intent(context, BluetoothPhoneService.class));
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Log.d(TAG, "Binding service");
+        return mBinder;
+    }
+
+    @Override
+    public void onCreate() {
+        Log.d(TAG, "onCreate");
+
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        if (mBluetoothAdapter == null) {
+            Log.d(TAG, "BluetoothPhoneService shutting down, no BT Adapter found.");
+            return;
+        }
+        mBluetoothAdapter.getProfileProxy(this, mProfileListener, BluetoothProfile.HEADSET);
+
+        IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+        registerReceiver(mBluetoothAdapterReceiver, intentFilter);
+
+        CallsManager.getInstance().addListener(mCallsManagerListener);
+        updateHeadsetWithCallState();
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.d(TAG, "onDestroy");
+        CallsManager.getInstance().removeListener(mCallsManagerListener);
+        super.onDestroy();
+    }
+
+    private boolean processChld(int chld) {
+        CallsManager callsManager = CallsManager.getInstance();
+        Call activeCall = callsManager.getActiveCall();
+        Call ringingCall = callsManager.getRingingCall();
+        Call heldCall = callsManager.getHeldCall();
+
+        if (chld == CHLD_TYPE_RELEASEHELD) {
+            if (ringingCall != null) {
+                callsManager.rejectCall(ringingCall, false, null);
+                return true;
+            } else if (heldCall != null) {
+                callsManager.disconnectCall(heldCall);
+                return true;
+            }
+        } else if (chld == CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD) {
+            if (activeCall != null) {
+                callsManager.disconnectCall(activeCall);
+                if (ringingCall != null) {
+                    callsManager.answerCall(ringingCall, 0);
+                } else if (heldCall != null) {
+                    callsManager.unholdCall(heldCall);
+                }
+                return true;
+            }
+        } else if (chld == CHLD_TYPE_HOLDACTIVE_ACCEPTHELD) {
+            if (ringingCall != null) {
+                callsManager.answerCall(ringingCall, 0);
+                return true;
+            } else if (heldCall != null) {
+                // CallsManager will hold any active calls when unhold() is called on a
+                // currently-held call.
+                callsManager.unholdCall(heldCall);
+                return true;
+            } else if (activeCall != null) {
+                callsManager.holdCall(activeCall);
+                return true;
+            }
+        } else if (chld == CHLD_TYPE_ADDHELDTOCONF) {
+            if (activeCall != null) {
+                List<Call> conferenceable = activeCall.getConferenceableCalls();
+                if (!conferenceable.isEmpty()) {
+                    callsManager.conference(activeCall, conferenceable.get(0));
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private void enforceModifyPermission() {
+        enforceCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE, null);
+    }
+
+    private <T> T sendSynchronousRequest(int message) {
+        return sendSynchronousRequest(message, 0);
+    }
+
+    private <T> T sendSynchronousRequest(int message, int param) {
+        MainThreadRequest request = new MainThreadRequest(param);
+        mHandler.obtainMessage(message, request).sendToTarget();
+        synchronized (request) {
+            while (request.result == null) {
+                try {
+                    request.wait();
+                } catch (InterruptedException e) {
+                    // Do nothing, go back and wait until the request is complete.
+                }
+            }
+        }
+        if (request.result != null) {
+            @SuppressWarnings("unchecked")
+            T retval = (T) request.result;
+            return retval;
+        }
+        return null;
+    }
+
+    private void updateHeadsetWithCallState() {
+        CallsManager callsManager = getCallsManager();
+        Call activeCall = callsManager.getActiveCall();
+        Call ringingCall = callsManager.getRingingCall();
+        Call heldCall = callsManager.getHeldCall();
+
+        int bluetoothCallState = getBluetoothCallStateForUpdate();
+
+        String ringingAddress = null;
+        int ringingAddressType = 128;
+        if (ringingCall != null) {
+            ringingAddress = ringingCall.getHandle().getSchemeSpecificPart();
+            if (ringingAddress != null) {
+                ringingAddressType = PhoneNumberUtils.toaFromString(ringingAddress);
+            }
+        }
+        if (ringingAddress == null) {
+            ringingAddress = "";
+        }
+
+        Log.d(TAG, "updateHeadsetWithCallState " +
+                "numActive %s, " +
+                "numHeld %s, " +
+                "callState %s, " +
+                "ringing number %s, " +
+                "ringing type %s",
+                activeCall,
+                heldCall,
+                bluetoothCallState,
+                ringingAddress,
+                ringingAddressType);
+
+
+        if (mBluetoothHeadset != null) {
+            mBluetoothHeadset.phoneStateChanged(
+                    activeCall == null ? 0 : 1,
+                    heldCall == null ? 0 : 1,
+                    bluetoothCallState,
+                    ringingAddress,
+                    ringingAddressType);
+        }
+    }
+
+    private int getBluetoothCallStateForUpdate() {
+        CallsManager callsManager = getCallsManager();
+        Call ringingCall = callsManager.getRingingCall();
+
+        //
+        // !! 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) {
+            bluetoothCallState = CALL_STATE_INCOMING;
+        } else if (callsManager.getDialingOrConnectingCall() != null) {
+            bluetoothCallState = CALL_STATE_ALERTING;
+        }
+        return bluetoothCallState;
+    }
+
+    private CallsManager getCallsManager() {
+        return CallsManager.getInstance();
+    }
+
+    /**
+     * 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() {
+        TelecomApp app = (TelecomApp) getApplication();
+        PhoneAccountRegistrar registry = app.getPhoneAccountRegistrar();
+        Call call = getCallsManager().getForegroundCall();
+
+        PhoneAccount account = null;
+        if (call != null) {
+            // First try to get the network name of the foreground call.
+            account = registry.getPhoneAccount(call.getTargetPhoneAccount());
+        }
+
+        if (account == null) {
+            // Second, Try to get the label for the default Phone Account.
+            account = registry.getPhoneAccount(
+                    registry.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL));
+        }
+        return account;
+    }
+}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 4b14fca..826afb4 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -299,6 +299,14 @@
         return mTtyManager.getCurrentTtyMode();
     }
 
+    void addListener(CallsManagerListener listener) {
+        mListeners.add(listener);
+    }
+
+    void removeListener(CallsManagerListener listener) {
+        mListeners.remove(listener);
+    }
+
     /**
      * Starts the process to attach the call to a connection service.
      *
@@ -794,6 +802,22 @@
         return true;
     }
 
+    Call getRingingCall() {
+        return getFirstCallWithState(CallState.RINGING);
+    }
+
+    Call getActiveCall() {
+        return getFirstCallWithState(CallState.ACTIVE);
+    }
+
+    Call getDialingOrConnectingCall() {
+        return getFirstCallWithState(CallState.DIALING, CallState.CONNECTING);
+    }
+
+    Call getHeldCall() {
+        return getFirstCallWithState(CallState.ON_HOLD);
+    }
+
     Call getFirstCallWithState(int... states) {
         return getFirstCallWithState(null, states);
     }
diff --git a/src/com/android/server/telecom/TelecomApp.java b/src/com/android/server/telecom/TelecomApp.java
index eaa89ac..4941a35 100644
--- a/src/com/android/server/telecom/TelecomApp.java
+++ b/src/com/android/server/telecom/TelecomApp.java
@@ -20,8 +20,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.os.UserHandle;
 import android.os.ServiceManager;
+import android.os.UserHandle;
 
 /**
  * Top-level Application class for Telecom.
@@ -67,6 +67,9 @@
             mTelecomService = new TelecomServiceImpl(mMissedCallNotifier, mPhoneAccountRegistrar,
                     mCallsManager, this);
             ServiceManager.addService(Context.TELECOM_SERVICE, mTelecomService);
+
+            // Start the BluetoothPhoneService
+            BluetoothPhoneService.start(this);
         }
     }