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