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