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.Manifest;
21 import android.annotation.CallbackExecutor;
22 import android.annotation.FlaggedApi;
23 import android.annotation.IntRange;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.annotation.RequiresPermission;
27 import android.annotation.SdkConstant;
28 import android.annotation.SdkConstant.SdkConstantType;
29 import android.annotation.SuppressLint;
30 import android.annotation.SystemApi;
31 import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
32 import android.content.AttributionSource;
33 import android.content.Context;
34 import android.os.IBinder;
35 import android.os.RemoteException;
36 import android.util.CloseGuard;
37 import android.util.Log;
38 
39 import com.android.bluetooth.flags.Flags;
40 import com.android.internal.annotations.GuardedBy;
41 
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Objects;
47 import java.util.concurrent.Executor;
48 import java.util.function.Consumer;
49 
50 /**
51  * This class provides the public APIs to control the Bluetooth Volume Control service.
52  *
53  * <p>BluetoothVolumeControl is a proxy object for controlling the Bluetooth VC Service via IPC. Use
54  * {@link BluetoothAdapter#getProfileProxy} to get the BluetoothVolumeControl proxy object.
55  *
56  * @hide
57  */
58 @SystemApi
59 public final class BluetoothVolumeControl implements BluetoothProfile, AutoCloseable {
60     private static final String TAG = "BluetoothVolumeControl";
61     private static final boolean DBG = true;
62     private static final boolean VDBG = false;
63 
64     private CloseGuard mCloseGuard;
65 
66     @GuardedBy("mCallbackExecutorMap")
67     private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>();
68 
69     /**
70      * This class provides a callback that is invoked when volume offset value changes on the remote
71      * device.
72      *
73      * <p>In order to balance volume on the group of Le Audio devices, Volume Offset Control Service
74      * (VOCS) shall be used. User can verify if the remote device supports VOCS by calling {@link
75      * #isVolumeOffsetAvailable(device)}.
76      *
77      * @hide
78      */
79     @SystemApi
80     public interface Callback {
81         /**
82          * Callback invoked when callback is registered and when volume offset changes on the remote
83          * device. Change can be triggered autonomously by the remote device or after volume offset
84          * change on the user request done by calling {@link #setVolumeOffset(device, volumeOffset)}
85          *
86          * @param device remote device whose volume offset changed
87          * @param volumeOffset latest volume offset for this device
88          * @deprecated Use new callback which give information about a VOCS instance ID
89          * @hide
90          */
91         @Deprecated
92         @SystemApi
onVolumeOffsetChanged( @onNull BluetoothDevice device, @IntRange(from = -255, to = 255) int volumeOffset)93         default void onVolumeOffsetChanged(
94                 @NonNull BluetoothDevice device,
95                 @IntRange(from = -255, to = 255) int volumeOffset) {}
96 
97         /**
98          * Callback invoked when callback is registered and when volume offset changes on the remote
99          * device. Change can be triggered autonomously by the remote device or after volume offset
100          * change on the user request done by calling {@link #setVolumeOffset(device, instanceId,
101          * volumeOffset)}
102          *
103          * @param device remote device whose volume offset changed
104          * @param instanceId identifier of VOCS instance on the remote device
105          * @param volumeOffset latest volume offset for this VOCS instance
106          * @hide
107          */
108         @FlaggedApi(Flags.FLAG_LEAUDIO_MULTIPLE_VOCS_INSTANCES_API)
109         @SystemApi
onVolumeOffsetChanged( @onNull BluetoothDevice device, @IntRange(from = 1, to = 255) int instanceId, @IntRange(from = -255, to = 255) int volumeOffset)110         default void onVolumeOffsetChanged(
111                 @NonNull BluetoothDevice device,
112                 @IntRange(from = 1, to = 255) int instanceId,
113                 @IntRange(from = -255, to = 255) int volumeOffset) {
114             if (instanceId == 1) {
115                 onVolumeOffsetChanged(device, volumeOffset);
116             }
117         }
118 
119         /**
120          * Callback invoked when callback is registered and when audio location changes on the
121          * remote device. Change can be triggered autonomously by the remote device.
122          *
123          * @param device remote device whose audio location changed
124          * @param instanceId identifier of VOCS instance on the remote device
125          * @param audioLocation latest audio location for this VOCS instance
126          * @hide
127          */
128         @FlaggedApi(Flags.FLAG_LEAUDIO_MULTIPLE_VOCS_INSTANCES_API)
129         @SystemApi
onVolumeOffsetAudioLocationChanged( @onNull BluetoothDevice device, @IntRange(from = 1, to = 255) int instanceId, @IntRange(from = -255, to = 255) int audioLocation)130         default void onVolumeOffsetAudioLocationChanged(
131                 @NonNull BluetoothDevice device,
132                 @IntRange(from = 1, to = 255) int instanceId,
133                 @IntRange(from = -255, to = 255) int audioLocation) {}
134 
135         /**
136          * Callback invoked when callback is registered and when audio description changes on the
137          * remote device. Change can be triggered autonomously by the remote device.
138          *
139          * @param device remote device whose audio description changed
140          * @param instanceId identifier of VOCS instance on the remote device
141          * @param audioDescription latest audio description for this VOCS instance
142          * @hide
143          */
144         @FlaggedApi(Flags.FLAG_LEAUDIO_MULTIPLE_VOCS_INSTANCES_API)
145         @SystemApi
onVolumeOffsetAudioDescriptionChanged( @onNull BluetoothDevice device, @IntRange(from = 1, to = 255) int instanceId, @NonNull String audioDescription)146         default void onVolumeOffsetAudioDescriptionChanged(
147                 @NonNull BluetoothDevice device,
148                 @IntRange(from = 1, to = 255) int instanceId,
149                 @NonNull String audioDescription) {}
150 
151         /**
152          * Callback for le audio connected device volume level change
153          *
154          * <p>The valid volume range is [0, 255], as defined in 2.3.1.1 Volume_Setting field of
155          * Volume Control Service, Version 1.0.
156          *
157          * @param device remote device whose volume changed
158          * @param volume level
159          * @hide
160          */
161         @FlaggedApi(Flags.FLAG_LEAUDIO_BROADCAST_VOLUME_CONTROL_FOR_CONNECTED_DEVICES)
162         @SystemApi
onDeviceVolumeChanged( @onNull BluetoothDevice device, @IntRange(from = 0, to = 255) int volume)163         default void onDeviceVolumeChanged(
164                 @NonNull BluetoothDevice device, @IntRange(from = 0, to = 255) int volume) {}
165     }
166 
167     @SuppressLint("AndroidFrameworkBluetoothPermission")
168     private final IBluetoothVolumeControlCallback mCallback =
169             new VolumeControlNotifyCallback(mCallbackExecutorMap);
170 
171     private class VolumeControlNotifyCallback extends IBluetoothVolumeControlCallback.Stub {
172         private final Map<Callback, Executor> mCallbackMap;
173 
VolumeControlNotifyCallback(Map<Callback, Executor> callbackMap)174         VolumeControlNotifyCallback(Map<Callback, Executor> callbackMap) {
175             mCallbackMap = callbackMap;
176         }
177 
forEach(Consumer<BluetoothVolumeControl.Callback> consumer)178         private void forEach(Consumer<BluetoothVolumeControl.Callback> consumer) {
179             synchronized (mCallbackMap) {
180                 mCallbackMap.forEach(
181                         (callback, executor) -> executor.execute(() -> consumer.accept(callback)));
182             }
183         }
184 
185         @Override
onVolumeOffsetChanged( @onNull BluetoothDevice device, int instanceId, int volumeOffset)186         public void onVolumeOffsetChanged(
187                 @NonNull BluetoothDevice device, int instanceId, int volumeOffset) {
188             Attributable.setAttributionSource(device, mAttributionSource);
189             if (Flags.leaudioMultipleVocsInstancesApi()) {
190                 forEach((cb) -> cb.onVolumeOffsetChanged(device, instanceId, volumeOffset));
191             }
192         }
193 
194         @Override
onVolumeOffsetAudioLocationChanged( @onNull BluetoothDevice device, int instanceId, int audioLocation)195         public void onVolumeOffsetAudioLocationChanged(
196                 @NonNull BluetoothDevice device, int instanceId, int audioLocation) {
197             Attributable.setAttributionSource(device, mAttributionSource);
198             forEach(
199                     (cb) ->
200                             cb.onVolumeOffsetAudioLocationChanged(
201                                     device, instanceId, audioLocation));
202         }
203 
204         @Override
onVolumeOffsetAudioDescriptionChanged( @onNull BluetoothDevice device, int instanceId, String audioDescription)205         public void onVolumeOffsetAudioDescriptionChanged(
206                 @NonNull BluetoothDevice device, int instanceId, String audioDescription) {
207             Attributable.setAttributionSource(device, mAttributionSource);
208             forEach(
209                     (cb) ->
210                             cb.onVolumeOffsetAudioDescriptionChanged(
211                                     device, instanceId, audioDescription));
212         }
213 
214         @Override
onDeviceVolumeChanged(@onNull BluetoothDevice device, int volume)215         public void onDeviceVolumeChanged(@NonNull BluetoothDevice device, int volume) {
216             Attributable.setAttributionSource(device, mAttributionSource);
217             forEach((cb) -> cb.onDeviceVolumeChanged(device, volume));
218         }
219     }
220 
221     /**
222      * Intent used to broadcast the change in connection state of the Volume Control profile.
223      *
224      * <p>This intent will have 3 extras:
225      *
226      * <ul>
227      *   <li>{@link #EXTRA_STATE} - The current state of the profile.
228      *   <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
229      *   <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
230      * </ul>
231      *
232      * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link
233      * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link
234      * #STATE_DISCONNECTING}.
235      *
236      * @hide
237      */
238     @SystemApi
239     @SuppressLint("ActionValue")
240     @RequiresBluetoothConnectPermission
241     @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
242     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
243     public static final String ACTION_CONNECTION_STATE_CHANGED =
244             "android.bluetooth.volume-control.profile.action.CONNECTION_STATE_CHANGED";
245 
246     private BluetoothAdapter mAdapter;
247     private final AttributionSource mAttributionSource;
248 
249     private IBluetoothVolumeControl mService;
250 
251     /**
252      * Create a BluetoothVolumeControl proxy object for interacting with the local Bluetooth Volume
253      * Control service.
254      */
BluetoothVolumeControl(Context context, BluetoothAdapter adapter)255     /*package*/ BluetoothVolumeControl(Context context, BluetoothAdapter adapter) {
256         mAdapter = adapter;
257         mAttributionSource = adapter.getAttributionSource();
258         mService = null;
259 
260         mCloseGuard = new CloseGuard();
261         mCloseGuard.open("close");
262     }
263 
264     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
265     @SuppressWarnings("Finalize") // TODO(b/314811467)
finalize()266     protected void finalize() {
267         if (mCloseGuard != null) {
268             mCloseGuard.warnIfOpen();
269         }
270         close();
271     }
272 
273     /**
274      * Close this VolumeControl server instance.
275      *
276      * <p>Application should call this method as early as possible after it is done with this
277      * VolumeControl server.
278      */
279     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
280     @Override
close()281     public void close() {
282         if (VDBG) log("close()");
283 
284         mAdapter.closeProfileProxy(this);
285     }
286 
287     /** @hide */
288     @Override
onServiceConnected(IBinder service)289     public void onServiceConnected(IBinder service) {
290         mService = IBluetoothVolumeControl.Stub.asInterface(service);
291         // re-register the service-to-app callback
292         synchronized (mCallbackExecutorMap) {
293             if (mCallbackExecutorMap.isEmpty()) {
294                 return;
295             }
296             try {
297                 mService.registerCallback(mCallback, mAttributionSource);
298             } catch (RemoteException e) {
299                 Log.e(TAG, "onBluetoothServiceUp: Failed to register VolumeControl callback", e);
300             }
301         }
302     }
303 
304     /** @hide */
305     @Override
onServiceDisconnected()306     public void onServiceDisconnected() {
307         mService = null;
308     }
309 
getService()310     private IBluetoothVolumeControl getService() {
311         return mService;
312     }
313 
314     /** @hide */
315     @Override
getAdapter()316     public BluetoothAdapter getAdapter() {
317         return mAdapter;
318     }
319 
320     /**
321      * Get the list of connected devices. Currently at most one.
322      *
323      * @return list of connected devices
324      * @hide
325      */
326     @SystemApi
327     @RequiresBluetoothConnectPermission
328     @RequiresPermission(
329             allOf = {
330                 android.Manifest.permission.BLUETOOTH_CONNECT,
331                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
332             })
getConnectedDevices()333     public @NonNull List<BluetoothDevice> getConnectedDevices() {
334         if (DBG) log("getConnectedDevices()");
335         final IBluetoothVolumeControl service = getService();
336         if (service == null) {
337             Log.w(TAG, "Proxy not attached to service");
338             if (DBG) log(Log.getStackTraceString(new Throwable()));
339         } else if (isEnabled()) {
340             try {
341                 return Attributable.setAttributionSource(
342                         service.getConnectedDevices(mAttributionSource), mAttributionSource);
343             } catch (RemoteException e) {
344                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
345             }
346         }
347         return Collections.emptyList();
348     }
349 
350     /**
351      * Get the list of devices matching specified states. Currently at most one.
352      *
353      * @return list of matching devices
354      * @hide
355      */
356     @RequiresBluetoothConnectPermission
357     @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
getDevicesMatchingConnectionStates(int[] states)358     public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
359         if (DBG) log("getDevicesMatchingStates()");
360         final IBluetoothVolumeControl service = getService();
361         if (service == null) {
362             Log.w(TAG, "Proxy not attached to service");
363             if (DBG) log(Log.getStackTraceString(new Throwable()));
364         } else if (isEnabled()) {
365             try {
366                 return Attributable.setAttributionSource(
367                         service.getDevicesMatchingConnectionStates(states, mAttributionSource),
368                         mAttributionSource);
369             } catch (RemoteException e) {
370                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
371             }
372         }
373         return Collections.emptyList();
374     }
375 
376     /**
377      * Get connection state of device
378      *
379      * @return device connection state
380      * @hide
381      */
382     @RequiresBluetoothConnectPermission
383     @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
getConnectionState(BluetoothDevice device)384     public int getConnectionState(BluetoothDevice device) {
385         if (DBG) log("getConnectionState(" + device + ")");
386         final IBluetoothVolumeControl service = getService();
387         if (service == null) {
388             Log.w(TAG, "Proxy not attached to service");
389             if (DBG) log(Log.getStackTraceString(new Throwable()));
390         } else if (isEnabled() && isValidDevice(device)) {
391             try {
392                 return service.getConnectionState(device, mAttributionSource);
393             } catch (RemoteException e) {
394                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
395             }
396         }
397         return BluetoothProfile.STATE_DISCONNECTED;
398     }
399 
400     /**
401      * Register a {@link Callback} that will be invoked during the operation of this profile.
402      *
403      * <p>Repeated registration of the same <var>callback</var> object will have no effect after the
404      * first call to this method, even when the <var>executor</var> is different. API caller would
405      * have to call {@link #unregisterCallback(Callback)} with the same callback object before
406      * registering it again.
407      *
408      * @param executor an {@link Executor} to execute given callback
409      * @param callback user implementation of the {@link Callback}
410      * @throws IllegalArgumentException if a null executor, sink, or callback is given
411      * @hide
412      */
413     @SystemApi
414     @RequiresBluetoothConnectPermission
415     @RequiresPermission(
416             allOf = {
417                 android.Manifest.permission.BLUETOOTH_CONNECT,
418                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
419             })
registerCallback( @onNull @allbackExecutor Executor executor, @NonNull Callback callback)420     public void registerCallback(
421             @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) {
422         Objects.requireNonNull(executor, "executor cannot be null");
423         Objects.requireNonNull(callback, "callback cannot be null");
424         if (DBG) log("registerCallback");
425         synchronized (mCallbackExecutorMap) {
426             if (!mAdapter.isEnabled()) {
427                 /* If Bluetooth is off, just store callback and it will be registered
428                  * when Bluetooth is on
429                  */
430                 mCallbackExecutorMap.put(callback, executor);
431                 return;
432             }
433 
434             // Adds the passed in callback to our map of callbacks to executors
435             if (mCallbackExecutorMap.containsKey(callback)) {
436                 throw new IllegalArgumentException("This callback has already been registered");
437             }
438 
439             final IBluetoothVolumeControl service = getService();
440             if (service == null) {
441                 return;
442             }
443             try {
444                 /* If the callback map is empty, we register the service-to-app callback.
445                  *  Otherwise, callback is registered in mCallbackExecutorMap and we just notify
446                  *  user over callback with current values.
447                  */
448                 boolean isRegisterCallbackRequired = mCallbackExecutorMap.isEmpty();
449                 mCallbackExecutorMap.put(callback, executor);
450 
451                 if (isRegisterCallbackRequired) {
452                     service.registerCallback(mCallback, mAttributionSource);
453                 } else {
454                     service.notifyNewRegisteredCallback(
455                             new VolumeControlNotifyCallback(Map.of(callback, executor)),
456                             mAttributionSource);
457                 }
458             } catch (RemoteException e) {
459                 mCallbackExecutorMap.remove(callback);
460                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
461                 throw e.rethrowAsRuntimeException();
462             }
463         }
464     }
465 
466     /**
467      * Unregister the specified {@link Callback}.
468      *
469      * <p>The same {@link Callback} object used when calling {@link #registerCallback(Executor,
470      * Callback)} must be used.
471      *
472      * <p>Callbacks are automatically unregistered when application process goes away
473      *
474      * @param callback user implementation of the {@link Callback}
475      * @throws IllegalArgumentException when callback is null or when no callback is registered
476      * @hide
477      */
478     @SystemApi
479     @RequiresBluetoothConnectPermission
480     @RequiresPermission(
481             allOf = {
482                 android.Manifest.permission.BLUETOOTH_CONNECT,
483                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
484             })
unregisterCallback(@onNull Callback callback)485     public void unregisterCallback(@NonNull Callback callback) {
486         Objects.requireNonNull(callback, "callback cannot be null");
487         if (DBG) log("unregisterCallback");
488         synchronized (mCallbackExecutorMap) {
489             if (mCallbackExecutorMap.remove(callback) == null) {
490                 throw new IllegalArgumentException("This callback has not been registered");
491             }
492 
493             if (!mCallbackExecutorMap.isEmpty()) {
494                 return;
495             }
496         }
497 
498         // If the callback map is empty, we unregister the service-to-app callback
499         try {
500             final IBluetoothVolumeControl service = getService();
501             if (service != null) {
502                 service.unregisterCallback(mCallback, mAttributionSource);
503             }
504         } catch (RemoteException e) {
505             Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
506             throw e.rethrowAsRuntimeException();
507         } catch (IllegalStateException e) {
508             Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
509         }
510     }
511 
512     /**
513      * Tells the remote device to set a volume offset to the absolute volume.
514      *
515      * @param device {@link BluetoothDevice} representing the remote device
516      * @param volumeOffset volume offset to be set on the remote device
517      * @deprecated Use new method which allows for choosing a VOCS instance. This method will always
518      *     use the first instance.
519      * @hide
520      */
521     @Deprecated
522     @SystemApi
523     @RequiresBluetoothConnectPermission
524     @RequiresPermission(
525             allOf = {
526                 android.Manifest.permission.BLUETOOTH_CONNECT,
527                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
528             })
setVolumeOffset( @onNull BluetoothDevice device, @IntRange(from = -255, to = 255) int volumeOffset)529     public void setVolumeOffset(
530             @NonNull BluetoothDevice device, @IntRange(from = -255, to = 255) int volumeOffset) {
531         final int defaultInstanceId = 1;
532         setVolumeOffsetInternal(device, defaultInstanceId, volumeOffset);
533     }
534 
535     /**
536      * Tells the remote device to set a volume offset to the absolute volume. One device might have
537      * multiple VOCS instances. This instances could be i.e. different speakers or sound types as
538      * media/voice/notification.
539      *
540      * @param device {@link BluetoothDevice} representing the remote device
541      * @param instanceId identifier of VOCS instance on the remote device. Identifiers are numerated
542      *     from 1. Number of them was notified by callbacks and it can be read using {@link
543      *     #getNumberOfVolumeOffsetInstances(BluetoothDevice)}. Providing non existing instance ID
544      *     will be ignored
545      * @param volumeOffset volume offset to be set on VOCS instance
546      * @hide
547      */
548     @FlaggedApi(Flags.FLAG_LEAUDIO_MULTIPLE_VOCS_INSTANCES_API)
549     @SystemApi
550     @RequiresBluetoothConnectPermission
551     @RequiresPermission(
552             allOf = {
553                 android.Manifest.permission.BLUETOOTH_CONNECT,
554                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
555             })
setVolumeOffset( @onNull BluetoothDevice device, @IntRange(from = 1, to = 255) int instanceId, @IntRange(from = -255, to = 255) int volumeOffset)556     public void setVolumeOffset(
557             @NonNull BluetoothDevice device,
558             @IntRange(from = 1, to = 255) int instanceId,
559             @IntRange(from = -255, to = 255) int volumeOffset) {
560         setVolumeOffsetInternal(device, instanceId, volumeOffset);
561     }
562 
563     /**
564      * INTERNAL HELPER METHOD, DO NOT MAKE PUBLIC
565      *
566      * <p>Tells the remote device to set a volume offset to the absolute volume. One device might
567      * have multiple VOCS instances. This instances could be i.e. different speakers or sound types
568      * as media/voice/notification.
569      *
570      * @param device {@link BluetoothDevice} representing the remote device
571      * @param instanceId identifier of VOCS instance on the remote device. Identifiers are numerated
572      *     from 1. Number of them was notified by callbacks and it can be read using {@link
573      *     #getNumberOfVolumeOffsetInstances(BluetoothDevice)}. Providing non existing instance ID
574      *     will be ignored
575      * @param volumeOffset volume offset to be set on VOCS instance
576      * @hide
577      */
setVolumeOffsetInternal( @onNull BluetoothDevice device, @IntRange(from = 1, to = 255) int instanceId, @IntRange(from = -255, to = 255) int volumeOffset)578     private void setVolumeOffsetInternal(
579             @NonNull BluetoothDevice device,
580             @IntRange(from = 1, to = 255) int instanceId,
581             @IntRange(from = -255, to = 255) int volumeOffset) {
582         if (DBG) {
583             log(
584                     "setVolumeOffset("
585                             + device
586                             + "/"
587                             + instanceId
588                             + " volumeOffset: "
589                             + volumeOffset
590                             + ")");
591         }
592         final IBluetoothVolumeControl service = getService();
593         if (service == null) {
594             Log.w(TAG, "Proxy not attached to service");
595             if (DBG) log(Log.getStackTraceString(new Throwable()));
596         } else if (isEnabled()) {
597             try {
598                 service.setVolumeOffset(device, instanceId, volumeOffset, mAttributionSource);
599             } catch (RemoteException e) {
600                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
601             }
602         }
603     }
604 
605     /**
606      * Provides information about the possibility to set volume offset on the remote device. If the
607      * remote device supports Volume Offset Control Service, it is automatically connected.
608      *
609      * @param device {@link BluetoothDevice} representing the remote device
610      * @return {@code true} if volume offset function is supported and available to use on the
611      *     remote device. When Bluetooth is off, the return value should always be {@code false}.
612      * @hide
613      */
614     @SystemApi
615     @RequiresBluetoothConnectPermission
616     @RequiresPermission(
617             allOf = {
618                 android.Manifest.permission.BLUETOOTH_CONNECT,
619                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
620             })
isVolumeOffsetAvailable(@onNull BluetoothDevice device)621     public boolean isVolumeOffsetAvailable(@NonNull BluetoothDevice device) {
622         if (DBG) log("isVolumeOffsetAvailable(" + device + ")");
623         final IBluetoothVolumeControl service = getService();
624         if (service == null) {
625             Log.w(TAG, "Proxy not attached to service");
626             if (DBG) log(Log.getStackTraceString(new Throwable()));
627             return false;
628         }
629 
630         if (!isEnabled()) {
631             return false;
632         }
633 
634         try {
635             return service.isVolumeOffsetAvailable(device, mAttributionSource);
636         } catch (RemoteException e) {
637             Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
638         }
639 
640         return false;
641     }
642 
643     /**
644      * Provides information about the number of volume offset instances
645      *
646      * @param device {@link BluetoothDevice} representing the remote device
647      * @return number of VOCS instances. When Bluetooth is off, the return value is 0.
648      * @hide
649      */
650     @FlaggedApi(Flags.FLAG_LEAUDIO_MULTIPLE_VOCS_INSTANCES_API)
651     @SystemApi
652     @RequiresBluetoothConnectPermission
653     @RequiresPermission(
654             allOf = {
655                 android.Manifest.permission.BLUETOOTH_CONNECT,
656                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
657             })
getNumberOfVolumeOffsetInstances(@onNull BluetoothDevice device)658     public int getNumberOfVolumeOffsetInstances(@NonNull BluetoothDevice device) {
659         if (DBG) log("getNumberOfVolumeOffsetInstances(" + device + ")");
660         final IBluetoothVolumeControl service = getService();
661         final int defaultValue = 0;
662 
663         if (service == null) {
664             Log.w(TAG, "Proxy not attached to service");
665             if (DBG) log(Log.getStackTraceString(new Throwable()));
666             return defaultValue;
667         }
668 
669         if (!isEnabled()) {
670             return defaultValue;
671         }
672         try {
673             return service.getNumberOfVolumeOffsetInstances(device, mAttributionSource);
674         } catch (RemoteException e) {
675             Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
676         }
677 
678         return defaultValue;
679     }
680 
681     /**
682      * Set connection policy of the profile
683      *
684      * <p>The device should already be paired. Connection policy can be one of {@link
685      * #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, {@link
686      * #CONNECTION_POLICY_UNKNOWN}
687      *
688      * @param device Paired bluetooth device
689      * @param connectionPolicy is the connection policy to set to for this profile
690      * @return true if connectionPolicy is set, false on error
691      * @hide
692      */
693     @SystemApi
694     @RequiresBluetoothConnectPermission
695     @RequiresPermission(
696             allOf = {
697                 android.Manifest.permission.BLUETOOTH_CONNECT,
698                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
699             })
setConnectionPolicy( @onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)700     public boolean setConnectionPolicy(
701             @NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) {
702         if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
703         final IBluetoothVolumeControl service = getService();
704         if (service == null) {
705             Log.w(TAG, "Proxy not attached to service");
706             if (DBG) log(Log.getStackTraceString(new Throwable()));
707         } else if (isEnabled()
708                 && isValidDevice(device)
709                 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
710                         || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
711             try {
712                 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource);
713             } catch (RemoteException e) {
714                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
715             }
716         }
717         return false;
718     }
719 
720     /**
721      * Get the connection policy of the profile.
722      *
723      * <p>The connection policy can be any of: {@link #CONNECTION_POLICY_ALLOWED}, {@link
724      * #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
725      *
726      * @param device Bluetooth device
727      * @return connection policy of the device
728      * @hide
729      */
730     @SystemApi
731     @RequiresBluetoothConnectPermission
732     @RequiresPermission(
733             allOf = {
734                 android.Manifest.permission.BLUETOOTH_CONNECT,
735                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
736             })
getConnectionPolicy(@onNull BluetoothDevice device)737     public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
738         if (VDBG) log("getConnectionPolicy(" + device + ")");
739         final IBluetoothVolumeControl service = getService();
740         if (service == null) {
741             Log.w(TAG, "Proxy not attached to service");
742             if (DBG) log(Log.getStackTraceString(new Throwable()));
743         } else if (isEnabled() && isValidDevice(device)) {
744             try {
745                 return service.getConnectionPolicy(device, mAttributionSource);
746             } catch (RemoteException e) {
747                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
748             }
749         }
750         return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
751     }
752 
753     /**
754      * Set volume for the le audio device
755      *
756      * <p>This provides volume control for connected remote device directly by volume control
757      * service. The valid volume range is [0, 255], as defined in 2.3.1.1 Volume_Setting field of
758      * Volume Control Service, Version 1.0.
759      *
760      * <p>For le audio unicast devices volume control, application should consider to use {@link
761      * BluetoothLeAudio#setVolume} instead to control active device volume.
762      *
763      * @param device {@link BluetoothDevice} representing the remote device
764      * @param volume level to set
765      * @param isGroupOperation {@code true} if Application wants to perform this operation for all
766      *     coordinated set members throughout this session. Otherwise, caller would have to control
767      *     individual device volume.
768      * @throws IllegalArgumentException if volume is not in the range [0, 255].
769      * @hide
770      */
771     @FlaggedApi(Flags.FLAG_LEAUDIO_BROADCAST_VOLUME_CONTROL_FOR_CONNECTED_DEVICES)
772     @SystemApi
773     @RequiresBluetoothConnectPermission
774     @RequiresPermission(
775             allOf = {
776                 android.Manifest.permission.BLUETOOTH_CONNECT,
777                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
778             })
setDeviceVolume( @onNull BluetoothDevice device, @IntRange(from = 0, to = 255) int volume, boolean isGroupOperation)779     public void setDeviceVolume(
780             @NonNull BluetoothDevice device,
781             @IntRange(from = 0, to = 255) int volume,
782             boolean isGroupOperation) {
783         if (volume < 0 || volume > 255) {
784             throw new IllegalArgumentException("illegal volume " + volume);
785         }
786         final IBluetoothVolumeControl service = getService();
787         if (service == null) {
788             Log.w(TAG, "Proxy not attached to service");
789             if (DBG) log(Log.getStackTraceString(new Throwable()));
790         } else if (isEnabled()) {
791             try {
792                 service.setDeviceVolume(device, volume, isGroupOperation, mAttributionSource);
793             } catch (RemoteException e) {
794                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
795             }
796         }
797     }
798 
isEnabled()799     private boolean isEnabled() {
800         return mAdapter.getState() == BluetoothAdapter.STATE_ON;
801     }
802 
isValidDevice(@ullable BluetoothDevice device)803     private static boolean isValidDevice(@Nullable BluetoothDevice device) {
804         return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
805     }
806 
log(String msg)807     private static void log(String msg) {
808         Log.d(TAG, msg);
809     }
810 }
811