1 /* 2 * Copyright 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 android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothHearingAid; 21 import android.bluetooth.BluetoothProfile; 22 import android.bluetooth.BluetoothUuid; 23 import android.bluetooth.IBluetoothHearingAid; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.media.AudioManager; 29 import android.os.HandlerThread; 30 import android.os.ParcelUuid; 31 import android.util.Log; 32 import android.util.StatsLog; 33 34 import com.android.bluetooth.BluetoothMetricsProto; 35 import com.android.bluetooth.Utils; 36 import com.android.bluetooth.a2dp.A2dpService; 37 import com.android.bluetooth.btservice.AdapterService; 38 import com.android.bluetooth.btservice.MetricsLogger; 39 import com.android.bluetooth.btservice.ProfileService; 40 import com.android.bluetooth.btservice.ServiceFactory; 41 import com.android.internal.annotations.VisibleForTesting; 42 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.concurrent.ConcurrentHashMap; 49 50 /** 51 * Provides Bluetooth HearingAid profile, as a service in the Bluetooth application. 52 * @hide 53 */ 54 public class HearingAidService extends ProfileService { 55 private static final boolean DBG = true; 56 private static final String TAG = "HearingAidService"; 57 58 // Upper limit of all HearingAid devices: Bonded or Connected 59 private static final int MAX_HEARING_AID_STATE_MACHINES = 10; 60 private static HearingAidService sHearingAidService; 61 62 private AdapterService mAdapterService; 63 private HandlerThread mStateMachinesThread; 64 private BluetoothDevice mPreviousAudioDevice; 65 66 @VisibleForTesting 67 HearingAidNativeInterface mHearingAidNativeInterface; 68 @VisibleForTesting 69 AudioManager mAudioManager; 70 71 private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines = 72 new HashMap<>(); 73 private final Map<BluetoothDevice, Long> mDeviceHiSyncIdMap = new ConcurrentHashMap<>(); 74 private final Map<BluetoothDevice, Integer> mDeviceCapabilitiesMap = new HashMap<>(); 75 private final Map<Long, Boolean> mHiSyncIdConnectedMap = new HashMap<>(); 76 private long mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; 77 78 private BroadcastReceiver mBondStateChangedReceiver; 79 private BroadcastReceiver mConnectionStateChangedReceiver; 80 81 private final ServiceFactory mFactory = new ServiceFactory(); 82 83 @Override initBinder()84 protected IProfileServiceBinder initBinder() { 85 return new BluetoothHearingAidBinder(this); 86 } 87 88 @Override create()89 protected void create() { 90 if (DBG) { 91 Log.d(TAG, "create()"); 92 } 93 } 94 95 @Override start()96 protected boolean start() { 97 if (DBG) { 98 Log.d(TAG, "start()"); 99 } 100 if (sHearingAidService != null) { 101 throw new IllegalStateException("start() called twice"); 102 } 103 104 // Get AdapterService, HearingAidNativeInterface, AudioManager. 105 // None of them can be null. 106 mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(), 107 "AdapterService cannot be null when HearingAidService starts"); 108 mHearingAidNativeInterface = Objects.requireNonNull(HearingAidNativeInterface.getInstance(), 109 "HearingAidNativeInterface cannot be null when HearingAidService starts"); 110 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 111 Objects.requireNonNull(mAudioManager, 112 "AudioManager cannot be null when HearingAidService starts"); 113 114 // Start handler thread for state machines 115 mStateMachines.clear(); 116 mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines"); 117 mStateMachinesThread.start(); 118 119 // Clear HiSyncId map, capabilities map and HiSyncId Connected map 120 mDeviceHiSyncIdMap.clear(); 121 mDeviceCapabilitiesMap.clear(); 122 mHiSyncIdConnectedMap.clear(); 123 124 // Setup broadcast receivers 125 IntentFilter filter = new IntentFilter(); 126 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 127 mBondStateChangedReceiver = new BondStateChangedReceiver(); 128 registerReceiver(mBondStateChangedReceiver, filter); 129 filter = new IntentFilter(); 130 filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); 131 mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver(); 132 registerReceiver(mConnectionStateChangedReceiver, filter); 133 134 // Mark service as started 135 setHearingAidService(this); 136 137 // Initialize native interface 138 mHearingAidNativeInterface.init(); 139 140 return true; 141 } 142 143 @Override stop()144 protected boolean stop() { 145 if (DBG) { 146 Log.d(TAG, "stop()"); 147 } 148 if (sHearingAidService == null) { 149 Log.w(TAG, "stop() called before start()"); 150 return true; 151 } 152 153 // Cleanup native interface 154 mHearingAidNativeInterface.cleanup(); 155 mHearingAidNativeInterface = null; 156 157 // Mark service as stopped 158 setHearingAidService(null); 159 160 // Unregister broadcast receivers 161 unregisterReceiver(mBondStateChangedReceiver); 162 mBondStateChangedReceiver = null; 163 unregisterReceiver(mConnectionStateChangedReceiver); 164 mConnectionStateChangedReceiver = null; 165 166 // Destroy state machines and stop handler thread 167 synchronized (mStateMachines) { 168 for (HearingAidStateMachine sm : mStateMachines.values()) { 169 sm.doQuit(); 170 sm.cleanup(); 171 } 172 mStateMachines.clear(); 173 } 174 175 // Clear HiSyncId map, capabilities map and HiSyncId Connected map 176 mDeviceHiSyncIdMap.clear(); 177 mDeviceCapabilitiesMap.clear(); 178 mHiSyncIdConnectedMap.clear(); 179 180 if (mStateMachinesThread != null) { 181 mStateMachinesThread.quitSafely(); 182 mStateMachinesThread = null; 183 } 184 185 // Clear AdapterService, HearingAidNativeInterface 186 mAudioManager = null; 187 mHearingAidNativeInterface = null; 188 mAdapterService = null; 189 190 return true; 191 } 192 193 @Override cleanup()194 protected void cleanup() { 195 if (DBG) { 196 Log.d(TAG, "cleanup()"); 197 } 198 } 199 200 /** 201 * Get the HearingAidService instance 202 * @return HearingAidService instance 203 */ getHearingAidService()204 public static synchronized HearingAidService getHearingAidService() { 205 if (sHearingAidService == null) { 206 Log.w(TAG, "getHearingAidService(): service is NULL"); 207 return null; 208 } 209 210 if (!sHearingAidService.isAvailable()) { 211 Log.w(TAG, "getHearingAidService(): service is not available"); 212 return null; 213 } 214 return sHearingAidService; 215 } 216 setHearingAidService(HearingAidService instance)217 private static synchronized void setHearingAidService(HearingAidService instance) { 218 if (DBG) { 219 Log.d(TAG, "setHearingAidService(): set to: " + instance); 220 } 221 sHearingAidService = instance; 222 } 223 connect(BluetoothDevice device)224 boolean connect(BluetoothDevice device) { 225 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); 226 if (DBG) { 227 Log.d(TAG, "connect(): " + device); 228 } 229 if (device == null) { 230 return false; 231 } 232 233 if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) { 234 return false; 235 } 236 ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device); 237 if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) { 238 Log.e(TAG, "Cannot connect to " + device + " : Remote does not have Hearing Aid UUID"); 239 return false; 240 } 241 242 long hiSyncId = mDeviceHiSyncIdMap.getOrDefault(device, 243 BluetoothHearingAid.HI_SYNC_ID_INVALID); 244 245 if (hiSyncId != mActiveDeviceHiSyncId 246 && hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID 247 && mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) { 248 for (BluetoothDevice connectedDevice : getConnectedDevices()) { 249 disconnect(connectedDevice); 250 } 251 } 252 253 synchronized (mStateMachines) { 254 HearingAidStateMachine smConnect = getOrCreateStateMachine(device); 255 if (smConnect == null) { 256 Log.e(TAG, "Cannot connect to " + device + " : no state machine"); 257 } 258 smConnect.sendMessage(HearingAidStateMachine.CONNECT); 259 } 260 261 for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) { 262 if (device.equals(storedDevice)) { 263 continue; 264 } 265 if (mDeviceHiSyncIdMap.getOrDefault(storedDevice, 266 BluetoothHearingAid.HI_SYNC_ID_INVALID) == hiSyncId) { 267 synchronized (mStateMachines) { 268 HearingAidStateMachine sm = getOrCreateStateMachine(storedDevice); 269 if (sm == null) { 270 Log.e(TAG, "Ignored connect request for " + device + " : no state machine"); 271 continue; 272 } 273 sm.sendMessage(HearingAidStateMachine.CONNECT); 274 } 275 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID 276 && !device.equals(storedDevice)) { 277 break; 278 } 279 } 280 } 281 return true; 282 } 283 disconnect(BluetoothDevice device)284 boolean disconnect(BluetoothDevice device) { 285 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); 286 if (DBG) { 287 Log.d(TAG, "disconnect(): " + device); 288 } 289 if (device == null) { 290 return false; 291 } 292 long hiSyncId = mDeviceHiSyncIdMap.getOrDefault(device, 293 BluetoothHearingAid.HI_SYNC_ID_INVALID); 294 295 for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) { 296 if (mDeviceHiSyncIdMap.getOrDefault(storedDevice, 297 BluetoothHearingAid.HI_SYNC_ID_INVALID) == hiSyncId) { 298 synchronized (mStateMachines) { 299 HearingAidStateMachine sm = mStateMachines.get(storedDevice); 300 if (sm == null) { 301 Log.e(TAG, "Ignored disconnect request for " + device 302 + " : no state machine"); 303 continue; 304 } 305 sm.sendMessage(HearingAidStateMachine.DISCONNECT); 306 } 307 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID 308 && !device.equals(storedDevice)) { 309 break; 310 } 311 } 312 } 313 return true; 314 } 315 getConnectedDevices()316 List<BluetoothDevice> getConnectedDevices() { 317 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 318 synchronized (mStateMachines) { 319 List<BluetoothDevice> devices = new ArrayList<>(); 320 for (HearingAidStateMachine sm : mStateMachines.values()) { 321 if (sm.isConnected()) { 322 devices.add(sm.getDevice()); 323 } 324 } 325 return devices; 326 } 327 } 328 329 /** 330 * Check any peer device is connected. 331 * The check considers any peer device is connected. 332 * 333 * @param device the peer device to connect to 334 * @return true if there are any peer device connected. 335 */ isConnectedPeerDevices(BluetoothDevice device)336 public boolean isConnectedPeerDevices(BluetoothDevice device) { 337 long hiSyncId = getHiSyncId(device); 338 if (getConnectedPeerDevices(hiSyncId).isEmpty()) { 339 return false; 340 } 341 return true; 342 } 343 344 /** 345 * Check whether can connect to a peer device. 346 * The check considers a number of factors during the evaluation. 347 * 348 * @param device the peer device to connect to 349 * @return true if connection is allowed, otherwise false 350 */ 351 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) okToConnect(BluetoothDevice device)352 public boolean okToConnect(BluetoothDevice device) { 353 // Check if this is an incoming connection in Quiet mode. 354 if (mAdapterService.isQuietModeEnabled()) { 355 Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled"); 356 return false; 357 } 358 // Check priority and accept or reject the connection. 359 int priority = getPriority(device); 360 int bondState = mAdapterService.getBondState(device); 361 // Allow this connection only if the device is bonded. Any attempt to connect while 362 // bonding would potentially lead to an unauthorized connection. 363 if (bondState != BluetoothDevice.BOND_BONDED) { 364 Log.w(TAG, "okToConnect: return false, bondState=" + bondState); 365 return false; 366 } else if (priority != BluetoothProfile.PRIORITY_UNDEFINED 367 && priority != BluetoothProfile.PRIORITY_ON 368 && priority != BluetoothProfile.PRIORITY_AUTO_CONNECT) { 369 // Otherwise, reject the connection if priority is not valid. 370 Log.w(TAG, "okToConnect: return false, priority=" + priority); 371 return false; 372 } 373 return true; 374 } 375 getDevicesMatchingConnectionStates(int[] states)376 List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 377 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 378 ArrayList<BluetoothDevice> devices = new ArrayList<>(); 379 if (states == null) { 380 return devices; 381 } 382 final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); 383 if (bondedDevices == null) { 384 return devices; 385 } 386 synchronized (mStateMachines) { 387 for (BluetoothDevice device : bondedDevices) { 388 final ParcelUuid[] featureUuids = device.getUuids(); 389 if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) { 390 continue; 391 } 392 int connectionState = BluetoothProfile.STATE_DISCONNECTED; 393 HearingAidStateMachine sm = mStateMachines.get(device); 394 if (sm != null) { 395 connectionState = sm.getConnectionState(); 396 } 397 for (int state : states) { 398 if (connectionState == state) { 399 devices.add(device); 400 break; 401 } 402 } 403 } 404 return devices; 405 } 406 } 407 408 /** 409 * Get the list of devices that have state machines. 410 * 411 * @return the list of devices that have state machines 412 */ 413 @VisibleForTesting getDevices()414 List<BluetoothDevice> getDevices() { 415 List<BluetoothDevice> devices = new ArrayList<>(); 416 synchronized (mStateMachines) { 417 for (HearingAidStateMachine sm : mStateMachines.values()) { 418 devices.add(sm.getDevice()); 419 } 420 return devices; 421 } 422 } 423 424 /** 425 * Get the HiSyncIdMap for testing 426 * 427 * @return mDeviceHiSyncIdMap 428 */ 429 @VisibleForTesting getHiSyncIdMap()430 Map<BluetoothDevice, Long> getHiSyncIdMap() { 431 return mDeviceHiSyncIdMap; 432 } 433 getConnectionState(BluetoothDevice device)434 int getConnectionState(BluetoothDevice device) { 435 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 436 synchronized (mStateMachines) { 437 HearingAidStateMachine sm = mStateMachines.get(device); 438 if (sm == null) { 439 return BluetoothProfile.STATE_DISCONNECTED; 440 } 441 return sm.getConnectionState(); 442 } 443 } 444 445 /** 446 * Set the priority of the Hearing Aid profile. 447 * 448 * @param device the remote device 449 * @param priority the priority of the profile 450 * @return true on success, otherwise false 451 */ setPriority(BluetoothDevice device, int priority)452 public boolean setPriority(BluetoothDevice device, int priority) { 453 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 454 if (DBG) { 455 Log.d(TAG, "Saved priority " + device + " = " + priority); 456 } 457 mAdapterService.getDatabase() 458 .setProfilePriority(device, BluetoothProfile.HEARING_AID, priority); 459 return true; 460 } 461 getPriority(BluetoothDevice device)462 public int getPriority(BluetoothDevice device) { 463 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 464 return mAdapterService.getDatabase() 465 .getProfilePriority(device, BluetoothProfile.HEARING_AID); 466 } 467 setVolume(int volume)468 void setVolume(int volume) { 469 mHearingAidNativeInterface.setVolume(volume); 470 } 471 getHiSyncId(BluetoothDevice device)472 long getHiSyncId(BluetoothDevice device) { 473 if (device == null) { 474 return BluetoothHearingAid.HI_SYNC_ID_INVALID; 475 } 476 return mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID); 477 } 478 getCapabilities(BluetoothDevice device)479 int getCapabilities(BluetoothDevice device) { 480 return mDeviceCapabilitiesMap.getOrDefault(device, -1); 481 } 482 483 /** 484 * Set the active device. 485 * @param device the new active device 486 * @return true on success, otherwise false 487 */ setActiveDevice(BluetoothDevice device)488 public boolean setActiveDevice(BluetoothDevice device) { 489 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); 490 if (DBG) { 491 Log.d(TAG, "setActiveDevice:" + device); 492 } 493 synchronized (mStateMachines) { 494 if (device == null) { 495 if (mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) { 496 reportActiveDevice(null); 497 mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; 498 } 499 return true; 500 } 501 if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { 502 Log.e(TAG, "setActiveDevice(" + device + "): failed because device not connected"); 503 return false; 504 } 505 Long deviceHiSyncId = mDeviceHiSyncIdMap.getOrDefault(device, 506 BluetoothHearingAid.HI_SYNC_ID_INVALID); 507 if (deviceHiSyncId != mActiveDeviceHiSyncId) { 508 // Give an early notification to A2DP that active device is being switched 509 // to Hearing Aids before the Audio Service. 510 final A2dpService a2dpService = mFactory.getA2dpService(); 511 if (a2dpService != null) { 512 if (DBG) { 513 Log.d(TAG, "earlyNotifyHearingAidActive for " + device); 514 } 515 a2dpService.earlyNotifyHearingAidActive(); 516 } 517 mActiveDeviceHiSyncId = deviceHiSyncId; 518 reportActiveDevice(device); 519 } 520 } 521 return true; 522 } 523 524 /** 525 * Get the connected physical Hearing Aid devices that are active 526 * 527 * @return the list of active devices. The first element is the left active 528 * device; the second element is the right active device. If either or both side 529 * is not active, it will be null on that position 530 */ getActiveDevices()531 public List<BluetoothDevice> getActiveDevices() { 532 if (DBG) { 533 Log.d(TAG, "getActiveDevices"); 534 } 535 ArrayList<BluetoothDevice> activeDevices = new ArrayList<>(); 536 activeDevices.add(null); 537 activeDevices.add(null); 538 synchronized (mStateMachines) { 539 if (mActiveDeviceHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) { 540 return activeDevices; 541 } 542 for (BluetoothDevice device : mDeviceHiSyncIdMap.keySet()) { 543 if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) { 544 continue; 545 } 546 if (mDeviceHiSyncIdMap.get(device) == mActiveDeviceHiSyncId) { 547 int deviceSide = getCapabilities(device) & 1; 548 if (deviceSide == BluetoothHearingAid.SIDE_RIGHT) { 549 activeDevices.set(1, device); 550 } else { 551 activeDevices.set(0, device); 552 } 553 } 554 } 555 } 556 return activeDevices; 557 } 558 messageFromNative(HearingAidStackEvent stackEvent)559 void messageFromNative(HearingAidStackEvent stackEvent) { 560 Objects.requireNonNull(stackEvent.device, 561 "Device should never be null, event: " + stackEvent); 562 563 if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_DEVICE_AVAILABLE) { 564 BluetoothDevice device = stackEvent.device; 565 int capabilities = stackEvent.valueInt1; 566 long hiSyncId = stackEvent.valueLong2; 567 if (DBG) { 568 Log.d(TAG, "Device available: device=" + device + " capabilities=" 569 + capabilities + " hiSyncId=" + hiSyncId); 570 } 571 mDeviceCapabilitiesMap.put(device, capabilities); 572 mDeviceHiSyncIdMap.put(device, hiSyncId); 573 return; 574 } 575 576 synchronized (mStateMachines) { 577 BluetoothDevice device = stackEvent.device; 578 HearingAidStateMachine sm = mStateMachines.get(device); 579 if (sm == null) { 580 if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) { 581 switch (stackEvent.valueInt1) { 582 case HearingAidStackEvent.CONNECTION_STATE_CONNECTED: 583 case HearingAidStackEvent.CONNECTION_STATE_CONNECTING: 584 sm = getOrCreateStateMachine(device); 585 break; 586 default: 587 break; 588 } 589 } 590 } 591 if (sm == null) { 592 Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent); 593 return; 594 } 595 sm.sendMessage(HearingAidStateMachine.STACK_EVENT, stackEvent); 596 } 597 } 598 getOrCreateStateMachine(BluetoothDevice device)599 private HearingAidStateMachine getOrCreateStateMachine(BluetoothDevice device) { 600 if (device == null) { 601 Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null"); 602 return null; 603 } 604 synchronized (mStateMachines) { 605 HearingAidStateMachine sm = mStateMachines.get(device); 606 if (sm != null) { 607 return sm; 608 } 609 // Limit the maximum number of state machines to avoid DoS attack 610 if (mStateMachines.size() >= MAX_HEARING_AID_STATE_MACHINES) { 611 Log.e(TAG, "Maximum number of HearingAid state machines reached: " 612 + MAX_HEARING_AID_STATE_MACHINES); 613 return null; 614 } 615 if (DBG) { 616 Log.d(TAG, "Creating a new state machine for " + device); 617 } 618 sm = HearingAidStateMachine.make(device, this, 619 mHearingAidNativeInterface, mStateMachinesThread.getLooper()); 620 mStateMachines.put(device, sm); 621 return sm; 622 } 623 } 624 625 /** 626 * Report the active device change to the active device manager and the media framework. 627 * @param device the new active device; or null if no active device 628 */ reportActiveDevice(BluetoothDevice device)629 private void reportActiveDevice(BluetoothDevice device) { 630 if (DBG) { 631 Log.d(TAG, "reportActiveDevice(" + device + ")"); 632 } 633 634 StatsLog.write(StatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, BluetoothProfile.HEARING_AID, 635 mAdapterService.obfuscateAddress(device)); 636 637 Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED); 638 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 639 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT 640 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 641 sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); 642 643 if (device == null) { 644 if (DBG) { 645 Log.d(TAG, "Set Hearing Aid audio to disconnected"); 646 } 647 boolean suppressNoisyIntent = 648 (getConnectionState(mPreviousAudioDevice) == BluetoothProfile.STATE_CONNECTED); 649 mAudioManager.setBluetoothHearingAidDeviceConnectionState( 650 mPreviousAudioDevice, BluetoothProfile.STATE_DISCONNECTED, 651 suppressNoisyIntent, 0); 652 mPreviousAudioDevice = null; 653 } else { 654 if (DBG) { 655 Log.d(TAG, "Set Hearing Aid audio to connected"); 656 } 657 if (mPreviousAudioDevice != null) { 658 mAudioManager.setBluetoothHearingAidDeviceConnectionState( 659 mPreviousAudioDevice, BluetoothProfile.STATE_DISCONNECTED, 660 true, 0); 661 } 662 mAudioManager.setBluetoothHearingAidDeviceConnectionState( 663 device, BluetoothProfile.STATE_CONNECTED, 664 true, 0); 665 mPreviousAudioDevice = device; 666 } 667 } 668 669 // Remove state machine if the bonding for a device is removed 670 private class BondStateChangedReceiver extends BroadcastReceiver { 671 @Override onReceive(Context context, Intent intent)672 public void onReceive(Context context, Intent intent) { 673 if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) { 674 return; 675 } 676 int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 677 BluetoothDevice.ERROR); 678 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 679 Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE"); 680 bondStateChanged(device, state); 681 } 682 } 683 684 /** 685 * Process a change in the bonding state for a device. 686 * 687 * @param device the device whose bonding state has changed 688 * @param bondState the new bond state for the device. Possible values are: 689 * {@link BluetoothDevice#BOND_NONE}, 690 * {@link BluetoothDevice#BOND_BONDING}, 691 * {@link BluetoothDevice#BOND_BONDED}. 692 */ 693 @VisibleForTesting bondStateChanged(BluetoothDevice device, int bondState)694 void bondStateChanged(BluetoothDevice device, int bondState) { 695 if (DBG) { 696 Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); 697 } 698 // Remove state machine if the bonding for a device is removed 699 if (bondState != BluetoothDevice.BOND_NONE) { 700 return; 701 } 702 mDeviceHiSyncIdMap.remove(device); 703 synchronized (mStateMachines) { 704 HearingAidStateMachine sm = mStateMachines.get(device); 705 if (sm == null) { 706 return; 707 } 708 if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) { 709 return; 710 } 711 removeStateMachine(device); 712 } 713 } 714 removeStateMachine(BluetoothDevice device)715 private void removeStateMachine(BluetoothDevice device) { 716 synchronized (mStateMachines) { 717 HearingAidStateMachine sm = mStateMachines.get(device); 718 if (sm == null) { 719 Log.w(TAG, "removeStateMachine: device " + device 720 + " does not have a state machine"); 721 return; 722 } 723 Log.i(TAG, "removeStateMachine: removing state machine for device: " + device); 724 sm.doQuit(); 725 sm.cleanup(); 726 mStateMachines.remove(device); 727 } 728 } 729 getConnectedPeerDevices(long hiSyncId)730 private List<BluetoothDevice> getConnectedPeerDevices(long hiSyncId) { 731 List<BluetoothDevice> result = new ArrayList<>(); 732 for (BluetoothDevice peerDevice : getConnectedDevices()) { 733 if (getHiSyncId(peerDevice) == hiSyncId) { 734 result.add(peerDevice); 735 } 736 } 737 return result; 738 } 739 740 @VisibleForTesting connectionStateChanged(BluetoothDevice device, int fromState, int toState)741 synchronized void connectionStateChanged(BluetoothDevice device, int fromState, 742 int toState) { 743 if ((device == null) || (fromState == toState)) { 744 Log.e(TAG, "connectionStateChanged: unexpected invocation. device=" + device 745 + " fromState=" + fromState + " toState=" + toState); 746 return; 747 } 748 if (toState == BluetoothProfile.STATE_CONNECTED) { 749 long myHiSyncId = getHiSyncId(device); 750 if (myHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID 751 || getConnectedPeerDevices(myHiSyncId).size() == 1) { 752 // Log hearing aid connection event if we are the first device in a set 753 // Or when the hiSyncId has not been found 754 MetricsLogger.logProfileConnectionEvent( 755 BluetoothMetricsProto.ProfileId.HEARING_AID); 756 } 757 if (!mHiSyncIdConnectedMap.getOrDefault(myHiSyncId, false)) { 758 setActiveDevice(device); 759 mHiSyncIdConnectedMap.put(myHiSyncId, true); 760 } 761 } 762 if (fromState == BluetoothProfile.STATE_CONNECTED && getConnectedDevices().isEmpty()) { 763 setActiveDevice(null); 764 long myHiSyncId = getHiSyncId(device); 765 mHiSyncIdConnectedMap.put(myHiSyncId, false); 766 } 767 // Check if the device is disconnected - if unbond, remove the state machine 768 if (toState == BluetoothProfile.STATE_DISCONNECTED) { 769 int bondState = mAdapterService.getBondState(device); 770 if (bondState == BluetoothDevice.BOND_NONE) { 771 if (DBG) { 772 Log.d(TAG, device + " is unbond. Remove state machine"); 773 } 774 removeStateMachine(device); 775 } 776 } 777 } 778 779 private class ConnectionStateChangedReceiver extends BroadcastReceiver { 780 @Override onReceive(Context context, Intent intent)781 public void onReceive(Context context, Intent intent) { 782 if (!BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { 783 return; 784 } 785 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 786 int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 787 int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); 788 connectionStateChanged(device, fromState, toState); 789 } 790 } 791 792 /** 793 * Binder object: must be a static class or memory leak may occur 794 */ 795 @VisibleForTesting 796 static class BluetoothHearingAidBinder extends IBluetoothHearingAid.Stub 797 implements IProfileServiceBinder { 798 private HearingAidService mService; 799 getService()800 private HearingAidService getService() { 801 if (!Utils.checkCaller()) { 802 Log.w(TAG, "HearingAid call not allowed for non-active user"); 803 return null; 804 } 805 806 if (mService != null && mService.isAvailable()) { 807 return mService; 808 } 809 return null; 810 } 811 BluetoothHearingAidBinder(HearingAidService svc)812 BluetoothHearingAidBinder(HearingAidService svc) { 813 mService = svc; 814 } 815 816 @Override cleanup()817 public void cleanup() { 818 mService = null; 819 } 820 821 @Override connect(BluetoothDevice device)822 public boolean connect(BluetoothDevice device) { 823 HearingAidService service = getService(); 824 if (service == null) { 825 return false; 826 } 827 return service.connect(device); 828 } 829 830 @Override disconnect(BluetoothDevice device)831 public boolean disconnect(BluetoothDevice device) { 832 HearingAidService service = getService(); 833 if (service == null) { 834 return false; 835 } 836 return service.disconnect(device); 837 } 838 839 @Override getConnectedDevices()840 public List<BluetoothDevice> getConnectedDevices() { 841 HearingAidService service = getService(); 842 if (service == null) { 843 return new ArrayList<>(); 844 } 845 return service.getConnectedDevices(); 846 } 847 848 @Override getDevicesMatchingConnectionStates(int[] states)849 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 850 HearingAidService service = getService(); 851 if (service == null) { 852 return new ArrayList<>(); 853 } 854 return service.getDevicesMatchingConnectionStates(states); 855 } 856 857 @Override getConnectionState(BluetoothDevice device)858 public int getConnectionState(BluetoothDevice device) { 859 HearingAidService service = getService(); 860 if (service == null) { 861 return BluetoothProfile.STATE_DISCONNECTED; 862 } 863 return service.getConnectionState(device); 864 } 865 866 @Override setActiveDevice(BluetoothDevice device)867 public boolean setActiveDevice(BluetoothDevice device) { 868 HearingAidService service = getService(); 869 if (service == null) { 870 return false; 871 } 872 return service.setActiveDevice(device); 873 } 874 875 @Override getActiveDevices()876 public List<BluetoothDevice> getActiveDevices() { 877 HearingAidService service = getService(); 878 if (service == null) { 879 return new ArrayList<>(); 880 } 881 return service.getActiveDevices(); 882 } 883 884 @Override setPriority(BluetoothDevice device, int priority)885 public boolean setPriority(BluetoothDevice device, int priority) { 886 HearingAidService service = getService(); 887 if (service == null) { 888 return false; 889 } 890 return service.setPriority(device, priority); 891 } 892 893 @Override getPriority(BluetoothDevice device)894 public int getPriority(BluetoothDevice device) { 895 HearingAidService service = getService(); 896 if (service == null) { 897 return BluetoothProfile.PRIORITY_UNDEFINED; 898 } 899 return service.getPriority(device); 900 } 901 902 @Override setVolume(int volume)903 public void setVolume(int volume) { 904 HearingAidService service = getService(); 905 if (service == null) { 906 return; 907 } 908 service.setVolume(volume); 909 } 910 911 @Override adjustVolume(int direction)912 public void adjustVolume(int direction) { 913 // TODO: Remove me? 914 } 915 916 @Override getVolume()917 public int getVolume() { 918 // TODO: Remove me? 919 return 0; 920 } 921 922 @Override getHiSyncId(BluetoothDevice device)923 public long getHiSyncId(BluetoothDevice device) { 924 HearingAidService service = getService(); 925 if (service == null) { 926 return BluetoothHearingAid.HI_SYNC_ID_INVALID; 927 } 928 return service.getHiSyncId(device); 929 } 930 931 @Override getDeviceSide(BluetoothDevice device)932 public int getDeviceSide(BluetoothDevice device) { 933 HearingAidService service = getService(); 934 if (service == null) { 935 return BluetoothHearingAid.SIDE_RIGHT; 936 } 937 return service.getCapabilities(device) & 1; 938 } 939 940 @Override getDeviceMode(BluetoothDevice device)941 public int getDeviceMode(BluetoothDevice device) { 942 HearingAidService service = getService(); 943 if (service == null) { 944 return BluetoothHearingAid.MODE_BINAURAL; 945 } 946 return service.getCapabilities(device) >> 1 & 1; 947 } 948 } 949 950 @Override dump(StringBuilder sb)951 public void dump(StringBuilder sb) { 952 super.dump(sb); 953 for (HearingAidStateMachine sm : mStateMachines.values()) { 954 sm.dump(sb); 955 } 956 } 957 } 958