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