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.annotation.IntDef; 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.SuppressLint; 26 import android.annotation.SystemApi; 27 import android.bluetooth.annotations.RequiresBluetoothConnectPermission; 28 import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission; 29 import android.bluetooth.annotations.RequiresLegacyBluetoothPermission; 30 import android.compat.annotation.UnsupportedAppUsage; 31 import android.content.AttributionSource; 32 import android.content.Context; 33 import android.os.Build; 34 import android.os.IBinder; 35 import android.os.Parcel; 36 import android.os.Parcelable; 37 import android.os.RemoteException; 38 import android.util.Log; 39 40 import java.lang.annotation.Retention; 41 import java.lang.annotation.RetentionPolicy; 42 import java.util.Collections; 43 import java.util.List; 44 45 /** 46 * This class provides the public APIs to control the Hearing Aid profile. 47 * 48 * <p>BluetoothHearingAid is a proxy object for controlling the Bluetooth Hearing Aid Service via 49 * IPC. Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothHearingAid proxy object. 50 * 51 * <p>Android only supports one set of connected Bluetooth Hearing Aid device at a time. Each method 52 * is protected with its appropriate permission. 53 */ 54 public final class BluetoothHearingAid implements BluetoothProfile { 55 private static final String TAG = "BluetoothHearingAid"; 56 private static final boolean DBG = true; 57 private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); 58 59 /** 60 * This class provides the APIs to get device's advertisement data. The advertisement data might 61 * be incomplete or not available. 62 * 63 * <p><a 64 * href=https://source.android.com/docs/core/connect/bluetooth/asha#advertisements-for-asha-gatt-service> 65 * documentation can be found here</a> 66 * 67 * @hide 68 */ 69 @SystemApi 70 public static final class AdvertisementServiceData implements Parcelable { 71 private static final String TAG = "AdvertisementData"; 72 73 private final int mCapability; 74 private final int mTruncatedHiSyncId; 75 76 /** 77 * Construct AdvertisementServiceData. 78 * 79 * @param capability hearing aid's capability 80 * @param truncatedHiSyncId truncated HiSyncId 81 * @hide 82 */ AdvertisementServiceData(int capability, int truncatedHiSyncId)83 public AdvertisementServiceData(int capability, int truncatedHiSyncId) { 84 if (DBG) { 85 Log.d(TAG, "capability:" + capability + " truncatedHiSyncId:" + truncatedHiSyncId); 86 } 87 mCapability = capability; 88 mTruncatedHiSyncId = truncatedHiSyncId; 89 } 90 91 /** 92 * Get the mode of the device based on its advertisement data. 93 * 94 * @hide 95 */ 96 @RequiresPermission( 97 allOf = { 98 android.Manifest.permission.BLUETOOTH_SCAN, 99 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 100 }) 101 @SystemApi 102 @DeviceMode getDeviceMode()103 public int getDeviceMode() { 104 if (VDBG) Log.v(TAG, "getDeviceMode()"); 105 return (mCapability >> 1) & 1; 106 } 107 AdvertisementServiceData(@onNull Parcel in)108 private AdvertisementServiceData(@NonNull Parcel in) { 109 mCapability = in.readInt(); 110 mTruncatedHiSyncId = in.readInt(); 111 } 112 113 /** 114 * Get the side of the device based on its advertisement data. 115 * 116 * @hide 117 */ 118 @RequiresPermission( 119 allOf = { 120 android.Manifest.permission.BLUETOOTH_SCAN, 121 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 122 }) 123 @SystemApi 124 @DeviceSide getDeviceSide()125 public int getDeviceSide() { 126 if (VDBG) Log.v(TAG, "getDeviceSide()"); 127 return mCapability & 1; 128 } 129 130 /** 131 * Check if {@link BluetoothHearingAid} marks itself as CSIP supported based on its 132 * advertisement data. 133 * 134 * @return {@code true} when CSIP is supported, {@code false} otherwise 135 * @hide 136 */ 137 @RequiresPermission( 138 allOf = { 139 android.Manifest.permission.BLUETOOTH_SCAN, 140 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 141 }) 142 @SystemApi isCsipSupported()143 public boolean isCsipSupported() { 144 if (VDBG) Log.v(TAG, "isCsipSupported()"); 145 return ((mCapability >> 2) & 1) != 0; 146 } 147 148 /** 149 * Get the truncated HiSyncId of the device based on its advertisement data. 150 * 151 * @hide 152 */ 153 @RequiresPermission( 154 allOf = { 155 android.Manifest.permission.BLUETOOTH_SCAN, 156 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 157 }) 158 @SystemApi getTruncatedHiSyncId()159 public int getTruncatedHiSyncId() { 160 if (VDBG) Log.v(TAG, "getTruncatedHiSyncId: " + mTruncatedHiSyncId); 161 return mTruncatedHiSyncId; 162 } 163 164 /** 165 * Check if another {@link AdvertisementServiceData} is likely a pair with current one. 166 * There is a possibility of a collision on truncated HiSyncId which leads to falsely 167 * identified as a pair. 168 * 169 * @param data another device's {@link AdvertisementServiceData} 170 * @return {@code true} if the devices are a likely pair, {@code false} otherwise 171 * @hide 172 */ 173 @RequiresPermission( 174 allOf = { 175 android.Manifest.permission.BLUETOOTH_SCAN, 176 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 177 }) 178 @SystemApi isInPairWith(@ullable AdvertisementServiceData data)179 public boolean isInPairWith(@Nullable AdvertisementServiceData data) { 180 if (VDBG) Log.v(TAG, "isInPairWith()"); 181 if (data == null) { 182 return false; 183 } 184 185 boolean bothSupportCsip = isCsipSupported() && data.isCsipSupported(); 186 boolean isDifferentSide = 187 (getDeviceSide() != SIDE_UNKNOWN && data.getDeviceSide() != SIDE_UNKNOWN) 188 && (getDeviceSide() != data.getDeviceSide()); 189 boolean isSameTruncatedHiSyncId = mTruncatedHiSyncId == data.mTruncatedHiSyncId; 190 return bothSupportCsip && isDifferentSide && isSameTruncatedHiSyncId; 191 } 192 193 /** @hide */ 194 @Override describeContents()195 public int describeContents() { 196 return 0; 197 } 198 199 @Override writeToParcel(@onNull Parcel dest, int flags)200 public void writeToParcel(@NonNull Parcel dest, int flags) { 201 dest.writeInt(mCapability); 202 dest.writeInt(mTruncatedHiSyncId); 203 } 204 205 public static final @NonNull Parcelable.Creator<AdvertisementServiceData> CREATOR = 206 new Parcelable.Creator<AdvertisementServiceData>() { 207 public AdvertisementServiceData createFromParcel(Parcel in) { 208 return new AdvertisementServiceData(in); 209 } 210 211 public AdvertisementServiceData[] newArray(int size) { 212 return new AdvertisementServiceData[size]; 213 } 214 }; 215 } 216 217 /** 218 * Intent used to broadcast the change in connection state of the Hearing Aid profile. Please 219 * note that in the binaural case, there will be two different LE devices for the left and right 220 * side and each device will have their own connection state changes.S 221 * 222 * <p>This intent will have 3 extras: 223 * 224 * <ul> 225 * <li>{@link #EXTRA_STATE} - The current state of the profile. 226 * <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. 227 * <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device. 228 * </ul> 229 * 230 * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link 231 * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link 232 * #STATE_DISCONNECTING}. 233 */ 234 @RequiresLegacyBluetoothPermission 235 @RequiresBluetoothConnectPermission 236 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 237 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 238 public static final String ACTION_CONNECTION_STATE_CHANGED = 239 "android.bluetooth.hearingaid.profile.action.CONNECTION_STATE_CHANGED"; 240 241 /** 242 * Intent used to broadcast the selection of a connected device as active. 243 * 244 * <p>This intent will have one extra: 245 * 246 * <ul> 247 * <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can be null if no device 248 * is active. 249 * </ul> 250 * 251 * @hide 252 */ 253 @SystemApi 254 @RequiresLegacyBluetoothPermission 255 @RequiresBluetoothConnectPermission 256 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 257 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 258 @SuppressLint("ActionValue") 259 public static final String ACTION_ACTIVE_DEVICE_CHANGED = 260 "android.bluetooth.hearingaid.profile.action.ACTIVE_DEVICE_CHANGED"; 261 262 /** @hide */ 263 @IntDef( 264 prefix = "SIDE_", 265 value = {SIDE_UNKNOWN, SIDE_LEFT, SIDE_RIGHT}) 266 @Retention(RetentionPolicy.SOURCE) 267 public @interface DeviceSide {} 268 269 /** 270 * Indicates the device side could not be read. 271 * 272 * @hide 273 */ 274 @SystemApi public static final int SIDE_UNKNOWN = -1; 275 276 /** 277 * This device represents Left Hearing Aid. 278 * 279 * @hide 280 */ 281 @SystemApi public static final int SIDE_LEFT = IBluetoothHearingAid.SIDE_LEFT; 282 283 /** 284 * This device represents Right Hearing Aid. 285 * 286 * @hide 287 */ 288 @SystemApi public static final int SIDE_RIGHT = IBluetoothHearingAid.SIDE_RIGHT; 289 290 /** @hide */ 291 @IntDef( 292 prefix = "MODE_", 293 value = {MODE_UNKNOWN, MODE_MONAURAL, MODE_BINAURAL}) 294 @Retention(RetentionPolicy.SOURCE) 295 public @interface DeviceMode {} 296 297 /** 298 * Indicates the device mode could not be read. 299 * 300 * @hide 301 */ 302 @SystemApi public static final int MODE_UNKNOWN = -1; 303 304 /** 305 * This device is Monaural. 306 * 307 * @hide 308 */ 309 @SystemApi public static final int MODE_MONAURAL = IBluetoothHearingAid.MODE_MONAURAL; 310 311 /** 312 * This device is Binaural (should receive only left or right audio). 313 * 314 * @hide 315 */ 316 @SystemApi public static final int MODE_BINAURAL = IBluetoothHearingAid.MODE_BINAURAL; 317 318 /** 319 * Indicates the HiSyncID could not be read and is unavailable. 320 * 321 * @hide 322 */ 323 @SystemApi public static final long HI_SYNC_ID_INVALID = 0; 324 325 private final BluetoothAdapter mAdapter; 326 private final AttributionSource mAttributionSource; 327 328 private IBluetoothHearingAid mService; 329 330 /** 331 * Create a BluetoothHearingAid proxy object for interacting with the local Bluetooth Hearing 332 * Aid service. 333 */ BluetoothHearingAid(Context context, BluetoothAdapter adapter)334 /* package */ BluetoothHearingAid(Context context, BluetoothAdapter adapter) { 335 mAdapter = adapter; 336 mAttributionSource = adapter.getAttributionSource(); 337 mService = null; 338 } 339 340 /** @hide */ 341 @Override onServiceConnected(IBinder service)342 public void onServiceConnected(IBinder service) { 343 mService = IBluetoothHearingAid.Stub.asInterface(service); 344 } 345 346 /** @hide */ 347 @Override onServiceDisconnected()348 public void onServiceDisconnected() { 349 mService = null; 350 } 351 getService()352 private IBluetoothHearingAid getService() { 353 return mService; 354 } 355 356 /** @hide */ 357 @Override getAdapter()358 public BluetoothAdapter getAdapter() { 359 return mAdapter; 360 } 361 362 /** 363 * Initiate connection to a profile of the remote bluetooth device. 364 * 365 * <p>This API returns false in scenarios like the profile on the device is already connected or 366 * Bluetooth is not turned on. When this API returns true, it is guaranteed that connection 367 * state intent for the profile will be broadcasted with the state. Users can get the connection 368 * state of the profile from this intent. 369 * 370 * @param device Remote Bluetooth Device 371 * @return false on immediate error, true otherwise 372 * @hide 373 */ 374 @RequiresBluetoothConnectPermission 375 @RequiresPermission( 376 allOf = { 377 android.Manifest.permission.BLUETOOTH_CONNECT, 378 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 379 }) connect(BluetoothDevice device)380 public boolean connect(BluetoothDevice device) { 381 if (DBG) Log.d(TAG, "connect(" + device + ")"); 382 final IBluetoothHearingAid service = getService(); 383 if (service == null) { 384 Log.w(TAG, "Proxy not attached to service"); 385 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 386 } else if (isEnabled() && isValidDevice(device)) { 387 try { 388 return service.connect(device, mAttributionSource); 389 } catch (RemoteException e) { 390 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 391 } 392 } 393 return false; 394 } 395 396 /** 397 * Initiate disconnection from a profile 398 * 399 * <p>This API will return false in scenarios like the profile on the Bluetooth device is not in 400 * connected state etc. When this API returns, true, it is guaranteed that the connection state 401 * change intent will be broadcasted with the state. Users can get the disconnection state of 402 * the profile from this intent. 403 * 404 * <p>If the disconnection is initiated by a remote device, the state will transition from 405 * {@link #STATE_CONNECTED} to {@link #STATE_DISCONNECTED}. If the disconnect is initiated by 406 * the host (local) device the state will transition from {@link #STATE_CONNECTED} to state 407 * {@link #STATE_DISCONNECTING} to state {@link #STATE_DISCONNECTED}. The transition to {@link 408 * #STATE_DISCONNECTING} can be used to distinguish between the two scenarios. 409 * 410 * @param device Remote Bluetooth Device 411 * @return false on immediate error, true otherwise 412 * @hide 413 */ 414 @RequiresBluetoothConnectPermission 415 @RequiresPermission( 416 allOf = { 417 android.Manifest.permission.BLUETOOTH_CONNECT, 418 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 419 }) disconnect(BluetoothDevice device)420 public boolean disconnect(BluetoothDevice device) { 421 if (DBG) Log.d(TAG, "disconnect(" + device + ")"); 422 final IBluetoothHearingAid service = getService(); 423 if (service == null) { 424 Log.w(TAG, "Proxy not attached to service"); 425 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 426 } else if (isEnabled() && isValidDevice(device)) { 427 try { 428 return service.disconnect(device, mAttributionSource); 429 } catch (RemoteException e) { 430 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 431 } 432 } 433 return false; 434 } 435 436 /** {@inheritDoc} */ 437 @Override 438 @RequiresBluetoothConnectPermission 439 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getConnectedDevices()440 public @NonNull List<BluetoothDevice> getConnectedDevices() { 441 if (VDBG) Log.v(TAG, "getConnectedDevices()"); 442 final IBluetoothHearingAid service = getService(); 443 if (service == null) { 444 Log.w(TAG, "Proxy not attached to service"); 445 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 446 } else if (isEnabled()) { 447 try { 448 return Attributable.setAttributionSource( 449 service.getConnectedDevices(mAttributionSource), mAttributionSource); 450 } catch (RemoteException e) { 451 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 452 } 453 } 454 return Collections.emptyList(); 455 } 456 457 /** {@inheritDoc} */ 458 @Override 459 @RequiresBluetoothConnectPermission 460 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 461 @NonNull getDevicesMatchingConnectionStates(@onNull int[] states)462 public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) { 463 if (VDBG) Log.v(TAG, "getDevicesMatchingStates()"); 464 final IBluetoothHearingAid service = getService(); 465 if (service == null) { 466 Log.w(TAG, "Proxy not attached to service"); 467 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 468 } else if (isEnabled()) { 469 try { 470 return Attributable.setAttributionSource( 471 service.getDevicesMatchingConnectionStates(states, mAttributionSource), 472 mAttributionSource); 473 } catch (RemoteException e) { 474 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 475 } 476 } 477 return Collections.emptyList(); 478 } 479 480 /** {@inheritDoc} */ 481 @Override 482 @RequiresBluetoothConnectPermission 483 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 484 @BluetoothProfile.BtProfileState getConnectionState(@onNull BluetoothDevice device)485 public int getConnectionState(@NonNull BluetoothDevice device) { 486 if (VDBG) Log.v(TAG, "getState(" + device + ")"); 487 final IBluetoothHearingAid service = getService(); 488 if (service == null) { 489 Log.w(TAG, "Proxy not attached to service"); 490 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 491 } else if (isEnabled() && isValidDevice(device)) { 492 try { 493 return service.getConnectionState(device, mAttributionSource); 494 } catch (RemoteException e) { 495 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 496 } 497 } 498 return BluetoothProfile.STATE_DISCONNECTED; 499 } 500 501 /** 502 * Select a connected device as active. 503 * 504 * <p>The active device selection is per profile. An active device's purpose is 505 * profile-specific. For example, Hearing Aid audio streaming is to the active Hearing Aid 506 * device. If a remote device is not connected, it cannot be selected as active. 507 * 508 * <p>This API returns false in scenarios like the profile on the device is not connected or 509 * Bluetooth is not turned on. When this API returns true, it is guaranteed that the {@link 510 * #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted with the active device. 511 * 512 * @param device the remote Bluetooth device. Could be null to clear the active device and stop 513 * streaming audio to a Bluetooth device. 514 * @return false on immediate error, true otherwise 515 * @hide 516 */ 517 @RequiresLegacyBluetoothAdminPermission 518 @RequiresBluetoothConnectPermission 519 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 520 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) setActiveDevice(@ullable BluetoothDevice device)521 public boolean setActiveDevice(@Nullable BluetoothDevice device) { 522 if (DBG) Log.d(TAG, "setActiveDevice(" + device + ")"); 523 final IBluetoothHearingAid service = getService(); 524 if (service == null) { 525 Log.w(TAG, "Proxy not attached to service"); 526 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 527 } else if (isEnabled() && ((device == null) || isValidDevice(device))) { 528 try { 529 return service.setActiveDevice(device, mAttributionSource); 530 } catch (RemoteException e) { 531 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 532 } 533 } 534 return false; 535 } 536 537 /** 538 * Get the connected physical Hearing Aid devices that are active 539 * 540 * @return the list of active devices. The first element is the left active device; the second 541 * element is the right active device. If either or both side is not active, it will be null 542 * on that position. Returns empty list on error. 543 * @hide 544 */ 545 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 546 @RequiresLegacyBluetoothPermission 547 @RequiresBluetoothConnectPermission 548 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getActiveDevices()549 public @NonNull List<BluetoothDevice> getActiveDevices() { 550 if (VDBG) Log.v(TAG, "getActiveDevices()"); 551 final IBluetoothHearingAid service = getService(); 552 if (service == null) { 553 Log.w(TAG, "Proxy not attached to service"); 554 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 555 } else if (isEnabled()) { 556 try { 557 return Attributable.setAttributionSource( 558 service.getActiveDevices(mAttributionSource), mAttributionSource); 559 } catch (RemoteException e) { 560 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 561 } 562 } 563 return Collections.emptyList(); 564 } 565 566 /** 567 * Set connection policy of the profile 568 * 569 * <p>The device should already be paired. Connection policy can be one of {@link 570 * #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, {@link 571 * #CONNECTION_POLICY_UNKNOWN} 572 * 573 * @param device Paired bluetooth device 574 * @param connectionPolicy is the connection policy to set to for this profile 575 * @return true if connectionPolicy is set, false on error 576 * @hide 577 */ 578 @SystemApi 579 @RequiresBluetoothConnectPermission 580 @RequiresPermission( 581 allOf = { 582 android.Manifest.permission.BLUETOOTH_CONNECT, 583 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 584 }) setConnectionPolicy( @onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)585 public boolean setConnectionPolicy( 586 @NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) { 587 if (DBG) Log.d(TAG, "setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); 588 verifyDeviceNotNull(device, "setConnectionPolicy"); 589 final IBluetoothHearingAid service = getService(); 590 if (service == null) { 591 Log.w(TAG, "Proxy not attached to service"); 592 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 593 } else if (isEnabled() 594 && isValidDevice(device) 595 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN 596 || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) { 597 try { 598 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource); 599 } catch (RemoteException e) { 600 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 601 } 602 } 603 return false; 604 } 605 606 /** 607 * Get the connection policy of the profile. 608 * 609 * <p>The connection policy can be any of: {@link #CONNECTION_POLICY_ALLOWED}, {@link 610 * #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN} 611 * 612 * @param device Bluetooth device 613 * @return connection policy of the device 614 * @hide 615 */ 616 @SystemApi 617 @RequiresBluetoothConnectPermission 618 @RequiresPermission( 619 allOf = { 620 android.Manifest.permission.BLUETOOTH_CONNECT, 621 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 622 }) getConnectionPolicy(@onNull BluetoothDevice device)623 public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) { 624 if (VDBG) Log.v(TAG, "getConnectionPolicy(" + device + ")"); 625 verifyDeviceNotNull(device, "getConnectionPolicy"); 626 final IBluetoothHearingAid service = getService(); 627 if (service == null) { 628 Log.w(TAG, "Proxy not attached to service"); 629 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 630 } else if (isEnabled() && isValidDevice(device)) { 631 try { 632 return service.getConnectionPolicy(device, mAttributionSource); 633 } catch (RemoteException e) { 634 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 635 } 636 } 637 return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 638 } 639 640 /** 641 * Helper for converting a state to a string. 642 * 643 * <p>For debug use only - strings are not internationalized. 644 * 645 * @hide 646 */ stateToString(int state)647 public static String stateToString(int state) { 648 switch (state) { 649 case STATE_DISCONNECTED: 650 return "disconnected"; 651 case STATE_CONNECTING: 652 return "connecting"; 653 case STATE_CONNECTED: 654 return "connected"; 655 case STATE_DISCONNECTING: 656 return "disconnecting"; 657 default: 658 return "<unknown state " + state + ">"; 659 } 660 } 661 662 /** 663 * Tells remote device to set an absolute volume. 664 * 665 * @param volume Absolute volume to be set on remote 666 * @hide 667 */ 668 @SystemApi 669 @RequiresBluetoothConnectPermission 670 @RequiresPermission( 671 allOf = { 672 android.Manifest.permission.BLUETOOTH_CONNECT, 673 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 674 }) setVolume(int volume)675 public void setVolume(int volume) { 676 if (DBG) Log.d(TAG, "setVolume(" + volume + ")"); 677 final IBluetoothHearingAid service = getService(); 678 if (service == null) { 679 Log.w(TAG, "Proxy not attached to service"); 680 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 681 } else if (isEnabled()) { 682 try { 683 service.setVolume(volume, mAttributionSource); 684 } catch (RemoteException e) { 685 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 686 } 687 } 688 } 689 690 /** 691 * Get the HiSyncId (unique hearing aid device identifier) of the device. 692 * 693 * <p><a href=https://source.android.com/devices/bluetooth/asha#hisyncid>HiSyncId documentation 694 * can be found here</a> 695 * 696 * @param device Bluetooth device 697 * @return the HiSyncId of the device 698 * @hide 699 */ 700 @SystemApi 701 @RequiresBluetoothConnectPermission 702 @RequiresPermission( 703 allOf = { 704 android.Manifest.permission.BLUETOOTH_CONNECT, 705 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 706 }) getHiSyncId(@onNull BluetoothDevice device)707 public long getHiSyncId(@NonNull BluetoothDevice device) { 708 if (VDBG) Log.v(TAG, "getHiSyncId(" + device + ")"); 709 verifyDeviceNotNull(device, "getHiSyncId"); 710 final IBluetoothHearingAid service = getService(); 711 if (service == null) { 712 Log.w(TAG, "Proxy not attached to service"); 713 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 714 } else if (isEnabled() && isValidDevice(device)) { 715 try { 716 return service.getHiSyncId(device, mAttributionSource); 717 } catch (RemoteException e) { 718 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 719 } 720 } 721 return HI_SYNC_ID_INVALID; 722 } 723 724 /** 725 * Get the side of the device. 726 * 727 * @param device Bluetooth device. 728 * @return the {@code SIDE_LEFT}, {@code SIDE_RIGHT} of the device, or {@code SIDE_UNKNOWN} if 729 * one is not available. 730 * @hide 731 */ 732 @SystemApi 733 @RequiresLegacyBluetoothPermission 734 @RequiresBluetoothConnectPermission 735 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 736 @DeviceSide getDeviceSide(@onNull BluetoothDevice device)737 public int getDeviceSide(@NonNull BluetoothDevice device) { 738 if (VDBG) Log.v(TAG, "getDeviceSide(" + device + ")"); 739 verifyDeviceNotNull(device, "getDeviceSide"); 740 final IBluetoothHearingAid service = getService(); 741 if (service == null) { 742 Log.w(TAG, "Proxy not attached to service"); 743 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 744 } else if (isEnabled() && isValidDevice(device)) { 745 try { 746 return service.getDeviceSide(device, mAttributionSource); 747 } catch (RemoteException e) { 748 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 749 } 750 } 751 return SIDE_UNKNOWN; 752 } 753 754 /** 755 * Get the mode of the device. 756 * 757 * @param device Bluetooth device 758 * @return the {@code MODE_MONAURAL}, {@code MODE_BINAURAL} of the device, or {@code 759 * MODE_UNKNOWN} if one is not available. 760 * @hide 761 */ 762 @SystemApi 763 @RequiresLegacyBluetoothPermission 764 @RequiresBluetoothConnectPermission 765 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 766 @DeviceMode getDeviceMode(@onNull BluetoothDevice device)767 public int getDeviceMode(@NonNull BluetoothDevice device) { 768 if (VDBG) Log.v(TAG, "getDeviceMode(" + device + ")"); 769 verifyDeviceNotNull(device, "getDeviceMode"); 770 final IBluetoothHearingAid service = getService(); 771 if (service == null) { 772 Log.w(TAG, "Proxy not attached to service"); 773 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 774 } else if (isEnabled() && isValidDevice(device)) { 775 try { 776 return service.getDeviceMode(device, mAttributionSource); 777 } catch (RemoteException e) { 778 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 779 } 780 } 781 return MODE_UNKNOWN; 782 } 783 784 /** 785 * Get ASHA device's advertisement service data. 786 * 787 * @param device discovered Bluetooth device 788 * @return {@link AdvertisementServiceData} 789 * @hide 790 */ 791 @SystemApi 792 @RequiresPermission( 793 allOf = { 794 android.Manifest.permission.BLUETOOTH_SCAN, 795 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 796 }) getAdvertisementServiceData( @onNull BluetoothDevice device)797 public @Nullable AdvertisementServiceData getAdvertisementServiceData( 798 @NonNull BluetoothDevice device) { 799 if (DBG) { 800 Log.d(TAG, "getAdvertisementServiceData()"); 801 } 802 final IBluetoothHearingAid service = getService(); 803 if (service == null || !isEnabled() || !isValidDevice(device)) { 804 Log.w(TAG, "Proxy not attached to service"); 805 if (DBG) { 806 Log.d(TAG, Log.getStackTraceString(new Throwable())); 807 } 808 } else { 809 try { 810 return service.getAdvertisementServiceData(device, mAttributionSource); 811 } catch (RemoteException e) { 812 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 813 } 814 } 815 return null; 816 } 817 isEnabled()818 private boolean isEnabled() { 819 if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; 820 return false; 821 } 822 verifyDeviceNotNull(BluetoothDevice device, String methodName)823 private void verifyDeviceNotNull(BluetoothDevice device, String methodName) { 824 if (device == null) { 825 Log.e(TAG, methodName + ": device param is null"); 826 throw new IllegalArgumentException("Device cannot be null"); 827 } 828 } 829 isValidDevice(BluetoothDevice device)830 private boolean isValidDevice(BluetoothDevice device) { 831 if (device == null) return false; 832 833 if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true; 834 return false; 835 } 836 } 837