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