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