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