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.BluetoothAdapter; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothHeadset; 22 import android.bluetooth.BluetoothHearingAid; 23 import android.bluetooth.BluetoothProfile; 24 import android.bluetooth.BluetoothLeAudio; 25 import android.content.Context; 26 import android.os.Message; 27 import android.telecom.Log; 28 import android.telecom.Logging.Session; 29 import android.util.SparseArray; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.os.SomeArgs; 33 import com.android.internal.util.IState; 34 import com.android.internal.util.State; 35 import com.android.internal.util.StateMachine; 36 import com.android.server.telecom.TelecomSystem; 37 import com.android.server.telecom.Timeouts; 38 39 import java.util.ArrayList; 40 import java.util.Collection; 41 import java.util.HashMap; 42 import java.util.HashSet; 43 import java.util.LinkedHashSet; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Objects; 47 import java.util.Optional; 48 import java.util.Set; 49 import java.util.concurrent.BlockingQueue; 50 import java.util.concurrent.LinkedBlockingQueue; 51 import java.util.concurrent.TimeUnit; 52 53 public class BluetoothRouteManager extends StateMachine { 54 private static final String LOG_TAG = BluetoothRouteManager.class.getSimpleName(); 55 56 private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{ 57 put(NEW_DEVICE_CONNECTED, "NEW_DEVICE_CONNECTED"); 58 put(LOST_DEVICE, "LOST_DEVICE"); 59 put(CONNECT_BT, "CONNECT_BT"); 60 put(DISCONNECT_BT, "DISCONNECT_BT"); 61 put(RETRY_BT_CONNECTION, "RETRY_BT_CONNECTION"); 62 put(BT_AUDIO_IS_ON, "BT_AUDIO_IS_ON"); 63 put(BT_AUDIO_LOST, "BT_AUDIO_LOST"); 64 put(CONNECTION_TIMEOUT, "CONNECTION_TIMEOUT"); 65 put(GET_CURRENT_STATE, "GET_CURRENT_STATE"); 66 put(RUN_RUNNABLE, "RUN_RUNNABLE"); 67 }}; 68 69 public static final String AUDIO_OFF_STATE_NAME = "AudioOff"; 70 public static final String AUDIO_CONNECTING_STATE_NAME_PREFIX = "Connecting"; 71 public static final String AUDIO_CONNECTED_STATE_NAME_PREFIX = "Connected"; 72 73 // Timeout for querying the current state from the state machine handler. 74 private static final int GET_STATE_TIMEOUT = 1000; 75 76 public interface BluetoothStateListener { onBluetoothDeviceListChanged()77 void onBluetoothDeviceListChanged(); onBluetoothActiveDevicePresent()78 void onBluetoothActiveDevicePresent(); onBluetoothActiveDeviceGone()79 void onBluetoothActiveDeviceGone(); onBluetoothAudioConnected()80 void onBluetoothAudioConnected(); onBluetoothAudioConnecting()81 void onBluetoothAudioConnecting(); onBluetoothAudioDisconnected()82 void onBluetoothAudioDisconnected(); 83 /** 84 * This gets called when we get an unexpected state change from Bluetooth. Their stack does 85 * weird things sometimes, so this is really a signal for the listener to refresh their 86 * internal state and make sure it matches up with what the BT stack is doing. 87 */ onUnexpectedBluetoothStateChange()88 void onUnexpectedBluetoothStateChange(); 89 } 90 91 /** 92 * Constants representing messages sent to the state machine. 93 * Messages are expected to be sent with {@link SomeArgs} as the obj. 94 * In all cases, arg1 will be the log session. 95 */ 96 // arg2: Address of the new device 97 public static final int NEW_DEVICE_CONNECTED = 1; 98 // arg2: Address of the lost device 99 public static final int LOST_DEVICE = 2; 100 101 // arg2 (optional): the address of the specific device to connect to. 102 public static final int CONNECT_BT = 100; 103 // No args. 104 public static final int DISCONNECT_BT = 101; 105 // arg2: the address of the device to connect to. 106 public static final int RETRY_BT_CONNECTION = 102; 107 108 // arg2: the address of the device that is on 109 public static final int BT_AUDIO_IS_ON = 200; 110 // arg2: the address of the device that lost BT audio 111 public static final int BT_AUDIO_LOST = 201; 112 113 // No args; only used internally 114 public static final int CONNECTION_TIMEOUT = 300; 115 116 // Get the current state and send it through the BlockingQueue<IState> provided as the object 117 // arg. 118 public static final int GET_CURRENT_STATE = 400; 119 120 // arg2: Runnable 121 public static final int RUN_RUNNABLE = 9001; 122 123 private static final int MAX_CONNECTION_RETRIES = 2; 124 125 // States 126 private final class AudioOffState extends State { 127 @Override getName()128 public String getName() { 129 return AUDIO_OFF_STATE_NAME; 130 } 131 132 @Override enter()133 public void enter() { 134 BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice(); 135 if (erroneouslyConnectedDevice != null) { 136 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " + 137 "Switching to audio-on state for that device.", erroneouslyConnectedDevice); 138 // change this to just transition to the new audio on state 139 transitionToActualState(); 140 } 141 cleanupStatesForDisconnectedDevices(); 142 if (mListener != null) { 143 mListener.onBluetoothAudioDisconnected(); 144 } 145 } 146 147 @Override processMessage(Message msg)148 public boolean processMessage(Message msg) { 149 if (msg.what == RUN_RUNNABLE) { 150 ((Runnable) msg.obj).run(); 151 return HANDLED; 152 } 153 154 SomeArgs args = (SomeArgs) msg.obj; 155 try { 156 switch (msg.what) { 157 case NEW_DEVICE_CONNECTED: 158 addDevice((String) args.arg2); 159 break; 160 case LOST_DEVICE: 161 removeDevice((String) args.arg2); 162 break; 163 case CONNECT_BT: 164 String actualAddress = connectBtAudio((String) args.arg2, 165 false /* switchingBtDevices*/); 166 167 if (actualAddress != null) { 168 transitionTo(getConnectingStateForAddress(actualAddress, 169 "AudioOff/CONNECT_BT")); 170 } else { 171 Log.w(LOG_TAG, "Tried to connect to %s but failed to connect to" + 172 " any BT device.", (String) args.arg2); 173 } 174 break; 175 case DISCONNECT_BT: 176 // Ignore. 177 break; 178 case RETRY_BT_CONNECTION: 179 Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2); 180 String retryAddress = connectBtAudio((String) args.arg2, args.argi1, 181 false /* switchingBtDevices*/); 182 183 if (retryAddress != null) { 184 transitionTo(getConnectingStateForAddress(retryAddress, 185 "AudioOff/RETRY_BT_CONNECTION")); 186 } else { 187 Log.i(LOG_TAG, "Retry failed."); 188 } 189 break; 190 case CONNECTION_TIMEOUT: 191 // Ignore. 192 break; 193 case BT_AUDIO_IS_ON: 194 String address = (String) args.arg2; 195 Log.w(LOG_TAG, "BT audio unexpectedly turned on from device %s", address); 196 transitionTo(getConnectedStateForAddress(address, 197 "AudioOff/BT_AUDIO_IS_ON")); 198 break; 199 case BT_AUDIO_LOST: 200 Log.i(LOG_TAG, "Received BT off for device %s while BT off.", 201 (String) args.arg2); 202 mListener.onUnexpectedBluetoothStateChange(); 203 break; 204 case GET_CURRENT_STATE: 205 BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3; 206 sink.offer(this); 207 break; 208 } 209 } finally { 210 args.recycle(); 211 } 212 return HANDLED; 213 } 214 } 215 216 private final class AudioConnectingState extends State { 217 private final String mDeviceAddress; 218 AudioConnectingState(String address)219 AudioConnectingState(String address) { 220 mDeviceAddress = address; 221 } 222 223 @Override getName()224 public String getName() { 225 return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress; 226 } 227 228 @Override enter()229 public void enter() { 230 SomeArgs args = SomeArgs.obtain(); 231 args.arg1 = Log.createSubsession(); 232 sendMessageDelayed(CONNECTION_TIMEOUT, args, 233 mTimeoutsAdapter.getBluetoothPendingTimeoutMillis( 234 mContext.getContentResolver())); 235 mListener.onBluetoothAudioConnecting(); 236 } 237 238 @Override exit()239 public void exit() { 240 removeMessages(CONNECTION_TIMEOUT); 241 } 242 243 @Override processMessage(Message msg)244 public boolean processMessage(Message msg) { 245 if (msg.what == RUN_RUNNABLE) { 246 ((Runnable) msg.obj).run(); 247 return HANDLED; 248 } 249 250 SomeArgs args = (SomeArgs) msg.obj; 251 String address = (String) args.arg2; 252 boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address); 253 try { 254 switch (msg.what) { 255 case NEW_DEVICE_CONNECTED: 256 // If the device isn't new, don't bother passing it up. 257 addDevice(address); 258 break; 259 case LOST_DEVICE: 260 removeDevice((String) args.arg2); 261 if (Objects.equals(address, mDeviceAddress)) { 262 transitionToActualState(); 263 } 264 break; 265 case CONNECT_BT: 266 if (!switchingBtDevices) { 267 // Ignore repeated connection attempts to the same device 268 break; 269 } 270 271 String actualAddress = connectBtAudio(address, 272 true /* switchingBtDevices*/); 273 if (actualAddress != null) { 274 transitionTo(getConnectingStateForAddress(actualAddress, 275 "AudioConnecting/CONNECT_BT")); 276 } else { 277 Log.w(LOG_TAG, "Tried to connect to %s but failed" + 278 " to connect to any BT device.", (String) args.arg2); 279 } 280 break; 281 case DISCONNECT_BT: 282 mDeviceManager.disconnectAudio(); 283 break; 284 case RETRY_BT_CONNECTION: 285 if (!switchingBtDevices) { 286 Log.d(LOG_TAG, "Retry message came through while connecting."); 287 break; 288 } 289 290 String retryAddress = connectBtAudio(address, args.argi1, 291 true /* switchingBtDevices*/); 292 if (retryAddress != null) { 293 transitionTo(getConnectingStateForAddress(retryAddress, 294 "AudioConnecting/RETRY_BT_CONNECTION")); 295 } else { 296 Log.i(LOG_TAG, "Retry failed."); 297 } 298 break; 299 case CONNECTION_TIMEOUT: 300 Log.i(LOG_TAG, "Connection with device %s timed out.", 301 mDeviceAddress); 302 transitionToActualState(); 303 break; 304 case BT_AUDIO_IS_ON: 305 if (Objects.equals(mDeviceAddress, address)) { 306 Log.i(LOG_TAG, "BT connection success for device %s.", mDeviceAddress); 307 transitionTo(mAudioConnectedStates.get(mDeviceAddress)); 308 } else { 309 Log.w(LOG_TAG, "In connecting state for device %s but %s" + 310 " is now connected", mDeviceAddress, address); 311 transitionTo(getConnectedStateForAddress(address, 312 "AudioConnecting/BT_AUDIO_IS_ON")); 313 } 314 break; 315 case BT_AUDIO_LOST: 316 if (Objects.equals(mDeviceAddress, address) || address == null) { 317 Log.i(LOG_TAG, "Connection with device %s failed.", 318 mDeviceAddress); 319 transitionToActualState(); 320 } else { 321 Log.w(LOG_TAG, "Got BT lost message for device %s while" + 322 " connecting to %s.", address, mDeviceAddress); 323 mListener.onUnexpectedBluetoothStateChange(); 324 } 325 break; 326 case GET_CURRENT_STATE: 327 BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3; 328 sink.offer(this); 329 break; 330 } 331 } finally { 332 args.recycle(); 333 } 334 return HANDLED; 335 } 336 } 337 338 private final class AudioConnectedState extends State { 339 private final String mDeviceAddress; 340 AudioConnectedState(String address)341 AudioConnectedState(String address) { 342 mDeviceAddress = address; 343 } 344 345 @Override getName()346 public String getName() { 347 return AUDIO_CONNECTED_STATE_NAME_PREFIX + ":" + mDeviceAddress; 348 } 349 350 @Override enter()351 public void enter() { 352 // Remove any of the retries that are still in the queue once any device becomes 353 // connected. 354 removeMessages(RETRY_BT_CONNECTION); 355 // Remove and add to ensure that the device is at the top. 356 mMostRecentlyUsedDevices.remove(mDeviceAddress); 357 mMostRecentlyUsedDevices.add(mDeviceAddress); 358 mListener.onBluetoothAudioConnected(); 359 } 360 361 @Override processMessage(Message msg)362 public boolean processMessage(Message msg) { 363 if (msg.what == RUN_RUNNABLE) { 364 ((Runnable) msg.obj).run(); 365 return HANDLED; 366 } 367 368 SomeArgs args = (SomeArgs) msg.obj; 369 String address = (String) args.arg2; 370 boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address); 371 try { 372 switch (msg.what) { 373 case NEW_DEVICE_CONNECTED: 374 addDevice(address); 375 break; 376 case LOST_DEVICE: 377 removeDevice((String) args.arg2); 378 if (Objects.equals(address, mDeviceAddress)) { 379 transitionToActualState(); 380 } 381 break; 382 case CONNECT_BT: 383 if (!switchingBtDevices) { 384 // Ignore connection to already connected device but still notify 385 // CallAudioRouteStateMachine since this might be a switch from other 386 // to this already connected BT audio 387 mListener.onBluetoothAudioConnected(); 388 break; 389 } 390 391 String actualAddress = connectBtAudio(address, 392 true /* switchingBtDevices*/); 393 if (actualAddress != null) { 394 transitionTo(getConnectingStateForAddress(address, 395 "AudioConnected/CONNECT_BT")); 396 } else { 397 Log.w(LOG_TAG, "Tried to connect to %s but failed" + 398 " to connect to any BT device.", (String) args.arg2); 399 } 400 break; 401 case DISCONNECT_BT: 402 mDeviceManager.disconnectAudio(); 403 break; 404 case RETRY_BT_CONNECTION: 405 if (!switchingBtDevices) { 406 Log.d(LOG_TAG, "Retry message came through while connected."); 407 break; 408 } 409 410 String retryAddress = connectBtAudio(address, args.argi1, 411 true /* switchingBtDevices*/); 412 if (retryAddress != null) { 413 transitionTo(getConnectingStateForAddress(retryAddress, 414 "AudioConnected/RETRY_BT_CONNECTION")); 415 } else { 416 Log.i(LOG_TAG, "Retry failed."); 417 } 418 break; 419 case CONNECTION_TIMEOUT: 420 Log.w(LOG_TAG, "Received CONNECTION_TIMEOUT while connected."); 421 break; 422 case BT_AUDIO_IS_ON: 423 if (Objects.equals(mDeviceAddress, address)) { 424 Log.i(LOG_TAG, 425 "Received redundant BT_AUDIO_IS_ON for %s", mDeviceAddress); 426 } else { 427 Log.w(LOG_TAG, "In connected state for device %s but %s" + 428 " is now connected", mDeviceAddress, address); 429 transitionTo(getConnectedStateForAddress(address, 430 "AudioConnected/BT_AUDIO_IS_ON")); 431 } 432 break; 433 case BT_AUDIO_LOST: 434 if (Objects.equals(mDeviceAddress, address) || address == null) { 435 Log.i(LOG_TAG, "BT connection with device %s lost.", mDeviceAddress); 436 transitionToActualState(); 437 } else { 438 Log.w(LOG_TAG, "Got BT lost message for device %s while" + 439 " connected to %s.", address, mDeviceAddress); 440 mListener.onUnexpectedBluetoothStateChange(); 441 } 442 break; 443 case GET_CURRENT_STATE: 444 BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3; 445 sink.offer(this); 446 break; 447 } 448 } finally { 449 args.recycle(); 450 } 451 return HANDLED; 452 } 453 } 454 455 private final State mAudioOffState; 456 private final Map<String, AudioConnectingState> mAudioConnectingStates = new HashMap<>(); 457 private final Map<String, AudioConnectedState> mAudioConnectedStates = new HashMap<>(); 458 private final Set<State> statesToCleanUp = new HashSet<>(); 459 private final LinkedHashSet<String> mMostRecentlyUsedDevices = new LinkedHashSet<>(); 460 461 private final TelecomSystem.SyncRoot mLock; 462 private final Context mContext; 463 private final Timeouts.Adapter mTimeoutsAdapter; 464 465 private BluetoothStateListener mListener; 466 private BluetoothDeviceManager mDeviceManager; 467 // Tracks the active devices in the BT stack (HFP or hearing aid or le audio). 468 private BluetoothDevice mHfpActiveDeviceCache = null; 469 private BluetoothDevice mHearingAidActiveDeviceCache = null; 470 private BluetoothDevice mLeAudioActiveDeviceCache = null; 471 private BluetoothDevice mMostRecentlyReportedActiveDevice = null; 472 BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock, BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter)473 public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock, 474 BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter) { 475 super(BluetoothRouteManager.class.getSimpleName()); 476 mContext = context; 477 mLock = lock; 478 mDeviceManager = deviceManager; 479 mDeviceManager.setBluetoothRouteManager(this); 480 mTimeoutsAdapter = timeoutsAdapter; 481 482 mAudioOffState = new AudioOffState(); 483 addState(mAudioOffState); 484 setInitialState(mAudioOffState); 485 start(); 486 } 487 488 @Override onPreHandleMessage(Message msg)489 protected void onPreHandleMessage(Message msg) { 490 if (msg.obj != null && msg.obj instanceof SomeArgs) { 491 SomeArgs args = (SomeArgs) msg.obj; 492 493 Log.continueSession(((Session) args.arg1), "BRM.pM_" + msg.what); 494 Log.i(LOG_TAG, "%s received message: %s.", this, 495 MESSAGE_CODE_TO_NAME.get(msg.what)); 496 } else if (msg.what == RUN_RUNNABLE && msg.obj instanceof Runnable) { 497 Log.i(LOG_TAG, "Running runnable for testing"); 498 } else { 499 Log.w(LOG_TAG, "Message sent must be of type nonnull SomeArgs, but got " + 500 (msg.obj == null ? "null" : msg.obj.getClass().getSimpleName())); 501 Log.w(LOG_TAG, "The message was of code %d = %s", 502 msg.what, MESSAGE_CODE_TO_NAME.get(msg.what)); 503 } 504 } 505 506 @Override onPostHandleMessage(Message msg)507 protected void onPostHandleMessage(Message msg) { 508 Log.endSession(); 509 } 510 511 /** 512 * Returns whether there is a BT device available to route audio to. 513 * @return true if there is a device, false otherwise. 514 */ isBluetoothAvailable()515 public boolean isBluetoothAvailable() { 516 return mDeviceManager.getNumConnectedDevices() > 0; 517 } 518 519 /** 520 * This method needs be synchronized with the local looper because getCurrentState() depends 521 * on the internal state of the state machine being consistent. Therefore, there may be a 522 * delay when calling this method. 523 * @return 524 */ isBluetoothAudioConnectedOrPending()525 public boolean isBluetoothAudioConnectedOrPending() { 526 SomeArgs args = SomeArgs.obtain(); 527 args.arg1 = Log.createSubsession(); 528 BlockingQueue<IState> stateQueue = new LinkedBlockingQueue<>(); 529 // Use arg3 because arg2 is reserved for the device address 530 args.arg3 = stateQueue; 531 sendMessage(GET_CURRENT_STATE, args); 532 533 try { 534 IState currentState = stateQueue.poll(GET_STATE_TIMEOUT, TimeUnit.MILLISECONDS); 535 if (currentState == null) { 536 Log.w(LOG_TAG, "Failed to get a state from the state machine in time -- Handler " + 537 "stuck?"); 538 return false; 539 } 540 return currentState != mAudioOffState; 541 } catch (InterruptedException e) { 542 Log.w(LOG_TAG, "isBluetoothAudioConnectedOrPending -- interrupted getting state"); 543 return false; 544 } 545 } 546 547 /** 548 * Attempts to connect to Bluetooth audio. If the first connection attempt synchronously 549 * fails, schedules a retry at a later time. 550 * @param address The MAC address of the bluetooth device to connect to. If null, the most 551 * recently used device will be used. 552 */ connectBluetoothAudio(String address)553 public void connectBluetoothAudio(String address) { 554 SomeArgs args = SomeArgs.obtain(); 555 args.arg1 = Log.createSubsession(); 556 args.arg2 = address; 557 sendMessage(CONNECT_BT, args); 558 } 559 560 /** 561 * Disconnects Bluetooth audio. 562 */ disconnectBluetoothAudio()563 public void disconnectBluetoothAudio() { 564 SomeArgs args = SomeArgs.obtain(); 565 args.arg1 = Log.createSubsession(); 566 sendMessage(DISCONNECT_BT, args); 567 } 568 disconnectAudio()569 public void disconnectAudio() { 570 mDeviceManager.disconnectAudio(); 571 } 572 cacheHearingAidDevice()573 public void cacheHearingAidDevice() { 574 mDeviceManager.cacheHearingAidDevice(); 575 } 576 restoreHearingAidDevice()577 public void restoreHearingAidDevice() { 578 mDeviceManager.restoreHearingAidDevice(); 579 } 580 setListener(BluetoothStateListener listener)581 public void setListener(BluetoothStateListener listener) { 582 mListener = listener; 583 } 584 onDeviceAdded(String newDeviceAddress)585 public void onDeviceAdded(String newDeviceAddress) { 586 SomeArgs args = SomeArgs.obtain(); 587 args.arg1 = Log.createSubsession(); 588 args.arg2 = newDeviceAddress; 589 sendMessage(NEW_DEVICE_CONNECTED, args); 590 591 mListener.onBluetoothDeviceListChanged(); 592 } 593 onDeviceLost(String lostDeviceAddress)594 public void onDeviceLost(String lostDeviceAddress) { 595 SomeArgs args = SomeArgs.obtain(); 596 args.arg1 = Log.createSubsession(); 597 args.arg2 = lostDeviceAddress; 598 sendMessage(LOST_DEVICE, args); 599 600 mListener.onBluetoothDeviceListChanged(); 601 } 602 onAudioOn(String address)603 public void onAudioOn(String address) { 604 Session session = Log.createSubsession(); 605 SomeArgs args = SomeArgs.obtain(); 606 args.arg1 = session; 607 args.arg2 = address; 608 sendMessage(BT_AUDIO_IS_ON, args); 609 } 610 onAudioLost(String address)611 public void onAudioLost(String address) { 612 Session session = Log.createSubsession(); 613 SomeArgs args = SomeArgs.obtain(); 614 args.arg1 = session; 615 args.arg2 = address; 616 sendMessage(BT_AUDIO_LOST, args); 617 } 618 onActiveDeviceChanged(BluetoothDevice device, int deviceType)619 public void onActiveDeviceChanged(BluetoothDevice device, int deviceType) { 620 boolean wasActiveDevicePresent = hasBtActiveDevice(); 621 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) { 622 mLeAudioActiveDeviceCache = device; 623 if (device == null) { 624 mDeviceManager.clearLeAudioCommunicationDevice(); 625 } 626 } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) { 627 mHearingAidActiveDeviceCache = device; 628 if (device == null) { 629 mDeviceManager.clearHearingAidCommunicationDevice(); 630 } 631 } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) { 632 mHfpActiveDeviceCache = device; 633 } else { 634 return; 635 } 636 637 if (device != null) mMostRecentlyReportedActiveDevice = device; 638 639 boolean isActiveDevicePresent = hasBtActiveDevice(); 640 641 if (wasActiveDevicePresent && !isActiveDevicePresent) { 642 mListener.onBluetoothActiveDeviceGone(); 643 } else if (!wasActiveDevicePresent && isActiveDevicePresent) { 644 mListener.onBluetoothActiveDevicePresent(); 645 } 646 } 647 hasBtActiveDevice()648 public boolean hasBtActiveDevice() { 649 return mLeAudioActiveDeviceCache != null || 650 mHearingAidActiveDeviceCache != null || 651 mHfpActiveDeviceCache != null; 652 } 653 isCachedLeAudioDevice(BluetoothDevice device)654 public boolean isCachedLeAudioDevice(BluetoothDevice device) { 655 return mLeAudioActiveDeviceCache != null && mLeAudioActiveDeviceCache.equals(device); 656 } 657 isCachedHearingAidDevice(BluetoothDevice device)658 public boolean isCachedHearingAidDevice(BluetoothDevice device) { 659 return mHearingAidActiveDeviceCache != null && mHearingAidActiveDeviceCache.equals(device); 660 } 661 getConnectedDevices()662 public Collection<BluetoothDevice> getConnectedDevices() { 663 return mDeviceManager.getUniqueConnectedDevices(); 664 } 665 connectBtAudio(String address, boolean switchingBtDevices)666 private String connectBtAudio(String address, boolean switchingBtDevices) { 667 return connectBtAudio(address, 0, switchingBtDevices); 668 } 669 670 /** 671 * Initiates a connection to the BT address specified. 672 * Note: This method is not synchronized on the Telecom lock, so don't try and call back into 673 * Telecom from within it. 674 * @param address The address that should be tried first. May be null. 675 * @param retryCount The number of times this connection attempt has been retried. 676 * @param switchingBtDevices Used when there is existing audio connection to other Bt device. 677 * @return The address of the device that's actually being connected to, or null if no 678 * connection was successful. 679 */ connectBtAudio(String address, int retryCount, boolean switchingBtDevices)680 private String connectBtAudio(String address, int retryCount, boolean switchingBtDevices) { 681 Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices(); 682 Optional<BluetoothDevice> matchingDevice = deviceList.stream() 683 .filter(d -> Objects.equals(d.getAddress(), address)) 684 .findAny(); 685 686 if (switchingBtDevices) { 687 /* When new Bluetooth connects audio, make sure previous one has disconnected audio. */ 688 mDeviceManager.disconnectAudio(); 689 } 690 691 String actualAddress = matchingDevice.isPresent() 692 ? address : getActiveDeviceAddress(); 693 if (actualAddress == null) { 694 Log.i(this, "No device specified and BT stack has no active device." 695 + " Using arbitrary device"); 696 if (deviceList.size() > 0) { 697 actualAddress = deviceList.iterator().next().getAddress(); 698 } else { 699 Log.i(this, "No devices available at all. Not connecting."); 700 return null; 701 } 702 } 703 if (!matchingDevice.isPresent()) { 704 Log.i(this, "No device with address %s available. Using %s instead.", 705 address, actualAddress); 706 } 707 708 BluetoothDevice alreadyConnectedDevice = getBluetoothAudioConnectedDevice(); 709 if (alreadyConnectedDevice != null && alreadyConnectedDevice.getAddress().equals( 710 actualAddress)) { 711 Log.i(this, "trying to connect to already connected device -- skipping connection" 712 + " and going into the actual connected state."); 713 transitionToActualState(); 714 return null; 715 } 716 717 if (!mDeviceManager.connectAudio(actualAddress, switchingBtDevices)) { 718 boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES; 719 Log.w(LOG_TAG, "Could not connect to %s. Will %s", actualAddress, 720 shouldRetry ? "retry" : "not retry"); 721 if (shouldRetry) { 722 SomeArgs args = SomeArgs.obtain(); 723 args.arg1 = Log.createSubsession(); 724 args.arg2 = actualAddress; 725 args.argi1 = retryCount + 1; 726 sendMessageDelayed(RETRY_BT_CONNECTION, args, 727 mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis( 728 mContext.getContentResolver())); 729 } 730 return null; 731 } 732 733 return actualAddress; 734 } 735 736 private String getActiveDeviceAddress() { 737 if (mHfpActiveDeviceCache != null) { 738 return mHfpActiveDeviceCache.getAddress(); 739 } 740 if (mHearingAidActiveDeviceCache != null) { 741 return mHearingAidActiveDeviceCache.getAddress(); 742 } 743 if (mLeAudioActiveDeviceCache != null) { 744 return mLeAudioActiveDeviceCache.getAddress(); 745 } 746 return null; 747 } 748 749 private void transitionToActualState() { 750 BluetoothDevice possiblyAlreadyConnectedDevice = getBluetoothAudioConnectedDevice(); 751 if (possiblyAlreadyConnectedDevice != null) { 752 Log.i(LOG_TAG, "Device %s is already connected; going to AudioConnected.", 753 possiblyAlreadyConnectedDevice); 754 transitionTo(getConnectedStateForAddress( 755 possiblyAlreadyConnectedDevice.getAddress(), "transitionToActualState")); 756 } else { 757 transitionTo(mAudioOffState); 758 } 759 } 760 761 /** 762 * @return The BluetoothDevice that is connected to BT audio, null if none are connected. 763 */ 764 @VisibleForTesting 765 public BluetoothDevice getBluetoothAudioConnectedDevice() { 766 BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter(); 767 BluetoothHeadset bluetoothHeadset = mDeviceManager.getBluetoothHeadset(); 768 BluetoothHearingAid bluetoothHearingAid = mDeviceManager.getBluetoothHearingAid(); 769 BluetoothLeAudio bluetoothLeAudio = mDeviceManager.getLeAudioService(); 770 771 BluetoothDevice hfpAudioOnDevice = null; 772 BluetoothDevice hearingAidActiveDevice = null; 773 BluetoothDevice leAudioActiveDevice = null; 774 775 if (bluetoothAdapter == null) { 776 Log.i(this, "getBluetoothAudioConnectedDevice: no adapter available."); 777 return null; 778 } 779 if (bluetoothHeadset == null && bluetoothHearingAid == null && bluetoothLeAudio == null) { 780 Log.i(this, "getBluetoothAudioConnectedDevice: no service available."); 781 return null; 782 } 783 784 int activeDevices = 0; 785 if (bluetoothHeadset != null) { 786 for (BluetoothDevice device : bluetoothAdapter.getActiveDevices( 787 BluetoothProfile.HEADSET)) { 788 hfpAudioOnDevice = device; 789 break; 790 } 791 792 if (hfpAudioOnDevice != null && bluetoothHeadset.getAudioState(hfpAudioOnDevice) 793 == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { 794 hfpAudioOnDevice = null; 795 } else { 796 activeDevices++; 797 } 798 } 799 800 if (bluetoothHearingAid != null) { 801 if (mDeviceManager.isHearingAidSetAsCommunicationDevice()) { 802 for (BluetoothDevice device : bluetoothAdapter.getActiveDevices( 803 BluetoothProfile.HEARING_AID)) { 804 if (device != null) { 805 hearingAidActiveDevice = device; 806 activeDevices++; 807 break; 808 } 809 } 810 } 811 } 812 813 if (bluetoothLeAudio != null) { 814 if (mDeviceManager.isLeAudioCommunicationDevice()) { 815 for (BluetoothDevice device : bluetoothAdapter.getActiveDevices( 816 BluetoothProfile.LE_AUDIO)) { 817 if (device != null) { 818 leAudioActiveDevice = device; 819 activeDevices++; 820 break; 821 } 822 } 823 } 824 } 825 826 // Return the active device reported by either HFP, hearing aid or le audio. If more than 827 // one is reporting active devices, go with the most recent one as reported by the receiver. 828 if (activeDevices > 1) { 829 Log.i(this, "More than one profile reporting active devices. Going with the most" 830 + " recently reported active device: %s", mMostRecentlyReportedActiveDevice); 831 return mMostRecentlyReportedActiveDevice; 832 } 833 834 if (leAudioActiveDevice != null) { 835 return leAudioActiveDevice; 836 } 837 838 if (hearingAidActiveDevice != null) { 839 return hearingAidActiveDevice; 840 } 841 842 return hfpAudioOnDevice; 843 } 844 845 /** 846 * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an 847 * active connection. 848 * 849 * @return true if in-band ringing is enabled, false if in-band ringing is disabled 850 */ 851 @VisibleForTesting isInbandRingingEnabled()852 public boolean isInbandRingingEnabled() { 853 return mDeviceManager.isInbandRingingEnabled(); 854 } 855 addDevice(String address)856 private boolean addDevice(String address) { 857 if (mAudioConnectingStates.containsKey(address)) { 858 Log.i(this, "Attempting to add device %s twice.", address); 859 return false; 860 } 861 AudioConnectedState audioConnectedState = new AudioConnectedState(address); 862 AudioConnectingState audioConnectingState = new AudioConnectingState(address); 863 mAudioConnectingStates.put(address, audioConnectingState); 864 mAudioConnectedStates.put(address, audioConnectedState); 865 addState(audioConnectedState); 866 addState(audioConnectingState); 867 return true; 868 } 869 removeDevice(String address)870 private boolean removeDevice(String address) { 871 if (!mAudioConnectingStates.containsKey(address)) { 872 Log.i(this, "Attempting to remove already-removed device %s", address); 873 return false; 874 } 875 statesToCleanUp.add(mAudioConnectingStates.remove(address)); 876 statesToCleanUp.add(mAudioConnectedStates.remove(address)); 877 mMostRecentlyUsedDevices.remove(address); 878 return true; 879 } 880 getConnectingStateForAddress(String address, String error)881 private AudioConnectingState getConnectingStateForAddress(String address, String error) { 882 if (!mAudioConnectingStates.containsKey(address)) { 883 Log.w(LOG_TAG, "Device being connected to does not have a corresponding state: %s", 884 error); 885 addDevice(address); 886 } 887 return mAudioConnectingStates.get(address); 888 } 889 getConnectedStateForAddress(String address, String error)890 private AudioConnectedState getConnectedStateForAddress(String address, String error) { 891 if (!mAudioConnectedStates.containsKey(address)) { 892 Log.w(LOG_TAG, "Device already connected to does" + 893 " not have a corresponding state: %s", error); 894 addDevice(address); 895 } 896 return mAudioConnectedStates.get(address); 897 } 898 899 /** 900 * Removes the states for disconnected devices from the state machine. Called when entering 901 * AudioOff so that none of the states-to-be-removed are active. 902 */ cleanupStatesForDisconnectedDevices()903 private void cleanupStatesForDisconnectedDevices() { 904 for (State state : statesToCleanUp) { 905 if (state != null) { 906 removeState(state); 907 } 908 } 909 statesToCleanUp.clear(); 910 } 911 912 @VisibleForTesting setInitialStateForTesting(String stateName, BluetoothDevice device)913 public void setInitialStateForTesting(String stateName, BluetoothDevice device) { 914 sendMessage(RUN_RUNNABLE, (Runnable) () -> { 915 switch (stateName) { 916 case AUDIO_OFF_STATE_NAME: 917 transitionTo(mAudioOffState); 918 break; 919 case AUDIO_CONNECTING_STATE_NAME_PREFIX: 920 transitionTo(getConnectingStateForAddress(device.getAddress(), 921 "setInitialStateForTesting")); 922 break; 923 case AUDIO_CONNECTED_STATE_NAME_PREFIX: 924 transitionTo(getConnectedStateForAddress(device.getAddress(), 925 "setInitialStateForTesting")); 926 break; 927 } 928 Log.i(LOG_TAG, "transition for testing done: %s", stateName); 929 }); 930 } 931 932 @VisibleForTesting setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType)933 public void setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType) { 934 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) { 935 mLeAudioActiveDeviceCache = device; 936 } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) { 937 mHearingAidActiveDeviceCache = device; 938 } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) { 939 mHfpActiveDeviceCache = device; 940 } 941 } 942 } 943