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