HfpClientConnectionService.java revision 8b2711a9c8537df86e73a0d01b9c3f737f29c643
1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.android.bluetooth.hfpclient.connserv; 17 18import android.bluetooth.BluetoothAdapter; 19import android.bluetooth.BluetoothDevice; 20import android.bluetooth.BluetoothHeadsetClient; 21import android.bluetooth.BluetoothHeadsetClientCall; 22import android.bluetooth.BluetoothProfile; 23import android.content.BroadcastReceiver; 24import android.content.ComponentName; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.net.Uri; 29import android.os.Bundle; 30import android.os.Handler; 31import android.telecom.Connection; 32import android.telecom.ConnectionRequest; 33import android.telecom.ConnectionService; 34import android.telecom.PhoneAccount; 35import android.telecom.PhoneAccountHandle; 36import android.telecom.TelecomManager; 37import android.util.Log; 38 39import com.android.bluetooth.hfpclient.HeadsetClientService; 40 41import java.util.Arrays; 42import java.util.ArrayList; 43import java.util.Collection; 44import java.util.HashMap; 45import java.util.List; 46import java.util.Map; 47 48public class HfpClientConnectionService extends ConnectionService { 49 private static final String TAG = "HfpClientConnService"; 50 private static final boolean DBG = true; 51 52 public static final String HFP_SCHEME = "hfpc"; 53 54 private BluetoothAdapter mAdapter; 55 // Currently active device. 56 private BluetoothDevice mDevice; 57 // Phone account associated with the above device. 58 private PhoneAccount mDevicePhoneAccount; 59 // BluetoothHeadset proxy. 60 private BluetoothHeadsetClient mHeadsetProfile; 61 private TelecomManager mTelecomManager; 62 63 private Map<ConnectionKey, HfpClientConnection> mConnections = 64 new HashMap<ConnectionKey, HfpClientConnection>(); 65 private HfpClientConference mConference; 66 67 private boolean mPendingAcceptCall; 68 69 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 70 @Override 71 public void onReceive(Context context, Intent intent) { 72 Log.d(TAG, "onReceive " + intent); 73 String action = intent != null ? intent.getAction() : null; 74 75 if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { 76 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 77 int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 78 79 if (newState == BluetoothProfile.STATE_CONNECTED) { 80 Log.d(TAG, "Established connection with " + device); 81 synchronized (HfpClientConnectionService.this) { 82 if (device.equals(mDevice)) { 83 // We are already connected and this message can be safeuly ignored. 84 Log.w(TAG, "Got connected for previously connected device, ignoring."); 85 } else { 86 // Since we are connected to a new device close down the previous 87 // account and register the new one. 88 if (mDevicePhoneAccount != null) { 89 mTelecomManager.unregisterPhoneAccount( 90 mDevicePhoneAccount.getAccountHandle()); 91 } 92 // Reset the device and the phone account associated. 93 mDevice = device; 94 mDevicePhoneAccount = 95 getAccount(HfpClientConnectionService.this, device); 96 mTelecomManager.registerPhoneAccount(mDevicePhoneAccount); 97 mTelecomManager.enablePhoneAccount( 98 mDevicePhoneAccount.getAccountHandle(), true); 99 mTelecomManager.setUserSelectedOutgoingPhoneAccount( 100 mDevicePhoneAccount.getAccountHandle()); 101 } 102 } 103 104 // Add any existing calls to the telecom stack. 105 if (mHeadsetProfile != null) { 106 List<BluetoothHeadsetClientCall> calls = 107 mHeadsetProfile.getCurrentCalls(mDevice); 108 Log.d(TAG, "Got calls " + calls); 109 if (calls == null) { 110 // We can get null as a return if we are not connected. Hence there may 111 // be a race in getting the broadcast and HFP Client getting 112 // disconnected before broadcast gets delivered. 113 Log.w(TAG, "Got connected but calls were null, ignoring the broadcast"); 114 return; 115 } 116 for (BluetoothHeadsetClientCall call : calls) { 117 handleCall(call); 118 } 119 } else { 120 Log.e(TAG, "headset profile is null, ignoring broadcast."); 121 } 122 } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { 123 Log.d(TAG, "Disconnecting from " + device); 124 // Disconnect any inflight calls from the connection service. 125 synchronized (HfpClientConnectionService.this) { 126 if (device.equals(mDevice)) { 127 Log.d(TAG, "Resetting state for " + device); 128 mDevice = null; 129 disconnectAll(); 130 mTelecomManager.unregisterPhoneAccount( 131 mDevicePhoneAccount.getAccountHandle()); 132 mDevicePhoneAccount = null; 133 } 134 } 135 } 136 } else if (BluetoothHeadsetClient.ACTION_CALL_CHANGED.equals(action)) { 137 // If we are not connected, then when we actually do get connected -- the calls should 138 // be added (see ACTION_CONNECTION_STATE_CHANGED intent above). 139 handleCall((BluetoothHeadsetClientCall) 140 intent.getParcelableExtra(BluetoothHeadsetClient.EXTRA_CALL)); 141 Log.d(TAG, mConnections.size() + " remaining"); 142 } 143 } 144 }; 145 146 @Override 147 public void onCreate() { 148 super.onCreate(); 149 Log.d(TAG, "onCreate"); 150 mAdapter = BluetoothAdapter.getDefaultAdapter(); 151 mTelecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE); 152 mAdapter.getProfileProxy(this, mServiceListener, BluetoothProfile.HEADSET_CLIENT); 153 } 154 155 @Override 156 public void onDestroy() { 157 Log.d(TAG, "onDestroy called"); 158 // Close the profile. 159 if (mHeadsetProfile != null) { 160 mAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT, mHeadsetProfile); 161 } 162 163 // Unregister the broadcast receiver. 164 try { 165 unregisterReceiver(mBroadcastReceiver); 166 } catch (IllegalArgumentException ex) { 167 Log.w(TAG, "Receiver was not registered."); 168 } 169 170 // Unregister the phone account. This should ideally happen when disconnection ensues but in 171 // case the service crashes we may need to force clean. 172 synchronized (this) { 173 mDevice = null; 174 if (mDevicePhoneAccount != null) { 175 mTelecomManager.unregisterPhoneAccount(mDevicePhoneAccount.getAccountHandle()); 176 mDevicePhoneAccount = null; 177 } 178 } 179 } 180 181 @Override 182 public int onStartCommand(Intent intent, int flags, int startId) { 183 Log.d(TAG, "onStartCommand " + intent); 184 // In order to make sure that the service is sticky (recovers from errors when HFP 185 // connection is still active) and to stop it we need a special intent since stopService 186 // only recreates it. 187 if (intent.getBooleanExtra(HeadsetClientService.HFP_CLIENT_STOP_TAG, false)) { 188 // Stop the service. 189 stopSelf(); 190 return 0; 191 } else { 192 IntentFilter filter = new IntentFilter(); 193 filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); 194 filter.addAction(BluetoothHeadsetClient.ACTION_CALL_CHANGED); 195 registerReceiver(mBroadcastReceiver, filter); 196 return START_STICKY; 197 } 198 } 199 200 // Find the connection specified by the key, also update the key with ID if present. 201 private synchronized HfpClientConnection findConnectionAndUpdateKey(ConnectionKey key) { 202 if (DBG) { 203 Log.d(TAG, "findConnectionAndUpdateKey local key set " + mConnections.toString()); 204 } 205 206 HfpClientConnection conn = mConnections.get(key); 207 if (conn != null && key.getId() != ConnectionKey.INVALID_ID) { 208 Log.d(TAG, "Updating key for " + key.getPhoneNumber() + " to " + key.getId()); 209 mConnections.remove(key); 210 mConnections.put(key, conn); 211 } 212 return conn; 213 } 214 215 private void handleCall(BluetoothHeadsetClientCall call) { 216 Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null); 217 Log.d(TAG, "Got call " + call.toString(true) + "/" + number); 218 ConnectionKey incomingKey = ConnectionKey.getKey(call); 219 HfpClientConnection connection = findConnectionAndUpdateKey(incomingKey); 220 221 if (connection != null) { 222 connection.handleCallChanged(call); 223 } 224 225 if (connection == null) { 226 // Create the connection here, trigger Telecom to bind to us. 227 buildConnection(call.getDevice(), call, number); 228 229 PhoneAccountHandle handle = getHandle(); 230 TelecomManager manager = 231 (TelecomManager) getSystemService(Context.TELECOM_SERVICE); 232 233 // Depending on where this call originated make it an incoming call or outgoing 234 // (represented as unknown call in telecom since). Since BluetoothHeadsetClientCall is a 235 // parcelable we simply pack the entire object in there. 236 Bundle b = new Bundle(); 237 if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING || 238 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING || 239 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) { 240 // This is an outgoing call. Even if it is an active call we do not have a way of 241 // putting that parcelable in a seaprate field. 242 b.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, call); 243 manager.addNewUnknownCall(handle, b); 244 } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_INCOMING) { 245 // This is an incoming call. 246 b.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, call); 247 manager.addNewIncomingCall(handle, b); 248 } 249 } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) { 250 Log.d(TAG, "Removing number " + number); 251 synchronized (this) { 252 mConnections.remove(ConnectionKey.getKey(call)); 253 } 254 } 255 updateConferenceableConnections(); 256 } 257 258 // This method is called whenever there is a new incoming call (or right after BT connection). 259 @Override 260 public Connection onCreateIncomingConnection( 261 PhoneAccountHandle connectionManagerAccount, 262 ConnectionRequest request) { 263 Log.d(TAG, "onCreateIncomingConnection " + connectionManagerAccount + " req: " + request); 264 if (connectionManagerAccount != null && 265 !getHandle().equals(connectionManagerAccount)) { 266 Log.w(TAG, "HfpClient does not support having a connection manager"); 267 return null; 268 } 269 270 // We should already have a connection by this time. 271 BluetoothHeadsetClientCall call = 272 request.getExtras().getParcelable( 273 TelecomManager.EXTRA_INCOMING_CALL_EXTRAS); 274 HfpClientConnection connection = null; 275 276 synchronized (this) { 277 connection = mConnections.get(ConnectionKey.getKey(call)); 278 } 279 280 if (connection != null) { 281 connection.onAdded(); 282 updateConferenceableConnections(); 283 return connection; 284 } else { 285 Log.e(TAG, "Connection should exist in our db, if it doesn't we dont know how to " + 286 "handle this call."); 287 return null; 288 } 289 } 290 291 // This method is called *only if* Dialer UI is used to place an outgoing call. 292 @Override 293 public Connection onCreateOutgoingConnection( 294 PhoneAccountHandle connectionManagerAccount, 295 ConnectionRequest request) { 296 Log.d(TAG, "onCreateOutgoingConnection " + connectionManagerAccount); 297 if (connectionManagerAccount != null && 298 !getHandle().equals(connectionManagerAccount)) { 299 Log.w(TAG, "HfpClient does not support having a connection manager"); 300 return null; 301 } 302 303 HfpClientConnection connection = 304 buildConnection(getDevice(request.getAccountHandle()), null, request.getAddress()); 305 connection.onAdded(); 306 return connection; 307 } 308 309 // This method is called when: 310 // 1. Outgoing call created from the AG. 311 // 2. Call transfer from AG -> HF (on connection when existed call present). 312 @Override 313 public Connection onCreateUnknownConnection( 314 PhoneAccountHandle connectionManagerAccount, 315 ConnectionRequest request) { 316 Log.d(TAG, "onCreateUnknownConnection " + connectionManagerAccount); 317 if (connectionManagerAccount != null && 318 !getHandle().equals(connectionManagerAccount)) { 319 Log.w(TAG, "HfpClient does not support having a connection manager"); 320 return null; 321 } 322 323 // We should already have a connection by this time. 324 BluetoothHeadsetClientCall call = 325 request.getExtras().getParcelable( 326 TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS); 327 Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null); 328 329 HfpClientConnection connection = null; 330 synchronized (this) { 331 connection = mConnections.get(ConnectionKey.getKey(call)); 332 } 333 334 if (connection != null) { 335 connection.onAdded(); 336 updateConferenceableConnections(); 337 return connection; 338 } else { 339 Log.e(TAG, "Connection should exist in our db, if it doesn't we dont know how to " + 340 "handle this call " + call); 341 return null; 342 } 343 } 344 345 @Override 346 public void onConference(Connection connection1, Connection connection2) { 347 Log.d(TAG, "onConference " + connection1 + " " + connection2); 348 if (mConference == null) { 349 BluetoothDevice device = getDevice(getHandle()); 350 mConference = new HfpClientConference(getHandle(), device, mHeadsetProfile); 351 addConference(mConference); 352 } 353 mConference.setActive(); 354 if (connection1.getConference() == null) { 355 mConference.addConnection(connection1); 356 } 357 if (connection2.getConference() == null) { 358 mConference.addConnection(connection2); 359 } 360 } 361 362 private void updateConferenceableConnections() { 363 Collection<HfpClientConnection> all = mConnections.values(); 364 365 List<Connection> held = new ArrayList<>(); 366 List<Connection> active = new ArrayList<>(); 367 List<Connection> group = new ArrayList<>(); 368 for (HfpClientConnection connection : all) { 369 switch (connection.getState()) { 370 case Connection.STATE_ACTIVE: 371 active.add(connection); 372 break; 373 case Connection.STATE_HOLDING: 374 held.add(connection); 375 break; 376 default: 377 break; 378 } 379 if (connection.inConference()) { 380 group.add(connection); 381 } 382 } 383 for (Connection connection : held) { 384 connection.setConferenceableConnections(active); 385 } 386 for (Connection connection : active) { 387 connection.setConferenceableConnections(held); 388 } 389 if (group.size() > 1 && mConference == null) { 390 BluetoothDevice device = getDevice(getHandle()); 391 mConference = new HfpClientConference(getHandle(), device, mHeadsetProfile); 392 if (group.get(0).getState() == Connection.STATE_ACTIVE) { 393 mConference.setActive(); 394 } else { 395 mConference.setOnHold(); 396 } 397 for (Connection connection : group) { 398 mConference.addConnection(connection); 399 } 400 addConference(mConference); 401 } 402 if (mConference != null) { 403 List<Connection> toRemove = new ArrayList<>(); 404 for (Connection connection : mConference.getConnections()) { 405 if (!((HfpClientConnection) connection).inConference()) { 406 toRemove.add(connection); 407 } 408 } 409 for (Connection connection : toRemove) { 410 mConference.removeConnection(connection); 411 } 412 if (mConference.getConnections().size() <= 1) { 413 mConference.destroy(); 414 mConference = null; 415 } else { 416 List<Connection> notConferenced = new ArrayList<>(); 417 for (Connection connection : all) { 418 if (connection.getConference() == null && 419 (connection.getState() == Connection.STATE_HOLDING || 420 connection.getState() == Connection.STATE_ACTIVE)) { 421 if (((HfpClientConnection) connection).inConference()) { 422 mConference.addConnection(connection); 423 } else { 424 notConferenced.add(connection); 425 } 426 } 427 } 428 mConference.setConferenceableConnections(notConferenced); 429 } 430 } 431 } 432 433 private synchronized void disconnectAll() { 434 for (HfpClientConnection connection : mConnections.values()) { 435 connection.onHfpDisconnected(); 436 } 437 mConnections.clear(); 438 if (mConference != null) { 439 mConference.destroy(); 440 mConference = null; 441 } 442 } 443 444 private BluetoothDevice getDevice(PhoneAccountHandle handle) { 445 PhoneAccount account = mTelecomManager.getPhoneAccount(handle); 446 String btAddr = account.getAddress().getSchemeSpecificPart(); 447 return mAdapter.getRemoteDevice(btAddr); 448 } 449 450 private synchronized HfpClientConnection buildConnection( 451 BluetoothDevice device, BluetoothHeadsetClientCall call, Uri number) { 452 Log.d(TAG, "Creating connection on " + device + " for " + call + "/" + number); 453 HfpClientConnection connection = 454 new HfpClientConnection(this, device, mHeadsetProfile, call, number); 455 mConnections.put(new ConnectionKey(ConnectionKey.INVALID_ID, number), connection); 456 return connection; 457 } 458 459 BluetoothProfile.ServiceListener mServiceListener = new BluetoothProfile.ServiceListener() { 460 @Override 461 public void onServiceConnected(int profile, BluetoothProfile proxy) { 462 Log.d(TAG, "onServiceConnected"); 463 mHeadsetProfile = (BluetoothHeadsetClient) proxy; 464 465 List<BluetoothDevice> devices = mHeadsetProfile.getConnectedDevices(); 466 if (devices == null || devices.size() != 1) { 467 Log.w(TAG, "No connected or more than one connected devices found." + devices); 468 } else { // We have exactly one device connected. 469 Log.d(TAG, "Creating phone account."); 470 synchronized (HfpClientConnectionService.this) { 471 mDevice = devices.get(0); 472 mDevicePhoneAccount = getAccount(HfpClientConnectionService.this, mDevice); 473 mTelecomManager.registerPhoneAccount(mDevicePhoneAccount); 474 mTelecomManager.enablePhoneAccount( 475 mDevicePhoneAccount.getAccountHandle(), true); 476 mTelecomManager.setUserSelectedOutgoingPhoneAccount( 477 mDevicePhoneAccount.getAccountHandle()); 478 } 479 } 480 481 for (HfpClientConnection connection : mConnections.values()) { 482 connection.onHfpConnected(mHeadsetProfile); 483 } 484 485 List<BluetoothHeadsetClientCall> calls = mHeadsetProfile.getCurrentCalls(mDevice); 486 Log.d(TAG, "Got calls " + calls); 487 if (calls != null) { 488 for (BluetoothHeadsetClientCall call : calls) { 489 handleCall(call); 490 } 491 } 492 493 if (mPendingAcceptCall) { 494 mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_NONE); 495 mPendingAcceptCall = false; 496 } 497 } 498 499 @Override 500 public void onServiceDisconnected(int profile) { 501 Log.d(TAG, "onServiceDisconnected " + profile); 502 mHeadsetProfile = null; 503 disconnectAll(); 504 } 505 }; 506 507 public static boolean hasHfpClientEcc(BluetoothHeadsetClient client, BluetoothDevice device) { 508 Bundle features = client.getCurrentAgEvents(device); 509 return features == null ? false : 510 features.getBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ECC, false); 511 } 512 513 public synchronized PhoneAccountHandle getHandle() { 514 if (mDevicePhoneAccount == null) throw new IllegalStateException("Handle null??"); 515 return mDevicePhoneAccount.getAccountHandle(); 516 } 517 518 public static PhoneAccount getAccount(Context context, BluetoothDevice device) { 519 Uri addr = Uri.fromParts(HfpClientConnectionService.HFP_SCHEME, device.getAddress(), null); 520 PhoneAccountHandle handle = new PhoneAccountHandle( 521 new ComponentName(context, HfpClientConnectionService.class), device.getAddress()); 522 PhoneAccount account = 523 new PhoneAccount.Builder(handle, "HFP") 524 .setAddress(addr) 525 .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL)) 526 .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) 527 .build(); 528 Log.d(TAG, "phoneaccount: " + account); 529 return account; 530 } 531} 532