HearingAidService.java revision fc72b6542b8170ae77f89dcae60be2640ce0e990
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 17package com.android.bluetooth.hearingaid; 18 19import android.bluetooth.BluetoothAdapter; 20import android.bluetooth.BluetoothDevice; 21import android.bluetooth.BluetoothHearingAid; 22import android.bluetooth.BluetoothProfile; 23import android.bluetooth.BluetoothUuid; 24import android.bluetooth.IBluetoothHearingAid; 25import android.content.BroadcastReceiver; 26import android.content.Context; 27import android.content.Intent; 28import android.content.IntentFilter; 29import android.os.HandlerThread; 30import android.os.ParcelUuid; 31import android.provider.Settings; 32import android.support.annotation.VisibleForTesting; 33import android.util.Log; 34 35import com.android.bluetooth.Utils; 36import com.android.bluetooth.btservice.AdapterService; 37import com.android.bluetooth.btservice.ProfileService; 38 39import java.util.ArrayList; 40import java.util.HashMap; 41import java.util.List; 42import java.util.Map; 43import java.util.Objects; 44import java.util.Set; 45 46/** 47 * Provides Bluetooth HearingAid profile, as a service in the Bluetooth application. 48 * @hide 49 */ 50public class HearingAidService extends ProfileService { 51 private static final boolean DBG = false; 52 private static final String TAG = "HearingAidService"; 53 54 private static HearingAidService sHearingAidService; 55 56 private BluetoothAdapter mAdapter; 57 private AdapterService mAdapterService; 58 private HandlerThread mStateMachinesThread; 59 60 private BluetoothDevice mActiveDevice; 61 62 private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines = 63 new HashMap<>(); 64 private final Map<BluetoothDevice, Integer> mDeviceMap = new HashMap<>(); 65 66 // Upper limit of all HearingAid devices: Bonded or Connected 67 private static final int MAX_HEARING_AID_STATE_MACHINES = 10; 68 69 private BroadcastReceiver mBondStateChangedReceiver; 70 private BroadcastReceiver mConnectionStateChangedReceiver; 71 72 @Override 73 protected IProfileServiceBinder initBinder() { 74 return new BluetoothHearingAidBinder(this); 75 } 76 77 @Override 78 protected void create() { 79 if (DBG) { 80 Log.d(TAG, "create()"); 81 } 82 } 83 84 @Override 85 protected boolean start() { 86 if (DBG) { 87 Log.d(TAG, "start()"); 88 } 89 if (sHearingAidService != null) { 90 throw new IllegalStateException("start() called twice"); 91 } 92 93 // Get BluetoothAdapter, AdapterService, A2dpNativeInterface, AudioManager. 94 // None of them can be null. 95 mAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter(), 96 "BluetoothAdapter cannot be null when HearingAidService starts"); 97 mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(), 98 "AdapterService cannot be null when HearingAidService starts"); 99 // TODO: Add native interface 100 101 // Start handler thread for state machines 102 mStateMachines.clear(); 103 mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines"); 104 mStateMachinesThread.start(); 105 106 // Initialize native interface 107 // TODO: Init native interface 108 109 // Setup broadcast receivers 110 IntentFilter filter = new IntentFilter(); 111 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 112 mBondStateChangedReceiver = new BondStateChangedReceiver(); 113 registerReceiver(mBondStateChangedReceiver, filter); 114 filter = new IntentFilter(); 115 filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); 116 mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver(); 117 registerReceiver(mConnectionStateChangedReceiver, filter); 118 119 // Mark service as started 120 setHearingAidService(this); 121 122 // Clear active device 123 setActiveDevice(null); 124 125 return true; 126 } 127 128 @Override 129 protected boolean stop() { 130 if (DBG) { 131 Log.d(TAG, "stop()"); 132 } 133 if (sHearingAidService == null) { 134 Log.w(TAG, "stop() called before start()"); 135 return true; 136 } 137 138 // Clear active device 139 setActiveDevice(null); 140 141 // Mark service as stopped 142 setHearingAidService(null); 143 144 // Unregister broadcast receivers 145 unregisterReceiver(mBondStateChangedReceiver); 146 mBondStateChangedReceiver = null; 147 unregisterReceiver(mConnectionStateChangedReceiver); 148 mConnectionStateChangedReceiver = null; 149 150 // Cleanup native interface 151 // TODO: Cleanup native interface 152 153 // Destroy state machines and stop handler thread 154 synchronized (mStateMachines) { 155 for (HearingAidStateMachine sm : mStateMachines.values()) { 156 sm.doQuit(); 157 sm.cleanup(); 158 } 159 mStateMachines.clear(); 160 } 161 162 if (mStateMachinesThread != null) { 163 mStateMachinesThread.quitSafely(); 164 mStateMachinesThread = null; 165 } 166 167 // Clear BluetoothAdapter, AdapterService, HearingAidNativeInterface 168 // TODO: Set native interface to null 169 mAdapterService = null; 170 mAdapter = null; 171 172 return true; 173 } 174 175 @Override 176 protected void cleanup() { 177 if (DBG) { 178 Log.d(TAG, "cleanup()"); 179 } 180 } 181 182 /** 183 * Get the HearingAidService instance 184 * @return HearingAidService instance 185 */ 186 public static synchronized HearingAidService getHearingAidService() { 187 if (sHearingAidService == null) { 188 Log.w(TAG, "getHearingAidService(): service is NULL"); 189 return null; 190 } 191 192 if (!sHearingAidService.isAvailable()) { 193 Log.w(TAG, "getHearingAidService(): service is not available"); 194 return null; 195 } 196 return sHearingAidService; 197 } 198 199 private static synchronized void setHearingAidService(HearingAidService instance) { 200 if (DBG) { 201 Log.d(TAG, "setHearingAidService(): set to: " + instance); 202 } 203 sHearingAidService = instance; 204 } 205 206 boolean connect(BluetoothDevice device) { 207 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); 208 if (DBG) { 209 Log.d(TAG, "connect(): " + device); 210 } 211 212 if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) { 213 return false; 214 } 215 ParcelUuid[] featureUuids = device.getUuids(); 216 if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) { 217 Log.e(TAG, "Cannot connect to " + device + " : Remote does not have HearingAid UUID"); 218 return false; 219 } 220 221 synchronized (mStateMachines) { 222 HearingAidStateMachine smConnect = getOrCreateStateMachine(device); 223 if (smConnect == null) { 224 Log.e(TAG, "Cannot connect to " + device + " : no state machine"); 225 return false; 226 } 227 smConnect.sendMessage(HearingAidStateMachine.CONNECT); 228 return true; 229 } 230 } 231 232 boolean disconnect(BluetoothDevice device) { 233 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); 234 if (DBG) { 235 Log.d(TAG, "disconnect(): " + device); 236 } 237 238 int hiSyncId = mDeviceMap.get(device); 239 for (BluetoothDevice storedDevice : mDeviceMap.keySet()) { 240 if (mDeviceMap.get(storedDevice) != hiSyncId) { 241 continue; 242 } 243 synchronized (mStateMachines) { 244 HearingAidStateMachine sm = mStateMachines.get(device); 245 if (sm == null) { 246 Log.e(TAG, "Ignored disconnect request for " + device + " : no state machine"); 247 continue; 248 } 249 sm.sendMessage(HearingAidStateMachine.DISCONNECT); 250 } 251 } 252 return true; 253 } 254 255 List<BluetoothDevice> getConnectedDevices() { 256 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 257 synchronized (mStateMachines) { 258 List<BluetoothDevice> devices = new ArrayList<>(); 259 for (HearingAidStateMachine sm : mStateMachines.values()) { 260 if (sm.isConnected()) { 261 devices.add(sm.getDevice()); 262 } 263 } 264 return devices; 265 } 266 } 267 268 /** 269 * Check whether can connect to a peer device. 270 * The check considers a number of factors during the evaluation. 271 * 272 * @param device the peer device to connect to 273 * @return true if connection is allowed, otherwise false 274 */ 275 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) 276 public boolean okToConnect(BluetoothDevice device) { 277 // Check if this is an incoming connection in Quiet mode. 278 if (mAdapterService.isQuietModeEnabled()) { 279 Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled"); 280 return false; 281 } 282 // Check priority and accept or reject the connection 283 int priority = getPriority(device); 284 int bondState = mAdapterService.getBondState(device); 285 // Allow the connection only if the device is bonded or bonding. 286 if ((priority == BluetoothProfile.PRIORITY_UNDEFINED) 287 && (bondState == BluetoothDevice.BOND_NONE)) { 288 Log.e(TAG, "okToConnect: cannot connect to " + device + " : priority=" + priority 289 + " bondState=" + bondState); 290 return false; 291 } 292 if (priority <= BluetoothProfile.PRIORITY_OFF) { 293 Log.e(TAG, "okToConnect: cannot connect to " + device + " : priority=" + priority); 294 return false; 295 } 296 return true; 297 } 298 299 List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 300 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 301 synchronized (mStateMachines) { 302 List<BluetoothDevice> devices = new ArrayList<>(); 303 Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices(); 304 305 for (BluetoothDevice device : bondedDevices) { 306 ParcelUuid[] featureUuids = device.getUuids(); 307 if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) { 308 continue; 309 } 310 int connectionState = BluetoothProfile.STATE_DISCONNECTED; 311 HearingAidStateMachine sm = mStateMachines.get(device); 312 if (sm != null) { 313 connectionState = sm.getConnectionState(); 314 } 315 for (int state : states) { 316 if (connectionState == state) { 317 devices.add(device); 318 } 319 } 320 } 321 return devices; 322 } 323 } 324 325 /** 326 * Get the list of devices that have state machines. 327 * 328 * @return the list of devices that have state machines 329 */ 330 @VisibleForTesting 331 List<BluetoothDevice> getDevices() { 332 List<BluetoothDevice> devices = new ArrayList<>(); 333 synchronized (mStateMachines) { 334 for (HearingAidStateMachine sm : mStateMachines.values()) { 335 devices.add(sm.getDevice()); 336 } 337 return devices; 338 } 339 } 340 341 int getConnectionState(BluetoothDevice device) { 342 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 343 synchronized (mStateMachines) { 344 HearingAidStateMachine sm = mStateMachines.get(device); 345 if (sm == null) { 346 return BluetoothProfile.STATE_DISCONNECTED; 347 } 348 return sm.getConnectionState(); 349 } 350 } 351 352 /** 353 * Set the active device. 354 * 355 * @param device the active device 356 * @return true on success, otherwise false 357 */ 358 public synchronized boolean setActiveDevice(BluetoothDevice device) { 359 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); 360 361 return false; 362 } 363 364 /** 365 * Get the active device. 366 * 367 * @return the active device or null if no device is active 368 */ 369 public synchronized BluetoothDevice getActiveDevice() { 370 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 371 synchronized (mStateMachines) { 372 return mActiveDevice; 373 } 374 } 375 376 private synchronized boolean isActiveDevice(BluetoothDevice device) { 377 synchronized (mStateMachines) { 378 return (device != null) && Objects.equals(device, mActiveDevice); 379 } 380 } 381 382 /** 383 * Set the priority of the Hearing Aid profile. 384 * 385 * @param device the remote device 386 * @param priority the priority of the profile 387 * @return true on success, otherwise false 388 */ 389 public boolean setPriority(BluetoothDevice device, int priority) { 390 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 391 Settings.Global.putInt(getContentResolver(), 392 Settings.Global.getBluetoothHearingAidPriorityKey(device.getAddress()), priority); 393 if (DBG) { 394 Log.d(TAG, "Saved priority " + device + " = " + priority); 395 } 396 return true; 397 } 398 399 public int getPriority(BluetoothDevice device) { 400 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 401 int priority = Settings.Global.getInt(getContentResolver(), 402 Settings.Global.getBluetoothHearingAidPriorityKey(device.getAddress()), 403 BluetoothProfile.PRIORITY_UNDEFINED); 404 return priority; 405 } 406 407 private HearingAidStateMachine getOrCreateStateMachine(BluetoothDevice device) { 408 if (device == null) { 409 Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null"); 410 return null; 411 } 412 synchronized (mStateMachines) { 413 HearingAidStateMachine sm = mStateMachines.get(device); 414 if (sm != null) { 415 return sm; 416 } 417 // Limit the maximum number of state machines to avoid DoS attack 418 if (mStateMachines.size() >= MAX_HEARING_AID_STATE_MACHINES) { 419 Log.e(TAG, "Maximum number of HearingAid state machines reached: " 420 + MAX_HEARING_AID_STATE_MACHINES); 421 return null; 422 } 423 if (DBG) { 424 Log.d(TAG, "Creating a new state machine for " + device); 425 } 426 sm = HearingAidStateMachine.make(device, this, mStateMachinesThread.getLooper()); 427 mStateMachines.put(device, sm); 428 return sm; 429 } 430 } 431 432 private void broadcastActiveDevice(BluetoothDevice device) { 433 if (DBG) { 434 Log.d(TAG, "broadcastActiveDevice(" + device + ")"); 435 } 436 437 Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED); 438 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 439 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT 440 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 441 sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); 442 } 443 444 // Remove state machine if the bonding for a device is removed 445 private class BondStateChangedReceiver extends BroadcastReceiver { 446 @Override 447 public void onReceive(Context context, Intent intent) { 448 if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) { 449 return; 450 } 451 int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 452 BluetoothDevice.ERROR); 453 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 454 Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE"); 455 bondStateChanged(device, state); 456 } 457 } 458 459 /** 460 * Process a change in the bonding state for a device. 461 * 462 * @param device the device whose bonding state has changed 463 * @param bondState the new bond state for the device. Possible values are: 464 * {@link BluetoothDevice#BOND_NONE}, 465 * {@link BluetoothDevice#BOND_BONDING}, 466 * {@link BluetoothDevice#BOND_BONDED}. 467 */ 468 @VisibleForTesting 469 void bondStateChanged(BluetoothDevice device, int bondState) { 470 if (DBG) { 471 Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); 472 } 473 // Remove state machine if the bonding for a device is removed 474 if (bondState != BluetoothDevice.BOND_NONE) { 475 return; 476 } 477 synchronized (mStateMachines) { 478 HearingAidStateMachine sm = mStateMachines.get(device); 479 if (sm == null) { 480 return; 481 } 482 if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) { 483 return; 484 } 485 removeStateMachine(device); 486 } 487 } 488 489 private void removeStateMachine(BluetoothDevice device) { 490 synchronized (mStateMachines) { 491 HearingAidStateMachine sm = mStateMachines.get(device); 492 if (sm == null) { 493 Log.w(TAG, "removeStateMachine: device " + device 494 + " does not have a state machine"); 495 return; 496 } 497 Log.i(TAG, "removeStateMachine: removing state machine for device: " + device); 498 sm.doQuit(); 499 sm.cleanup(); 500 mStateMachines.remove(device); 501 } 502 } 503 504 private synchronized void connectionStateChanged(BluetoothDevice device, int fromState, 505 int toState) { 506 if ((device == null) || (fromState == toState)) { 507 return; 508 } 509 // Check if the device is disconnected - if unbond, remove the state machine 510 if (toState == BluetoothProfile.STATE_DISCONNECTED) { 511 int bondState = mAdapterService.getBondState(device); 512 if (bondState == BluetoothDevice.BOND_NONE) { 513 removeStateMachine(device); 514 } 515 } 516 } 517 518 private class ConnectionStateChangedReceiver extends BroadcastReceiver { 519 @Override 520 public void onReceive(Context context, Intent intent) { 521 if (!BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { 522 return; 523 } 524 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 525 int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 526 int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); 527 connectionStateChanged(device, fromState, toState); 528 } 529 } 530 531 /** 532 * Binder object: must be a static class or memory leak may occur 533 */ 534 @VisibleForTesting 535 static class BluetoothHearingAidBinder extends IBluetoothHearingAid.Stub 536 implements IProfileServiceBinder { 537 private HearingAidService mService; 538 539 private HearingAidService getService() { 540 if (!Utils.checkCaller()) { 541 Log.w(TAG, "HearingAid call not allowed for non-active user"); 542 return null; 543 } 544 545 if (mService != null && mService.isAvailable()) { 546 return mService; 547 } 548 return null; 549 } 550 551 @VisibleForTesting 552 HearingAidService getServiceForTesting() { 553 if (mService != null && mService.isAvailable()) { 554 return mService; 555 } 556 return null; 557 } 558 559 BluetoothHearingAidBinder(HearingAidService svc) { 560 mService = svc; 561 } 562 563 @Override 564 public void cleanup() { 565 mService = null; 566 } 567 568 @Override 569 public boolean connect(BluetoothDevice device) { 570 HearingAidService service = getService(); 571 if (service == null) { 572 return false; 573 } 574 return service.connect(device); 575 } 576 577 @Override 578 public boolean disconnect(BluetoothDevice device) { 579 HearingAidService service = getService(); 580 if (service == null) { 581 return false; 582 } 583 return service.disconnect(device); 584 } 585 586 @Override 587 public List<BluetoothDevice> getConnectedDevices() { 588 HearingAidService service = getService(); 589 if (service == null) { 590 return new ArrayList<>(0); 591 } 592 return service.getConnectedDevices(); 593 } 594 595 @Override 596 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 597 HearingAidService service = getService(); 598 if (service == null) { 599 return new ArrayList<>(0); 600 } 601 return service.getDevicesMatchingConnectionStates(states); 602 } 603 604 @Override 605 public int getConnectionState(BluetoothDevice device) { 606 HearingAidService service = getService(); 607 if (service == null) { 608 return BluetoothProfile.STATE_DISCONNECTED; 609 } 610 return service.getConnectionState(device); 611 } 612 613 @Override 614 public boolean setPriority(BluetoothDevice device, int priority) { 615 HearingAidService service = getService(); 616 if (service == null) { 617 return false; 618 } 619 return service.setPriority(device, priority); 620 } 621 622 @Override 623 public int getPriority(BluetoothDevice device) { 624 HearingAidService service = getService(); 625 if (service == null) { 626 return BluetoothProfile.PRIORITY_UNDEFINED; 627 } 628 return service.getPriority(device); 629 } 630 631 @Override 632 public void setVolume(int volume) { 633 } 634 635 @Override 636 public void adjustVolume(int direction) { 637 } 638 639 @Override 640 public int getVolume() { 641 return 0; 642 } 643 644 @Override 645 public long getHiSyncId(BluetoothDevice device) { 646 return 0; 647 } 648 649 @Override 650 public int getDeviceSide(BluetoothDevice device) { 651 return 0; 652 } 653 654 @Override 655 public int getDeviceMode(BluetoothDevice device) { 656 return 0; 657 } 658 } 659 660 @Override 661 public void dump(StringBuilder sb) { 662 super.dump(sb); 663 ProfileService.println(sb, "mActiveDevice: " + mActiveDevice); 664 for (HearingAidStateMachine sm : mStateMachines.values()) { 665 sm.dump(sb); 666 } 667 } 668} 669