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 com.android.bluetooth.vc; 19 20 import static android.Manifest.permission.BLUETOOTH_CONNECT; 21 22 import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission; 23 24 import android.annotation.RequiresPermission; 25 import android.bluetooth.BluetoothDevice; 26 import android.bluetooth.BluetoothProfile; 27 import android.bluetooth.BluetoothUuid; 28 import android.bluetooth.IBluetoothCsipSetCoordinator; 29 import android.bluetooth.IBluetoothLeAudio; 30 import android.bluetooth.IBluetoothVolumeControl; 31 import android.bluetooth.IBluetoothVolumeControlCallback; 32 import android.content.AttributionSource; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.media.AudioManager; 36 import android.os.Handler; 37 import android.os.HandlerThread; 38 import android.os.Looper; 39 import android.os.ParcelUuid; 40 import android.os.RemoteCallbackList; 41 import android.os.RemoteException; 42 import android.sysprop.BluetoothProperties; 43 import android.util.Log; 44 45 import com.android.bluetooth.Utils; 46 import com.android.bluetooth.btservice.AdapterService; 47 import com.android.bluetooth.btservice.ProfileService; 48 import com.android.bluetooth.btservice.ServiceFactory; 49 import com.android.bluetooth.btservice.storage.DatabaseManager; 50 import com.android.bluetooth.csip.CsipSetCoordinatorService; 51 import com.android.bluetooth.flags.Flags; 52 import com.android.bluetooth.le_audio.LeAudioService; 53 import com.android.internal.annotations.VisibleForTesting; 54 55 import libcore.util.SneakyThrow; 56 57 import java.util.ArrayList; 58 import java.util.Arrays; 59 import java.util.Collections; 60 import java.util.HashMap; 61 import java.util.List; 62 import java.util.Map; 63 import java.util.Objects; 64 import java.util.Optional; 65 import java.util.concurrent.ExecutionException; 66 import java.util.concurrent.Executors; 67 import java.util.concurrent.FutureTask; 68 import java.util.concurrent.TimeUnit; 69 import java.util.concurrent.TimeoutException; 70 71 public class VolumeControlService extends ProfileService { 72 private static final String TAG = "VolumeControlService"; 73 74 // Timeout for state machine thread join, to prevent potential ANR. 75 private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1000; 76 77 // Upper limit of all VolumeControl devices: Bonded or Connected 78 private static final int MAX_VC_STATE_MACHINES = 10; 79 private static final int LE_AUDIO_MAX_VOL = 255; 80 81 private static VolumeControlService sVolumeControlService; 82 83 private AdapterService mAdapterService; 84 private DatabaseManager mDatabaseManager; 85 private HandlerThread mStateMachinesThread; 86 private Handler mHandler = null; 87 88 @VisibleForTesting RemoteCallbackList<IBluetoothVolumeControlCallback> mCallbacks; 89 90 @VisibleForTesting 91 static class VolumeControlOffsetDescriptor { 92 Map<Integer, Descriptor> mVolumeOffsets; 93 94 private static class Descriptor { Descriptor()95 Descriptor() { 96 mValue = 0; 97 mLocation = 0; 98 mDescription = null; 99 } 100 101 int mValue; 102 int mLocation; 103 String mDescription; 104 } 105 ; 106 VolumeControlOffsetDescriptor()107 VolumeControlOffsetDescriptor() { 108 mVolumeOffsets = new HashMap<>(); 109 } 110 size()111 int size() { 112 return mVolumeOffsets.size(); 113 } 114 add(int id)115 void add(int id) { 116 Descriptor d = mVolumeOffsets.get(id); 117 if (d == null) { 118 mVolumeOffsets.put(id, new Descriptor()); 119 } 120 } 121 setValue(int id, int value)122 boolean setValue(int id, int value) { 123 Descriptor d = mVolumeOffsets.get(id); 124 if (d == null) { 125 return false; 126 } 127 d.mValue = value; 128 return true; 129 } 130 getValue(int id)131 int getValue(int id) { 132 Descriptor d = mVolumeOffsets.get(id); 133 if (d == null) { 134 return 0; 135 } 136 return d.mValue; 137 } 138 setDescription(int id, String desc)139 boolean setDescription(int id, String desc) { 140 Descriptor d = mVolumeOffsets.get(id); 141 if (d == null) { 142 return false; 143 } 144 d.mDescription = desc; 145 return true; 146 } 147 getDescription(int id)148 String getDescription(int id) { 149 Descriptor d = mVolumeOffsets.get(id); 150 if (d == null) { 151 return null; 152 } 153 return d.mDescription; 154 } 155 setLocation(int id, int location)156 boolean setLocation(int id, int location) { 157 Descriptor d = mVolumeOffsets.get(id); 158 if (d == null) { 159 return false; 160 } 161 d.mLocation = location; 162 return true; 163 } 164 getLocation(int id)165 int getLocation(int id) { 166 Descriptor d = mVolumeOffsets.get(id); 167 if (d == null) { 168 return 0; 169 } 170 return d.mLocation; 171 } 172 remove(int id)173 void remove(int id) { 174 mVolumeOffsets.remove(id); 175 } 176 clear()177 void clear() { 178 mVolumeOffsets.clear(); 179 } 180 dump(StringBuilder sb)181 void dump(StringBuilder sb) { 182 for (Map.Entry<Integer, Descriptor> entry : mVolumeOffsets.entrySet()) { 183 Descriptor descriptor = entry.getValue(); 184 Integer id = entry.getKey(); 185 ProfileService.println(sb, " Id: " + id); 186 ProfileService.println(sb, " value: " + descriptor.mValue); 187 ProfileService.println(sb, " location: " + descriptor.mLocation); 188 ProfileService.println(sb, " description: " + descriptor.mDescription); 189 } 190 } 191 } 192 193 VolumeControlNativeInterface mVolumeControlNativeInterface; 194 @VisibleForTesting AudioManager mAudioManager; 195 196 private final Map<BluetoothDevice, VolumeControlStateMachine> mStateMachines = new HashMap<>(); 197 private final Map<BluetoothDevice, VolumeControlOffsetDescriptor> mAudioOffsets = 198 new HashMap<>(); 199 private final Map<Integer, Integer> mGroupVolumeCache = new HashMap<>(); 200 private final Map<Integer, Boolean> mGroupMuteCache = new HashMap<>(); 201 private final Map<BluetoothDevice, Integer> mDeviceVolumeCache = new HashMap<>(); 202 203 @VisibleForTesting ServiceFactory mFactory = new ServiceFactory(); 204 VolumeControlService(Context ctx)205 public VolumeControlService(Context ctx) { 206 super(ctx); 207 } 208 isEnabled()209 public static boolean isEnabled() { 210 return BluetoothProperties.isProfileVcpControllerEnabled().orElse(false); 211 } 212 213 @Override initBinder()214 protected IProfileServiceBinder initBinder() { 215 return new BluetoothVolumeControlBinder(this); 216 } 217 218 @Override start()219 public void start() { 220 Log.d(TAG, "start()"); 221 if (sVolumeControlService != null) { 222 throw new IllegalStateException("start() called twice"); 223 } 224 225 // Get AdapterService, VolumeControlNativeInterface, DatabaseManager, AudioManager. 226 // None of them can be null. 227 mAdapterService = 228 Objects.requireNonNull( 229 AdapterService.getAdapterService(), 230 "AdapterService cannot be null when VolumeControlService starts"); 231 mDatabaseManager = 232 Objects.requireNonNull( 233 mAdapterService.getDatabase(), 234 "DatabaseManager cannot be null when VolumeControlService starts"); 235 mVolumeControlNativeInterface = 236 Objects.requireNonNull( 237 VolumeControlNativeInterface.getInstance(), 238 "VolumeControlNativeInterface cannot be null when VolumeControlService" 239 + " starts"); 240 mAudioManager = getSystemService(AudioManager.class); 241 Objects.requireNonNull( 242 mAudioManager, "AudioManager cannot be null when VolumeControlService starts"); 243 244 // Start handler thread for state machines 245 mHandler = new Handler(Looper.getMainLooper()); 246 mStateMachines.clear(); 247 mStateMachinesThread = new HandlerThread("VolumeControlService.StateMachines"); 248 mStateMachinesThread.start(); 249 250 mAudioOffsets.clear(); 251 mGroupVolumeCache.clear(); 252 mGroupMuteCache.clear(); 253 mDeviceVolumeCache.clear(); 254 mCallbacks = new RemoteCallbackList<IBluetoothVolumeControlCallback>(); 255 256 // Mark service as started 257 setVolumeControlService(this); 258 259 // Initialize native interface 260 mVolumeControlNativeInterface.init(); 261 } 262 263 @Override stop()264 public void stop() { 265 Log.d(TAG, "stop()"); 266 if (sVolumeControlService == null) { 267 Log.w(TAG, "stop() called before start()"); 268 return; 269 } 270 271 // Mark service as stopped 272 setVolumeControlService(null); 273 274 // Destroy state machines and stop handler thread 275 synchronized (mStateMachines) { 276 for (VolumeControlStateMachine sm : mStateMachines.values()) { 277 sm.doQuit(); 278 sm.cleanup(); 279 } 280 mStateMachines.clear(); 281 } 282 283 if (mStateMachinesThread != null) { 284 try { 285 mStateMachinesThread.quitSafely(); 286 mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS); 287 mStateMachinesThread = null; 288 } catch (InterruptedException e) { 289 // Do not rethrow as we are shutting down anyway 290 } 291 } 292 293 // Unregister handler and remove all queued messages. 294 if (mHandler != null) { 295 mHandler.removeCallbacksAndMessages(null); 296 mHandler = null; 297 } 298 299 // Cleanup native interface 300 mVolumeControlNativeInterface.cleanup(); 301 mVolumeControlNativeInterface = null; 302 303 mAudioOffsets.clear(); 304 mGroupVolumeCache.clear(); 305 mGroupMuteCache.clear(); 306 mDeviceVolumeCache.clear(); 307 308 // Clear AdapterService, VolumeControlNativeInterface 309 mAudioManager = null; 310 mVolumeControlNativeInterface = null; 311 mAdapterService = null; 312 313 if (mCallbacks != null) { 314 mCallbacks.kill(); 315 mCallbacks = null; 316 } 317 } 318 319 @Override cleanup()320 public void cleanup() { 321 Log.d(TAG, "cleanup()"); 322 } 323 324 /** 325 * Get the VolumeControlService instance 326 * 327 * @return VolumeControlService instance 328 */ getVolumeControlService()329 public static synchronized VolumeControlService getVolumeControlService() { 330 if (sVolumeControlService == null) { 331 Log.w(TAG, "getVolumeControlService(): service is NULL"); 332 return null; 333 } 334 335 if (!sVolumeControlService.isAvailable()) { 336 Log.w(TAG, "getVolumeControlService(): service is not available"); 337 return null; 338 } 339 return sVolumeControlService; 340 } 341 342 @VisibleForTesting setVolumeControlService(VolumeControlService instance)343 static synchronized void setVolumeControlService(VolumeControlService instance) { 344 Log.d(TAG, "setVolumeControlService(): set to: " + instance); 345 sVolumeControlService = instance; 346 } 347 348 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) connect(BluetoothDevice device)349 public boolean connect(BluetoothDevice device) { 350 enforceCallingOrSelfPermission( 351 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 352 Log.d(TAG, "connect(): " + device); 353 if (device == null) { 354 return false; 355 } 356 357 if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 358 return false; 359 } 360 ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device); 361 if (!Utils.arrayContains(featureUuids, BluetoothUuid.VOLUME_CONTROL)) { 362 Log.e( 363 TAG, 364 "Cannot connect to " + device + " : Remote does not have Volume Control UUID"); 365 return false; 366 } 367 368 synchronized (mStateMachines) { 369 VolumeControlStateMachine smConnect = getOrCreateStateMachine(device); 370 if (smConnect == null) { 371 Log.e(TAG, "Cannot connect to " + device + " : no state machine"); 372 } 373 smConnect.sendMessage(VolumeControlStateMachine.CONNECT); 374 } 375 376 return true; 377 } 378 379 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) disconnect(BluetoothDevice device)380 public boolean disconnect(BluetoothDevice device) { 381 enforceCallingOrSelfPermission( 382 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 383 Log.d(TAG, "disconnect(): " + device); 384 if (device == null) { 385 return false; 386 } 387 synchronized (mStateMachines) { 388 VolumeControlStateMachine sm = getOrCreateStateMachine(device); 389 if (sm != null) { 390 sm.sendMessage(VolumeControlStateMachine.DISCONNECT); 391 } 392 } 393 394 return true; 395 } 396 getConnectedDevices()397 public List<BluetoothDevice> getConnectedDevices() { 398 synchronized (mStateMachines) { 399 List<BluetoothDevice> devices = new ArrayList<>(); 400 for (VolumeControlStateMachine sm : mStateMachines.values()) { 401 if (sm.isConnected()) { 402 devices.add(sm.getDevice()); 403 } 404 } 405 return devices; 406 } 407 } 408 409 /** 410 * Check whether can connect to a peer device. The check considers a number of factors during 411 * the evaluation. 412 * 413 * @param device the peer device to connect to 414 * @return true if connection is allowed, otherwise false 415 */ 416 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) okToConnect(BluetoothDevice device)417 public boolean okToConnect(BluetoothDevice device) { 418 /* Make sure device is valid */ 419 if (device == null) { 420 Log.e(TAG, "okToConnect: Invalid device"); 421 return false; 422 } 423 // Check if this is an incoming connection in Quiet mode. 424 if (mAdapterService.isQuietModeEnabled()) { 425 Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled"); 426 return false; 427 } 428 // Check connectionPolicy and accept or reject the connection. 429 int connectionPolicy = getConnectionPolicy(device); 430 int bondState = mAdapterService.getBondState(device); 431 // Allow this connection only if the device is bonded. Any attempt to connect while 432 // bonding would potentially lead to an unauthorized connection. 433 if (bondState != BluetoothDevice.BOND_BONDED) { 434 Log.w(TAG, "okToConnect: return false, bondState=" + bondState); 435 return false; 436 } else if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN 437 && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) { 438 // Otherwise, reject the connection if connectionPolicy is not valid. 439 Log.w(TAG, "okToConnect: return false, connectionPolicy=" + connectionPolicy); 440 return false; 441 } 442 return true; 443 } 444 445 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) getDevicesMatchingConnectionStates(int[] states)446 List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 447 enforceCallingOrSelfPermission( 448 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 449 ArrayList<BluetoothDevice> devices = new ArrayList<>(); 450 if (states == null) { 451 return devices; 452 } 453 final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); 454 if (bondedDevices == null) { 455 return devices; 456 } 457 synchronized (mStateMachines) { 458 for (BluetoothDevice device : bondedDevices) { 459 final ParcelUuid[] featureUuids = device.getUuids(); 460 if (!Utils.arrayContains(featureUuids, BluetoothUuid.VOLUME_CONTROL)) { 461 continue; 462 } 463 int connectionState = BluetoothProfile.STATE_DISCONNECTED; 464 VolumeControlStateMachine sm = mStateMachines.get(device); 465 if (sm != null) { 466 connectionState = sm.getConnectionState(); 467 } 468 for (int state : states) { 469 if (connectionState == state) { 470 devices.add(device); 471 break; 472 } 473 } 474 } 475 return devices; 476 } 477 } 478 479 /** 480 * Get the list of devices that have state machines. 481 * 482 * @return the list of devices that have state machines 483 */ 484 @VisibleForTesting getDevices()485 List<BluetoothDevice> getDevices() { 486 List<BluetoothDevice> devices = new ArrayList<>(); 487 synchronized (mStateMachines) { 488 for (VolumeControlStateMachine sm : mStateMachines.values()) { 489 devices.add(sm.getDevice()); 490 } 491 return devices; 492 } 493 } 494 495 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getConnectionState(BluetoothDevice device)496 public int getConnectionState(BluetoothDevice device) { 497 enforceCallingOrSelfPermission(BLUETOOTH_CONNECT, "Need BLUETOOTH_CONNECT permission"); 498 synchronized (mStateMachines) { 499 VolumeControlStateMachine sm = mStateMachines.get(device); 500 if (sm == null) { 501 return BluetoothProfile.STATE_DISCONNECTED; 502 } 503 return sm.getConnectionState(); 504 } 505 } 506 507 /** 508 * Set connection policy of the profile and connects it if connectionPolicy is {@link 509 * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link 510 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} 511 * 512 * <p>The device should already be paired. Connection policy can be one of: {@link 513 * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link 514 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link 515 * BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 516 * 517 * @param device the remote device 518 * @param connectionPolicy is the connection policy to set to for this profile 519 * @return true on success, otherwise false 520 */ setConnectionPolicy(BluetoothDevice device, int connectionPolicy)521 public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { 522 Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); 523 mDatabaseManager.setProfileConnectionPolicy( 524 device, BluetoothProfile.VOLUME_CONTROL, connectionPolicy); 525 if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { 526 connect(device); 527 } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 528 disconnect(device); 529 } 530 return true; 531 } 532 getConnectionPolicy(BluetoothDevice device)533 public int getConnectionPolicy(BluetoothDevice device) { 534 return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.VOLUME_CONTROL); 535 } 536 isVolumeOffsetAvailable(BluetoothDevice device)537 boolean isVolumeOffsetAvailable(BluetoothDevice device) { 538 VolumeControlOffsetDescriptor offsets = mAudioOffsets.get(device); 539 if (offsets == null) { 540 Log.i(TAG, " There is no offset service for device: " + device); 541 return false; 542 } 543 Log.i(TAG, " Offset service available for device: " + device); 544 return true; 545 } 546 getNumberOfVolumeOffsetInstances(BluetoothDevice device)547 int getNumberOfVolumeOffsetInstances(BluetoothDevice device) { 548 VolumeControlOffsetDescriptor offsets = mAudioOffsets.get(device); 549 if (offsets == null) { 550 Log.i(TAG, " There is no offset service for device: " + device); 551 return 0; 552 } 553 554 int numberOfInstances = offsets.size(); 555 556 Log.i(TAG, "Number of VOCS: " + numberOfInstances + ", for device: " + device); 557 return numberOfInstances; 558 } 559 setVolumeOffset(BluetoothDevice device, int instanceId, int volumeOffset)560 void setVolumeOffset(BluetoothDevice device, int instanceId, int volumeOffset) { 561 VolumeControlOffsetDescriptor offsets = mAudioOffsets.get(device); 562 if (offsets == null) { 563 Log.e(TAG, " There is no offset service for device: " + device); 564 return; 565 } 566 567 int numberOfInstances = offsets.size(); 568 if (instanceId > numberOfInstances) { 569 Log.e( 570 TAG, 571 "Selected VOCS instance ID: " 572 + instanceId 573 + ", exceed available IDs: " 574 + numberOfInstances 575 + ", for device: " 576 + device); 577 return; 578 } 579 580 int value = offsets.getValue(instanceId); 581 if (value == volumeOffset) { 582 /* Nothing to do - offset already applied */ 583 return; 584 } 585 586 mVolumeControlNativeInterface.setExtAudioOutVolumeOffset(device, instanceId, volumeOffset); 587 } 588 setDeviceVolume(BluetoothDevice device, int volume, boolean isGroupOp)589 void setDeviceVolume(BluetoothDevice device, int volume, boolean isGroupOp) { 590 if (!Flags.leaudioBroadcastVolumeControlForConnectedDevices()) { 591 return; 592 } 593 Log.d( 594 TAG, 595 "setDeviceVolume: " + device + ", volume: " + volume + ", isGroupOp: " + isGroupOp); 596 597 LeAudioService leAudioService = mFactory.getLeAudioService(); 598 if (leAudioService == null) { 599 Log.e(TAG, "leAudioService not available"); 600 return; 601 } 602 int groupId = leAudioService.getGroupId(device); 603 if (groupId == IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID) { 604 Log.e(TAG, "Device not a part of a group"); 605 return; 606 } 607 608 if (isGroupOp) { 609 setGroupVolume(groupId, volume); 610 } else { 611 Log.i(TAG, "Setting individual device volume"); 612 mDeviceVolumeCache.put(device, volume); 613 mVolumeControlNativeInterface.setVolume(device, volume); 614 } 615 } 616 setGroupVolume(int groupId, int volume)617 public void setGroupVolume(int groupId, int volume) { 618 if (volume < 0) { 619 Log.w(TAG, "Tried to set invalid volume " + volume + ". Ignored."); 620 return; 621 } 622 623 mGroupVolumeCache.put(groupId, volume); 624 mVolumeControlNativeInterface.setGroupVolume(groupId, volume); 625 626 // We only receive the volume change and mute state needs to be acquired manually 627 Boolean isGroupMute = mGroupMuteCache.getOrDefault(groupId, false); 628 Boolean isStreamMute = mAudioManager.isStreamMute(getBluetoothContextualVolumeStream()); 629 630 /* Note: AudioService keeps volume levels for each stream and for each device type, 631 * however it stores the mute state only for the stream type but not for each individual 632 * device type. When active device changes, it's volume level gets aplied, but mute state 633 * is not, but can be either derived from the volume level or just unmuted like for A2DP. 634 * Also setting volume level > 0 to audio system will implicitly unmute the stream. 635 * However LeAudio devices can keep their volume level high, while keeping it mute so we 636 * have to explicitly unmute the remote device. 637 */ 638 if (!isGroupMute.equals(isStreamMute)) { 639 Log.w( 640 TAG, 641 "Mute state mismatch, stream mute: " 642 + isStreamMute 643 + ", device group mute: " 644 + isGroupMute 645 + ", new volume: " 646 + volume); 647 if (isStreamMute) { 648 Log.i(TAG, "Mute the group " + groupId); 649 muteGroup(groupId); 650 } 651 if (!isStreamMute && (volume > 0)) { 652 Log.i(TAG, "Unmute the group " + groupId); 653 unmuteGroup(groupId); 654 } 655 } 656 } 657 getGroupVolume(int groupId)658 public int getGroupVolume(int groupId) { 659 return mGroupVolumeCache.getOrDefault( 660 groupId, IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME); 661 } 662 663 /** 664 * Get device cached volume. 665 * 666 * @param device the device 667 * @return the cached volume 668 */ getDeviceVolume(BluetoothDevice device)669 public int getDeviceVolume(BluetoothDevice device) { 670 return mDeviceVolumeCache.getOrDefault( 671 device, IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME); 672 } 673 674 /** 675 * This should be called by LeAudioService when LE Audio group change it active state. 676 * 677 * @param groupId the group identifier 678 * @param active indicator if group is active or not 679 */ setGroupActive(int groupId, boolean active)680 public void setGroupActive(int groupId, boolean active) { 681 Log.d(TAG, "setGroupActive: " + groupId + ", active: " + active); 682 if (!active) { 683 /* For now we don't need to handle group inactivation */ 684 return; 685 } 686 687 int groupVolume = getGroupVolume(groupId); 688 Boolean groupMute = getGroupMute(groupId); 689 690 if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) { 691 /* Don't need to show volume when activating known device. */ 692 updateGroupCacheAndAudioSystem(groupId, groupVolume, groupMute, /* showInUI*/ false); 693 } 694 } 695 696 /** 697 * @param groupId the group identifier 698 */ getGroupMute(int groupId)699 public Boolean getGroupMute(int groupId) { 700 return mGroupMuteCache.getOrDefault(groupId, false); 701 } 702 mute(BluetoothDevice device)703 public void mute(BluetoothDevice device) { 704 mVolumeControlNativeInterface.mute(device); 705 } 706 muteGroup(int groupId)707 public void muteGroup(int groupId) { 708 mGroupMuteCache.put(groupId, true); 709 mVolumeControlNativeInterface.muteGroup(groupId); 710 } 711 unmute(BluetoothDevice device)712 public void unmute(BluetoothDevice device) { 713 mVolumeControlNativeInterface.unmute(device); 714 } 715 unmuteGroup(int groupId)716 public void unmuteGroup(int groupId) { 717 mGroupMuteCache.put(groupId, false); 718 mVolumeControlNativeInterface.unmuteGroup(groupId); 719 } 720 notifyNewCallbackOfKnownVolumeInfo(IBluetoothVolumeControlCallback callback)721 void notifyNewCallbackOfKnownVolumeInfo(IBluetoothVolumeControlCallback callback) { 722 Log.d(TAG, "notifyNewCallbackOfKnownVolumeInfo"); 723 724 // notify volume offset 725 for (Map.Entry<BluetoothDevice, VolumeControlOffsetDescriptor> entry : 726 mAudioOffsets.entrySet()) { 727 VolumeControlOffsetDescriptor descriptor = entry.getValue(); 728 729 for (int id = 1; id <= descriptor.size(); id++) { 730 BluetoothDevice device = entry.getKey(); 731 int offset = descriptor.getValue(id); 732 int location = descriptor.getLocation(id); 733 String description = descriptor.getDescription(id); 734 735 Log.d( 736 TAG, 737 "notifyNewCallbackOfKnownVolumeInfo," 738 + (" device: " + device) 739 + (", id: " + id) 740 + (", offset: " + offset) 741 + (", location: " + location) 742 + (", description: " + description)); 743 try { 744 callback.onVolumeOffsetChanged(device, id, offset); 745 if (Flags.leaudioMultipleVocsInstancesApi()) { 746 callback.onVolumeOffsetAudioLocationChanged(device, id, location); 747 callback.onVolumeOffsetAudioDescriptionChanged(device, id, description); 748 } 749 } catch (RemoteException e) { 750 // Dead client -- continue 751 } 752 } 753 } 754 755 if (Flags.leaudioBroadcastVolumeControlForConnectedDevices()) { 756 // using tempCallbackList is a hack to keep using 'notifyDevicesVolumeChanged' 757 // without making any extra modification 758 RemoteCallbackList<IBluetoothVolumeControlCallback> tempCallbackList = 759 new RemoteCallbackList<>(); 760 761 tempCallbackList.register(callback); 762 notifyDevicesVolumeChanged(tempCallbackList, getDevices(), Optional.empty()); 763 tempCallbackList.unregister(callback); 764 } 765 } 766 registerCallback(IBluetoothVolumeControlCallback callback)767 void registerCallback(IBluetoothVolumeControlCallback callback) { 768 Log.d(TAG, "registerCallback: " + callback); 769 /* Here we keep all the user callbacks */ 770 mCallbacks.register(callback); 771 772 notifyNewCallbackOfKnownVolumeInfo(callback); 773 } 774 notifyNewRegisteredCallback(IBluetoothVolumeControlCallback callback)775 void notifyNewRegisteredCallback(IBluetoothVolumeControlCallback callback) { 776 Log.d(TAG, "notifyNewRegisteredCallback: " + callback); 777 notifyNewCallbackOfKnownVolumeInfo(callback); 778 } 779 handleGroupNodeAdded(int groupId, BluetoothDevice device)780 public void handleGroupNodeAdded(int groupId, BluetoothDevice device) { 781 // Ignore disconnected device, its volume will be set once it connects 782 synchronized (mStateMachines) { 783 VolumeControlStateMachine sm = mStateMachines.get(device); 784 if (sm == null) { 785 return; 786 } 787 if (sm.getConnectionState() != BluetoothProfile.STATE_CONNECTED) { 788 return; 789 } 790 } 791 792 // Correct the volume level only if device was already reported as connected. 793 boolean can_change_volume = false; 794 synchronized (mStateMachines) { 795 VolumeControlStateMachine sm = mStateMachines.get(device); 796 if (sm != null) { 797 can_change_volume = (sm.getConnectionState() == BluetoothProfile.STATE_CONNECTED); 798 } 799 } 800 801 // If group volume has already changed, the new group member should set it 802 if (can_change_volume) { 803 Integer groupVolume = 804 mGroupVolumeCache.getOrDefault( 805 groupId, IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME); 806 if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) { 807 Log.i(TAG, "Setting value:" + groupVolume + " to " + device); 808 mVolumeControlNativeInterface.setVolume(device, groupVolume); 809 } 810 811 Boolean isGroupMuted = mGroupMuteCache.getOrDefault(groupId, false); 812 Log.i(TAG, "Setting mute:" + isGroupMuted + " to " + device); 813 if (isGroupMuted) { 814 mVolumeControlNativeInterface.mute(device); 815 } else { 816 mVolumeControlNativeInterface.unmute(device); 817 } 818 } 819 } 820 updateGroupCacheAndAudioSystem(int groupId, int volume, boolean mute, boolean showInUI)821 void updateGroupCacheAndAudioSystem(int groupId, int volume, boolean mute, boolean showInUI) { 822 Log.d( 823 TAG, 824 " updateGroupCacheAndAudioSystem: groupId: " 825 + groupId 826 + ", vol: " 827 + volume 828 + ", mute: " 829 + mute 830 + ", showInUI" 831 + showInUI); 832 833 mGroupVolumeCache.put(groupId, volume); 834 mGroupMuteCache.put(groupId, mute); 835 836 if (Flags.leaudioBroadcastVolumeControlForConnectedDevices()) { 837 LeAudioService leAudioService = mFactory.getLeAudioService(); 838 if (leAudioService != null) { 839 int currentlyActiveGroupId = leAudioService.getActiveGroupId(); 840 if (currentlyActiveGroupId == IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID 841 || groupId != currentlyActiveGroupId) { 842 Log.i( 843 TAG, 844 "Skip updating to audio system if not updating volume for current" 845 + " active group"); 846 return; 847 } 848 } else { 849 Log.w(TAG, "leAudioService not available"); 850 } 851 } 852 853 int streamType = getBluetoothContextualVolumeStream(); 854 int flags = AudioManager.FLAG_BLUETOOTH_ABS_VOLUME; 855 if (showInUI) { 856 flags |= AudioManager.FLAG_SHOW_UI; 857 } 858 859 mAudioManager.setStreamVolume(streamType, getAudioDeviceVolume(streamType, volume), flags); 860 861 if (mAudioManager.isStreamMute(streamType) != mute) { 862 int adjustment = mute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE; 863 mAudioManager.adjustStreamVolume(streamType, adjustment, flags); 864 } 865 } 866 handleVolumeControlChanged( BluetoothDevice device, int groupId, int volume, boolean mute, boolean isAutonomous)867 void handleVolumeControlChanged( 868 BluetoothDevice device, int groupId, int volume, boolean mute, boolean isAutonomous) { 869 870 if (isAutonomous && device != null) { 871 Log.e(TAG, "We expect only group notification for autonomous updates"); 872 return; 873 } 874 875 if (groupId == IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID) { 876 LeAudioService leAudioService = mFactory.getLeAudioService(); 877 if (leAudioService == null) { 878 Log.e(TAG, "leAudioService not available"); 879 return; 880 } 881 groupId = leAudioService.getGroupId(device); 882 } 883 884 if (groupId == IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID) { 885 Log.e(TAG, "Device not a part of the group"); 886 return; 887 } 888 889 int groupVolume = getGroupVolume(groupId); 890 Boolean groupMute = getGroupMute(groupId); 891 892 if (Flags.leaudioBroadcastVolumeControlForConnectedDevices()) { 893 Log.i(TAG, "handleVolumeControlChanged: " + device + "; volume: " + volume); 894 if (device == null) { 895 // notify group devices volume changed 896 LeAudioService leAudioService = mFactory.getLeAudioService(); 897 if (leAudioService != null) { 898 notifyDevicesVolumeChanged( 899 mCallbacks, 900 leAudioService.getGroupDevices(groupId), 901 Optional.of(volume)); 902 } else { 903 Log.w(TAG, "leAudioService not available"); 904 } 905 } else { 906 // notify device volume changed 907 notifyDevicesVolumeChanged(mCallbacks, Arrays.asList(device), Optional.of(volume)); 908 } 909 } 910 911 if (groupVolume == IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) { 912 /* We are here, because system was just started and LeAudio device just connected. 913 * In such case, we take Volume stored on remote device and apply it to our cache and 914 * audio system. 915 * Note, to match BR/EDR behavior, don't show volume change in UI here 916 */ 917 updateGroupCacheAndAudioSystem(groupId, volume, mute, false); 918 return; 919 } 920 921 if (!isAutonomous) { 922 /* If the change is triggered by Android device, the stream is already changed. 923 * However it might be called with isAutonomous, one the first read of after 924 * reconnection. Make sure device has group volume. Also it might happen that 925 * remote side send us wrong value - lets check it. 926 */ 927 928 if ((groupVolume == volume) && (groupMute == mute)) { 929 Log.i(TAG, " Volume:" + volume + ", mute:" + mute + " confirmed by remote side."); 930 return; 931 } 932 933 if (device != null) { 934 // Correct the volume level only if device was already reported as connected. 935 boolean can_change_volume = false; 936 synchronized (mStateMachines) { 937 VolumeControlStateMachine sm = mStateMachines.get(device); 938 if (sm != null) { 939 can_change_volume = 940 (sm.getConnectionState() == BluetoothProfile.STATE_CONNECTED); 941 } 942 } 943 944 if (can_change_volume && (groupVolume != volume)) { 945 Log.i(TAG, "Setting value:" + groupVolume + " to " + device); 946 mVolumeControlNativeInterface.setVolume(device, groupVolume); 947 } 948 if (can_change_volume && (groupMute != mute)) { 949 Log.i(TAG, "Setting mute:" + groupMute + " to " + device); 950 if (groupMute) { 951 mVolumeControlNativeInterface.mute(device); 952 } else { 953 mVolumeControlNativeInterface.unmute(device); 954 } 955 } 956 } else { 957 Log.e( 958 TAG, 959 "Volume changed did not succeed. Volume: " 960 + volume 961 + " expected volume: " 962 + groupVolume); 963 } 964 } else { 965 /* Received group notification for autonomous change. Update cache and audio system. */ 966 updateGroupCacheAndAudioSystem(groupId, volume, mute, true); 967 } 968 } 969 getAudioDeviceGroupVolume(int groupId)970 public int getAudioDeviceGroupVolume(int groupId) { 971 int volume = getGroupVolume(groupId); 972 if (getGroupMute(groupId)) { 973 Log.w( 974 TAG, 975 "Volume level is " 976 + volume 977 + ", but muted. Will report 0 for the audio device."); 978 volume = 0; 979 } 980 981 if (volume == IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) return -1; 982 return getAudioDeviceVolume(getBluetoothContextualVolumeStream(), volume); 983 } 984 getAudioDeviceVolume(int streamType, int bleVolume)985 int getAudioDeviceVolume(int streamType, int bleVolume) { 986 int deviceMaxVolume = mAudioManager.getStreamMaxVolume(streamType); 987 988 // TODO: Investigate what happens in classic BT when BT volume is changed to zero. 989 double deviceVolume = (double) (bleVolume * deviceMaxVolume) / LE_AUDIO_MAX_VOL; 990 return (int) Math.round(deviceVolume); 991 } 992 993 // Copied from AudioService.getBluetoothContextualVolumeStream() and modified it. getBluetoothContextualVolumeStream()994 int getBluetoothContextualVolumeStream() { 995 int mode = mAudioManager.getMode(); 996 997 Log.d(TAG, "Volume mode: " + mode + "0: normal, 1: ring, 2,3: call"); 998 999 switch (mode) { 1000 case AudioManager.MODE_IN_COMMUNICATION: 1001 case AudioManager.MODE_IN_CALL: 1002 return AudioManager.STREAM_VOICE_CALL; 1003 case AudioManager.MODE_RINGTONE: 1004 if (Flags.leaudioVolumeChangeOnRingtoneFix()) { 1005 Log.d(TAG, " Update during ringtone applied to voice call"); 1006 return AudioManager.STREAM_VOICE_CALL; 1007 } 1008 // fall through 1009 case AudioManager.MODE_NORMAL: 1010 default: 1011 // other conditions will influence the stream type choice, read on... 1012 break; 1013 } 1014 return AudioManager.STREAM_MUSIC; 1015 } 1016 handleDeviceAvailable(BluetoothDevice device, int numberOfExternalOutputs)1017 void handleDeviceAvailable(BluetoothDevice device, int numberOfExternalOutputs) { 1018 if (numberOfExternalOutputs == 0) { 1019 Log.i(TAG, "Volume offset not available"); 1020 return; 1021 } 1022 1023 VolumeControlOffsetDescriptor offsets = mAudioOffsets.get(device); 1024 if (offsets == null) { 1025 offsets = new VolumeControlOffsetDescriptor(); 1026 mAudioOffsets.put(device, offsets); 1027 } else if (offsets.size() != numberOfExternalOutputs) { 1028 Log.i(TAG, "Number of offset changed: "); 1029 offsets.clear(); 1030 } 1031 1032 /* Stack delivers us number of audio outputs. 1033 * Offset ids a countinous from 1 to number_of_ext_outputs*/ 1034 for (int i = 1; i <= numberOfExternalOutputs; i++) { 1035 offsets.add(i); 1036 mVolumeControlNativeInterface.getExtAudioOutVolumeOffset(device, i); 1037 mVolumeControlNativeInterface.getExtAudioOutLocation(device, i); 1038 mVolumeControlNativeInterface.getExtAudioOutDescription(device, i); 1039 } 1040 } 1041 handleDeviceExtAudioOffsetChanged(BluetoothDevice device, int id, int value)1042 void handleDeviceExtAudioOffsetChanged(BluetoothDevice device, int id, int value) { 1043 Log.d(TAG, " device: " + device + " offset_id: " + id + " value: " + value); 1044 VolumeControlOffsetDescriptor offsets = mAudioOffsets.get(device); 1045 if (offsets == null) { 1046 Log.e(TAG, " Offsets not found for device: " + device); 1047 return; 1048 } 1049 offsets.setValue(id, value); 1050 1051 if (mCallbacks == null) { 1052 return; 1053 } 1054 1055 int n = mCallbacks.beginBroadcast(); 1056 for (int i = 0; i < n; i++) { 1057 try { 1058 mCallbacks.getBroadcastItem(i).onVolumeOffsetChanged(device, id, value); 1059 } catch (RemoteException e) { 1060 continue; 1061 } 1062 } 1063 mCallbacks.finishBroadcast(); 1064 } 1065 handleDeviceExtAudioLocationChanged(BluetoothDevice device, int id, int location)1066 void handleDeviceExtAudioLocationChanged(BluetoothDevice device, int id, int location) { 1067 Log.d(TAG, " device: " + device + " offset_id: " + id + " location: " + location); 1068 1069 VolumeControlOffsetDescriptor offsets = mAudioOffsets.get(device); 1070 if (offsets == null) { 1071 Log.e(TAG, " Offsets not found for device: " + device); 1072 return; 1073 } 1074 offsets.setLocation(id, location); 1075 1076 if (Flags.leaudioMultipleVocsInstancesApi()) { 1077 if (mCallbacks == null) { 1078 return; 1079 } 1080 1081 int n = mCallbacks.beginBroadcast(); 1082 for (int i = 0; i < n; i++) { 1083 try { 1084 mCallbacks 1085 .getBroadcastItem(i) 1086 .onVolumeOffsetAudioLocationChanged(device, id, location); 1087 } catch (RemoteException e) { 1088 continue; 1089 } 1090 } 1091 mCallbacks.finishBroadcast(); 1092 } 1093 } 1094 handleDeviceExtAudioDescriptionChanged( BluetoothDevice device, int id, String description)1095 void handleDeviceExtAudioDescriptionChanged( 1096 BluetoothDevice device, int id, String description) { 1097 Log.d(TAG, " device: " + device + " offset_id: " + id + " description: " + description); 1098 1099 VolumeControlOffsetDescriptor offsets = mAudioOffsets.get(device); 1100 if (offsets == null) { 1101 Log.e(TAG, " Offsets not found for device: " + device); 1102 return; 1103 } 1104 offsets.setDescription(id, description); 1105 1106 if (Flags.leaudioMultipleVocsInstancesApi()) { 1107 if (mCallbacks == null) { 1108 return; 1109 } 1110 1111 int n = mCallbacks.beginBroadcast(); 1112 for (int i = 0; i < n; i++) { 1113 try { 1114 mCallbacks 1115 .getBroadcastItem(i) 1116 .onVolumeOffsetAudioDescriptionChanged(device, id, description); 1117 } catch (RemoteException e) { 1118 continue; 1119 } 1120 } 1121 mCallbacks.finishBroadcast(); 1122 } 1123 } 1124 messageFromNative(VolumeControlStackEvent stackEvent)1125 void messageFromNative(VolumeControlStackEvent stackEvent) { 1126 Log.d(TAG, "messageFromNative: " + stackEvent); 1127 1128 if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED) { 1129 handleVolumeControlChanged( 1130 stackEvent.device, 1131 stackEvent.valueInt1, 1132 stackEvent.valueInt2, 1133 stackEvent.valueBool1, 1134 stackEvent.valueBool2); 1135 return; 1136 } 1137 1138 Objects.requireNonNull( 1139 stackEvent.device, "Device should never be null, event: " + stackEvent); 1140 1141 Intent intent = null; 1142 1143 if (intent != null) { 1144 intent.addFlags( 1145 Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT 1146 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 1147 sendBroadcast(intent, BLUETOOTH_CONNECT); 1148 return; 1149 } 1150 1151 BluetoothDevice device = stackEvent.device; 1152 if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_DEVICE_AVAILABLE) { 1153 handleDeviceAvailable(device, stackEvent.valueInt1); 1154 return; 1155 } 1156 1157 if (stackEvent.type 1158 == VolumeControlStackEvent.EVENT_TYPE_EXT_AUDIO_OUT_VOL_OFFSET_CHANGED) { 1159 handleDeviceExtAudioOffsetChanged(device, stackEvent.valueInt1, stackEvent.valueInt2); 1160 return; 1161 } 1162 1163 if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_EXT_AUDIO_OUT_LOCATION_CHANGED) { 1164 handleDeviceExtAudioLocationChanged(device, stackEvent.valueInt1, stackEvent.valueInt2); 1165 return; 1166 } 1167 1168 if (stackEvent.type 1169 == VolumeControlStackEvent.EVENT_TYPE_EXT_AUDIO_OUT_DESCRIPTION_CHANGED) { 1170 handleDeviceExtAudioDescriptionChanged( 1171 device, stackEvent.valueInt1, stackEvent.valueString1); 1172 return; 1173 } 1174 1175 synchronized (mStateMachines) { 1176 VolumeControlStateMachine sm = mStateMachines.get(device); 1177 if (sm == null) { 1178 if (stackEvent.type 1179 == VolumeControlStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) { 1180 switch (stackEvent.valueInt1) { 1181 case VolumeControlStackEvent.CONNECTION_STATE_CONNECTED: 1182 case VolumeControlStackEvent.CONNECTION_STATE_CONNECTING: 1183 sm = getOrCreateStateMachine(device); 1184 break; 1185 default: 1186 break; 1187 } 1188 } 1189 } 1190 if (sm == null) { 1191 Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent); 1192 return; 1193 } 1194 sm.sendMessage(VolumeControlStateMachine.STACK_EVENT, stackEvent); 1195 } 1196 } 1197 getOrCreateStateMachine(BluetoothDevice device)1198 private VolumeControlStateMachine getOrCreateStateMachine(BluetoothDevice device) { 1199 if (device == null) { 1200 Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null"); 1201 return null; 1202 } 1203 synchronized (mStateMachines) { 1204 VolumeControlStateMachine sm = mStateMachines.get(device); 1205 if (sm != null) { 1206 return sm; 1207 } 1208 // Limit the maximum number of state machines to avoid DoS attack 1209 if (mStateMachines.size() >= MAX_VC_STATE_MACHINES) { 1210 Log.e( 1211 TAG, 1212 "Maximum number of VolumeControl state machines reached: " 1213 + MAX_VC_STATE_MACHINES); 1214 return null; 1215 } 1216 Log.d(TAG, "Creating a new state machine for " + device); 1217 sm = 1218 VolumeControlStateMachine.make( 1219 device, 1220 this, 1221 mVolumeControlNativeInterface, 1222 mStateMachinesThread.getLooper()); 1223 mStateMachines.put(device, sm); 1224 return sm; 1225 } 1226 } 1227 1228 /** 1229 * Notify devices with volume level 1230 * 1231 * <p>In case of handleVolumeControlChanged, volume level is known from native layer caller. 1232 * Notify the clients with the volume level directly and update the volume cache. In case of 1233 * newly registered callback, volume level is unknown from caller, notify the clients with 1234 * cached volume level from either device or group. 1235 * 1236 * @param callbacks list of callbacks 1237 * @param devices list of devices to notify volume changed 1238 * @param volume volume level 1239 */ notifyDevicesVolumeChanged( RemoteCallbackList<IBluetoothVolumeControlCallback> callbacks, List<BluetoothDevice> devices, Optional<Integer> volume)1240 private void notifyDevicesVolumeChanged( 1241 RemoteCallbackList<IBluetoothVolumeControlCallback> callbacks, 1242 List<BluetoothDevice> devices, 1243 Optional<Integer> volume) { 1244 if (callbacks == null) { 1245 Log.e(TAG, "callbacks is null"); 1246 return; 1247 } 1248 1249 LeAudioService leAudioService = mFactory.getLeAudioService(); 1250 if (leAudioService == null) { 1251 Log.e(TAG, "leAudioService not available"); 1252 return; 1253 } 1254 1255 for (BluetoothDevice dev : devices) { 1256 int cachedVolume = IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME; 1257 if (!volume.isPresent()) { 1258 int groupId = leAudioService.getGroupId(dev); 1259 if (groupId == IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID) { 1260 Log.e(TAG, "Device not a part of a group"); 1261 continue; 1262 } 1263 // if device volume is available, notify with device volume, otherwise group volume 1264 cachedVolume = getDeviceVolume(dev); 1265 if (cachedVolume == IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) { 1266 cachedVolume = getGroupVolume(groupId); 1267 } 1268 } 1269 int broadcastVolume = cachedVolume; 1270 if (volume.isPresent()) { 1271 broadcastVolume = volume.get(); 1272 mDeviceVolumeCache.put(dev, broadcastVolume); 1273 } 1274 int n = callbacks.beginBroadcast(); 1275 for (int i = 0; i < n; i++) { 1276 try { 1277 callbacks.getBroadcastItem(i).onDeviceVolumeChanged(dev, broadcastVolume); 1278 } catch (RemoteException e) { 1279 continue; 1280 } 1281 } 1282 callbacks.finishBroadcast(); 1283 } 1284 } 1285 1286 /** Process a change in the bonding state for a device */ handleBondStateChanged(BluetoothDevice device, int fromState, int toState)1287 public void handleBondStateChanged(BluetoothDevice device, int fromState, int toState) { 1288 mHandler.post(() -> bondStateChanged(device, toState)); 1289 } 1290 1291 /** 1292 * Remove state machine if the bonding for a device is removed 1293 * 1294 * @param device the device whose bonding state has changed 1295 * @param bondState the new bond state for the device. Possible values are: {@link 1296 * BluetoothDevice#BOND_NONE}, {@link BluetoothDevice#BOND_BONDING}, {@link 1297 * BluetoothDevice#BOND_BONDED}. 1298 */ 1299 @VisibleForTesting bondStateChanged(BluetoothDevice device, int bondState)1300 void bondStateChanged(BluetoothDevice device, int bondState) { 1301 Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); 1302 // Remove state machine if the bonding for a device is removed 1303 if (bondState != BluetoothDevice.BOND_NONE) { 1304 return; 1305 } 1306 1307 synchronized (mStateMachines) { 1308 VolumeControlStateMachine sm = mStateMachines.get(device); 1309 if (sm == null) { 1310 return; 1311 } 1312 if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) { 1313 Log.i(TAG, "Disconnecting device because it was unbonded."); 1314 disconnect(device); 1315 return; 1316 } 1317 removeStateMachine(device); 1318 } 1319 } 1320 removeStateMachine(BluetoothDevice device)1321 private void removeStateMachine(BluetoothDevice device) { 1322 synchronized (mStateMachines) { 1323 VolumeControlStateMachine sm = mStateMachines.get(device); 1324 if (sm == null) { 1325 Log.w( 1326 TAG, 1327 "removeStateMachine: device " + device + " does not have a state machine"); 1328 return; 1329 } 1330 Log.i(TAG, "removeStateMachine: removing state machine for device: " + device); 1331 sm.doQuit(); 1332 sm.cleanup(); 1333 mStateMachines.remove(device); 1334 } 1335 } 1336 handleConnectionStateChanged(BluetoothDevice device, int fromState, int toState)1337 void handleConnectionStateChanged(BluetoothDevice device, int fromState, int toState) { 1338 mHandler.post(() -> connectionStateChanged(device, fromState, toState)); 1339 } 1340 1341 @VisibleForTesting connectionStateChanged(BluetoothDevice device, int fromState, int toState)1342 synchronized void connectionStateChanged(BluetoothDevice device, int fromState, int toState) { 1343 if (!isAvailable()) { 1344 Log.w(TAG, "connectionStateChanged: service is not available"); 1345 return; 1346 } 1347 1348 if ((device == null) || (fromState == toState)) { 1349 Log.e( 1350 TAG, 1351 "connectionStateChanged: unexpected invocation. device=" 1352 + device 1353 + " fromState=" 1354 + fromState 1355 + " toState=" 1356 + toState); 1357 return; 1358 } 1359 1360 // Check if the device is disconnected - if unbond, remove the state machine 1361 if (toState == BluetoothProfile.STATE_DISCONNECTED) { 1362 int bondState = mAdapterService.getBondState(device); 1363 if (bondState == BluetoothDevice.BOND_NONE) { 1364 Log.d(TAG, device + " is unbond. Remove state machine"); 1365 removeStateMachine(device); 1366 } 1367 } else if (toState == BluetoothProfile.STATE_CONNECTED) { 1368 // Restore the group volume if it was changed while the device was not yet connected. 1369 CsipSetCoordinatorService csipClient = mFactory.getCsipSetCoordinatorService(); 1370 if (csipClient != null) { 1371 Integer groupId = csipClient.getGroupId(device, BluetoothUuid.CAP); 1372 if (groupId != IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID) { 1373 Integer groupVolume = 1374 mGroupVolumeCache.getOrDefault( 1375 groupId, IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME); 1376 if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) { 1377 mVolumeControlNativeInterface.setVolume(device, groupVolume); 1378 } 1379 1380 Boolean groupMute = mGroupMuteCache.getOrDefault(groupId, false); 1381 if (groupMute) { 1382 mVolumeControlNativeInterface.mute(device); 1383 } else { 1384 mVolumeControlNativeInterface.unmute(device); 1385 } 1386 } 1387 } else { 1388 /* It could happen when Bluetooth is stopping while VC is getting 1389 * connection event 1390 */ 1391 Log.w(TAG, "CSIP is not available"); 1392 } 1393 } 1394 mAdapterService.handleProfileConnectionStateChange( 1395 BluetoothProfile.VOLUME_CONTROL, device, fromState, toState); 1396 } 1397 1398 /** Binder object: must be a static class or memory leak may occur */ 1399 @VisibleForTesting 1400 static class BluetoothVolumeControlBinder extends IBluetoothVolumeControl.Stub 1401 implements IProfileServiceBinder { 1402 @VisibleForTesting boolean mIsTesting = false; 1403 private VolumeControlService mService; 1404 1405 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getService(AttributionSource source)1406 private VolumeControlService getService(AttributionSource source) { 1407 if (mIsTesting) { 1408 return mService; 1409 } 1410 if (!Utils.checkServiceAvailable(mService, TAG) 1411 || !Utils.checkCallerIsSystemOrActiveOrManagedUser(mService, TAG) 1412 || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) { 1413 return null; 1414 } 1415 return mService; 1416 } 1417 BluetoothVolumeControlBinder(VolumeControlService svc)1418 BluetoothVolumeControlBinder(VolumeControlService svc) { 1419 mService = svc; 1420 } 1421 1422 @Override cleanup()1423 public void cleanup() { 1424 mService = null; 1425 } 1426 1427 @Override connect(BluetoothDevice device, AttributionSource source)1428 public boolean connect(BluetoothDevice device, AttributionSource source) { 1429 Objects.requireNonNull(device, "device cannot be null"); 1430 Objects.requireNonNull(source, "source cannot be null"); 1431 1432 VolumeControlService service = getService(source); 1433 if (service == null) { 1434 return false; 1435 } 1436 1437 return service.connect(device); 1438 } 1439 1440 @Override disconnect(BluetoothDevice device, AttributionSource source)1441 public boolean disconnect(BluetoothDevice device, AttributionSource source) { 1442 Objects.requireNonNull(device, "device cannot be null"); 1443 Objects.requireNonNull(source, "source cannot be null"); 1444 1445 VolumeControlService service = getService(source); 1446 if (service == null) { 1447 return false; 1448 } 1449 1450 return service.disconnect(device); 1451 } 1452 1453 @Override getConnectedDevices(AttributionSource source)1454 public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { 1455 Objects.requireNonNull(source, "source cannot be null"); 1456 1457 VolumeControlService service = getService(source); 1458 if (service == null) { 1459 return Collections.emptyList(); 1460 } 1461 1462 enforceBluetoothPrivilegedPermission(service); 1463 return service.getConnectedDevices(); 1464 } 1465 1466 @Override getDevicesMatchingConnectionStates( int[] states, AttributionSource source)1467 public List<BluetoothDevice> getDevicesMatchingConnectionStates( 1468 int[] states, AttributionSource source) { 1469 Objects.requireNonNull(source, "source cannot be null"); 1470 1471 VolumeControlService service = getService(source); 1472 if (service == null) { 1473 return Collections.emptyList(); 1474 } 1475 1476 return service.getDevicesMatchingConnectionStates(states); 1477 } 1478 1479 @Override getConnectionState(BluetoothDevice device, AttributionSource source)1480 public int getConnectionState(BluetoothDevice device, AttributionSource source) { 1481 Objects.requireNonNull(device, "device cannot be null"); 1482 Objects.requireNonNull(source, "source cannot be null"); 1483 1484 VolumeControlService service = getService(source); 1485 if (service == null) { 1486 return BluetoothProfile.STATE_DISCONNECTED; 1487 } 1488 1489 return service.getConnectionState(device); 1490 } 1491 1492 @Override setConnectionPolicy( BluetoothDevice device, int connectionPolicy, AttributionSource source)1493 public boolean setConnectionPolicy( 1494 BluetoothDevice device, int connectionPolicy, AttributionSource source) { 1495 Objects.requireNonNull(device, "device cannot be null"); 1496 Objects.requireNonNull(source, "source cannot be null"); 1497 1498 VolumeControlService service = getService(source); 1499 if (service == null) { 1500 return false; 1501 } 1502 1503 enforceBluetoothPrivilegedPermission(service); 1504 return service.setConnectionPolicy(device, connectionPolicy); 1505 } 1506 1507 @Override getConnectionPolicy(BluetoothDevice device, AttributionSource source)1508 public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { 1509 Objects.requireNonNull(device, "device cannot be null"); 1510 Objects.requireNonNull(source, "source cannot be null"); 1511 1512 VolumeControlService service = getService(source); 1513 if (service == null) { 1514 return BluetoothProfile.CONNECTION_POLICY_UNKNOWN; 1515 } 1516 1517 enforceBluetoothPrivilegedPermission(service); 1518 return service.getConnectionPolicy(device); 1519 } 1520 1521 @Override isVolumeOffsetAvailable(BluetoothDevice device, AttributionSource source)1522 public boolean isVolumeOffsetAvailable(BluetoothDevice device, AttributionSource source) { 1523 Objects.requireNonNull(device, "device cannot be null"); 1524 Objects.requireNonNull(source, "source cannot be null"); 1525 1526 VolumeControlService service = getService(source); 1527 if (service == null) { 1528 return false; 1529 } 1530 1531 enforceBluetoothPrivilegedPermission(service); 1532 return service.isVolumeOffsetAvailable(device); 1533 } 1534 1535 @Override getNumberOfVolumeOffsetInstances( BluetoothDevice device, AttributionSource source)1536 public int getNumberOfVolumeOffsetInstances( 1537 BluetoothDevice device, AttributionSource source) { 1538 Objects.requireNonNull(device, "device cannot be null"); 1539 Objects.requireNonNull(source, "source cannot be null"); 1540 1541 VolumeControlService service = getService(source); 1542 if (service == null) { 1543 return 0; 1544 } 1545 1546 enforceBluetoothPrivilegedPermission(service); 1547 return service.getNumberOfVolumeOffsetInstances(device); 1548 } 1549 1550 @Override setVolumeOffset( BluetoothDevice device, int instanceId, int volumeOffset, AttributionSource source)1551 public void setVolumeOffset( 1552 BluetoothDevice device, 1553 int instanceId, 1554 int volumeOffset, 1555 AttributionSource source) { 1556 Objects.requireNonNull(device, "device cannot be null"); 1557 Objects.requireNonNull(source, "source cannot be null"); 1558 1559 VolumeControlService service = getService(source); 1560 if (service == null) { 1561 return; 1562 } 1563 1564 enforceBluetoothPrivilegedPermission(service); 1565 service.setVolumeOffset(device, instanceId, volumeOffset); 1566 } 1567 1568 @Override setDeviceVolume( BluetoothDevice device, int volume, boolean isGroupOp, AttributionSource source)1569 public void setDeviceVolume( 1570 BluetoothDevice device, int volume, boolean isGroupOp, AttributionSource source) { 1571 Objects.requireNonNull(device, "device cannot be null"); 1572 Objects.requireNonNull(source, "source cannot be null"); 1573 1574 VolumeControlService service = getService(source); 1575 if (service == null) { 1576 return; 1577 } 1578 1579 enforceBluetoothPrivilegedPermission(service); 1580 service.setDeviceVolume(device, volume, isGroupOp); 1581 } 1582 1583 @Override setGroupVolume(int groupId, int volume, AttributionSource source)1584 public void setGroupVolume(int groupId, int volume, AttributionSource source) { 1585 Objects.requireNonNull(source, "source cannot be null"); 1586 1587 VolumeControlService service = getService(source); 1588 if (service == null) { 1589 return; 1590 } 1591 1592 service.setGroupVolume(groupId, volume); 1593 } 1594 1595 @Override getGroupVolume(int groupId, AttributionSource source)1596 public int getGroupVolume(int groupId, AttributionSource source) { 1597 Objects.requireNonNull(source, "source cannot be null"); 1598 1599 VolumeControlService service = getService(source); 1600 if (service == null) { 1601 return 0; 1602 } 1603 1604 return service.getGroupVolume(groupId); 1605 } 1606 1607 @Override setGroupActive(int groupId, boolean active, AttributionSource source)1608 public void setGroupActive(int groupId, boolean active, AttributionSource source) { 1609 Objects.requireNonNull(source, "source cannot be null"); 1610 1611 VolumeControlService service = getService(source); 1612 if (service == null) { 1613 return; 1614 } 1615 1616 service.setGroupActive(groupId, active); 1617 } 1618 1619 @Override mute(BluetoothDevice device, AttributionSource source)1620 public void mute(BluetoothDevice device, AttributionSource source) { 1621 Objects.requireNonNull(device, "device cannot be null"); 1622 Objects.requireNonNull(source, "source cannot be null"); 1623 1624 VolumeControlService service = getService(source); 1625 if (service == null) { 1626 return; 1627 } 1628 1629 service.mute(device); 1630 } 1631 1632 @Override muteGroup(int groupId, AttributionSource source)1633 public void muteGroup(int groupId, AttributionSource source) { 1634 Objects.requireNonNull(source, "source cannot be null"); 1635 1636 VolumeControlService service = getService(source); 1637 if (service == null) { 1638 return; 1639 } 1640 1641 service.muteGroup(groupId); 1642 } 1643 1644 @Override unmute(BluetoothDevice device, AttributionSource source)1645 public void unmute(BluetoothDevice device, AttributionSource source) { 1646 Objects.requireNonNull(device, "device cannot be null"); 1647 Objects.requireNonNull(source, "source cannot be null"); 1648 1649 VolumeControlService service = getService(source); 1650 if (service == null) { 1651 return; 1652 } 1653 1654 service.unmute(device); 1655 } 1656 1657 @Override unmuteGroup(int groupId, AttributionSource source)1658 public void unmuteGroup(int groupId, AttributionSource source) { 1659 Objects.requireNonNull(source, "source cannot be null"); 1660 1661 VolumeControlService service = getService(source); 1662 if (service == null) { 1663 return; 1664 } 1665 1666 service.unmuteGroup(groupId); 1667 } 1668 postAndWait(Handler handler, Runnable runnable)1669 private void postAndWait(Handler handler, Runnable runnable) { 1670 FutureTask<Void> task = new FutureTask(Executors.callable(runnable)); 1671 1672 handler.post(task); 1673 try { 1674 task.get(1, TimeUnit.SECONDS); 1675 } catch (TimeoutException | InterruptedException e) { 1676 SneakyThrow.sneakyThrow(e); 1677 } catch (ExecutionException e) { 1678 SneakyThrow.sneakyThrow(e.getCause()); 1679 } 1680 } 1681 1682 @Override registerCallback( IBluetoothVolumeControlCallback callback, AttributionSource source)1683 public void registerCallback( 1684 IBluetoothVolumeControlCallback callback, AttributionSource source) { 1685 Objects.requireNonNull(callback, "callback cannot be null"); 1686 Objects.requireNonNull(source, "source cannot be null"); 1687 1688 VolumeControlService service = getService(source); 1689 if (service == null) { 1690 return; 1691 } 1692 1693 enforceBluetoothPrivilegedPermission(service); 1694 postAndWait(service.mHandler, () -> service.registerCallback(callback)); 1695 } 1696 1697 @Override notifyNewRegisteredCallback( IBluetoothVolumeControlCallback callback, AttributionSource source)1698 public void notifyNewRegisteredCallback( 1699 IBluetoothVolumeControlCallback callback, AttributionSource source) { 1700 Objects.requireNonNull(callback, "callback cannot be null"); 1701 Objects.requireNonNull(source, "source cannot be null"); 1702 1703 VolumeControlService service = getService(source); 1704 if (service == null) { 1705 return; 1706 } 1707 1708 enforceBluetoothPrivilegedPermission(service); 1709 postAndWait(service.mHandler, () -> service.notifyNewRegisteredCallback(callback)); 1710 } 1711 1712 @Override unregisterCallback( IBluetoothVolumeControlCallback callback, AttributionSource source)1713 public void unregisterCallback( 1714 IBluetoothVolumeControlCallback callback, AttributionSource source) { 1715 Objects.requireNonNull(callback, "callback cannot be null"); 1716 Objects.requireNonNull(source, "source cannot be null"); 1717 1718 VolumeControlService service = getService(source); 1719 if (service == null) { 1720 return; 1721 } 1722 1723 enforceBluetoothPrivilegedPermission(service); 1724 postAndWait(service.mHandler, () -> service.mCallbacks.unregister(callback)); 1725 } 1726 } 1727 1728 @Override dump(StringBuilder sb)1729 public void dump(StringBuilder sb) { 1730 super.dump(sb); 1731 for (VolumeControlStateMachine sm : mStateMachines.values()) { 1732 sm.dump(sb); 1733 } 1734 1735 for (Map.Entry<BluetoothDevice, VolumeControlOffsetDescriptor> entry : 1736 mAudioOffsets.entrySet()) { 1737 VolumeControlOffsetDescriptor descriptor = entry.getValue(); 1738 BluetoothDevice device = entry.getKey(); 1739 ProfileService.println(sb, " Device: " + device); 1740 ProfileService.println(sb, " Volume offset cnt: " + descriptor.size()); 1741 descriptor.dump(sb); 1742 } 1743 for (Map.Entry<Integer, Integer> entry : mGroupVolumeCache.entrySet()) { 1744 Boolean isMute = mGroupMuteCache.getOrDefault(entry.getKey(), false); 1745 ProfileService.println( 1746 sb, 1747 " GroupId: " 1748 + entry.getKey() 1749 + " volume: " 1750 + entry.getValue() 1751 + ", mute: " 1752 + isMute); 1753 } 1754 } 1755 } 1756