1 /*
2  * Copyright 2021 HIMSA II K/S - www.himsa.com.
3  * Represented by EHIMA - www.ehima.com
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package android.bluetooth;
19 
20 import android.annotation.CallbackExecutor;
21 import android.annotation.FlaggedApi;
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.RequiresPermission;
26 import android.annotation.SdkConstant;
27 import android.annotation.SuppressLint;
28 import android.annotation.SystemApi;
29 import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
30 import android.content.AttributionSource;
31 import android.content.Context;
32 import android.os.IBinder;
33 import android.os.RemoteException;
34 import android.util.CloseGuard;
35 import android.util.Log;
36 
37 import com.android.bluetooth.flags.Flags;
38 
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 import java.util.Collections;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Objects;
46 import java.util.concurrent.Executor;
47 
48 /**
49  * This class provides a public APIs to control the Bluetooth Hearing Access Profile client service.
50  *
51  * <p>BluetoothHapClient is a proxy object for controlling the Bluetooth HAP Service client via IPC.
52  * Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothHapClient proxy object.
53  *
54  * @hide
55  */
56 @SystemApi
57 public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable {
58     private static final String TAG = "BluetoothHapClient";
59     private static final boolean DBG = false;
60     private static final boolean VDBG = false;
61 
62     private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>();
63 
64     private CloseGuard mCloseGuard;
65 
66     /**
67      * This class provides callbacks mechanism for the BluetoothHapClient profile.
68      *
69      * @hide
70      */
71     @SystemApi
72     public interface Callback {
73         /** @hide */
74         @Retention(RetentionPolicy.SOURCE)
75         @IntDef(
76                 value = {
77                     // needed for future release compatibility
78                     BluetoothStatusCodes.ERROR_UNKNOWN,
79                     BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST,
80                     BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
81                     BluetoothStatusCodes.REASON_REMOTE_REQUEST,
82                     BluetoothStatusCodes.REASON_SYSTEM_POLICY,
83                 })
84         @interface PresetSelectionReason {}
85 
86         /**
87          * Invoked to inform about HA device's currently active preset.
88          *
89          * @param device remote device,
90          * @param presetIndex the currently active preset index.
91          * @param reason reason for the selected preset change
92          * @hide
93          */
94         @SystemApi
onPresetSelected( @onNull BluetoothDevice device, int presetIndex, @PresetSelectionReason int reason)95         void onPresetSelected(
96                 @NonNull BluetoothDevice device,
97                 int presetIndex,
98                 @PresetSelectionReason int reason);
99 
100         /** @hide */
101         @Retention(RetentionPolicy.SOURCE)
102         @IntDef(
103                 value = {
104                     // needed for future release compatibility
105                     BluetoothStatusCodes.ERROR_UNKNOWN,
106                     BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
107                     BluetoothStatusCodes.REASON_SYSTEM_POLICY,
108                     BluetoothStatusCodes.ERROR_REMOTE_OPERATION_REJECTED,
109                     BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
110                     BluetoothStatusCodes.ERROR_HAP_INVALID_PRESET_INDEX,
111                 })
112         @interface PresetSelectionFailureReason {}
113 
114         /**
115          * Invoked inform about the result of a failed preset change attempt.
116          *
117          * @param device remote device,
118          * @param reason failure reason.
119          * @hide
120          */
121         @SystemApi
onPresetSelectionFailed( @onNull BluetoothDevice device, @PresetSelectionFailureReason int reason)122         void onPresetSelectionFailed(
123                 @NonNull BluetoothDevice device, @PresetSelectionFailureReason int reason);
124 
125         /** @hide */
126         @Retention(RetentionPolicy.SOURCE)
127         @IntDef(
128                 value = {
129                     // needed for future release compatibility
130                     BluetoothStatusCodes.ERROR_UNKNOWN,
131                     BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
132                     BluetoothStatusCodes.REASON_SYSTEM_POLICY,
133                     BluetoothStatusCodes.ERROR_REMOTE_OPERATION_REJECTED,
134                     BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
135                     BluetoothStatusCodes.ERROR_HAP_INVALID_PRESET_INDEX,
136                     BluetoothStatusCodes.ERROR_CSIP_INVALID_GROUP_ID,
137                 })
138         @interface GroupPresetSelectionFailureReason {}
139 
140         /**
141          * Invoked to inform about the result of a failed preset change attempt.
142          *
143          * <p>The implementation will try to restore the state for every device back to original
144          *
145          * @param hapGroupId valid HAP group ID,
146          * @param reason failure reason.
147          * @hide
148          */
149         @SystemApi
onPresetSelectionForGroupFailed( int hapGroupId, @GroupPresetSelectionFailureReason int reason)150         void onPresetSelectionForGroupFailed(
151                 int hapGroupId, @GroupPresetSelectionFailureReason int reason);
152 
153         /** @hide */
154         @Retention(RetentionPolicy.SOURCE)
155         @IntDef(
156                 value = {
157                     // needed for future release compatibility
158                     BluetoothStatusCodes.ERROR_UNKNOWN,
159                     BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST,
160                     BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
161                     BluetoothStatusCodes.REASON_REMOTE_REQUEST,
162                     BluetoothStatusCodes.REASON_SYSTEM_POLICY,
163                 })
164         @interface PresetInfoChangeReason {}
165 
166         /**
167          * Invoked to inform about the preset list changes.
168          *
169          * @param device remote device,
170          * @param presetInfoList a list of all preset information on the target device
171          * @param reason reason for the preset list change
172          * @hide
173          */
174         @SystemApi
onPresetInfoChanged( @onNull BluetoothDevice device, @NonNull List<BluetoothHapPresetInfo> presetInfoList, @PresetInfoChangeReason int reason)175         void onPresetInfoChanged(
176                 @NonNull BluetoothDevice device,
177                 @NonNull List<BluetoothHapPresetInfo> presetInfoList,
178                 @PresetInfoChangeReason int reason);
179 
180         /** @hide */
181         @Retention(RetentionPolicy.SOURCE)
182         @IntDef(
183                 value = {
184                     // needed for future release compatibility
185                     BluetoothStatusCodes.ERROR_UNKNOWN,
186                     BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
187                     BluetoothStatusCodes.REASON_SYSTEM_POLICY,
188                     BluetoothStatusCodes.ERROR_REMOTE_OPERATION_REJECTED,
189                     BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
190                     BluetoothStatusCodes.ERROR_HAP_PRESET_NAME_TOO_LONG,
191                     BluetoothStatusCodes.ERROR_HAP_INVALID_PRESET_INDEX,
192                 })
193         @interface PresetNameChangeFailureReason {}
194 
195         /**
196          * Invoked to inform about the failed preset rename attempt.
197          *
198          * @param device remote device
199          * @param reason Failure reason code.
200          * @hide
201          */
202         @SystemApi
onSetPresetNameFailed( @onNull BluetoothDevice device, @PresetNameChangeFailureReason int reason)203         void onSetPresetNameFailed(
204                 @NonNull BluetoothDevice device, @PresetNameChangeFailureReason int reason);
205 
206         /** @hide */
207         @Retention(RetentionPolicy.SOURCE)
208         @IntDef(
209                 value = {
210                     // needed for future release compatibility
211                     BluetoothStatusCodes.ERROR_UNKNOWN,
212                     BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
213                     BluetoothStatusCodes.REASON_SYSTEM_POLICY,
214                     BluetoothStatusCodes.ERROR_REMOTE_OPERATION_REJECTED,
215                     BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
216                     BluetoothStatusCodes.ERROR_HAP_PRESET_NAME_TOO_LONG,
217                     BluetoothStatusCodes.ERROR_HAP_INVALID_PRESET_INDEX,
218                     BluetoothStatusCodes.ERROR_CSIP_INVALID_GROUP_ID,
219                 })
220         @interface GroupPresetNameChangeFailureReason {}
221 
222         /**
223          * Invoked to inform about the failed preset rename attempt.
224          *
225          * <p>The implementation will try to restore the state for every device back to original
226          *
227          * @param hapGroupId valid HAP group ID,
228          * @param reason Failure reason code.
229          * @hide
230          */
231         @SystemApi
onSetPresetNameForGroupFailed( int hapGroupId, @GroupPresetNameChangeFailureReason int reason)232         void onSetPresetNameForGroupFailed(
233                 int hapGroupId, @GroupPresetNameChangeFailureReason int reason);
234     }
235 
236     @SuppressLint("AndroidFrameworkBluetoothPermission")
237     private final IBluetoothHapClientCallback mCallback =
238             new IBluetoothHapClientCallback.Stub() {
239                 @Override
240                 public void onPresetSelected(
241                         @NonNull BluetoothDevice device, int presetIndex, int reasonCode) {
242                     Attributable.setAttributionSource(device, mAttributionSource);
243                     for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry :
244                             mCallbackExecutorMap.entrySet()) {
245                         BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
246                         Executor executor = callbackExecutorEntry.getValue();
247                         executor.execute(
248                                 () -> callback.onPresetSelected(device, presetIndex, reasonCode));
249                     }
250                 }
251 
252                 @Override
253                 public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int status) {
254                     Attributable.setAttributionSource(device, mAttributionSource);
255                     for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry :
256                             mCallbackExecutorMap.entrySet()) {
257                         BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
258                         Executor executor = callbackExecutorEntry.getValue();
259                         executor.execute(() -> callback.onPresetSelectionFailed(device, status));
260                     }
261                 }
262 
263                 @Override
264                 public void onPresetSelectionForGroupFailed(int hapGroupId, int statusCode) {
265                     for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry :
266                             mCallbackExecutorMap.entrySet()) {
267                         BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
268                         Executor executor = callbackExecutorEntry.getValue();
269                         executor.execute(
270                                 () ->
271                                         callback.onPresetSelectionForGroupFailed(
272                                                 hapGroupId, statusCode));
273                     }
274                 }
275 
276                 @Override
277                 public void onPresetInfoChanged(
278                         @NonNull BluetoothDevice device,
279                         @NonNull List<BluetoothHapPresetInfo> presetInfoList,
280                         int statusCode) {
281                     Attributable.setAttributionSource(device, mAttributionSource);
282                     for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry :
283                             mCallbackExecutorMap.entrySet()) {
284                         BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
285                         Executor executor = callbackExecutorEntry.getValue();
286                         executor.execute(
287                                 () ->
288                                         callback.onPresetInfoChanged(
289                                                 device, presetInfoList, statusCode));
290                     }
291                 }
292 
293                 @Override
294                 public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int status) {
295                     Attributable.setAttributionSource(device, mAttributionSource);
296                     for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry :
297                             mCallbackExecutorMap.entrySet()) {
298                         BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
299                         Executor executor = callbackExecutorEntry.getValue();
300                         executor.execute(() -> callback.onSetPresetNameFailed(device, status));
301                     }
302                 }
303 
304                 @Override
305                 public void onSetPresetNameForGroupFailed(int hapGroupId, int status) {
306                     for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry :
307                             mCallbackExecutorMap.entrySet()) {
308                         BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
309                         Executor executor = callbackExecutorEntry.getValue();
310                         executor.execute(
311                                 () -> callback.onSetPresetNameForGroupFailed(hapGroupId, status));
312                     }
313                 }
314             };
315 
316     /**
317      * Intent used to broadcast the change in connection state of the Hearing Access Profile Client
318      * service. Please note that in the binaural case, there will be two different LE devices for
319      * the left and right side and each device will have their own connection state changes.
320      *
321      * <p>This intent will have 3 extras:
322      *
323      * <ul>
324      *   <li>{@link #EXTRA_STATE} - The current state of the profile.
325      *   <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
326      *   <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
327      * </ul>
328      *
329      * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link
330      * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link
331      * #STATE_DISCONNECTING}.
332      *
333      * @hide
334      */
335     @SystemApi
336     @RequiresBluetoothConnectPermission
337     @RequiresPermission(
338             allOf = {
339                 android.Manifest.permission.BLUETOOTH_CONNECT,
340                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
341             })
342     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
343     public static final String ACTION_HAP_CONNECTION_STATE_CHANGED =
344             "android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED";
345 
346     /**
347      * Intent used to broadcast the device availability change and the availability of its presets.
348      * Please note that in the binaural case, there will be two different LE devices for the left
349      * and right side and each device will have their own availability event.
350      *
351      * <p>This intent will have 2 extras:
352      *
353      * <ul>
354      *   <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
355      *   <li>{@link #EXTRA_HAP_FEATURES} - Supported features map.
356      * </ul>
357      *
358      * @hide
359      */
360     @RequiresBluetoothConnectPermission
361     @RequiresPermission(
362             allOf = {
363                 android.Manifest.permission.BLUETOOTH_CONNECT,
364                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
365             })
366     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
367     public static final String ACTION_HAP_DEVICE_AVAILABLE =
368             "android.bluetooth.action.HAP_DEVICE_AVAILABLE";
369 
370     /**
371      * Contains a list of all available presets
372      *
373      * @hide
374      */
375     public static final String EXTRA_HAP_FEATURES = "android.bluetooth.extra.HAP_FEATURES";
376 
377     /**
378      * Represents an invalid index value. This is usually value returned in a currently active
379      * preset request for a device which is not connected. This value shouldn't be used in the API
380      * calls.
381      *
382      * @hide
383      */
384     @SystemApi
385     @FlaggedApi(Flags.FLAG_SETTINGS_CAN_CONTROL_HAP_PRESET)
386     public static final int PRESET_INDEX_UNAVAILABLE = IBluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
387 
388     /**
389      * Hearing aid type value. Indicates this Bluetooth device is belongs to a binaural hearing aid
390      * set. A binaural hearing aid set is two hearing aids that form a Coordinated Set, one for the
391      * right ear and one for the left ear of the user. Typically used by a user with bilateral
392      * hearing loss.
393      *
394      * @hide
395      */
396     @SystemApi public static final int TYPE_BINAURAL = 0b00;
397 
398     /**
399      * Hearing aid type value. Indicates this Bluetooth device is a single hearing aid for the left
400      * or the right ear. Typically used by a user with unilateral hearing loss.
401      *
402      * @hide
403      */
404     @SystemApi public static final int TYPE_MONAURAL = 0b01;
405 
406     /**
407      * Hearing aid type value. Indicates this Bluetooth device is two hearing aids with a connection
408      * to one another that expose a single Bluetooth radio interface.
409      *
410      * @hide
411      */
412     @SystemApi public static final int TYPE_BANDED = 0b10;
413 
414     /**
415      * Hearing aid type value. This value is reserved for future use.
416      *
417      * @hide
418      */
419     @SystemApi public static final int TYPE_RFU = 0b11;
420 
421     /** @hide */
422     @Retention(RetentionPolicy.SOURCE)
423     @IntDef(
424             flag = true,
425             value = {
426                 TYPE_BINAURAL,
427                 TYPE_MONAURAL,
428                 TYPE_BANDED,
429                 TYPE_RFU,
430             })
431     @interface HearingAidType {}
432 
433     /**
434      * Feature mask value.
435      *
436      * @hide
437      */
438     public static final int FEATURE_HEARING_AID_TYPE_MASK = 0b11;
439 
440     /**
441      * Feature mask value.
442      *
443      * @hide
444      */
445     public static final int FEATURE_SYNCHRONIZATED_PRESETS_MASK =
446             1 << IBluetoothHapClient.FEATURE_BIT_NUM_SYNCHRONIZATED_PRESETS;
447 
448     /**
449      * Feature mask value.
450      *
451      * @hide
452      */
453     public static final int FEATURE_INDEPENDENT_PRESETS_MASK =
454             1 << IBluetoothHapClient.FEATURE_BIT_NUM_INDEPENDENT_PRESETS;
455 
456     /**
457      * Feature mask value.
458      *
459      * @hide
460      */
461     public static final int FEATURE_DYNAMIC_PRESETS_MASK =
462             1 << IBluetoothHapClient.FEATURE_BIT_NUM_DYNAMIC_PRESETS;
463 
464     /**
465      * Feature mask value.
466      *
467      * @hide
468      */
469     public static final int FEATURE_WRITABLE_PRESETS_MASK =
470             1 << IBluetoothHapClient.FEATURE_BIT_NUM_WRITABLE_PRESETS;
471 
472     /** @hide */
473     @Retention(RetentionPolicy.SOURCE)
474     @IntDef(
475             flag = true,
476             value = {
477                 FEATURE_HEARING_AID_TYPE_MASK,
478                 FEATURE_SYNCHRONIZATED_PRESETS_MASK,
479                 FEATURE_INDEPENDENT_PRESETS_MASK,
480                 FEATURE_DYNAMIC_PRESETS_MASK,
481                 FEATURE_WRITABLE_PRESETS_MASK,
482             })
483     @interface FeatureMask {}
484 
485     private final BluetoothAdapter mAdapter;
486     private final AttributionSource mAttributionSource;
487 
488     private IBluetoothHapClient mService;
489 
490     /**
491      * Create a BluetoothHapClient proxy object for interacting with the local Bluetooth Hearing
492      * Access Profile (HAP) client.
493      */
BluetoothHapClient(Context context, BluetoothAdapter adapter)494     /*package*/ BluetoothHapClient(Context context, BluetoothAdapter adapter) {
495         mAdapter = adapter;
496         mAttributionSource = mAdapter.getAttributionSource();
497         mService = null;
498 
499         mCloseGuard = new CloseGuard();
500         mCloseGuard.open("close");
501     }
502 
503     /** @hide */
504     @SuppressWarnings("Finalize") // TODO(b/314811467)
finalize()505     protected void finalize() {
506         if (mCloseGuard != null) {
507             mCloseGuard.warnIfOpen();
508         }
509         close();
510     }
511 
512     /** @hide */
513     @Override
close()514     public void close() {
515         if (VDBG) log("close()");
516 
517         mAdapter.closeProfileProxy(this);
518     }
519 
520     /** @hide */
521     @Override
onServiceConnected(IBinder service)522     public void onServiceConnected(IBinder service) {
523         mService = IBluetoothHapClient.Stub.asInterface(service);
524         // re-register the service-to-app callback
525         synchronized (mCallbackExecutorMap) {
526             if (mCallbackExecutorMap.isEmpty()) {
527                 return;
528             }
529 
530             try {
531                 if (service != null) {
532                     mService.registerCallback(mCallback, mAttributionSource);
533                 }
534             } catch (RemoteException e) {
535                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
536             }
537         }
538     }
539 
540     /** @hide */
541     @Override
onServiceDisconnected()542     public void onServiceDisconnected() {
543         mService = null;
544     }
545 
getService()546     private IBluetoothHapClient getService() {
547         return mService;
548     }
549 
550     /** @hide */
551     @Override
getAdapter()552     public BluetoothAdapter getAdapter() {
553         return mAdapter;
554     }
555 
556     /**
557      * Register a {@link Callback} that will be invoked during the operation of this profile.
558      *
559      * <p>Repeated registration of the same <var>callback</var> object after the first call to this
560      * method will result with IllegalArgumentException being thrown, even when the
561      * <var>executor</var> is different. API caller would have to call {@link
562      * #unregisterCallback(Callback)} with the same callback object before registering it again.
563      *
564      * @param executor an {@link Executor} to execute given callback
565      * @param callback user implementation of the {@link Callback}
566      * @throws NullPointerException if a null executor, or callback is given, or
567      *     IllegalArgumentException if the same <var>callback</var> is already registered.
568      * @hide
569      */
570     @SystemApi
571     @RequiresBluetoothConnectPermission
572     @RequiresPermission(
573             allOf = {
574                 android.Manifest.permission.BLUETOOTH_CONNECT,
575                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
576             })
registerCallback( @onNull @allbackExecutor Executor executor, @NonNull Callback callback)577     public void registerCallback(
578             @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) {
579         Objects.requireNonNull(executor, "executor cannot be null");
580         Objects.requireNonNull(callback, "callback cannot be null");
581 
582         if (DBG) log("registerCallback");
583 
584         synchronized (mCallbackExecutorMap) {
585             // If the callback map is empty, we register the service-to-app callback
586             if (mCallbackExecutorMap.isEmpty()) {
587                 if (!isEnabled()) {
588                     /* If Bluetooth is off, just store callback and it will be registered
589                      * when Bluetooth is on
590                      */
591                     mCallbackExecutorMap.put(callback, executor);
592                     return;
593                 }
594                 try {
595                     final IBluetoothHapClient service = getService();
596                     if (service != null) {
597                         service.registerCallback(mCallback, mAttributionSource);
598                     }
599                 } catch (RemoteException e) {
600                     throw e.rethrowAsRuntimeException();
601                 }
602             }
603 
604             // Adds the passed in callback to our map of callbacks to executors
605             if (mCallbackExecutorMap.containsKey(callback)) {
606                 throw new IllegalArgumentException("This callback has already been registered");
607             }
608             mCallbackExecutorMap.put(callback, executor);
609         }
610     }
611 
612     /**
613      * Unregister the specified {@link Callback}.
614      *
615      * <p>The same {@link Callback} object used when calling {@link #registerCallback(Executor,
616      * Callback)} must be used.
617      *
618      * <p>Callbacks are automatically unregistered when application process goes away
619      *
620      * @param callback user implementation of the {@link Callback}
621      * @throws NullPointerException when callback is null or IllegalArgumentException when no
622      *     callback is registered
623      * @hide
624      */
625     @SystemApi
626     @RequiresBluetoothConnectPermission
627     @RequiresPermission(
628             allOf = {
629                 android.Manifest.permission.BLUETOOTH_CONNECT,
630                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
631             })
unregisterCallback(@onNull Callback callback)632     public void unregisterCallback(@NonNull Callback callback) {
633         Objects.requireNonNull(callback, "callback cannot be null");
634 
635         if (DBG) log("unregisterCallback");
636 
637         synchronized (mCallbackExecutorMap) {
638             if (mCallbackExecutorMap.remove(callback) == null) {
639                 throw new IllegalArgumentException("This callback has not been registered");
640             }
641         }
642 
643         // If the callback map is empty, we unregister the service-to-app callback
644         if (mCallbackExecutorMap.isEmpty()) {
645             try {
646                 final IBluetoothHapClient service = getService();
647                 if (service != null) {
648                     service.unregisterCallback(mCallback, mAttributionSource);
649                 }
650             } catch (RemoteException e) {
651                 throw e.rethrowAsRuntimeException();
652             }
653         }
654     }
655 
656     /**
657      * Set connection policy of the profile
658      *
659      * <p>The device should already be paired. Connection policy can be one of {@link
660      * #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, {@link
661      * #CONNECTION_POLICY_UNKNOWN}
662      *
663      * @param device Paired bluetooth device
664      * @param connectionPolicy is the connection policy to set to for this profile
665      * @return {@code true} if connectionPolicy is set, {@code false} on error
666      * @hide
667      */
668     @SystemApi
669     @RequiresBluetoothConnectPermission
670     @RequiresPermission(
671             allOf = {
672                 android.Manifest.permission.BLUETOOTH_CONNECT,
673                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
674             })
setConnectionPolicy( @onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)675     public boolean setConnectionPolicy(
676             @NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) {
677         if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
678         Objects.requireNonNull(device, "BluetoothDevice cannot be null");
679         final IBluetoothHapClient service = getService();
680         if (service == null) {
681             Log.w(TAG, "Proxy not attached to service");
682             if (DBG) log(Log.getStackTraceString(new Throwable()));
683         } else if (mAdapter.isEnabled()
684                 && isValidDevice(device)
685                 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
686                         || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
687             try {
688                 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource);
689             } catch (RemoteException e) {
690                 throw e.rethrowAsRuntimeException();
691             }
692         }
693         return false;
694     }
695 
696     /**
697      * Get the connection policy of the profile.
698      *
699      * <p>The connection policy can be any of: {@link #CONNECTION_POLICY_ALLOWED}, {@link
700      * #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
701      *
702      * @param device Bluetooth device
703      * @return connection policy of the device or {@link #CONNECTION_POLICY_FORBIDDEN} if device is
704      *     null
705      * @hide
706      */
707     @SystemApi
708     @RequiresBluetoothConnectPermission
709     @RequiresPermission(
710             allOf = {
711                 android.Manifest.permission.BLUETOOTH_CONNECT,
712                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
713             })
getConnectionPolicy(@ullable BluetoothDevice device)714     public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) {
715         if (VDBG) log("getConnectionPolicy(" + device + ")");
716         final IBluetoothHapClient service = getService();
717         if (service == null) {
718             Log.w(TAG, "Proxy not attached to service");
719             if (DBG) log(Log.getStackTraceString(new Throwable()));
720         } else if (mAdapter.isEnabled() && isValidDevice(device)) {
721             try {
722                 return service.getConnectionPolicy(device, mAttributionSource);
723             } catch (RemoteException e) {
724                 throw e.rethrowAsRuntimeException();
725             }
726         }
727         return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
728     }
729 
730     /**
731      * {@inheritDoc}
732      *
733      * @hide
734      */
735     @SystemApi
736     @RequiresBluetoothConnectPermission
737     @RequiresPermission(
738             allOf = {
739                 android.Manifest.permission.BLUETOOTH_CONNECT,
740                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
741             })
742     @Override
getConnectedDevices()743     public @NonNull List<BluetoothDevice> getConnectedDevices() {
744         if (VDBG) Log.d(TAG, "getConnectedDevices()");
745         final IBluetoothHapClient service = getService();
746         if (service == null) {
747             Log.w(TAG, "Proxy not attached to service");
748             if (DBG) log(Log.getStackTraceString(new Throwable()));
749         } else if (isEnabled()) {
750             try {
751                 return Attributable.setAttributionSource(
752                         service.getConnectedDevices(mAttributionSource), mAttributionSource);
753             } catch (RemoteException e) {
754                 throw e.rethrowAsRuntimeException();
755             }
756         }
757         return Collections.emptyList();
758     }
759 
760     /**
761      * {@inheritDoc}
762      *
763      * @hide
764      */
765     @SystemApi
766     @RequiresBluetoothConnectPermission
767     @RequiresPermission(
768             allOf = {
769                 android.Manifest.permission.BLUETOOTH_CONNECT,
770                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
771             })
772     @Override
773     @NonNull
getDevicesMatchingConnectionStates(@onNull int[] states)774     public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) {
775         if (VDBG) Log.d(TAG, "getDevicesMatchingConnectionStates()");
776         final IBluetoothHapClient service = getService();
777         if (service == null) {
778             Log.w(TAG, "Proxy not attached to service");
779             if (DBG) log(Log.getStackTraceString(new Throwable()));
780         } else if (isEnabled()) {
781             try {
782                 return Attributable.setAttributionSource(
783                         service.getDevicesMatchingConnectionStates(states, mAttributionSource),
784                         mAttributionSource);
785             } catch (RemoteException e) {
786                 throw e.rethrowAsRuntimeException();
787             }
788         }
789         return Collections.emptyList();
790     }
791 
792     /**
793      * {@inheritDoc}
794      *
795      * @hide
796      */
797     @SystemApi
798     @RequiresBluetoothConnectPermission
799     @RequiresPermission(
800             allOf = {
801                 android.Manifest.permission.BLUETOOTH_CONNECT,
802                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
803             })
804     @Override
805     @BluetoothProfile.BtProfileState
getConnectionState(@onNull BluetoothDevice device)806     public int getConnectionState(@NonNull BluetoothDevice device) {
807         if (VDBG) Log.d(TAG, "getConnectionState(" + device + ")");
808         final IBluetoothHapClient service = getService();
809         if (service == null) {
810             Log.w(TAG, "Proxy not attached to service");
811             if (DBG) log(Log.getStackTraceString(new Throwable()));
812         } else if (isEnabled() && isValidDevice(device)) {
813             try {
814                 return service.getConnectionState(device, mAttributionSource);
815             } catch (RemoteException e) {
816                 throw e.rethrowAsRuntimeException();
817             }
818         }
819         return BluetoothProfile.STATE_DISCONNECTED;
820     }
821 
822     /**
823      * Gets the group identifier, which can be used in the group related part of the API.
824      *
825      * <p>Users are expected to get group identifier for each of the connected device to discover
826      * the device grouping. This allows them to make an informed decision which devices can be
827      * controlled by single group API call and which require individual device calls.
828      *
829      * <p>Note that some binaural HA devices may not support group operations, therefore are not
830      * considered a valid HAP group. In such case -1 is returned even if such device is a valid Le
831      * Audio Coordinated Set member.
832      *
833      * @return valid group identifier or -1
834      * @hide
835      */
836     @SystemApi
837     @FlaggedApi(Flags.FLAG_SETTINGS_CAN_CONTROL_HAP_PRESET)
838     @RequiresBluetoothConnectPermission
839     @RequiresPermission(
840             allOf = {
841                 android.Manifest.permission.BLUETOOTH_CONNECT,
842                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
843             })
getHapGroup(@onNull BluetoothDevice device)844     public int getHapGroup(@NonNull BluetoothDevice device) {
845         final IBluetoothHapClient service = getService();
846         if (service == null) {
847             Log.w(TAG, "Proxy not attached to service");
848             if (DBG) log(Log.getStackTraceString(new Throwable()));
849         } else if (isEnabled() && isValidDevice(device)) {
850             try {
851                 return service.getHapGroup(device, mAttributionSource);
852             } catch (RemoteException e) {
853                 throw e.rethrowAsRuntimeException();
854             }
855         }
856         return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
857     }
858 
859     /**
860      * Gets the currently active preset for a HA device.
861      *
862      * @param device is the device for which we want to set the active preset
863      * @return active preset index
864      * @hide
865      */
866     @SystemApi
867     @FlaggedApi(Flags.FLAG_SETTINGS_CAN_CONTROL_HAP_PRESET)
868     @RequiresBluetoothConnectPermission
869     @RequiresPermission(
870             allOf = {
871                 android.Manifest.permission.BLUETOOTH_CONNECT,
872                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
873             })
getActivePresetIndex(@onNull BluetoothDevice device)874     public int getActivePresetIndex(@NonNull BluetoothDevice device) {
875         final IBluetoothHapClient service = getService();
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                 return service.getActivePresetIndex(device, mAttributionSource);
882             } catch (RemoteException e) {
883                 throw e.rethrowAsRuntimeException();
884             }
885         }
886         return PRESET_INDEX_UNAVAILABLE;
887     }
888 
889     /**
890      * Get the currently active preset info for a remote device.
891      *
892      * @param device is the device for which we want to get the preset name
893      * @return currently active preset info if selected, null if preset info is not available for
894      *     the remote device
895      * @hide
896      */
897     @SystemApi
898     @RequiresBluetoothConnectPermission
899     @RequiresPermission(
900             allOf = {
901                 android.Manifest.permission.BLUETOOTH_CONNECT,
902                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
903             })
getActivePresetInfo(@onNull BluetoothDevice device)904     public @Nullable BluetoothHapPresetInfo getActivePresetInfo(@NonNull BluetoothDevice device) {
905         final IBluetoothHapClient service = getService();
906         if (service == null) {
907             Log.w(TAG, "Proxy not attached to service");
908             if (DBG) log(Log.getStackTraceString(new Throwable()));
909         } else if (isEnabled() && isValidDevice(device)) {
910             try {
911                 return service.getActivePresetInfo(device, mAttributionSource);
912             } catch (RemoteException e) {
913                 throw e.rethrowAsRuntimeException();
914             }
915         }
916 
917         return null;
918     }
919 
920     /**
921      * Selects the currently active preset for a HA device
922      *
923      * <p>On success, {@link Callback#onPresetSelected(BluetoothDevice, int, int)} will be called
924      * with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure, {@link
925      * Callback#onPresetSelectionFailed(BluetoothDevice, int)} will be called.
926      *
927      * @param device is the device for which we want to set the active preset
928      * @param presetIndex is an index of one of the available presets
929      * @hide
930      */
931     @SystemApi
932     @RequiresBluetoothConnectPermission
933     @RequiresPermission(
934             allOf = {
935                 android.Manifest.permission.BLUETOOTH_CONNECT,
936                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
937             })
selectPreset(@onNull BluetoothDevice device, int presetIndex)938     public void selectPreset(@NonNull BluetoothDevice device, int presetIndex) {
939         final IBluetoothHapClient service = getService();
940         if (service == null) {
941             Log.w(TAG, "Proxy not attached to service");
942             if (DBG) log(Log.getStackTraceString(new Throwable()));
943         } else if (isEnabled() && isValidDevice(device)) {
944             try {
945                 service.selectPreset(device, presetIndex, mAttributionSource);
946             } catch (RemoteException e) {
947                 throw e.rethrowAsRuntimeException();
948             }
949         }
950     }
951 
952     /**
953      * Selects the currently active preset for a Hearing Aid device group.
954      *
955      * <p>This group call may replace multiple device calls if those are part of the valid HAS
956      * group. Note that binaural HA devices may or may not support group.
957      *
958      * <p>On success, {@link Callback#onPresetSelected(BluetoothDevice, int, int)} will be called
959      * for each device within the group with reason code {@link
960      * BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure, {@link
961      * Callback#onPresetSelectionForGroupFailed(int, int)} will be called for the group.
962      *
963      * @param groupId is the device group identifier for which want to set the active preset
964      * @param presetIndex is an index of one of the available presets
965      * @hide
966      */
967     @SystemApi
968     @RequiresBluetoothConnectPermission
969     @RequiresPermission(
970             allOf = {
971                 android.Manifest.permission.BLUETOOTH_CONNECT,
972                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
973             })
selectPresetForGroup(int groupId, int presetIndex)974     public void selectPresetForGroup(int groupId, int presetIndex) {
975         final IBluetoothHapClient service = getService();
976         if (service == null) {
977             Log.w(TAG, "Proxy not attached to service");
978             if (DBG) log(Log.getStackTraceString(new Throwable()));
979         } else if (isEnabled()) {
980             try {
981                 service.selectPresetForGroup(groupId, presetIndex, mAttributionSource);
982             } catch (RemoteException e) {
983                 throw e.rethrowAsRuntimeException();
984             }
985         }
986     }
987 
988     /**
989      * Sets the next preset as a currently active preset for a HA device
990      *
991      * <p>Note that the meaning of 'next' is HA device implementation specific and does not
992      * necessarily mean a higher preset index.
993      *
994      * @param device is the device for which we want to set the active preset
995      * @hide
996      */
997     @SystemApi
998     @FlaggedApi(Flags.FLAG_SETTINGS_CAN_CONTROL_HAP_PRESET)
999     @RequiresBluetoothConnectPermission
1000     @RequiresPermission(
1001             allOf = {
1002                 android.Manifest.permission.BLUETOOTH_CONNECT,
1003                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1004             })
switchToNextPreset(@onNull BluetoothDevice device)1005     public void switchToNextPreset(@NonNull BluetoothDevice device) {
1006         final IBluetoothHapClient service = getService();
1007         if (service == null) {
1008             Log.w(TAG, "Proxy not attached to service");
1009             if (DBG) log(Log.getStackTraceString(new Throwable()));
1010         } else if (isEnabled() && isValidDevice(device)) {
1011             try {
1012                 service.switchToNextPreset(device, mAttributionSource);
1013             } catch (RemoteException e) {
1014                 throw e.rethrowAsRuntimeException();
1015             }
1016         }
1017     }
1018 
1019     /**
1020      * Sets the next preset as a currently active preset for a HA device group
1021      *
1022      * <p>Note that the meaning of 'next' is HA device implementation specific and does not
1023      * necessarily mean a higher preset index.
1024      *
1025      * <p>This group call may replace multiple device calls if those are part of the valid HAS
1026      * group. Note that binaural HA devices may or may not support group.
1027      *
1028      * @param groupId is the device group identifier for which want to set the active preset
1029      * @hide
1030      */
1031     @SystemApi
1032     @FlaggedApi(Flags.FLAG_SETTINGS_CAN_CONTROL_HAP_PRESET)
1033     @RequiresBluetoothConnectPermission
1034     @RequiresPermission(
1035             allOf = {
1036                 android.Manifest.permission.BLUETOOTH_CONNECT,
1037                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1038             })
switchToNextPresetForGroup(int groupId)1039     public void switchToNextPresetForGroup(int groupId) {
1040         final IBluetoothHapClient service = getService();
1041         if (service == null) {
1042             Log.w(TAG, "Proxy not attached to service");
1043             if (DBG) log(Log.getStackTraceString(new Throwable()));
1044         } else if (isEnabled()) {
1045             try {
1046                 service.switchToNextPresetForGroup(groupId, mAttributionSource);
1047             } catch (RemoteException e) {
1048                 throw e.rethrowAsRuntimeException();
1049             }
1050         }
1051     }
1052 
1053     /**
1054      * Sets the previous preset as a currently active preset for a HA device.
1055      *
1056      * <p>Note that the meaning of 'previous' is HA device implementation specific and does not
1057      * necessarily mean a lower preset index.
1058      *
1059      * @param device is the device for which we want to set the active preset
1060      * @hide
1061      */
1062     @SystemApi
1063     @FlaggedApi(Flags.FLAG_SETTINGS_CAN_CONTROL_HAP_PRESET)
1064     @RequiresBluetoothConnectPermission
1065     @RequiresPermission(
1066             allOf = {
1067                 android.Manifest.permission.BLUETOOTH_CONNECT,
1068                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1069             })
switchToPreviousPreset(@onNull BluetoothDevice device)1070     public void switchToPreviousPreset(@NonNull BluetoothDevice device) {
1071         final IBluetoothHapClient service = getService();
1072         if (service == null) {
1073             Log.w(TAG, "Proxy not attached to service");
1074             if (DBG) log(Log.getStackTraceString(new Throwable()));
1075         } else if (isEnabled() && isValidDevice(device)) {
1076             try {
1077                 service.switchToPreviousPreset(device, mAttributionSource);
1078             } catch (RemoteException e) {
1079                 throw e.rethrowAsRuntimeException();
1080             }
1081         }
1082     }
1083 
1084     /**
1085      * Sets the previous preset as a currently active preset for a HA device group
1086      *
1087      * <p>Note the meaning of 'previous' is HA device implementation specific and does not
1088      * necessarily mean a lower preset index.
1089      *
1090      * <p>This group call may replace multiple device calls if those are part of the valid HAS
1091      * group. Note that binaural HA devices may or may not support group.
1092      *
1093      * @param groupId is the device group identifier for which want to set the active preset
1094      * @hide
1095      */
1096     @SystemApi
1097     @FlaggedApi(Flags.FLAG_SETTINGS_CAN_CONTROL_HAP_PRESET)
1098     @RequiresBluetoothConnectPermission
1099     @RequiresPermission(
1100             allOf = {
1101                 android.Manifest.permission.BLUETOOTH_CONNECT,
1102                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1103             })
switchToPreviousPresetForGroup(int groupId)1104     public void switchToPreviousPresetForGroup(int groupId) {
1105         final IBluetoothHapClient service = getService();
1106         if (service == null) {
1107             Log.w(TAG, "Proxy not attached to service");
1108             if (DBG) log(Log.getStackTraceString(new Throwable()));
1109         } else if (isEnabled()) {
1110             try {
1111                 service.switchToPreviousPresetForGroup(groupId, mAttributionSource);
1112             } catch (RemoteException e) {
1113                 throw e.rethrowAsRuntimeException();
1114             }
1115         }
1116     }
1117 
1118     /**
1119      * Requests the preset info
1120      *
1121      * @param device is the device for which we want to get the preset name
1122      * @param presetIndex is an index of one of the available presets
1123      * @return preset info
1124      * @hide
1125      */
1126     @SystemApi
1127     @FlaggedApi(Flags.FLAG_SETTINGS_CAN_CONTROL_HAP_PRESET)
1128     @RequiresBluetoothConnectPermission
1129     @RequiresPermission(
1130             allOf = {
1131                 android.Manifest.permission.BLUETOOTH_CONNECT,
1132                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1133             })
1134     @Nullable
getPresetInfo(@onNull BluetoothDevice device, int presetIndex)1135     public BluetoothHapPresetInfo getPresetInfo(@NonNull BluetoothDevice device, int presetIndex) {
1136         final IBluetoothHapClient service = getService();
1137         if (service == null) {
1138             Log.w(TAG, "Proxy not attached to service");
1139             if (DBG) log(Log.getStackTraceString(new Throwable()));
1140         } else if (isEnabled() && isValidDevice(device)) {
1141             try {
1142                 return service.getPresetInfo(device, presetIndex, mAttributionSource);
1143             } catch (RemoteException e) {
1144                 throw e.rethrowAsRuntimeException();
1145             }
1146         }
1147         return null;
1148     }
1149 
1150     /**
1151      * Get all preset info for a particular device
1152      *
1153      * @param device is the device for which we want to get all presets info
1154      * @return a list of all known preset info
1155      * @hide
1156      */
1157     @SystemApi
1158     @RequiresBluetoothConnectPermission
1159     @RequiresPermission(
1160             allOf = {
1161                 android.Manifest.permission.BLUETOOTH_CONNECT,
1162                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1163             })
getAllPresetInfo(@onNull BluetoothDevice device)1164     public @NonNull List<BluetoothHapPresetInfo> getAllPresetInfo(@NonNull BluetoothDevice device) {
1165         final IBluetoothHapClient service = getService();
1166         if (service == null) {
1167             Log.w(TAG, "Proxy not attached to service");
1168             if (DBG) log(Log.getStackTraceString(new Throwable()));
1169         } else if (isEnabled() && isValidDevice(device)) {
1170             try {
1171                 return service.getAllPresetInfo(device, mAttributionSource);
1172             } catch (RemoteException e) {
1173                 throw e.rethrowAsRuntimeException();
1174             }
1175         }
1176         return Collections.emptyList();
1177     }
1178 
1179     /**
1180      * Requests HAP features
1181      *
1182      * @param device is the device for which we want to get features for
1183      * @return features value with feature bits set
1184      * @hide
1185      */
1186     @RequiresBluetoothConnectPermission
1187     @RequiresPermission(
1188             allOf = {
1189                 android.Manifest.permission.BLUETOOTH_CONNECT,
1190                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1191             })
getFeatures(@onNull BluetoothDevice device)1192     public int getFeatures(@NonNull BluetoothDevice device) {
1193         final IBluetoothHapClient service = getService();
1194         if (service == null) {
1195             Log.w(TAG, "Proxy not attached to service");
1196             if (DBG) log(Log.getStackTraceString(new Throwable()));
1197         } else if (isEnabled() && isValidDevice(device)) {
1198             try {
1199                 return service.getFeatures(device, mAttributionSource);
1200             } catch (RemoteException e) {
1201                 throw e.rethrowAsRuntimeException();
1202             }
1203         }
1204         return 0x00;
1205     }
1206 
1207     /**
1208      * Retrieves hearing aid type from feature value.
1209      *
1210      * @param device is the device for which we want to get the hearing aid type
1211      * @return hearing aid type
1212      * @hide
1213      */
1214     @SystemApi
1215     @RequiresBluetoothConnectPermission
1216     @RequiresPermission(
1217             allOf = {
1218                 android.Manifest.permission.BLUETOOTH_CONNECT,
1219                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1220             })
1221     @HearingAidType
getHearingAidType(@onNull BluetoothDevice device)1222     public int getHearingAidType(@NonNull BluetoothDevice device) {
1223         return getFeatures(device) & FEATURE_HEARING_AID_TYPE_MASK;
1224     }
1225 
1226     /**
1227      * Retrieves if this device supports synchronized presets or not from feature value.
1228      *
1229      * @param device is the device for which we want to know if it supports synchronized presets
1230      * @return {@code true} if the device supports synchronized presets, {@code false} otherwise
1231      * @hide
1232      */
1233     @SystemApi
1234     @RequiresBluetoothConnectPermission
1235     @RequiresPermission(
1236             allOf = {
1237                 android.Manifest.permission.BLUETOOTH_CONNECT,
1238                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1239             })
supportsSynchronizedPresets(@onNull BluetoothDevice device)1240     public boolean supportsSynchronizedPresets(@NonNull BluetoothDevice device) {
1241         return (getFeatures(device) & FEATURE_SYNCHRONIZATED_PRESETS_MASK)
1242                 == FEATURE_SYNCHRONIZATED_PRESETS_MASK;
1243     }
1244 
1245     /**
1246      * Retrieves if this device supports independent presets or not from feature value.
1247      *
1248      * @param device is the device for which we want to know if it supports independent presets
1249      * @return {@code true} if the device supports independent presets, {@code false} otherwise
1250      * @hide
1251      */
1252     @SystemApi
1253     @RequiresBluetoothConnectPermission
1254     @RequiresPermission(
1255             allOf = {
1256                 android.Manifest.permission.BLUETOOTH_CONNECT,
1257                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1258             })
supportsIndependentPresets(@onNull BluetoothDevice device)1259     public boolean supportsIndependentPresets(@NonNull BluetoothDevice device) {
1260         return (getFeatures(device) & FEATURE_INDEPENDENT_PRESETS_MASK)
1261                 == FEATURE_INDEPENDENT_PRESETS_MASK;
1262     }
1263 
1264     /**
1265      * Retrieves if this device supports dynamic presets or not from feature value.
1266      *
1267      * @param device is the device for which we want to know if it supports dynamic presets
1268      * @return {@code true} if the device supports dynamic presets, {@code false} otherwise
1269      * @hide
1270      */
1271     @SystemApi
1272     @RequiresBluetoothConnectPermission
1273     @RequiresPermission(
1274             allOf = {
1275                 android.Manifest.permission.BLUETOOTH_CONNECT,
1276                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1277             })
supportsDynamicPresets(@onNull BluetoothDevice device)1278     public boolean supportsDynamicPresets(@NonNull BluetoothDevice device) {
1279         return (getFeatures(device) & FEATURE_DYNAMIC_PRESETS_MASK) == FEATURE_DYNAMIC_PRESETS_MASK;
1280     }
1281 
1282     /**
1283      * Retrieves if this device supports writable presets or not from feature value.
1284      *
1285      * @param device is the device for which we want to know if it supports writable presets
1286      * @return {@code true} if the device supports writable presets, {@code false} otherwise
1287      * @hide
1288      */
1289     @SystemApi
1290     @RequiresBluetoothConnectPermission
1291     @RequiresPermission(
1292             allOf = {
1293                 android.Manifest.permission.BLUETOOTH_CONNECT,
1294                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1295             })
supportsWritablePresets(@onNull BluetoothDevice device)1296     public boolean supportsWritablePresets(@NonNull BluetoothDevice device) {
1297         return (getFeatures(device) & FEATURE_WRITABLE_PRESETS_MASK)
1298                 == FEATURE_WRITABLE_PRESETS_MASK;
1299     }
1300 
1301     /**
1302      * Sets the preset name for a particular device
1303      *
1304      * <p>Note that the name length is restricted to 40 characters.
1305      *
1306      * <p>On success, {@link Callback#onPresetInfoChanged(BluetoothDevice, List, int)} with a new
1307      * name will be called and reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On
1308      * failure, {@link Callback#onSetPresetNameFailed(BluetoothDevice, int)} will be called.
1309      *
1310      * @param device is the device for which we want to get the preset name
1311      * @param presetIndex is an index of one of the available presets
1312      * @param name is a new name for a preset, maximum length is 40 characters
1313      * @hide
1314      */
1315     @SystemApi
1316     @RequiresBluetoothConnectPermission
1317     @RequiresPermission(
1318             allOf = {
1319                 android.Manifest.permission.BLUETOOTH_CONNECT,
1320                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1321             })
setPresetName( @onNull BluetoothDevice device, int presetIndex, @NonNull String name)1322     public void setPresetName(
1323             @NonNull BluetoothDevice device, int presetIndex, @NonNull String name) {
1324         final IBluetoothHapClient service = getService();
1325         if (service == null) {
1326             Log.w(TAG, "Proxy not attached to service");
1327             if (DBG) log(Log.getStackTraceString(new Throwable()));
1328         } else if (isEnabled() && isValidDevice(device)) {
1329             try {
1330                 service.setPresetName(device, presetIndex, name, mAttributionSource);
1331             } catch (RemoteException e) {
1332                 throw e.rethrowAsRuntimeException();
1333             }
1334         }
1335     }
1336 
1337     /**
1338      * Sets the name for a hearing aid preset.
1339      *
1340      * <p>Note that the name length is restricted to 40 characters.
1341      *
1342      * <p>On success, {@link Callback#onPresetInfoChanged(BluetoothDevice, List, int)} with a new
1343      * name will be called for each device within the group with reason code {@link
1344      * BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure, {@link
1345      * Callback#onSetPresetNameForGroupFailed(int, int)} will be invoked
1346      *
1347      * @param groupId is the device group identifier
1348      * @param presetIndex is an index of one of the available presets
1349      * @param name is a new name for a preset, maximum length is 40 characters
1350      * @hide
1351      */
1352     @SystemApi
1353     @RequiresBluetoothConnectPermission
1354     @RequiresPermission(
1355             allOf = {
1356                 android.Manifest.permission.BLUETOOTH_CONNECT,
1357                 android.Manifest.permission.BLUETOOTH_PRIVILEGED
1358             })
setPresetNameForGroup(int groupId, int presetIndex, @NonNull String name)1359     public void setPresetNameForGroup(int groupId, int presetIndex, @NonNull String name) {
1360         final IBluetoothHapClient service = getService();
1361         if (service == null) {
1362             Log.w(TAG, "Proxy not attached to service");
1363             if (DBG) log(Log.getStackTraceString(new Throwable()));
1364         } else if (isEnabled()) {
1365             try {
1366                 service.setPresetNameForGroup(groupId, presetIndex, name, mAttributionSource);
1367             } catch (RemoteException e) {
1368                 throw e.rethrowAsRuntimeException();
1369             }
1370         }
1371     }
1372 
isEnabled()1373     private boolean isEnabled() {
1374         if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
1375         return false;
1376     }
1377 
isValidDevice(BluetoothDevice device)1378     private boolean isValidDevice(BluetoothDevice device) {
1379         if (device == null) return false;
1380 
1381         if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
1382         return false;
1383     }
1384 
log(String msg)1385     private static void log(String msg) {
1386         Log.d(TAG, msg);
1387     }
1388 }
1389