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