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 android.bluetooth; 18 19 import android.Manifest; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.RequiresPermission; 23 import android.annotation.SdkConstant; 24 import android.annotation.SdkConstant.SdkConstantType; 25 import android.annotation.UnsupportedAppUsage; 26 import android.content.Context; 27 import android.os.Binder; 28 import android.os.IBinder; 29 import android.os.RemoteException; 30 import android.util.Log; 31 32 import java.util.ArrayList; 33 import java.util.List; 34 35 /** 36 * This class provides the public APIs to control the Hearing Aid profile. 37 * 38 * <p>BluetoothHearingAid is a proxy object for controlling the Bluetooth Hearing Aid 39 * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get 40 * the BluetoothHearingAid proxy object. 41 * 42 * <p> Android only supports one set of connected Bluetooth Hearing Aid device at a time. Each 43 * method is protected with its appropriate permission. 44 */ 45 public final class BluetoothHearingAid implements BluetoothProfile { 46 private static final String TAG = "BluetoothHearingAid"; 47 private static final boolean DBG = true; 48 private static final boolean VDBG = false; 49 50 /** 51 * Intent used to broadcast the change in connection state of the Hearing Aid 52 * profile. Please note that in the binaural case, there will be two different LE devices for 53 * the left and right side and each device will have their own connection state changes.S 54 * 55 * <p>This intent will have 3 extras: 56 * <ul> 57 * <li> {@link #EXTRA_STATE} - The current state of the profile. </li> 58 * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li> 59 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> 60 * </ul> 61 * 62 * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of 63 * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, 64 * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. 65 * 66 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to 67 * receive. 68 */ 69 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 70 public static final String ACTION_CONNECTION_STATE_CHANGED = 71 "android.bluetooth.hearingaid.profile.action.CONNECTION_STATE_CHANGED"; 72 73 /** 74 * Intent used to broadcast the selection of a connected device as active. 75 * 76 * <p>This intent will have one extra: 77 * <ul> 78 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can 79 * be null if no device is active. </li> 80 * </ul> 81 * 82 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to 83 * receive. 84 * 85 * @hide 86 */ 87 @UnsupportedAppUsage 88 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 89 public static final String ACTION_ACTIVE_DEVICE_CHANGED = 90 "android.bluetooth.hearingaid.profile.action.ACTIVE_DEVICE_CHANGED"; 91 92 /** 93 * This device represents Left Hearing Aid. 94 * 95 * @hide 96 */ 97 public static final int SIDE_LEFT = IBluetoothHearingAid.SIDE_LEFT; 98 99 /** 100 * This device represents Right Hearing Aid. 101 * 102 * @hide 103 */ 104 public static final int SIDE_RIGHT = IBluetoothHearingAid.SIDE_RIGHT; 105 106 /** 107 * This device is Monaural. 108 * 109 * @hide 110 */ 111 public static final int MODE_MONAURAL = IBluetoothHearingAid.MODE_MONAURAL; 112 113 /** 114 * This device is Binaural (should receive only left or right audio). 115 * 116 * @hide 117 */ 118 public static final int MODE_BINAURAL = IBluetoothHearingAid.MODE_BINAURAL; 119 120 /** 121 * Indicates the HiSyncID could not be read and is unavailable. 122 * 123 * @hide 124 */ 125 public static final long HI_SYNC_ID_INVALID = IBluetoothHearingAid.HI_SYNC_ID_INVALID; 126 127 private BluetoothAdapter mAdapter; 128 private final BluetoothProfileConnector<IBluetoothHearingAid> mProfileConnector = 129 new BluetoothProfileConnector(this, BluetoothProfile.HEARING_AID, 130 "BluetoothHearingAid", IBluetoothHearingAid.class.getName()) { 131 @Override 132 public IBluetoothHearingAid getServiceInterface(IBinder service) { 133 return IBluetoothHearingAid.Stub.asInterface(Binder.allowBlocking(service)); 134 } 135 }; 136 137 /** 138 * Create a BluetoothHearingAid proxy object for interacting with the local 139 * Bluetooth Hearing Aid service. 140 */ BluetoothHearingAid(Context context, ServiceListener listener)141 /*package*/ BluetoothHearingAid(Context context, ServiceListener listener) { 142 mAdapter = BluetoothAdapter.getDefaultAdapter(); 143 mProfileConnector.connect(context, listener); 144 } 145 close()146 /*package*/ void close() { 147 mProfileConnector.disconnect(); 148 } 149 getService()150 private IBluetoothHearingAid getService() { 151 return mProfileConnector.getService(); 152 } 153 154 /** 155 * Initiate connection to a profile of the remote bluetooth device. 156 * 157 * <p> This API returns false in scenarios like the profile on the 158 * device is already connected or Bluetooth is not turned on. 159 * When this API returns true, it is guaranteed that 160 * connection state intent for the profile will be broadcasted with 161 * the state. Users can get the connection state of the profile 162 * from this intent. 163 * 164 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 165 * permission. 166 * 167 * @param device Remote Bluetooth Device 168 * @return false on immediate error, true otherwise 169 * @hide 170 */ connect(BluetoothDevice device)171 public boolean connect(BluetoothDevice device) { 172 if (DBG) log("connect(" + device + ")"); 173 final IBluetoothHearingAid service = getService(); 174 try { 175 if (service != null && isEnabled() && isValidDevice(device)) { 176 return service.connect(device); 177 } 178 if (service == null) Log.w(TAG, "Proxy not attached to service"); 179 return false; 180 } catch (RemoteException e) { 181 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 182 return false; 183 } 184 } 185 186 /** 187 * Initiate disconnection from a profile 188 * 189 * <p> This API will return false in scenarios like the profile on the 190 * Bluetooth device is not in connected state etc. When this API returns, 191 * true, it is guaranteed that the connection state change 192 * intent will be broadcasted with the state. Users can get the 193 * disconnection state of the profile from this intent. 194 * 195 * <p> If the disconnection is initiated by a remote device, the state 196 * will transition from {@link #STATE_CONNECTED} to 197 * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the 198 * host (local) device the state will transition from 199 * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to 200 * state {@link #STATE_DISCONNECTED}. The transition to 201 * {@link #STATE_DISCONNECTING} can be used to distinguish between the 202 * two scenarios. 203 * 204 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 205 * permission. 206 * 207 * @param device Remote Bluetooth Device 208 * @return false on immediate error, true otherwise 209 * @hide 210 */ disconnect(BluetoothDevice device)211 public boolean disconnect(BluetoothDevice device) { 212 if (DBG) log("disconnect(" + device + ")"); 213 final IBluetoothHearingAid service = getService(); 214 try { 215 if (service != null && isEnabled() && isValidDevice(device)) { 216 return service.disconnect(device); 217 } 218 if (service == null) Log.w(TAG, "Proxy not attached to service"); 219 return false; 220 } catch (RemoteException e) { 221 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 222 return false; 223 } 224 } 225 226 /** 227 * {@inheritDoc} 228 */ 229 @Override getConnectedDevices()230 public @NonNull List<BluetoothDevice> getConnectedDevices() { 231 if (VDBG) log("getConnectedDevices()"); 232 final IBluetoothHearingAid service = getService(); 233 try { 234 if (service != null && isEnabled()) { 235 return service.getConnectedDevices(); 236 } 237 if (service == null) Log.w(TAG, "Proxy not attached to service"); 238 return new ArrayList<BluetoothDevice>(); 239 } catch (RemoteException e) { 240 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 241 return new ArrayList<BluetoothDevice>(); 242 } 243 } 244 245 /** 246 * {@inheritDoc} 247 */ 248 @Override getDevicesMatchingConnectionStates( @onNull int[] states)249 public @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates( 250 @NonNull int[] states) { 251 if (VDBG) log("getDevicesMatchingStates()"); 252 final IBluetoothHearingAid service = getService(); 253 try { 254 if (service != null && isEnabled()) { 255 return service.getDevicesMatchingConnectionStates(states); 256 } 257 if (service == null) Log.w(TAG, "Proxy not attached to service"); 258 return new ArrayList<BluetoothDevice>(); 259 } catch (RemoteException e) { 260 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 261 return new ArrayList<BluetoothDevice>(); 262 } 263 } 264 265 /** 266 * {@inheritDoc} 267 */ 268 @Override getConnectionState( @onNull BluetoothDevice device)269 public @BluetoothProfile.BtProfileState int getConnectionState( 270 @NonNull BluetoothDevice device) { 271 if (VDBG) log("getState(" + device + ")"); 272 final IBluetoothHearingAid service = getService(); 273 try { 274 if (service != null && isEnabled() 275 && isValidDevice(device)) { 276 return service.getConnectionState(device); 277 } 278 if (service == null) Log.w(TAG, "Proxy not attached to service"); 279 return BluetoothProfile.STATE_DISCONNECTED; 280 } catch (RemoteException e) { 281 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 282 return BluetoothProfile.STATE_DISCONNECTED; 283 } 284 } 285 286 /** 287 * Select a connected device as active. 288 * 289 * The active device selection is per profile. An active device's 290 * purpose is profile-specific. For example, Hearing Aid audio 291 * streaming is to the active Hearing Aid device. If a remote device 292 * is not connected, it cannot be selected as active. 293 * 294 * <p> This API returns false in scenarios like the profile on the 295 * device is not connected or Bluetooth is not turned on. 296 * When this API returns true, it is guaranteed that the 297 * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted 298 * with the active device. 299 * 300 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 301 * permission. 302 * 303 * @param device the remote Bluetooth device. Could be null to clear 304 * the active device and stop streaming audio to a Bluetooth device. 305 * @return false on immediate error, true otherwise 306 * @hide 307 */ 308 @UnsupportedAppUsage setActiveDevice(@ullable BluetoothDevice device)309 public boolean setActiveDevice(@Nullable BluetoothDevice device) { 310 if (DBG) log("setActiveDevice(" + device + ")"); 311 final IBluetoothHearingAid service = getService(); 312 try { 313 if (service != null && isEnabled() 314 && ((device == null) || isValidDevice(device))) { 315 service.setActiveDevice(device); 316 return true; 317 } 318 if (service == null) Log.w(TAG, "Proxy not attached to service"); 319 return false; 320 } catch (RemoteException e) { 321 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 322 return false; 323 } 324 } 325 326 /** 327 * Get the connected physical Hearing Aid devices that are active 328 * 329 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} 330 * permission. 331 * 332 * @return the list of active devices. The first element is the left active 333 * device; the second element is the right active device. If either or both side 334 * is not active, it will be null on that position. Returns empty list on error. 335 * @hide 336 */ 337 @UnsupportedAppUsage 338 @RequiresPermission(Manifest.permission.BLUETOOTH) getActiveDevices()339 public List<BluetoothDevice> getActiveDevices() { 340 if (VDBG) log("getActiveDevices()"); 341 final IBluetoothHearingAid service = getService(); 342 try { 343 if (service != null && isEnabled()) { 344 return service.getActiveDevices(); 345 } 346 if (service == null) Log.w(TAG, "Proxy not attached to service"); 347 return new ArrayList<>(); 348 } catch (RemoteException e) { 349 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 350 return new ArrayList<>(); 351 } 352 } 353 354 /** 355 * Set priority of the profile 356 * 357 * <p> The device should already be paired. 358 * Priority can be one of {@link #PRIORITY_ON} orgetBluetoothManager 359 * {@link #PRIORITY_OFF}, 360 * 361 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 362 * permission. 363 * 364 * @param device Paired bluetooth device 365 * @param priority 366 * @return true if priority is set, false on error 367 * @hide 368 */ setPriority(BluetoothDevice device, int priority)369 public boolean setPriority(BluetoothDevice device, int priority) { 370 if (DBG) log("setPriority(" + device + ", " + priority + ")"); 371 final IBluetoothHearingAid service = getService(); 372 try { 373 if (service != null && isEnabled() 374 && isValidDevice(device)) { 375 if (priority != BluetoothProfile.PRIORITY_OFF 376 && priority != BluetoothProfile.PRIORITY_ON) { 377 return false; 378 } 379 return service.setPriority(device, priority); 380 } 381 if (service == null) Log.w(TAG, "Proxy not attached to service"); 382 return false; 383 } catch (RemoteException e) { 384 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 385 return false; 386 } 387 } 388 389 /** 390 * Get the priority of the profile. 391 * 392 * <p> The priority can be any of: 393 * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF}, 394 * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED} 395 * 396 * @param device Bluetooth device 397 * @return priority of the device 398 * @hide 399 */ 400 @RequiresPermission(Manifest.permission.BLUETOOTH) getPriority(BluetoothDevice device)401 public int getPriority(BluetoothDevice device) { 402 if (VDBG) log("getPriority(" + device + ")"); 403 final IBluetoothHearingAid service = getService(); 404 try { 405 if (service != null && isEnabled() 406 && isValidDevice(device)) { 407 return service.getPriority(device); 408 } 409 if (service == null) Log.w(TAG, "Proxy not attached to service"); 410 return BluetoothProfile.PRIORITY_OFF; 411 } catch (RemoteException e) { 412 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 413 return BluetoothProfile.PRIORITY_OFF; 414 } 415 } 416 417 /** 418 * Helper for converting a state to a string. 419 * 420 * For debug use only - strings are not internationalized. 421 * 422 * @hide 423 */ stateToString(int state)424 public static String stateToString(int state) { 425 switch (state) { 426 case STATE_DISCONNECTED: 427 return "disconnected"; 428 case STATE_CONNECTING: 429 return "connecting"; 430 case STATE_CONNECTED: 431 return "connected"; 432 case STATE_DISCONNECTING: 433 return "disconnecting"; 434 default: 435 return "<unknown state " + state + ">"; 436 } 437 } 438 439 /** 440 * Get the volume of the device. 441 * 442 * <p> The volume is between -128 dB (mute) to 0 dB. 443 * 444 * @return volume of the hearing aid device. 445 * @hide 446 */ 447 @RequiresPermission(Manifest.permission.BLUETOOTH) getVolume()448 public int getVolume() { 449 if (VDBG) { 450 log("getVolume()"); 451 } 452 final IBluetoothHearingAid service = getService(); 453 try { 454 if (service != null && isEnabled()) { 455 return service.getVolume(); 456 } 457 if (service == null) Log.w(TAG, "Proxy not attached to service"); 458 return 0; 459 } catch (RemoteException e) { 460 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 461 return 0; 462 } 463 } 464 465 /** 466 * Tells remote device to adjust volume. Uses the following values: 467 * <ul> 468 * <li>{@link AudioManager#ADJUST_LOWER}</li> 469 * <li>{@link AudioManager#ADJUST_RAISE}</li> 470 * <li>{@link AudioManager#ADJUST_MUTE}</li> 471 * <li>{@link AudioManager#ADJUST_UNMUTE}</li> 472 * </ul> 473 * 474 * @param direction One of the supported adjust values. 475 * @hide 476 */ 477 @RequiresPermission(Manifest.permission.BLUETOOTH) adjustVolume(int direction)478 public void adjustVolume(int direction) { 479 if (DBG) log("adjustVolume(" + direction + ")"); 480 481 final IBluetoothHearingAid service = getService(); 482 try { 483 if (service == null) { 484 Log.w(TAG, "Proxy not attached to service"); 485 return; 486 } 487 488 if (!isEnabled()) return; 489 490 service.adjustVolume(direction); 491 } catch (RemoteException e) { 492 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 493 } 494 } 495 496 /** 497 * Tells remote device to set an absolute volume. 498 * 499 * @param volume Absolute volume to be set on remote 500 * @hide 501 */ setVolume(int volume)502 public void setVolume(int volume) { 503 if (DBG) Log.d(TAG, "setVolume(" + volume + ")"); 504 505 final IBluetoothHearingAid service = getService(); 506 try { 507 if (service == null) { 508 Log.w(TAG, "Proxy not attached to service"); 509 return; 510 } 511 512 if (!isEnabled()) return; 513 514 service.setVolume(volume); 515 } catch (RemoteException e) { 516 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 517 } 518 } 519 520 /** 521 * Get the CustomerId of the device. 522 * 523 * @param device Bluetooth device 524 * @return the CustomerId of the device 525 * @hide 526 */ 527 @RequiresPermission(Manifest.permission.BLUETOOTH) getHiSyncId(BluetoothDevice device)528 public long getHiSyncId(BluetoothDevice device) { 529 if (VDBG) { 530 log("getCustomerId(" + device + ")"); 531 } 532 final IBluetoothHearingAid service = getService(); 533 try { 534 if (service == null) { 535 Log.w(TAG, "Proxy not attached to service"); 536 return HI_SYNC_ID_INVALID; 537 } 538 539 if (!isEnabled() || !isValidDevice(device)) return HI_SYNC_ID_INVALID; 540 541 return service.getHiSyncId(device); 542 } catch (RemoteException e) { 543 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 544 return HI_SYNC_ID_INVALID; 545 } 546 } 547 548 /** 549 * Get the side of the device. 550 * 551 * @param device Bluetooth device. 552 * @return SIDE_LEFT or SIDE_RIGHT 553 * @hide 554 */ 555 @RequiresPermission(Manifest.permission.BLUETOOTH) getDeviceSide(BluetoothDevice device)556 public int getDeviceSide(BluetoothDevice device) { 557 if (VDBG) { 558 log("getDeviceSide(" + device + ")"); 559 } 560 final IBluetoothHearingAid service = getService(); 561 try { 562 if (service != null && isEnabled() 563 && isValidDevice(device)) { 564 return service.getDeviceSide(device); 565 } 566 if (service == null) Log.w(TAG, "Proxy not attached to service"); 567 return SIDE_LEFT; 568 } catch (RemoteException e) { 569 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 570 return SIDE_LEFT; 571 } 572 } 573 574 /** 575 * Get the mode of the device. 576 * 577 * @param device Bluetooth device 578 * @return MODE_MONAURAL or MODE_BINAURAL 579 * @hide 580 */ 581 @RequiresPermission(Manifest.permission.BLUETOOTH) getDeviceMode(BluetoothDevice device)582 public int getDeviceMode(BluetoothDevice device) { 583 if (VDBG) { 584 log("getDeviceMode(" + device + ")"); 585 } 586 final IBluetoothHearingAid service = getService(); 587 try { 588 if (service != null && isEnabled() 589 && isValidDevice(device)) { 590 return service.getDeviceMode(device); 591 } 592 if (service == null) Log.w(TAG, "Proxy not attached to service"); 593 return MODE_MONAURAL; 594 } catch (RemoteException e) { 595 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 596 return MODE_MONAURAL; 597 } 598 } 599 isEnabled()600 private boolean isEnabled() { 601 if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; 602 return false; 603 } 604 isValidDevice(BluetoothDevice device)605 private boolean isValidDevice(BluetoothDevice device) { 606 if (device == null) return false; 607 608 if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true; 609 return false; 610 } 611 log(String msg)612 private static void log(String msg) { 613 Log.d(TAG, msg); 614 } 615 } 616