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);
}
}