| /* |
| * 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; |
| |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothManager; |
| import android.bluetooth.BluetoothProfile; |
| import android.content.ComponentName; |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.ParcelUuid; |
| 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 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 TelecomManager mTelecomManager; |
| |
| private HeadsetClientServiceInterface mServiceInterface = new HeadsetClientServiceInterface(); |
| |
| private final Map<BluetoothDevice, HfpClientDeviceBlock> mDeviceBlocks = new HashMap<>(); |
| |
| //--------------------------------------------------------------------------------------------// |
| // SINGLETON MANAGEMENT // |
| //--------------------------------------------------------------------------------------------// |
| |
| private static final Object INSTANCE_LOCK = new Object(); |
| private static HfpClientConnectionService sHfpClientConnectionService; |
| |
| private void setInstance(HfpClientConnectionService instance) { |
| synchronized (INSTANCE_LOCK) { |
| sHfpClientConnectionService = instance; |
| } |
| } |
| |
| private static HfpClientConnectionService getInstance() { |
| synchronized (INSTANCE_LOCK) { |
| return sHfpClientConnectionService; |
| } |
| } |
| |
| private void clearInstance() { |
| synchronized (INSTANCE_LOCK) { |
| if (sHfpClientConnectionService == this) { |
| setInstance(null); |
| } |
| } |
| } |
| |
| //--------------------------------------------------------------------------------------------// |
| // MESSAGES FROM HEADSET CLIENT SERVICE // |
| //--------------------------------------------------------------------------------------------// |
| |
| /** |
| * Send a device connection state changed event to this service |
| */ |
| public static void onConnectionStateChanged(BluetoothDevice device, int newState, |
| int oldState) { |
| HfpClientConnectionService service = getInstance(); |
| if (service == null) { |
| Log.e(TAG, "onConnectionStateChanged: HFP Client Connection Service not started"); |
| return; |
| } |
| service.onConnectionStateChangedInternal(device, newState, oldState); |
| } |
| |
| /** |
| * Send a device call state changed event to this service |
| */ |
| public static void onCallChanged(BluetoothDevice device, HfpClientCall call) { |
| HfpClientConnectionService service = getInstance(); |
| if (service == null) { |
| Log.e(TAG, "onCallChanged: HFP Client Connection Service not started"); |
| return; |
| } |
| service.onCallChangedInternal(device, call); |
| } |
| |
| /** |
| * Send a device audio state changed event to this service |
| */ |
| public static void onAudioStateChanged(BluetoothDevice device, int newState, int oldState) { |
| HfpClientConnectionService service = getInstance(); |
| if (service == null) { |
| Log.e(TAG, "onAudioStateChanged: HFP Client Connection Service not started"); |
| return; |
| } |
| service.onAudioStateChangedInternal(device, newState, oldState); |
| } |
| |
| //--------------------------------------------------------------------------------------------// |
| // HANDLE MESSAGES FROM HEADSET CLIENT SERVICE // |
| //--------------------------------------------------------------------------------------------// |
| |
| private void onConnectionStateChangedInternal(BluetoothDevice device, int newState, |
| int oldState) { |
| if (newState == BluetoothProfile.STATE_CONNECTED) { |
| if (DBG) { |
| Log.d(TAG, "Established connection with " + device); |
| } |
| |
| HfpClientDeviceBlock block = createBlockForDevice(device); |
| if (block == 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=" + device); |
| return; |
| } |
| block.cleanup(); |
| } |
| } |
| } |
| |
| private void onCallChangedInternal(BluetoothDevice device, HfpClientCall call) { |
| HfpClientDeviceBlock block = findBlockForDevice(device); |
| 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); |
| } |
| |
| private void onAudioStateChangedInternal(BluetoothDevice device, int newState, int oldState) { |
| HfpClientDeviceBlock block = findBlockForDevice(device); |
| if (block == null) { |
| Log.w(TAG, "Device audio state changed but no block for device=" + device); |
| return; |
| } |
| block.onAudioStateChange(newState, oldState); |
| } |
| |
| //--------------------------------------------------------------------------------------------// |
| // SERVICE SETUP AND TEAR DOWN // |
| //--------------------------------------------------------------------------------------------// |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| if (DBG) { |
| Log.d(TAG, "onCreate"); |
| } |
| mTelecomManager = getSystemService(TelecomManager.class); |
| if (mTelecomManager != null) mTelecomManager.clearPhoneAccounts(); |
| |
| List<BluetoothDevice> devices = mServiceInterface.getConnectedDevices(); |
| if (devices != null) { |
| for (BluetoothDevice device : devices) { |
| createBlockForDevice(device); |
| } |
| } |
| |
| setInstance(this); |
| } |
| |
| @Override |
| public void onDestroy() { |
| if (DBG) { |
| Log.d(TAG, "onDestroy called"); |
| } |
| |
| // Unregister the phone account. This should ideally happen when disconnection ensues but in |
| // case the service crashes we may need to force clean. |
| disconnectAll(); |
| |
| clearInstance(); |
| } |
| |
| 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; |
| } |
| return START_STICKY; |
| } |
| |
| //--------------------------------------------------------------------------------------------// |
| // TELECOM CONNECTION SERVICE FUNCTIONS // |
| //--------------------------------------------------------------------------------------------// |
| |
| // 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. |
| ParcelUuid callUuid = |
| request.getExtras().getParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS); |
| HfpClientConnection connection = |
| block.onCreateIncomingConnection((callUuid != null ? callUuid.getUuid() : null)); |
| 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()); |
| 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. |
| ParcelUuid callUuid = |
| request.getExtras().getParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS); |
| HfpClientConnection connection = |
| block.onCreateUnknownConnection((callUuid != null ? callUuid.getUuid() : null)); |
| 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); |
| } |
| |
| //--------------------------------------------------------------------------------------------// |
| // DEVICE MANAGEMENT // |
| //--------------------------------------------------------------------------------------------// |
| |
| private BluetoothDevice getDevice(PhoneAccountHandle handle) { |
| BluetoothAdapter adapter = getSystemService(BluetoothManager.class).getAdapter(); |
| PhoneAccount account = mTelecomManager.getPhoneAccount(handle); |
| if (account == null) { |
| return null; |
| } |
| String btAddr = account.getAddress().getSchemeSpecificPart(); |
| return adapter.getRemoteDevice(btAddr); |
| } |
| |
| // 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 = |
| HfpClientDeviceBlock.Factory.build(device, this, mServiceInterface); |
| 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) { |
| BluetoothDevice device = getDevice(handle); |
| Log.d(TAG, "Finding block for handle " + handle + " device " + device); |
| return mDeviceBlocks.get(device); |
| } |
| |
| PhoneAccount createAccount(BluetoothDevice device) { |
| Uri addr = Uri.fromParts(HfpClientConnectionService.HFP_SCHEME, device.getAddress(), null); |
| PhoneAccountHandle handle = |
| new PhoneAccountHandle(new ComponentName(this, HfpClientConnectionService.class), |
| device.getAddress()); |
| |
| int capabilities = PhoneAccount.CAPABILITY_CALL_PROVIDER; |
| if (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; |
| } |
| |
| private Map<BluetoothDevice, HfpClientDeviceBlock> getDeviceBlocks() { |
| return mDeviceBlocks; |
| } |
| |
| /** |
| * Dump the state of the HfpClientConnectionService and internal objects |
| */ |
| public static void dump(StringBuilder sb) { |
| HfpClientConnectionService instance = getInstance(); |
| sb.append(" HfpClientConnectionService:\n"); |
| |
| if (instance == null) { |
| sb.append(" null"); |
| } else { |
| sb.append(" Devices:\n"); |
| for (HfpClientDeviceBlock block : instance.getDeviceBlocks().values()) { |
| sb.append(" " + block.toString() + "\n"); |
| } |
| } |
| } |
| } |