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