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