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(); onBluetoothAudioDisconnected()81 void onBluetoothAudioDisconnected(); 82 /** 83 * This gets called when we get an unexpected state change from Bluetooth. Their stack does 84 * weird things sometimes, so this is really a signal for the listener to refresh their 85 * internal state and make sure it matches up with what the BT stack is doing. 86 */ onUnexpectedBluetoothStateChange()87 void onUnexpectedBluetoothStateChange(); 88 } 89 90 /** 91 * Constants representing messages sent to the state machine. 92 * Messages are expected to be sent with {@link SomeArgs} as the obj. 93 * In all cases, arg1 will be the log session. 94 */ 95 // arg2: Address of the new device 96 public static final int NEW_DEVICE_CONNECTED = 1; 97 // arg2: Address of the lost device 98 public static final int LOST_DEVICE = 2; 99 100 // arg2 (optional): the address of the specific device to connect to. 101 public static final int CONNECT_BT = 100; 102 // No args. 103 public static final int DISCONNECT_BT = 101; 104 // arg2: the address of the device to connect to. 105 public static final int RETRY_BT_CONNECTION = 102; 106 107 // arg2: the address of the device that is on 108 public static final int BT_AUDIO_IS_ON = 200; 109 // arg2: the address of the device that lost BT audio 110 public static final int BT_AUDIO_LOST = 201; 111 112 // No args; only used internally 113 public static final int CONNECTION_TIMEOUT = 300; 114 115 // Get the current state and send it through the BlockingQueue<IState> provided as the object 116 // arg. 117 public static final int GET_CURRENT_STATE = 400; 118 119 // arg2: Runnable 120 public static final int RUN_RUNNABLE = 9001; 121 122 private static final int MAX_CONNECTION_RETRIES = 2; 123 124 // States 125 private final class AudioOffState extends State { 126 @Override getName()127 public String getName() { 128 return AUDIO_OFF_STATE_NAME; 129 } 130 131 @Override enter()132 public void enter() { 133 BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice(); 134 if (erroneouslyConnectedDevice != null) { 135 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " + 136 "Switching to audio-on state for that device.", erroneouslyConnectedDevice); 137 // change this to just transition to the new audio on state 138 transitionToActualState(); 139 } 140 cleanupStatesForDisconnectedDevices(); 141 if (mListener != null) { 142 mListener.onBluetoothAudioDisconnected(); 143 } 144 } 145 146 @Override processMessage(Message msg)147 public boolean processMessage(Message msg) { 148 if (msg.what == RUN_RUNNABLE) { 149 ((Runnable) msg.obj).run(); 150 return HANDLED; 151 } 152 153 SomeArgs args = (SomeArgs) msg.obj; 154 try { 155 switch (msg.what) { 156 case NEW_DEVICE_CONNECTED: 157 addDevice((String) args.arg2); 158 break; 159 case LOST_DEVICE: 160 removeDevice((String) args.arg2); 161 break; 162 case CONNECT_BT: 163 String actualAddress = connectBtAudio((String) args.arg2, 164 false /* switchingBtDevices*/); 165 166 if (actualAddress != null) { 167 transitionTo(getConnectingStateForAddress(actualAddress, 168 "AudioOff/CONNECT_BT")); 169 } else { 170 Log.w(LOG_TAG, "Tried to connect to %s but failed to connect to" + 171 " any BT device.", (String) args.arg2); 172 } 173 break; 174 case DISCONNECT_BT: 175 // Ignore. 176 break; 177 case RETRY_BT_CONNECTION: 178 Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2); 179 String retryAddress = connectBtAudio((String) args.arg2, args.argi1, 180 false /* switchingBtDevices*/); 181 182 if (retryAddress != null) { 183 transitionTo(getConnectingStateForAddress(retryAddress, 184 "AudioOff/RETRY_BT_CONNECTION")); 185 } else { 186 Log.i(LOG_TAG, "Retry failed."); 187 } 188 break; 189 case CONNECTION_TIMEOUT: 190 // Ignore. 191 break; 192 case BT_AUDIO_IS_ON: 193 String address = (String) args.arg2; 194 Log.w(LOG_TAG, "BT audio unexpectedly turned on from device %s", address); 195 transitionTo(getConnectedStateForAddress(address, 196 "AudioOff/BT_AUDIO_IS_ON")); 197 break; 198 case BT_AUDIO_LOST: 199 Log.i(LOG_TAG, "Received BT off for device %s while BT off.", 200 (String) args.arg2); 201 mListener.onUnexpectedBluetoothStateChange(); 202 break; 203 case GET_CURRENT_STATE: 204 BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3; 205 sink.offer(this); 206 break; 207 } 208 } finally { 209 args.recycle(); 210 } 211 return HANDLED; 212 } 213 } 214 215 private final class AudioConnectingState extends State { 216 private final String mDeviceAddress; 217 AudioConnectingState(String address)218 AudioConnectingState(String address) { 219 mDeviceAddress = address; 220 } 221 222 @Override getName()223 public String getName() { 224 return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress; 225 } 226 227 @Override enter()228 public void enter() { 229 SomeArgs args = SomeArgs.obtain(); 230 args.arg1 = Log.createSubsession(); 231 sendMessageDelayed(CONNECTION_TIMEOUT, args, 232 mTimeoutsAdapter.getBluetoothPendingTimeoutMillis( 233 mContext.getContentResolver())); 234 // Pretend like audio is connected when communicating w/ CARSM. 235 mListener.onBluetoothAudioConnected(); 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 BluetoothHeadset bluetoothHeadset = mDeviceManager.getBluetoothHeadset(); 854 if (bluetoothHeadset == null) { 855 Log.i(this, "isInbandRingingEnabled: no headset service available."); 856 return false; 857 } 858 return bluetoothHeadset.isInbandRingingEnabled(); 859 } 860 addDevice(String address)861 private boolean addDevice(String address) { 862 if (mAudioConnectingStates.containsKey(address)) { 863 Log.i(this, "Attempting to add device %s twice.", address); 864 return false; 865 } 866 AudioConnectedState audioConnectedState = new AudioConnectedState(address); 867 AudioConnectingState audioConnectingState = new AudioConnectingState(address); 868 mAudioConnectingStates.put(address, audioConnectingState); 869 mAudioConnectedStates.put(address, audioConnectedState); 870 addState(audioConnectedState); 871 addState(audioConnectingState); 872 return true; 873 } 874 removeDevice(String address)875 private boolean removeDevice(String address) { 876 if (!mAudioConnectingStates.containsKey(address)) { 877 Log.i(this, "Attempting to remove already-removed device %s", address); 878 return false; 879 } 880 statesToCleanUp.add(mAudioConnectingStates.remove(address)); 881 statesToCleanUp.add(mAudioConnectedStates.remove(address)); 882 mMostRecentlyUsedDevices.remove(address); 883 return true; 884 } 885 getConnectingStateForAddress(String address, String error)886 private AudioConnectingState getConnectingStateForAddress(String address, String error) { 887 if (!mAudioConnectingStates.containsKey(address)) { 888 Log.w(LOG_TAG, "Device being connected to does not have a corresponding state: %s", 889 error); 890 addDevice(address); 891 } 892 return mAudioConnectingStates.get(address); 893 } 894 getConnectedStateForAddress(String address, String error)895 private AudioConnectedState getConnectedStateForAddress(String address, String error) { 896 if (!mAudioConnectedStates.containsKey(address)) { 897 Log.w(LOG_TAG, "Device already connected to does" + 898 " not have a corresponding state: %s", error); 899 addDevice(address); 900 } 901 return mAudioConnectedStates.get(address); 902 } 903 904 /** 905 * Removes the states for disconnected devices from the state machine. Called when entering 906 * AudioOff so that none of the states-to-be-removed are active. 907 */ cleanupStatesForDisconnectedDevices()908 private void cleanupStatesForDisconnectedDevices() { 909 for (State state : statesToCleanUp) { 910 if (state != null) { 911 removeState(state); 912 } 913 } 914 statesToCleanUp.clear(); 915 } 916 917 @VisibleForTesting setInitialStateForTesting(String stateName, BluetoothDevice device)918 public void setInitialStateForTesting(String stateName, BluetoothDevice device) { 919 sendMessage(RUN_RUNNABLE, (Runnable) () -> { 920 switch (stateName) { 921 case AUDIO_OFF_STATE_NAME: 922 transitionTo(mAudioOffState); 923 break; 924 case AUDIO_CONNECTING_STATE_NAME_PREFIX: 925 transitionTo(getConnectingStateForAddress(device.getAddress(), 926 "setInitialStateForTesting")); 927 break; 928 case AUDIO_CONNECTED_STATE_NAME_PREFIX: 929 transitionTo(getConnectedStateForAddress(device.getAddress(), 930 "setInitialStateForTesting")); 931 break; 932 } 933 Log.i(LOG_TAG, "transition for testing done: %s", stateName); 934 }); 935 } 936 937 @VisibleForTesting setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType)938 public void setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType) { 939 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) { 940 mLeAudioActiveDeviceCache = device; 941 } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) { 942 mHearingAidActiveDeviceCache = device; 943 } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) { 944 mHfpActiveDeviceCache = device; 945 } 946 } 947 } 948