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 17 package com.android.server.telecom.bluetooth; 18 19 import android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothHeadset; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.os.Message; 26 import android.telecom.Log; 27 import android.telecom.Logging.Session; 28 import android.util.SparseArray; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.internal.os.SomeArgs; 32 import com.android.internal.util.IState; 33 import com.android.internal.util.State; 34 import com.android.internal.util.StateMachine; 35 import com.android.server.telecom.BluetoothHeadsetProxy; 36 import com.android.server.telecom.TelecomSystem; 37 import com.android.server.telecom.Timeouts; 38 39 import java.util.HashMap; 40 import java.util.HashSet; 41 import java.util.LinkedHashSet; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Objects; 45 import java.util.Optional; 46 import java.util.Set; 47 import java.util.concurrent.CountDownLatch; 48 import java.util.concurrent.TimeUnit; 49 50 public class BluetoothRouteManager extends StateMachine { 51 private static final String LOG_TAG = BluetoothRouteManager.class.getSimpleName(); 52 53 private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{ 54 put(NEW_DEVICE_CONNECTED, "NEW_DEVICE_CONNECTED"); 55 put(LOST_DEVICE, "LOST_DEVICE"); 56 put(CONNECT_HFP, "CONNECT_HFP"); 57 put(DISCONNECT_HFP, "DISCONNECT_HFP"); 58 put(RETRY_HFP_CONNECTION, "RETRY_HFP_CONNECTION"); 59 put(HFP_IS_ON, "HFP_IS_ON"); 60 put(HFP_LOST, "HFP_LOST"); 61 put(CONNECTION_TIMEOUT, "CONNECTION_TIMEOUT"); 62 put(RUN_RUNNABLE, "RUN_RUNNABLE"); 63 }}; 64 65 // Constants for compatiblity with current CARSM/CARPA 66 // TODO: delete and replace with new direct interface to CARPA. 67 public static final int BLUETOOTH_UNINITIALIZED = 0; 68 public static final int BLUETOOTH_DISCONNECTED = 1; 69 public static final int BLUETOOTH_DEVICE_CONNECTED = 2; 70 public static final int BLUETOOTH_AUDIO_PENDING = 3; 71 public static final int BLUETOOTH_AUDIO_CONNECTED = 4; 72 73 public static final String AUDIO_OFF_STATE_NAME = "AudioOff"; 74 public static final String AUDIO_CONNECTING_STATE_NAME_PREFIX = "Connecting"; 75 public static final String AUDIO_CONNECTED_STATE_NAME_PREFIX = "Connected"; 76 77 public interface BluetoothStateListener { 78 void onBluetoothStateChange(int oldState, int newState); 79 } 80 81 // Broadcast receiver to receive audio state change broadcasts from the BT stack 82 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 83 @Override 84 public void onReceive(Context context, Intent intent) { 85 Log.startSession("BRM.oR"); 86 try { 87 String action = intent.getAction(); 88 89 if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { 90 int bluetoothHeadsetAudioState = 91 intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, 92 BluetoothHeadset.STATE_AUDIO_DISCONNECTED); 93 BluetoothDevice device = 94 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 95 if (device == null) { 96 Log.w(BluetoothRouteManager.this, "Got null device from broadcast. " + 97 "Ignoring."); 98 return; 99 } 100 101 Log.i(BluetoothRouteManager.this, "Device %s transitioned to audio state %d", 102 device.getAddress(), bluetoothHeadsetAudioState); 103 Session session = Log.createSubsession(); 104 SomeArgs args = SomeArgs.obtain(); 105 args.arg1 = session; 106 args.arg2 = device.getAddress(); 107 switch (bluetoothHeadsetAudioState) { 108 case BluetoothHeadset.STATE_AUDIO_CONNECTED: 109 sendMessage(HFP_IS_ON, args); 110 break; 111 case BluetoothHeadset.STATE_AUDIO_DISCONNECTED: 112 sendMessage(HFP_LOST, args); 113 break; 114 } 115 } 116 } finally { 117 Log.endSession(); 118 } 119 } 120 }; 121 122 /** 123 * Constants representing messages sent to the state machine. 124 * Messages are expected to be sent with {@link SomeArgs} as the obj. 125 * In all cases, arg1 will be the log session. 126 */ 127 // arg2: Address of the new device 128 public static final int NEW_DEVICE_CONNECTED = 1; 129 // arg2: Address of the lost device 130 public static final int LOST_DEVICE = 2; 131 132 // arg2 (optional): the address of the specific device to connect to. 133 public static final int CONNECT_HFP = 100; 134 // No args. 135 public static final int DISCONNECT_HFP = 101; 136 // arg2: the address of the device to connect to. 137 public static final int RETRY_HFP_CONNECTION = 102; 138 139 // arg2: the address of the device that is on 140 public static final int HFP_IS_ON = 200; 141 // arg2: the address of the device that lost HFP 142 public static final int HFP_LOST = 201; 143 144 // No args; only used internally 145 public static final int CONNECTION_TIMEOUT = 300; 146 147 // arg2: Runnable 148 public static final int RUN_RUNNABLE = 9001; 149 150 // States 151 private final class AudioOffState extends State { 152 @Override 153 public String getName() { 154 return AUDIO_OFF_STATE_NAME; 155 } 156 157 @Override 158 public void enter() { 159 BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice(); 160 if (erroneouslyConnectedDevice != null) { 161 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " + 162 "Disconnecting.", erroneouslyConnectedDevice); 163 disconnectAudio(); 164 } 165 cleanupStatesForDisconnectedDevices(); 166 } 167 168 @Override 169 public boolean processMessage(Message msg) { 170 if (msg.what == RUN_RUNNABLE) { 171 ((Runnable) msg.obj).run(); 172 return HANDLED; 173 } 174 175 SomeArgs args = (SomeArgs) msg.obj; 176 try { 177 switch (msg.what) { 178 case NEW_DEVICE_CONNECTED: 179 // If the device isn't new, don't bother passing it up. 180 if (addDevice((String) args.arg2)) { 181 // TODO: replace with new interface 182 if (mDeviceManager.getNumConnectedDevices() == 1) { 183 mListener.onBluetoothStateChange( 184 BLUETOOTH_DISCONNECTED, BLUETOOTH_DEVICE_CONNECTED); 185 } 186 } 187 break; 188 case LOST_DEVICE: 189 // If the device has already been removed, don't bother passing it up. 190 if (removeDevice((String) args.arg2)) { 191 // TODO: replace with new interface 192 if (mDeviceManager.getNumConnectedDevices() == 0) { 193 mListener.onBluetoothStateChange( 194 BLUETOOTH_DEVICE_CONNECTED, BLUETOOTH_DISCONNECTED); 195 } 196 } 197 break; 198 case CONNECT_HFP: 199 String actualAddress = connectHfpAudio((String) args.arg2); 200 201 if (actualAddress != null) { 202 mListener.onBluetoothStateChange(BLUETOOTH_DEVICE_CONNECTED, 203 BLUETOOTH_AUDIO_PENDING); 204 transitionTo(getConnectingStateForAddress(actualAddress, 205 "AudioOff/CONNECT_HFP")); 206 } else { 207 Log.w(LOG_TAG, "Tried to connect to %s but failed to connect to" + 208 " any HFP device.", (String) args.arg2); 209 } 210 break; 211 case DISCONNECT_HFP: 212 // Ignore. 213 break; 214 case RETRY_HFP_CONNECTION: 215 Log.i(LOG_TAG, "Retrying HFP connection to %s", (String) args.arg2); 216 String retryAddress = connectHfpAudio((String) args.arg2, false); 217 218 if (retryAddress != null) { 219 mListener.onBluetoothStateChange(BLUETOOTH_DEVICE_CONNECTED, 220 BLUETOOTH_AUDIO_PENDING); 221 transitionTo(getConnectingStateForAddress(retryAddress, 222 "AudioOff/RETRY_HFP_CONNECTION")); 223 } else { 224 Log.i(LOG_TAG, "Retry failed."); 225 } 226 break; 227 case CONNECTION_TIMEOUT: 228 // Ignore. 229 break; 230 case HFP_IS_ON: 231 String address = (String) args.arg2; 232 Log.w(LOG_TAG, "HFP audio unexpectedly turned on from device %s", address); 233 mListener.onBluetoothStateChange(BLUETOOTH_DEVICE_CONNECTED, 234 BLUETOOTH_AUDIO_CONNECTED); 235 transitionTo(getConnectedStateForAddress(address, "AudioOff/HFP_IS_ON")); 236 break; 237 case HFP_LOST: 238 Log.i(LOG_TAG, "Received HFP off for device %s while HFP off.", 239 (String) args.arg2); 240 break; 241 } 242 } finally { 243 args.recycle(); 244 } 245 return HANDLED; 246 } 247 } 248 249 private final class AudioConnectingState extends State { 250 private final String mDeviceAddress; 251 252 AudioConnectingState(String address) { 253 mDeviceAddress = address; 254 } 255 256 @Override 257 public String getName() { 258 return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress; 259 } 260 261 @Override 262 public void enter() { 263 SomeArgs args = SomeArgs.obtain(); 264 args.arg1 = Log.createSubsession(); 265 sendMessageDelayed(CONNECTION_TIMEOUT, args, 266 mTimeoutsAdapter.getBluetoothPendingTimeoutMillis( 267 mContext.getContentResolver())); 268 } 269 270 @Override 271 public void exit() { 272 removeMessages(CONNECTION_TIMEOUT); 273 } 274 275 @Override 276 public boolean processMessage(Message msg) { 277 if (msg.what == RUN_RUNNABLE) { 278 ((Runnable) msg.obj).run(); 279 return HANDLED; 280 } 281 282 SomeArgs args = (SomeArgs) msg.obj; 283 String address = (String) args.arg2; 284 try { 285 switch (msg.what) { 286 case NEW_DEVICE_CONNECTED: 287 // If the device isn't new, don't bother passing it up. 288 if (addDevice(address)) { 289 // TODO: replace with new interface 290 if (mDeviceManager.getNumConnectedDevices() == 1) { 291 Log.w(LOG_TAG, "Newly connected device is only device" + 292 " while audio pending."); 293 } 294 } 295 break; 296 case LOST_DEVICE: 297 removeDevice((String) args.arg2); 298 299 if (Objects.equals(address, mDeviceAddress)) { 300 String newAddress = connectHfpAudio(null); 301 if (newAddress != null) { 302 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING, 303 BLUETOOTH_AUDIO_PENDING); 304 transitionTo(getConnectingStateForAddress(newAddress, 305 "AudioConnecting/LOST_DEVICE")); 306 } else { 307 int numConnectedDevices = mDeviceManager.getNumConnectedDevices(); 308 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING, 309 numConnectedDevices == 0 ? BLUETOOTH_DISCONNECTED : 310 BLUETOOTH_DEVICE_CONNECTED); 311 transitionTo(mAudioOffState); 312 } 313 } 314 break; 315 case CONNECT_HFP: 316 if (Objects.equals(mDeviceAddress, address)) { 317 // Ignore repeated connection attempts to the same device 318 break; 319 } 320 String actualAddress = connectHfpAudio(address); 321 322 if (actualAddress != null) { 323 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING, 324 BLUETOOTH_AUDIO_PENDING); 325 transitionTo(getConnectingStateForAddress(actualAddress, 326 "AudioConnecting/CONNECT_HFP")); 327 } else { 328 Log.w(LOG_TAG, "Tried to connect to %s but failed" + 329 " to connect to any HFP device.", (String) args.arg2); 330 } 331 break; 332 case DISCONNECT_HFP: 333 disconnectAudio(); 334 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING, 335 BLUETOOTH_DEVICE_CONNECTED); 336 transitionTo(mAudioOffState); 337 break; 338 case RETRY_HFP_CONNECTION: 339 if (Objects.equals(address, mDeviceAddress)) { 340 Log.d(LOG_TAG, "Retry message came through while connecting."); 341 } else { 342 String retryAddress = connectHfpAudio(address, false); 343 if (retryAddress != null) { 344 transitionTo(getConnectingStateForAddress(retryAddress, 345 "AudioConnecting/RETRY_HFP_CONNECTION")); 346 } else { 347 Log.i(LOG_TAG, "Retry failed."); 348 } 349 } 350 break; 351 case CONNECTION_TIMEOUT: 352 Log.i(LOG_TAG, "Connection with device %s timed out.", 353 mDeviceAddress); 354 transitionToActualState(BLUETOOTH_AUDIO_PENDING); 355 break; 356 case HFP_IS_ON: 357 if (Objects.equals(mDeviceAddress, address)) { 358 Log.i(LOG_TAG, "HFP connection success for device %s.", mDeviceAddress); 359 transitionTo(mAudioConnectedStates.get(mDeviceAddress)); 360 } else { 361 Log.w(LOG_TAG, "In connecting state for device %s but %s" + 362 " is now connected", mDeviceAddress, address); 363 transitionTo(getConnectedStateForAddress(address, 364 "AudioConnecting/HFP_IS_ON")); 365 } 366 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING, 367 BLUETOOTH_AUDIO_CONNECTED); 368 break; 369 case HFP_LOST: 370 if (Objects.equals(mDeviceAddress, address)) { 371 Log.i(LOG_TAG, "Connection with device %s failed.", 372 mDeviceAddress); 373 transitionToActualState(BLUETOOTH_AUDIO_PENDING); 374 } else { 375 Log.w(LOG_TAG, "Got HFP lost message for device %s while" + 376 " connecting to %s.", address, mDeviceAddress); 377 } 378 break; 379 } 380 } finally { 381 args.recycle(); 382 } 383 return HANDLED; 384 } 385 } 386 387 private final class AudioConnectedState extends State { 388 private final String mDeviceAddress; 389 390 AudioConnectedState(String address) { 391 mDeviceAddress = address; 392 } 393 394 @Override 395 public String getName() { 396 return AUDIO_CONNECTED_STATE_NAME_PREFIX + ":" + mDeviceAddress; 397 } 398 399 @Override 400 public void enter() { 401 // Remove any of the retries that are still in the queue once any device becomes 402 // connected. 403 removeMessages(RETRY_HFP_CONNECTION); 404 // Remove and add to ensure that the device is at the top. 405 mMostRecentlyUsedDevices.remove(mDeviceAddress); 406 mMostRecentlyUsedDevices.add(mDeviceAddress); 407 } 408 409 @Override 410 public boolean processMessage(Message msg) { 411 if (msg.what == RUN_RUNNABLE) { 412 ((Runnable) msg.obj).run(); 413 return HANDLED; 414 } 415 416 SomeArgs args = (SomeArgs) msg.obj; 417 String address = (String) args.arg2; 418 try { 419 switch (msg.what) { 420 case NEW_DEVICE_CONNECTED: 421 // If the device isn't new, don't bother passing it up. 422 if (addDevice(address)) { 423 // TODO: Replace with new interface 424 if (mDeviceManager.getNumConnectedDevices() == 1) { 425 Log.w(LOG_TAG, "Newly connected device is only" + 426 " device while audio connected."); 427 } 428 } 429 break; 430 case LOST_DEVICE: 431 removeDevice((String) args.arg2); 432 433 if (Objects.equals(address, mDeviceAddress)) { 434 String newAddress = connectHfpAudio(null); 435 if (newAddress != null) { 436 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED, 437 BLUETOOTH_AUDIO_PENDING); 438 transitionTo(getConnectingStateForAddress(newAddress, 439 "AudioConnected/LOST_DEVICE")); 440 } else { 441 int numConnectedDevices = mDeviceManager.getNumConnectedDevices(); 442 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED, 443 numConnectedDevices == 0 ? BLUETOOTH_DISCONNECTED : 444 BLUETOOTH_DEVICE_CONNECTED); 445 transitionTo(mAudioOffState); 446 } 447 } 448 break; 449 case CONNECT_HFP: 450 if (Objects.equals(mDeviceAddress, address)) { 451 // Ignore connection to already connected device. 452 break; 453 } 454 String actualAddress = connectHfpAudio(address); 455 456 if (actualAddress != null) { 457 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED, 458 BLUETOOTH_AUDIO_PENDING); 459 transitionTo(getConnectingStateForAddress(address, 460 "AudioConnected/CONNECT_HFP")); 461 } else { 462 Log.w(LOG_TAG, "Tried to connect to %s but failed" + 463 " to connect to any HFP device.", (String) args.arg2); 464 } 465 break; 466 case DISCONNECT_HFP: 467 disconnectAudio(); 468 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED, 469 BLUETOOTH_DEVICE_CONNECTED); 470 transitionTo(mAudioOffState); 471 break; 472 case RETRY_HFP_CONNECTION: 473 if (Objects.equals(address, mDeviceAddress)) { 474 Log.d(LOG_TAG, "Retry message came through while connected."); 475 } else { 476 String retryAddress = connectHfpAudio(address, false); 477 if (retryAddress != null) { 478 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED, 479 BLUETOOTH_AUDIO_PENDING); 480 transitionTo(getConnectingStateForAddress(retryAddress, 481 "AudioConnected/RETRY_HFP_CONNECTION")); 482 } else { 483 Log.i(LOG_TAG, "Retry failed."); 484 } 485 } 486 break; 487 case CONNECTION_TIMEOUT: 488 Log.w(LOG_TAG, "Received CONNECTION_TIMEOUT while connected."); 489 break; 490 case HFP_IS_ON: 491 if (Objects.equals(mDeviceAddress, address)) { 492 Log.i(LOG_TAG, "Received redundant HFP_IS_ON for %s", mDeviceAddress); 493 } else { 494 Log.w(LOG_TAG, "In connected state for device %s but %s" + 495 " is now connected", mDeviceAddress, address); 496 transitionTo(getConnectedStateForAddress(address, 497 "AudioConnected/HFP_IS_ON")); 498 } 499 break; 500 case HFP_LOST: 501 if (Objects.equals(mDeviceAddress, address)) { 502 Log.i(LOG_TAG, "HFP connection with device %s lost.", mDeviceAddress); 503 String nextAddress = connectHfpAudio(null, mDeviceAddress); 504 if (nextAddress == null) { 505 Log.i(LOG_TAG, "No suitable fallback device. Going to AUDIO_OFF."); 506 transitionToActualState(BLUETOOTH_AUDIO_CONNECTED); 507 } else { 508 mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED, 509 BLUETOOTH_AUDIO_PENDING); 510 transitionTo(getConnectingStateForAddress(nextAddress, 511 "AudioConnected/HFP_LOST")); 512 } 513 } else { 514 Log.w(LOG_TAG, "Got HFP lost message for device %s while" + 515 " connected to %s.", address, mDeviceAddress); 516 } 517 break; 518 } 519 } finally { 520 args.recycle(); 521 } 522 return HANDLED; 523 } 524 } 525 526 private final State mAudioOffState; 527 private final Map<String, AudioConnectingState> mAudioConnectingStates = new HashMap<>(); 528 private final Map<String, AudioConnectedState> mAudioConnectedStates = new HashMap<>(); 529 private final Set<State> statesToCleanUp = new HashSet<>(); 530 private final LinkedHashSet<String> mMostRecentlyUsedDevices = new LinkedHashSet<>(); 531 532 private final TelecomSystem.SyncRoot mLock; 533 private final Context mContext; 534 private final Timeouts.Adapter mTimeoutsAdapter; 535 536 private BluetoothStateListener mListener; 537 private BluetoothDeviceManager mDeviceManager; 538 539 public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock, 540 BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter) { 541 super(BluetoothRouteManager.class.getSimpleName()); 542 mContext = context; 543 mLock = lock; 544 mDeviceManager = deviceManager; 545 mDeviceManager.setBluetoothRouteManager(this); 546 mTimeoutsAdapter = timeoutsAdapter; 547 548 IntentFilter intentFilter = new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); 549 context.registerReceiver(mReceiver, intentFilter); 550 551 mAudioOffState = new AudioOffState(); 552 addState(mAudioOffState); 553 setInitialState(mAudioOffState); 554 start(); 555 } 556 557 @Override 558 protected void onPreHandleMessage(Message msg) { 559 if (msg.obj != null && msg.obj instanceof SomeArgs) { 560 SomeArgs args = (SomeArgs) msg.obj; 561 562 Log.continueSession(((Session) args.arg1), "BRM.pM_" + msg.what); 563 Log.i(LOG_TAG, "Message received: %s.", MESSAGE_CODE_TO_NAME.get(msg.what)); 564 } else if (msg.what == RUN_RUNNABLE && msg.obj instanceof Runnable) { 565 Log.i(LOG_TAG, "Running runnable for testing"); 566 } else { 567 Log.w(LOG_TAG, "Message sent must be of type nonnull SomeArgs, but got " + 568 (msg.obj == null ? "null" : msg.obj.getClass().getSimpleName())); 569 Log.w(LOG_TAG, "The message was of code %d = %s", 570 msg.what, MESSAGE_CODE_TO_NAME.get(msg.what)); 571 } 572 } 573 574 @Override 575 protected void onPostHandleMessage(Message msg) { 576 Log.endSession(); 577 } 578 579 /** 580 * Returns whether there is a HFP device available to route audio to. 581 * @return true if there is a device, false otherwise. 582 */ 583 public boolean isBluetoothAvailable() { 584 return mDeviceManager.getNumConnectedDevices() > 0; 585 } 586 587 /** 588 * This method needs be synchronized with the local looper because getCurrentState() depends 589 * on the internal state of the state machine being consistent. Therefore, there may be a 590 * delay when calling this method. 591 * @return 592 */ 593 public boolean isBluetoothAudioConnectedOrPending() { 594 IState[] state = new IState[] {null}; 595 CountDownLatch latch = new CountDownLatch(1); 596 Runnable r = () -> { 597 state[0] = getCurrentState(); 598 latch.countDown(); 599 }; 600 sendMessage(RUN_RUNNABLE, r); 601 try { 602 latch.await(1000, TimeUnit.MILLISECONDS); 603 } catch (InterruptedException e) { 604 Log.w(LOG_TAG, "isBluetoothAudioConnectedOrPending -- interrupted getting state"); 605 return false; 606 } 607 return (state[0] != null) && (state[0] != mAudioOffState); 608 } 609 610 /** 611 * Attempts to connect to Bluetooth audio. If the first connection attempt synchronously 612 * fails, schedules a retry at a later time. 613 * @param address The MAC address of the bluetooth device to connect to. If null, the most 614 * recently used device will be used. 615 */ 616 public void connectBluetoothAudio(String address) { 617 SomeArgs args = SomeArgs.obtain(); 618 args.arg1 = Log.createSubsession(); 619 args.arg2 = address; 620 sendMessage(CONNECT_HFP, args); 621 } 622 623 /** 624 * Disconnects Bluetooth HFP audio. 625 */ 626 public void disconnectBluetoothAudio() { 627 SomeArgs args = SomeArgs.obtain(); 628 args.arg1 = Log.createSubsession(); 629 sendMessage(DISCONNECT_HFP, args); 630 } 631 632 public void setListener(BluetoothStateListener listener) { 633 mListener = listener; 634 } 635 636 public void onDeviceAdded(BluetoothDevice newDevice) { 637 SomeArgs args = SomeArgs.obtain(); 638 args.arg1 = Log.createSubsession(); 639 args.arg2 = newDevice.getAddress(); 640 sendMessage(NEW_DEVICE_CONNECTED, args); 641 } 642 643 public void onDeviceLost(BluetoothDevice lostDevice) { 644 SomeArgs args = SomeArgs.obtain(); 645 args.arg1 = Log.createSubsession(); 646 args.arg2 = lostDevice.getAddress(); 647 sendMessage(LOST_DEVICE, args); 648 } 649 650 private String connectHfpAudio(String address) { 651 return connectHfpAudio(address, true, null); 652 } 653 654 private String connectHfpAudio(String address, boolean shouldRetry) { 655 return connectHfpAudio(address, shouldRetry, null); 656 } 657 658 private String connectHfpAudio(String address, String excludeAddress) { 659 return connectHfpAudio(address, true, excludeAddress); 660 } 661 662 /** 663 * Initiates a HFP connection to the BT address specified. 664 * Note: This method is not synchronized on the Telecom lock, so don't try and call back into 665 * Telecom from within it. 666 * @param address The address that should be tried first. May be null. 667 * @param shouldRetry true if there should be a retry-with-backoff if connection is 668 * immediately unsuccessful, false otherwise. 669 * @param excludeAddress Don't connect to this address. 670 * @return The address of the device that's actually being connected to, or null if no 671 * connection was successful. 672 */ 673 private String connectHfpAudio(String address, boolean shouldRetry, String excludeAddress) { 674 BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService(); 675 if (bluetoothHeadset == null) { 676 Log.i(this, "connectHfpAudio: no headset service available."); 677 return null; 678 } 679 List<BluetoothDevice> deviceList = bluetoothHeadset.getConnectedDevices(); 680 Optional<BluetoothDevice> matchingDevice = deviceList.stream() 681 .filter(d -> Objects.equals(d.getAddress(), address)) 682 .findAny(); 683 684 String actualAddress = matchingDevice.isPresent() ? 685 address : getPreferredDevice(excludeAddress); 686 if (!matchingDevice.isPresent()) { 687 Log.i(this, "No device with address %s available. Using %s instead.", 688 address, actualAddress); 689 } 690 if (actualAddress != null && !connectAudio(actualAddress)) { 691 Log.w(LOG_TAG, "Could not connect to %s. Will %s", shouldRetry ? "retry" : "not retry"); 692 if (shouldRetry) { 693 SomeArgs args = SomeArgs.obtain(); 694 args.arg1 = Log.createSubsession(); 695 args.arg2 = actualAddress; 696 sendMessageDelayed(RETRY_HFP_CONNECTION, args, 697 mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis( 698 mContext.getContentResolver())); 699 } 700 return null; 701 } 702 703 return actualAddress; 704 } 705 706 private String getPreferredDevice(String excludeAddress) { 707 String preferredDevice = null; 708 for (String address : mMostRecentlyUsedDevices) { 709 if (!Objects.equals(excludeAddress, address)) { 710 preferredDevice = address; 711 } 712 } 713 if (preferredDevice == null) { 714 return mDeviceManager.getMostRecentlyConnectedDevice(excludeAddress); 715 } 716 return preferredDevice; 717 } 718 719 private void transitionToActualState(int currentBtState) { 720 BluetoothDevice possiblyAlreadyConnectedDevice = getBluetoothAudioConnectedDevice(); 721 if (possiblyAlreadyConnectedDevice != null) { 722 Log.i(LOG_TAG, "Device %s is already connected; going to AudioConnected.", 723 possiblyAlreadyConnectedDevice); 724 transitionTo(getConnectedStateForAddress( 725 possiblyAlreadyConnectedDevice.getAddress(), "transitionToActualState")); 726 // TODO: replace with new interface 727 mListener.onBluetoothStateChange(currentBtState, BLUETOOTH_AUDIO_CONNECTED); 728 } else { 729 transitionTo(mAudioOffState); 730 mListener.onBluetoothStateChange(currentBtState, 731 mDeviceManager.getNumConnectedDevices() > 0 ? 732 BLUETOOTH_DEVICE_CONNECTED : BLUETOOTH_DISCONNECTED); 733 } 734 } 735 736 /** 737 * @return The BluetoothDevice that is connected to BT audio, null if none are connected. 738 */ 739 @VisibleForTesting 740 public BluetoothDevice getBluetoothAudioConnectedDevice() { 741 BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService(); 742 if (bluetoothHeadset == null) { 743 Log.i(this, "getBluetoothAudioConnectedDevice: no headset service available."); 744 return null; 745 } 746 List<BluetoothDevice> deviceList = bluetoothHeadset.getConnectedDevices(); 747 748 for (int i = 0; i < deviceList.size(); i++) { 749 BluetoothDevice device = deviceList.get(i); 750 boolean isAudioOn = bluetoothHeadset.isAudioConnected(device); 751 Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn 752 + "for headset: " + device); 753 if (isAudioOn) { 754 return device; 755 } 756 } 757 return null; 758 } 759 760 private boolean connectAudio(String address) { 761 BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService(); 762 if (bluetoothHeadset == null) { 763 Log.w(this, "Trying to connect audio but no headset service exists."); 764 return false; 765 } 766 // TODO: update once connectAudio supports passing in a device. 767 return bluetoothHeadset.connectAudio(); 768 } 769 770 private void disconnectAudio() { 771 BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService(); 772 if (bluetoothHeadset == null) { 773 Log.w(this, "Trying to disconnect audio but no headset service exists."); 774 } else { 775 bluetoothHeadset.disconnectAudio(); 776 } 777 } 778 779 private boolean addDevice(String address) { 780 if (mAudioConnectingStates.containsKey(address)) { 781 Log.i(this, "Attempting to add device %s twice.", address); 782 return false; 783 } 784 AudioConnectedState audioConnectedState = new AudioConnectedState(address); 785 AudioConnectingState audioConnectingState = new AudioConnectingState(address); 786 mAudioConnectingStates.put(address, audioConnectingState); 787 mAudioConnectedStates.put(address, audioConnectedState); 788 addState(audioConnectedState); 789 addState(audioConnectingState); 790 return true; 791 } 792 793 private boolean removeDevice(String address) { 794 if (!mAudioConnectingStates.containsKey(address)) { 795 Log.i(this, "Attempting to remove already-removed device %s", address); 796 return false; 797 } 798 statesToCleanUp.add(mAudioConnectingStates.remove(address)); 799 statesToCleanUp.add(mAudioConnectedStates.remove(address)); 800 mMostRecentlyUsedDevices.remove(address); 801 return true; 802 } 803 804 private AudioConnectingState getConnectingStateForAddress(String address, String error) { 805 if (!mAudioConnectingStates.containsKey(address)) { 806 Log.w(LOG_TAG, "Device being connected to does not have a corresponding state: %s", 807 error); 808 addDevice(address); 809 } 810 return mAudioConnectingStates.get(address); 811 } 812 813 private AudioConnectedState getConnectedStateForAddress(String address, String error) { 814 if (!mAudioConnectedStates.containsKey(address)) { 815 Log.w(LOG_TAG, "Device already connected to does" + 816 " not have a corresponding state: %s", error); 817 addDevice(address); 818 } 819 return mAudioConnectedStates.get(address); 820 } 821 822 /** 823 * Removes the states for disconnected devices from the state machine. Called when entering 824 * AudioOff so that none of the states-to-be-removed are active. 825 */ 826 private void cleanupStatesForDisconnectedDevices() { 827 for (State state : statesToCleanUp) { 828 if (state != null) { 829 removeState(state); 830 } 831 } 832 statesToCleanUp.clear(); 833 } 834 835 @VisibleForTesting 836 public void setInitialStateForTesting(String stateName, BluetoothDevice device) { 837 switch (stateName) { 838 case AUDIO_OFF_STATE_NAME: 839 transitionTo(mAudioOffState); 840 break; 841 case AUDIO_CONNECTING_STATE_NAME_PREFIX: 842 transitionTo(getConnectingStateForAddress(device.getAddress(), 843 "setInitialStateForTesting")); 844 break; 845 case AUDIO_CONNECTED_STATE_NAME_PREFIX: 846 transitionTo(getConnectedStateForAddress(device.getAddress(), 847 "setInitialStateForTesting")); 848 break; 849 } 850 } 851 } 852