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