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