blob: 386bba4545c9dc5ed125bd28f0b66784c225c933 [file] [log] [blame]
/*
* 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);
}
}