• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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