| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.bluetooth.hfpclient.connserv; |
| |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHeadsetClient; |
| import android.bluetooth.BluetoothHeadsetClientCall; |
| import android.bluetooth.BluetoothProfile; |
| import android.bluetooth.hfpclient.connserv.BluetoothHeadsetClientProxy; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.telecom.Connection; |
| import android.telecom.ConnectionRequest; |
| import android.telecom.ConnectionService; |
| import android.telecom.PhoneAccount; |
| import android.telecom.PhoneAccountHandle; |
| import android.telecom.TelecomManager; |
| import android.util.Log; |
| |
| import com.android.bluetooth.hfpclient.HeadsetClientService; |
| |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| public class HfpClientConnectionService extends ConnectionService { |
| private static final String TAG = "HfpClientConnService"; |
| private static final boolean DBG = true; |
| |
| public static final String HFP_SCHEME = "hfpc"; |
| |
| private BluetoothAdapter mAdapter; |
| |
| // BluetoothHeadset proxy. |
| private BluetoothHeadsetClientProxy mHeadsetProfile; |
| private TelecomManager mTelecomManager; |
| |
| private final Map<BluetoothDevice, HfpClientDeviceBlock> mDeviceBlocks = new HashMap<>(); |
| |
| private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (DBG) { |
| Log.d(TAG, "onReceive " + intent); |
| } |
| String action = intent != null ? intent.getAction() : null; |
| |
| if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { |
| BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); |
| |
| if (newState == BluetoothProfile.STATE_CONNECTED) { |
| if (DBG) { |
| Log.d(TAG, "Established connection with " + device); |
| } |
| |
| HfpClientDeviceBlock block = null; |
| if ((block = createBlockForDevice(device)) == null) { |
| Log.w(TAG, "Block already exists for device " + device + " ignoring."); |
| } |
| } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { |
| if (DBG) { |
| Log.d(TAG, "Disconnecting from " + device); |
| } |
| |
| // Disconnect any inflight calls from the connection service. |
| synchronized (HfpClientConnectionService.this) { |
| HfpClientDeviceBlock block = mDeviceBlocks.remove(device); |
| if (block == null) { |
| Log.w(TAG, "Disconnect for device but no block " + device); |
| return; |
| } |
| block.cleanup(); |
| // Block should be subsequently garbage collected |
| block = null; |
| } |
| } |
| } else if (BluetoothHeadsetClient.ACTION_CALL_CHANGED.equals(action)) { |
| BluetoothHeadsetClientCall call = |
| intent.getParcelableExtra(BluetoothHeadsetClient.EXTRA_CALL); |
| BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| HfpClientDeviceBlock block = findBlockForDevice(call.getDevice()); |
| if (block == null) { |
| Log.w(TAG, "Call changed but no block for device " + device); |
| return; |
| } |
| |
| // If we are not connected, then when we actually do get connected -- |
| // the calls should |
| // be added (see ACTION_CONNECTION_STATE_CHANGED intent above). |
| block.handleCall(call); |
| } else if (BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED.equals(action)) { |
| BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, |
| BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED); |
| int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, |
| BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED); |
| HfpClientDeviceBlock block = findBlockForDevice(device); |
| if (block == null) { |
| Log.w(TAG, "Device audio state changed but no block for device"); |
| return; |
| } |
| block.onAudioStateChange(newState, oldState); |
| } |
| } |
| }; |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| if (DBG) { |
| Log.d(TAG, "onCreate"); |
| } |
| mAdapter = BluetoothAdapter.getDefaultAdapter(); |
| mTelecomManager = getSystemService(TelecomManager.class); |
| if (mTelecomManager != null) mTelecomManager.clearPhoneAccounts(); |
| mAdapter.getProfileProxy(this, mServiceListener, BluetoothProfile.HEADSET_CLIENT); |
| } |
| |
| @Override |
| public void onDestroy() { |
| if (DBG) { |
| Log.d(TAG, "onDestroy called"); |
| } |
| // Close the profile. |
| if (mHeadsetProfile != null) { |
| mAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT, |
| mHeadsetProfile.getProxiedBluetoothHeadsetClient()); |
| } |
| |
| // Unregister the broadcast receiver. |
| try { |
| unregisterReceiver(mBroadcastReceiver); |
| } catch (IllegalArgumentException ex) { |
| Log.w(TAG, "Receiver was not registered."); |
| } |
| |
| // Unregister the phone account. This should ideally happen when disconnection ensues but in |
| // case the service crashes we may need to force clean. |
| disconnectAll(); |
| } |
| |
| private synchronized void disconnectAll() { |
| for (Iterator<Map.Entry<BluetoothDevice, HfpClientDeviceBlock>> it = |
| mDeviceBlocks.entrySet().iterator(); it.hasNext(); ) { |
| it.next().getValue().cleanup(); |
| it.remove(); |
| } |
| } |
| |
| @Override |
| public int onStartCommand(Intent intent, int flags, int startId) { |
| if (DBG) { |
| Log.d(TAG, "onStartCommand " + intent); |
| } |
| // In order to make sure that the service is sticky (recovers from errors when HFP |
| // connection is still active) and to stop it we need a special intent since stopService |
| // only recreates it. |
| if (intent != null && intent.getBooleanExtra(HeadsetClientService.HFP_CLIENT_STOP_TAG, |
| false)) { |
| // Stop the service. |
| stopSelf(); |
| return 0; |
| } else { |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); |
| filter.addAction(BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED); |
| filter.addAction(BluetoothHeadsetClient.ACTION_CALL_CHANGED); |
| registerReceiver(mBroadcastReceiver, filter); |
| return START_STICKY; |
| } |
| } |
| |
| // This method is called whenever there is a new incoming call (or right after BT connection). |
| @Override |
| public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerAccount, |
| ConnectionRequest request) { |
| if (DBG) { |
| Log.d(TAG, |
| "onCreateIncomingConnection " + connectionManagerAccount + " req: " + request); |
| } |
| |
| HfpClientDeviceBlock block = findBlockForHandle(connectionManagerAccount); |
| if (block == null) { |
| Log.w(TAG, "HfpClient does not support having a connection manager"); |
| return null; |
| } |
| |
| // We should already have a connection by this time. |
| BluetoothHeadsetClientCall call = |
| request.getExtras().getParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS); |
| HfpClientConnection connection = block.onCreateIncomingConnection(call); |
| connection.setHfpClientConnectionService(this); |
| return connection; |
| } |
| |
| // This method is called *only if* Dialer UI is used to place an outgoing call. |
| @Override |
| public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerAccount, |
| ConnectionRequest request) { |
| if (DBG) { |
| Log.d(TAG, "onCreateOutgoingConnection " + connectionManagerAccount); |
| } |
| HfpClientDeviceBlock block = findBlockForHandle(connectionManagerAccount); |
| if (block == null) { |
| Log.w(TAG, "HfpClient does not support having a connection manager"); |
| return null; |
| } |
| HfpClientConnection connection = block.onCreateOutgoingConnection(request.getAddress()); |
| connection.setHfpClientConnectionService(this); |
| return connection; |
| } |
| |
| // This method is called when: |
| // 1. Outgoing call created from the AG. |
| // 2. Call transfer from AG -> HF (on connection when existed call present). |
| @Override |
| public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerAccount, |
| ConnectionRequest request) { |
| if (DBG) { |
| Log.d(TAG, "onCreateUnknownConnection " + connectionManagerAccount); |
| } |
| HfpClientDeviceBlock block = findBlockForHandle(connectionManagerAccount); |
| if (block == null) { |
| Log.w(TAG, "HfpClient does not support having a connection manager"); |
| return null; |
| } |
| |
| // We should already have a connection by this time. |
| BluetoothHeadsetClientCall call = |
| request.getExtras().getParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS); |
| HfpClientConnection connection = block.onCreateUnknownConnection(call); |
| connection.setHfpClientConnectionService(this); |
| return connection; |
| } |
| |
| @Override |
| public void onConference(Connection connection1, Connection connection2) { |
| if (DBG) { |
| Log.d(TAG, "onConference " + connection1 + " " + connection2); |
| } |
| |
| BluetoothDevice bd1 = ((HfpClientConnection) connection1).getDevice(); |
| BluetoothDevice bd2 = ((HfpClientConnection) connection2).getDevice(); |
| // We can only conference two connections on same device |
| if (!Objects.equals(bd1, bd2)) { |
| Log.e(TAG, |
| "Cannot conference calls from two different devices " + "bd1 " + bd1 + " bd2 " |
| + bd2 + " conn1 " + connection1 + "connection2 " + connection2); |
| return; |
| } |
| |
| HfpClientDeviceBlock block = findBlockForDevice(bd1); |
| block.onConference(connection1, connection2); |
| } |
| |
| private BluetoothDevice getDevice(PhoneAccountHandle handle) { |
| PhoneAccount account = mTelecomManager.getPhoneAccount(handle); |
| String btAddr = account.getAddress().getSchemeSpecificPart(); |
| return mAdapter.getRemoteDevice(btAddr); |
| } |
| |
| BluetoothProfile.ServiceListener mServiceListener = new BluetoothProfile.ServiceListener() { |
| @Override |
| public void onServiceConnected(int profile, BluetoothProfile proxy) { |
| if (DBG) { |
| Log.d(TAG, "onServiceConnected"); |
| } |
| mHeadsetProfile = new BluetoothHeadsetClientProxy((BluetoothHeadsetClient) proxy); |
| |
| List<BluetoothDevice> devices = mHeadsetProfile.getConnectedDevices(); |
| if (devices == null) { |
| Log.w(TAG, "No connected or more than one connected devices found." + devices); |
| return; |
| } |
| for (BluetoothDevice device : devices) { |
| if (DBG) { |
| Log.d(TAG, "Creating phone account for device " + device); |
| } |
| |
| // Creation of the block takes care of initializing the phone account and |
| // calls. |
| HfpClientDeviceBlock block = createBlockForDevice(device); |
| } |
| } |
| |
| @Override |
| public void onServiceDisconnected(int profile) { |
| if (DBG) { |
| Log.d(TAG, "onServiceDisconnected " + profile); |
| } |
| mHeadsetProfile = null; |
| disconnectAll(); |
| } |
| }; |
| |
| // Block management functions |
| synchronized HfpClientDeviceBlock createBlockForDevice(BluetoothDevice device) { |
| Log.d(TAG, "Creating block for device " + device); |
| if (mDeviceBlocks.containsKey(device)) { |
| Log.e(TAG, "Device already exists " + device + " blocks " + mDeviceBlocks); |
| return null; |
| } |
| |
| HfpClientDeviceBlock block = new HfpClientDeviceBlock(this, device, mHeadsetProfile); |
| mDeviceBlocks.put(device, block); |
| return block; |
| } |
| |
| synchronized HfpClientDeviceBlock findBlockForDevice(BluetoothDevice device) { |
| Log.d(TAG, "Finding block for device " + device + " blocks " + mDeviceBlocks); |
| return mDeviceBlocks.get(device); |
| } |
| |
| synchronized HfpClientDeviceBlock findBlockForHandle(PhoneAccountHandle handle) { |
| PhoneAccount account = mTelecomManager.getPhoneAccount(handle); |
| String btAddr = account.getAddress().getSchemeSpecificPart(); |
| BluetoothDevice device = mAdapter.getRemoteDevice(btAddr); |
| Log.d(TAG, "Finding block for handle " + handle + " device " + btAddr); |
| return mDeviceBlocks.get(device); |
| } |
| |
| // Util functions that may be used by various classes |
| public static PhoneAccount createAccount(Context context, BluetoothDevice device) { |
| Uri addr = Uri.fromParts(HfpClientConnectionService.HFP_SCHEME, device.getAddress(), null); |
| PhoneAccountHandle handle = |
| new PhoneAccountHandle(new ComponentName(context, HfpClientConnectionService.class), |
| device.getAddress()); |
| |
| int capabilities = PhoneAccount.CAPABILITY_CALL_PROVIDER; |
| if (context.getApplicationContext().getResources().getBoolean( |
| com.android.bluetooth.R.bool |
| .hfp_client_connection_service_support_emergency_call)) { |
| // Need to have an emergency call capability to place emergency call |
| capabilities |= PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS; |
| } |
| |
| PhoneAccount account = |
| new PhoneAccount.Builder(handle, "HFP " + device.toString()).setAddress(addr) |
| .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL)) |
| .setCapabilities(capabilities) |
| .build(); |
| if (DBG) { |
| Log.d(TAG, "phoneaccount: " + account); |
| } |
| return account; |
| } |
| |
| /** Checks if device has Enhanced Call Control (ECC) feature. */ |
| public static boolean hasHfpClientEcc(BluetoothHeadsetClientProxy client, |
| BluetoothDevice device) { |
| Bundle features = client.getCurrentAgEvents(device); |
| return features != null && features.getBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ECC, |
| false); |
| } |
| } |