1 /* 2 * Copyright (C) 2018 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.bluetooth.hearingaid; 18 19 import static android.Manifest.permission.BLUETOOTH_CONNECT; 20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; 21 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 22 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN; 23 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; 24 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; 25 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; 26 27 import static java.util.Objects.requireNonNull; 28 29 import android.bluetooth.BluetoothDevice; 30 import android.bluetooth.BluetoothHearingAid; 31 import android.bluetooth.BluetoothHearingAid.AdvertisementServiceData; 32 import android.bluetooth.BluetoothProfile; 33 import android.bluetooth.BluetoothUuid; 34 import android.content.Intent; 35 import android.media.AudioDeviceCallback; 36 import android.media.AudioDeviceInfo; 37 import android.media.AudioManager; 38 import android.media.BluetoothProfileConnectionInfo; 39 import android.os.Handler; 40 import android.os.HandlerThread; 41 import android.os.Looper; 42 import android.os.ParcelUuid; 43 import android.os.UserHandle; 44 import android.sysprop.BluetoothProperties; 45 import android.util.Log; 46 47 import com.android.bluetooth.BluetoothStatsLog; 48 import com.android.bluetooth.Utils; 49 import com.android.bluetooth.btservice.AdapterService; 50 import com.android.bluetooth.btservice.ProfileService; 51 import com.android.bluetooth.btservice.storage.DatabaseManager; 52 import com.android.bluetooth.flags.Flags; 53 import com.android.internal.annotations.VisibleForTesting; 54 55 import java.util.ArrayList; 56 import java.util.HashMap; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.concurrent.ConcurrentHashMap; 60 61 /** Provides Bluetooth HearingAid profile, as a service in the Bluetooth application. */ 62 public class HearingAidService extends ProfileService { 63 private static final String TAG = HearingAidService.class.getSimpleName(); 64 65 private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1000; 66 67 // Upper limit of all HearingAid devices: Bonded or Connected 68 private static final int MAX_HEARING_AID_STATE_MACHINES = 10; 69 70 private static HearingAidService sHearingAidService; 71 72 private final AdapterService mAdapterService; 73 private final DatabaseManager mDatabaseManager; 74 private final HearingAidNativeInterface mNativeInterface; 75 private final AudioManager mAudioManager; 76 private final HandlerThread mStateMachinesThread; 77 private final Looper mStateMachinesLooper; 78 private final Handler mHandler; 79 80 private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines = new HashMap<>(); 81 private final Map<BluetoothDevice, Long> mDeviceHiSyncIdMap = new ConcurrentHashMap<>(); 82 private final Map<BluetoothDevice, Integer> mDeviceCapabilitiesMap = new HashMap<>(); 83 private final Map<Long, Boolean> mHiSyncIdConnectedMap = new HashMap<>(); 84 private final AudioManagerOnAudioDevicesAddedCallback mAudioManagerOnAudioDevicesAddedCallback = 85 new AudioManagerOnAudioDevicesAddedCallback(); 86 private final AudioManagerOnAudioDevicesRemovedCallback 87 mAudioManagerOnAudioDevicesRemovedCallback = 88 new AudioManagerOnAudioDevicesRemovedCallback(); 89 90 private BluetoothDevice mActiveDevice; 91 private long mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; 92 HearingAidService(AdapterService adapterService)93 public HearingAidService(AdapterService adapterService) { 94 this(adapterService, null, HearingAidNativeInterface.getInstance()); 95 } 96 97 @VisibleForTesting HearingAidService( AdapterService adapterService, Looper looper, HearingAidNativeInterface nativeInterface)98 HearingAidService( 99 AdapterService adapterService, 100 Looper looper, 101 HearingAidNativeInterface nativeInterface) { 102 super(requireNonNull(adapterService)); 103 mAdapterService = adapterService; 104 mDatabaseManager = requireNonNull(mAdapterService.getDatabase()); 105 if (looper == null) { 106 mHandler = new Handler(requireNonNull(Looper.getMainLooper())); 107 mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines"); 108 mStateMachinesThread.start(); 109 mStateMachinesLooper = mStateMachinesThread.getLooper(); 110 } else { 111 mHandler = new Handler(looper); 112 mStateMachinesThread = null; 113 mStateMachinesLooper = looper; 114 } 115 mNativeInterface = requireNonNull(nativeInterface); 116 mAudioManager = requireNonNull(getSystemService(AudioManager.class)); 117 118 setHearingAidService(this); 119 mNativeInterface.init(); 120 } 121 isEnabled()122 public static boolean isEnabled() { 123 return BluetoothProperties.isProfileAshaCentralEnabled().orElse(true); 124 } 125 126 @Override initBinder()127 protected IProfileServiceBinder initBinder() { 128 return new HearingAidServiceBinder(this); 129 } 130 131 @Override cleanup()132 public void cleanup() { 133 Log.i(TAG, "Cleanup HearingAid Service"); 134 135 // Cleanup native interface 136 mNativeInterface.cleanup(); 137 138 // Mark service as stopped 139 setHearingAidService(null); 140 141 // Destroy state machines and stop handler thread 142 synchronized (mStateMachines) { 143 for (HearingAidStateMachine sm : mStateMachines.values()) { 144 sm.doQuit(); 145 } 146 mStateMachines.clear(); 147 } 148 149 // Clear HiSyncId map, capabilities map and HiSyncId Connected map 150 mDeviceHiSyncIdMap.clear(); 151 mDeviceCapabilitiesMap.clear(); 152 mHiSyncIdConnectedMap.clear(); 153 154 if (mStateMachinesThread != null) { 155 try { 156 mStateMachinesThread.quitSafely(); 157 mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS); 158 } catch (InterruptedException e) { 159 // Do not rethrow as we are shutting down anyway 160 } 161 } 162 163 mHandler.removeCallbacksAndMessages(null); 164 165 mAudioManager.unregisterAudioDeviceCallback(mAudioManagerOnAudioDevicesAddedCallback); 166 mAudioManager.unregisterAudioDeviceCallback(mAudioManagerOnAudioDevicesRemovedCallback); 167 } 168 169 /** 170 * Get the HearingAidService instance 171 * 172 * @return HearingAidService instance 173 */ getHearingAidService()174 public static synchronized HearingAidService getHearingAidService() { 175 if (sHearingAidService == null) { 176 Log.w(TAG, "getHearingAidService(): service is NULL"); 177 return null; 178 } 179 180 if (!sHearingAidService.isAvailable()) { 181 Log.w(TAG, "getHearingAidService(): service is not available"); 182 return null; 183 } 184 return sHearingAidService; 185 } 186 187 @VisibleForTesting setHearingAidService(HearingAidService instance)188 static synchronized void setHearingAidService(HearingAidService instance) { 189 Log.d(TAG, "setHearingAidService(): set to: " + instance); 190 sHearingAidService = instance; 191 } 192 193 /** 194 * Connects the hearing aid profile to the passed in device 195 * 196 * @param device is the device with which we will connect the hearing aid profile 197 * @return true if hearing aid profile successfully connected, false otherwise 198 */ connect(BluetoothDevice device)199 public boolean connect(BluetoothDevice device) { 200 Log.d(TAG, "connect(): " + device); 201 if (device == null) { 202 return false; 203 } 204 205 if (getConnectionPolicy(device) == CONNECTION_POLICY_FORBIDDEN) { 206 return false; 207 } 208 final ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device); 209 if (!Utils.arrayContains(featureUuids, BluetoothUuid.HEARING_AID)) { 210 Log.e(TAG, "Cannot connect to " + device + " : Remote does not have Hearing Aid UUID"); 211 return false; 212 } 213 214 long hiSyncId = 215 mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID); 216 217 if (hiSyncId != mActiveDeviceHiSyncId 218 && hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID 219 && mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) { 220 for (BluetoothDevice connectedDevice : getConnectedDevices()) { 221 disconnect(connectedDevice); 222 } 223 } 224 225 synchronized (mStateMachines) { 226 HearingAidStateMachine smConnect = getOrCreateStateMachine(device); 227 if (smConnect == null) { 228 Log.e(TAG, "Cannot connect to " + device + " : no state machine"); 229 } 230 smConnect.sendMessage(HearingAidStateMachine.MESSAGE_CONNECT); 231 } 232 233 for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) { 234 if (device.equals(storedDevice)) { 235 continue; 236 } 237 if (mDeviceHiSyncIdMap.getOrDefault( 238 storedDevice, BluetoothHearingAid.HI_SYNC_ID_INVALID) 239 == hiSyncId) { 240 synchronized (mStateMachines) { 241 HearingAidStateMachine sm = getOrCreateStateMachine(storedDevice); 242 if (sm == null) { 243 Log.e(TAG, "Ignored connect request for " + device + " : no state machine"); 244 continue; 245 } 246 sm.sendMessage(HearingAidStateMachine.MESSAGE_CONNECT); 247 } 248 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID 249 && !device.equals(storedDevice)) { 250 break; 251 } 252 } 253 } 254 return true; 255 } 256 257 /** 258 * Disconnects hearing aid profile for the passed in device 259 * 260 * @param device is the device with which we want to disconnected the hearing aid profile 261 * @return true if hearing aid profile successfully disconnected, false otherwise 262 */ disconnect(BluetoothDevice device)263 public boolean disconnect(BluetoothDevice device) { 264 Log.d(TAG, "disconnect(): " + device); 265 if (device == null) { 266 return false; 267 } 268 long hiSyncId = 269 mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID); 270 271 for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) { 272 if (mDeviceHiSyncIdMap.getOrDefault( 273 storedDevice, BluetoothHearingAid.HI_SYNC_ID_INVALID) 274 == hiSyncId) { 275 synchronized (mStateMachines) { 276 HearingAidStateMachine sm = mStateMachines.get(storedDevice); 277 if (sm == null) { 278 Log.e( 279 TAG, 280 "Ignored disconnect request for " + device + " : no state machine"); 281 continue; 282 } 283 sm.sendMessage(HearingAidStateMachine.MESSAGE_DISCONNECT); 284 } 285 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID 286 && !device.equals(storedDevice)) { 287 break; 288 } 289 } 290 } 291 return true; 292 } 293 getConnectedDevices()294 public List<BluetoothDevice> getConnectedDevices() { 295 synchronized (mStateMachines) { 296 List<BluetoothDevice> devices = new ArrayList<>(); 297 for (HearingAidStateMachine sm : mStateMachines.values()) { 298 if (sm.isConnected()) { 299 devices.add(sm.getDevice()); 300 } 301 } 302 return devices; 303 } 304 } 305 306 /** 307 * Check any peer device is connected. The check considers any peer device is connected. 308 * 309 * @param device the peer device to connect to 310 * @return true if there are any peer device connected. 311 */ isConnectedPeerDevices(BluetoothDevice device)312 public boolean isConnectedPeerDevices(BluetoothDevice device) { 313 long hiSyncId = getHiSyncId(device); 314 if (getConnectedPeerDevices(hiSyncId).isEmpty()) { 315 return false; 316 } 317 return true; 318 } 319 320 /** 321 * Check whether can connect to a peer device. The check considers a number of factors during 322 * the evaluation. 323 * 324 * @param device the peer device to connect to 325 * @return true if connection is allowed, otherwise false 326 */ 327 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) okToConnect(BluetoothDevice device)328 public boolean okToConnect(BluetoothDevice device) { 329 // Check if this is an incoming connection in Quiet mode. 330 if (mAdapterService.isQuietModeEnabled()) { 331 Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled"); 332 return false; 333 } 334 // Check connection policy and accept or reject the connection. 335 int connectionPolicy = getConnectionPolicy(device); 336 if (!Flags.donotValidateBondStateFromProfiles()) { 337 int bondState = mAdapterService.getBondState(device); 338 // Allow this connection only if the device is bonded. Any attempt to connect while 339 // bonding would potentially lead to an unauthorized connection. 340 if (bondState != BluetoothDevice.BOND_BONDED) { 341 Log.w(TAG, "okToConnect: return false, bondState=" + bondState); 342 return false; 343 } 344 } 345 if (connectionPolicy != CONNECTION_POLICY_UNKNOWN 346 && connectionPolicy != CONNECTION_POLICY_ALLOWED) { 347 // Otherwise, reject the connection if connectionPolicy is not valid. 348 Log.w(TAG, "okToConnect: return false, connectionPolicy=" + connectionPolicy); 349 return false; 350 } 351 return true; 352 } 353 getDevicesMatchingConnectionStates(int[] states)354 List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 355 ArrayList<BluetoothDevice> devices = new ArrayList<>(); 356 if (states == null) { 357 return devices; 358 } 359 final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); 360 if (bondedDevices == null) { 361 return devices; 362 } 363 synchronized (mStateMachines) { 364 for (BluetoothDevice device : bondedDevices) { 365 final ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device); 366 if (!Utils.arrayContains(featureUuids, BluetoothUuid.HEARING_AID)) { 367 continue; 368 } 369 int connectionState = STATE_DISCONNECTED; 370 HearingAidStateMachine sm = mStateMachines.get(device); 371 if (sm != null) { 372 connectionState = sm.getConnectionState(); 373 } 374 for (int state : states) { 375 if (connectionState == state) { 376 devices.add(device); 377 break; 378 } 379 } 380 } 381 return devices; 382 } 383 } 384 385 /** 386 * Get the list of devices that have state machines. 387 * 388 * @return the list of devices that have state machines 389 */ 390 @VisibleForTesting getDevices()391 List<BluetoothDevice> getDevices() { 392 List<BluetoothDevice> devices = new ArrayList<>(); 393 synchronized (mStateMachines) { 394 for (HearingAidStateMachine sm : mStateMachines.values()) { 395 devices.add(sm.getDevice()); 396 } 397 return devices; 398 } 399 } 400 401 /** 402 * Get the HiSyncIdMap for testing 403 * 404 * @return mDeviceHiSyncIdMap 405 */ 406 @VisibleForTesting getHiSyncIdMap()407 Map<BluetoothDevice, Long> getHiSyncIdMap() { 408 return mDeviceHiSyncIdMap; 409 } 410 411 /** 412 * Get the current connection state of the profile 413 * 414 * @param device is the remote bluetooth device 415 * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected, {@link 416 * BluetoothProfile#STATE_CONNECTING} if this profile is being connected, {@link 417 * BluetoothProfile#STATE_CONNECTED} if this profile is connected, or {@link 418 * BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected 419 */ getConnectionState(BluetoothDevice device)420 public int getConnectionState(BluetoothDevice device) { 421 synchronized (mStateMachines) { 422 HearingAidStateMachine sm = mStateMachines.get(device); 423 if (sm == null) { 424 return STATE_DISCONNECTED; 425 } 426 return sm.getConnectionState(); 427 } 428 } 429 430 /** 431 * Set connection policy of the profile and connects it if connectionPolicy is {@link 432 * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link 433 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} 434 * 435 * <p>The device should already be paired. Connection policy can be one of: {@link 436 * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link 437 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link 438 * BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 439 * 440 * @param device Paired bluetooth device 441 * @param connectionPolicy is the connection policy to set to for this profile 442 * @return true if connectionPolicy is set, false on error 443 */ setConnectionPolicy(BluetoothDevice device, int connectionPolicy)444 public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { 445 Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); 446 447 if (!mDatabaseManager.setProfileConnectionPolicy( 448 device, BluetoothProfile.HEARING_AID, connectionPolicy)) { 449 return false; 450 } 451 if (connectionPolicy == CONNECTION_POLICY_ALLOWED) { 452 connect(device); 453 } else if (connectionPolicy == CONNECTION_POLICY_FORBIDDEN) { 454 disconnect(device); 455 } 456 return true; 457 } 458 459 /** 460 * Get the connection policy of the profile. 461 * 462 * <p>The connection policy can be any of: {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, 463 * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link 464 * BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 465 * 466 * @param device Bluetooth device 467 * @return connection policy of the device 468 */ getConnectionPolicy(BluetoothDevice device)469 public int getConnectionPolicy(BluetoothDevice device) { 470 return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID); 471 } 472 setVolume(int volume)473 void setVolume(int volume) { 474 mNativeInterface.setVolume(volume); 475 } 476 getHiSyncId(BluetoothDevice device)477 public long getHiSyncId(BluetoothDevice device) { 478 if (device == null) { 479 return BluetoothHearingAid.HI_SYNC_ID_INVALID; 480 } 481 return mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID); 482 } 483 getCapabilities(BluetoothDevice device)484 int getCapabilities(BluetoothDevice device) { 485 return mDeviceCapabilitiesMap.getOrDefault(device, -1); 486 } 487 getAdvertisementServiceData(BluetoothDevice device)488 AdvertisementServiceData getAdvertisementServiceData(BluetoothDevice device) { 489 int capability = mAdapterService.getAshaCapability(device); 490 int id = mAdapterService.getAshaTruncatedHiSyncId(device); 491 if (capability < 0) { 492 Log.i(TAG, "device does not have AdvertisementServiceData"); 493 return null; 494 } 495 return new AdvertisementServiceData(capability, id); 496 } 497 498 /** 499 * Remove the active device. 500 * 501 * @param stopAudio whether to stop current media playback. 502 * @return true on success, otherwise false 503 */ removeActiveDevice(boolean stopAudio)504 public boolean removeActiveDevice(boolean stopAudio) { 505 Log.d(TAG, "removeActiveDevice: stopAudio=" + stopAudio); 506 synchronized (mStateMachines) { 507 if (mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) { 508 reportActiveDevice(null, stopAudio); 509 mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; 510 } 511 } 512 return true; 513 } 514 515 /** 516 * Set the active device. 517 * 518 * @param device the new active device. Should not be null. 519 * @return true on success, otherwise false 520 */ setActiveDevice(BluetoothDevice device)521 public boolean setActiveDevice(BluetoothDevice device) { 522 if (device == null) { 523 Log.e(TAG, "setActiveDevice: device should not be null!"); 524 return removeActiveDevice(true); 525 } 526 Log.d(TAG, "setActiveDevice: " + device); 527 synchronized (mStateMachines) { 528 /* No action needed since this is the same device as previously activated */ 529 if (device.equals(mActiveDevice)) { 530 Log.d(TAG, "setActiveDevice: The device is already active. Ignoring."); 531 return true; 532 } 533 534 if (getConnectionState(device) != STATE_CONNECTED) { 535 Log.e(TAG, "setActiveDevice(" + device + "): failed because device not connected"); 536 return false; 537 } 538 Long deviceHiSyncId = 539 mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID); 540 if (deviceHiSyncId != mActiveDeviceHiSyncId) { 541 mActiveDeviceHiSyncId = deviceHiSyncId; 542 reportActiveDevice(device, false); 543 } 544 } 545 return true; 546 } 547 548 /** 549 * Get the connected physical Hearing Aid devices that are active 550 * 551 * @return the list of active devices. The first element is the left active device; the second 552 * element is the right active device. If either or both side is not active, it will be null 553 * on that position 554 */ getActiveDevices()555 public List<BluetoothDevice> getActiveDevices() { 556 ArrayList<BluetoothDevice> activeDevices = new ArrayList<>(); 557 activeDevices.add(null); 558 activeDevices.add(null); 559 560 synchronized (mStateMachines) { 561 long activeDeviceHiSyncId = mActiveDeviceHiSyncId; 562 if (activeDeviceHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) { 563 return activeDevices; 564 } 565 566 mDeviceHiSyncIdMap.entrySet().stream() 567 .filter(entry -> activeDeviceHiSyncId == entry.getValue()) 568 .map(Map.Entry::getKey) 569 .filter(device -> getConnectionState(device) == STATE_CONNECTED) 570 .forEach( 571 device -> { 572 int deviceSide = getCapabilities(device) & 1; 573 if (deviceSide == BluetoothHearingAid.SIDE_RIGHT) { 574 activeDevices.set(1, device); 575 } else { 576 activeDevices.set(0, device); 577 } 578 }); 579 } 580 581 return activeDevices; 582 } 583 messageFromNative(HearingAidStackEvent stackEvent)584 void messageFromNative(HearingAidStackEvent stackEvent) { 585 requireNonNull(stackEvent.device); 586 587 if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_DEVICE_AVAILABLE) { 588 BluetoothDevice device = stackEvent.device; 589 int capabilities = stackEvent.valueInt1; 590 long hiSyncId = stackEvent.valueLong2; 591 Log.d( 592 TAG, 593 ("Device available: device=" + device) 594 + (" capabilities=" + capabilities) 595 + (" hiSyncId=" + hiSyncId)); 596 mDeviceCapabilitiesMap.put(device, capabilities); 597 mDeviceHiSyncIdMap.put(device, hiSyncId); 598 return; 599 } 600 601 synchronized (mStateMachines) { 602 BluetoothDevice device = stackEvent.device; 603 HearingAidStateMachine sm = mStateMachines.get(device); 604 if (sm == null) { 605 if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) { 606 switch (stackEvent.valueInt1) { 607 case STATE_CONNECTED: 608 case STATE_CONNECTING: 609 sm = getOrCreateStateMachine(device); 610 break; 611 default: 612 break; 613 } 614 } 615 } 616 if (sm == null) { 617 Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent); 618 return; 619 } 620 sm.sendMessage(HearingAidStateMachine.MESSAGE_STACK_EVENT, stackEvent); 621 } 622 } 623 notifyActiveDeviceChanged()624 private void notifyActiveDeviceChanged() { 625 mAdapterService.handleActiveDeviceChange(BluetoothProfile.HEARING_AID, mActiveDevice); 626 Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED); 627 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mActiveDevice); 628 intent.addFlags( 629 Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT 630 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 631 sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_CONNECT); 632 } 633 634 /* Notifications of audio device disconnection events. */ 635 private class AudioManagerOnAudioDevicesRemovedCallback extends AudioDeviceCallback { 636 @Override onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices)637 public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { 638 for (AudioDeviceInfo deviceInfo : removedDevices) { 639 if (deviceInfo.getType() == AudioDeviceInfo.TYPE_HEARING_AID) { 640 Log.d(TAG, " onAudioDevicesRemoved: device type: " + deviceInfo.getType()); 641 if (mAudioManager != null) { 642 notifyActiveDeviceChanged(); 643 mAudioManager.unregisterAudioDeviceCallback(this); 644 } else { 645 Log.w(TAG, "onAudioDevicesRemoved: mAudioManager is null"); 646 } 647 } 648 } 649 } 650 } 651 652 /* Notifications of audio device connection events. */ 653 private class AudioManagerOnAudioDevicesAddedCallback extends AudioDeviceCallback { 654 @Override onAudioDevicesAdded(AudioDeviceInfo[] addedDevices)655 public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { 656 for (AudioDeviceInfo deviceInfo : addedDevices) { 657 if (deviceInfo.getType() == AudioDeviceInfo.TYPE_HEARING_AID) { 658 Log.d(TAG, " onAudioDevicesAdded: device type: " + deviceInfo.getType()); 659 if (mAudioManager != null) { 660 notifyActiveDeviceChanged(); 661 mAudioManager.unregisterAudioDeviceCallback(this); 662 } else { 663 Log.w(TAG, "onAudioDevicesAdded: mAudioManager is null"); 664 } 665 } 666 } 667 } 668 } 669 getOrCreateStateMachine(BluetoothDevice device)670 private HearingAidStateMachine getOrCreateStateMachine(BluetoothDevice device) { 671 if (device == null) { 672 Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null"); 673 return null; 674 } 675 synchronized (mStateMachines) { 676 HearingAidStateMachine sm = mStateMachines.get(device); 677 if (sm != null) { 678 return sm; 679 } 680 // Limit the maximum number of state machines to avoid DoS attack 681 if (mStateMachines.size() >= MAX_HEARING_AID_STATE_MACHINES) { 682 Log.e( 683 TAG, 684 "Maximum number of HearingAid state machines reached: " 685 + MAX_HEARING_AID_STATE_MACHINES); 686 return null; 687 } 688 Log.d(TAG, "Creating a new state machine for " + device); 689 sm = new HearingAidStateMachine(this, device, mNativeInterface, mStateMachinesLooper); 690 sm.start(); 691 mStateMachines.put(device, sm); 692 return sm; 693 } 694 } 695 696 /** 697 * Report the active device change to the active device manager and the media framework. 698 * 699 * @param device the new active device; or null if no active device 700 * @param stopAudio whether to stop audio when device is null. 701 */ reportActiveDevice(BluetoothDevice device, boolean stopAudio)702 private void reportActiveDevice(BluetoothDevice device, boolean stopAudio) { 703 Log.d(TAG, "reportActiveDevice: device=" + device + " stopAudio=" + stopAudio); 704 705 if (device != null && stopAudio) { 706 Log.e(TAG, "Illegal arguments: stopAudio should be false when device is not null!"); 707 stopAudio = false; 708 } 709 710 // Note: This is just a safety check for handling illegal call - setActiveDevice(null). 711 if (device == null && stopAudio && getConnectionState(mActiveDevice) == STATE_CONNECTED) { 712 Log.e( 713 TAG, 714 "Illegal arguments: stopAudio should be false when the active hearing aid " 715 + "is still connected!"); 716 stopAudio = false; 717 } 718 719 BluetoothDevice previousAudioDevice = mActiveDevice; 720 721 mActiveDevice = device; 722 723 BluetoothStatsLog.write( 724 BluetoothStatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, 725 BluetoothProfile.HEARING_AID, 726 mAdapterService.obfuscateAddress(device), 727 mAdapterService.getMetricId(device)); 728 729 Log.d( 730 TAG, 731 "Hearing Aid audio: " 732 + previousAudioDevice 733 + " -> " 734 + device 735 + ". Stop audio: " 736 + stopAudio); 737 738 if (device != null) { 739 mAudioManager.registerAudioDeviceCallback( 740 mAudioManagerOnAudioDevicesAddedCallback, mHandler); 741 } else { 742 mAudioManager.registerAudioDeviceCallback( 743 mAudioManagerOnAudioDevicesRemovedCallback, mHandler); 744 } 745 746 mAudioManager.handleBluetoothActiveDeviceChanged( 747 device, 748 previousAudioDevice, 749 BluetoothProfileConnectionInfo.createHearingAidInfo(!stopAudio)); 750 } 751 752 /** Process a change in the bonding state for a device */ handleBondStateChanged(BluetoothDevice device, int fromState, int toState)753 public void handleBondStateChanged(BluetoothDevice device, int fromState, int toState) { 754 mHandler.post(() -> bondStateChanged(device, toState)); 755 } 756 757 /** 758 * Remove state machine if the bonding for a device is removed 759 * 760 * @param device the device whose bonding state has changed 761 * @param bondState the new bond state for the device. Possible values are: {@link 762 * BluetoothDevice#BOND_NONE}, {@link BluetoothDevice#BOND_BONDING}, {@link 763 * BluetoothDevice#BOND_BONDED}. 764 */ 765 @VisibleForTesting bondStateChanged(BluetoothDevice device, int bondState)766 void bondStateChanged(BluetoothDevice device, int bondState) { 767 Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); 768 // Remove state machine if the bonding for a device is removed 769 if (bondState != BluetoothDevice.BOND_NONE) { 770 return; 771 } 772 mDeviceHiSyncIdMap.remove(device); 773 synchronized (mStateMachines) { 774 HearingAidStateMachine sm = mStateMachines.get(device); 775 if (sm == null) { 776 return; 777 } 778 if (sm.getConnectionState() != STATE_DISCONNECTED) { 779 Log.i(TAG, "Disconnecting device because it was unbonded."); 780 disconnect(device); 781 return; 782 } 783 removeStateMachine(device); 784 } 785 } 786 removeStateMachine(BluetoothDevice device)787 private void removeStateMachine(BluetoothDevice device) { 788 synchronized (mStateMachines) { 789 HearingAidStateMachine sm = mStateMachines.get(device); 790 if (sm == null) { 791 Log.w( 792 TAG, 793 "removeStateMachine: device " + device + " does not have a state machine"); 794 return; 795 } 796 Log.i(TAG, "removeStateMachine: removing state machine for device: " + device); 797 sm.doQuit(); 798 mStateMachines.remove(device); 799 } 800 } 801 getConnectedPeerDevices(long hiSyncId)802 public List<BluetoothDevice> getConnectedPeerDevices(long hiSyncId) { 803 List<BluetoothDevice> result = new ArrayList<>(); 804 for (BluetoothDevice peerDevice : getConnectedDevices()) { 805 if (getHiSyncId(peerDevice) == hiSyncId) { 806 result.add(peerDevice); 807 } 808 } 809 return result; 810 } 811 connectionStateChanged(BluetoothDevice device, int fromState, int toState)812 synchronized void connectionStateChanged(BluetoothDevice device, int fromState, int toState) { 813 if ((device == null) || (fromState == toState)) { 814 Log.e( 815 TAG, 816 "connectionStateChanged: unexpected invocation. device=" 817 + device 818 + " fromState=" 819 + fromState 820 + " toState=" 821 + toState); 822 return; 823 } 824 if (toState == STATE_CONNECTED) { 825 long myHiSyncId = getHiSyncId(device); 826 if (!mHiSyncIdConnectedMap.getOrDefault(myHiSyncId, false)) { 827 mHiSyncIdConnectedMap.put(myHiSyncId, true); 828 } 829 } 830 if (fromState == STATE_CONNECTED && getConnectedDevices().isEmpty()) { 831 long myHiSyncId = getHiSyncId(device); 832 mHiSyncIdConnectedMap.put(myHiSyncId, false); 833 // ActiveDeviceManager will call removeActiveDevice(). 834 } 835 // Check if the device is disconnected - if unbond, remove the state machine 836 if (toState == STATE_DISCONNECTED) { 837 int bondState = mAdapterService.getBondState(device); 838 if (bondState == BluetoothDevice.BOND_NONE) { 839 Log.d(TAG, device + " is unbond. Remove state machine"); 840 removeStateMachine(device); 841 } 842 } 843 mAdapterService.notifyProfileConnectionStateChangeToGatt( 844 BluetoothProfile.HEARING_AID, fromState, toState); 845 mAdapterService 846 .getActiveDeviceManager() 847 .profileConnectionStateChanged( 848 BluetoothProfile.HEARING_AID, device, fromState, toState); 849 mAdapterService.updateProfileConnectionAdapterProperties( 850 device, BluetoothProfile.HEARING_AID, toState, fromState); 851 } 852 853 @Override dump(StringBuilder sb)854 public void dump(StringBuilder sb) { 855 super.dump(sb); 856 for (HearingAidStateMachine sm : mStateMachines.values()) { 857 sm.dump(sb); 858 } 859 } 860 } 861