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