1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settingslib.bluetooth;
18 
19 import android.bluetooth.BluetoothCsipSetCoordinator;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothProfile;
22 import android.bluetooth.BluetoothUuid;
23 import android.os.Build;
24 import android.os.ParcelUuid;
25 import android.util.Log;
26 
27 import androidx.annotation.ChecksSdkIntAtLeast;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import java.util.ArrayList;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.stream.Collectors;
37 
38 /**
39  * CsipDeviceManager manages the set of remote CSIP Bluetooth devices.
40  */
41 public class CsipDeviceManager {
42     private static final String TAG = "CsipDeviceManager";
43     private static final boolean DEBUG = BluetoothUtils.D;
44 
45     private final LocalBluetoothManager mBtManager;
46     private final List<CachedBluetoothDevice> mCachedDevices;
47 
CsipDeviceManager(LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> cachedDevices)48     CsipDeviceManager(LocalBluetoothManager localBtManager,
49             List<CachedBluetoothDevice> cachedDevices) {
50         mBtManager = localBtManager;
51         mCachedDevices = cachedDevices;
52     };
53 
initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice)54     void initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice) {
55         // Current it only supports the base uuid for CSIP and group this set in UI.
56         final int groupId = getBaseGroupId(newDevice.getDevice());
57         if (isValidGroupId(groupId)) {
58             log("initCsipDeviceIfNeeded: " + newDevice + " (group: " + groupId + ")");
59             // Once groupId is valid, assign groupId
60             newDevice.setGroupId(groupId);
61         }
62     }
63 
getBaseGroupId(BluetoothDevice device)64     private int getBaseGroupId(BluetoothDevice device) {
65         final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
66         final CsipSetCoordinatorProfile profileProxy = profileManager
67                 .getCsipSetCoordinatorProfile();
68         if (profileProxy != null) {
69             final Map<Integer, ParcelUuid> groupIdMap = profileProxy
70                     .getGroupUuidMapByDevice(device);
71             if (groupIdMap == null) {
72                 return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
73             }
74 
75             for (Map.Entry<Integer, ParcelUuid> entry : groupIdMap.entrySet()) {
76                 if (entry.getValue().equals(BluetoothUuid.CAP)) {
77                     return entry.getKey();
78                 }
79             }
80         }
81         return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
82     }
83 
setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice)84     boolean setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice) {
85         final int groupId = newDevice.getGroupId();
86         if (isValidGroupId(groupId)) {
87             final CachedBluetoothDevice mainDevice = getCachedDevice(groupId);
88             log("setMemberDeviceIfNeeded, main: " + mainDevice + ", member: " + newDevice);
89             // Just add one of the coordinated set from a pair in the list that is shown in the UI.
90             // Once there is other devices with the same groupId, to add new device as member
91             // devices.
92             if (mainDevice != null) {
93                 mainDevice.addMemberDevice(newDevice);
94                 newDevice.setName(mainDevice.getName());
95                 return true;
96             }
97         }
98         return false;
99     }
100 
isValidGroupId(int groupId)101     private boolean isValidGroupId(int groupId) {
102         return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
103     }
104 
105     /**
106      * To find the device with {@code groupId}.
107      *
108      * @param groupId The group id
109      * @return if we could find a device with this {@code groupId} return this device. Otherwise,
110      * return null.
111      */
getCachedDevice(int groupId)112     public CachedBluetoothDevice getCachedDevice(int groupId) {
113         log("getCachedDevice: groupId: " + groupId);
114         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
115             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
116             if (cachedDevice.getGroupId() == groupId) {
117                 log("getCachedDevice: found cachedDevice with the groupId: "
118                         + cachedDevice.getDevice().getAnonymizedAddress());
119                 return cachedDevice;
120             }
121         }
122         return null;
123     }
124 
125     // To collect all set member devices and call #onGroupIdChanged to group device by GroupId
updateCsipDevices()126     void updateCsipDevices() {
127         final Set<Integer> newGroupIdSet = new HashSet<Integer>();
128         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
129             // Do nothing if GroupId has been assigned
130             if (!isValidGroupId(cachedDevice.getGroupId())) {
131                 final int newGroupId = getBaseGroupId(cachedDevice.getDevice());
132                 // Do nothing if there is no GroupId on Bluetooth device
133                 if (isValidGroupId(newGroupId)) {
134                     cachedDevice.setGroupId(newGroupId);
135                     newGroupIdSet.add(newGroupId);
136                 }
137             }
138         }
139         for (int groupId : newGroupIdSet) {
140             onGroupIdChanged(groupId);
141         }
142     }
143 
144     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
isAtLeastT()145     private static boolean isAtLeastT() {
146         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
147     }
148 
149     // Group devices by groupId
150     @VisibleForTesting
onGroupIdChanged(int groupId)151     void onGroupIdChanged(int groupId) {
152         if (!isValidGroupId(groupId)) {
153             log("onGroupIdChanged: groupId is invalid");
154             return;
155         }
156         updateRelationshipOfGroupDevices(groupId);
157     }
158 
159     // @return {@code true}, the event is processed inside the method. It is for updating
160     // le audio device on group relationship when receiving connected or disconnected.
161     // @return {@code false}, it is not le audio device or to process it same as other profiles
onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state)162     boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
163             int state) {
164         log("onProfileConnectionStateChangedIfProcessed: " + cachedDevice + ", state: " + state);
165 
166         if (state != BluetoothProfile.STATE_CONNECTED
167                 && state != BluetoothProfile.STATE_DISCONNECTED) {
168             return false;
169         }
170         return updateRelationshipOfGroupDevices(cachedDevice.getGroupId());
171     }
172 
173     @VisibleForTesting
updateRelationshipOfGroupDevices(int groupId)174     boolean updateRelationshipOfGroupDevices(int groupId) {
175         if (!isValidGroupId(groupId)) {
176             log("The device is not group.");
177             return false;
178         }
179         log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString());
180 
181         // Get the preferred main device by getPreferredMainDeviceWithoutConectionState
182         List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId);
183         CachedBluetoothDevice preferredMainDevice =
184                 getPreferredMainDevice(groupId, groupDevicesList);
185         log("The preferredMainDevice= " + preferredMainDevice
186                 + " and the groupDevicesList of groupId= " + groupId
187                 + " =" + groupDevicesList);
188         return addMemberDevicesIntoMainDevice(groupId, preferredMainDevice);
189     }
190 
findMainDevice(CachedBluetoothDevice device)191     CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
192         if (device == null || mCachedDevices == null) {
193             return null;
194         }
195 
196         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
197             if (isValidGroupId(cachedDevice.getGroupId())) {
198                 Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
199                 if (memberSet.isEmpty()) {
200                     continue;
201                 }
202 
203                 for (CachedBluetoothDevice memberDevice : memberSet) {
204                     if (memberDevice != null && memberDevice.equals(device)) {
205                         return cachedDevice;
206                     }
207                 }
208             }
209         }
210         return null;
211     }
212 
213     /**
214      * Check if the {@code groupId} is existed.
215      *
216      * @param groupId The group id
217      * @return {@code true}, if we could find a device with this {@code groupId}; Otherwise,
218      * return {@code false}.
219      */
isExistedGroupId(int groupId)220     public boolean isExistedGroupId(int groupId) {
221         if (getCachedDevice(groupId) != null) {
222             return true;
223         }
224 
225         return false;
226     }
227 
228     @VisibleForTesting
getGroupDevicesFromAllOfDevicesList(int groupId)229     List<CachedBluetoothDevice> getGroupDevicesFromAllOfDevicesList(int groupId) {
230         List<CachedBluetoothDevice> groupDevicesList = new ArrayList<>();
231         if (!isValidGroupId(groupId)) {
232             return groupDevicesList;
233         }
234         for (CachedBluetoothDevice item : mCachedDevices) {
235             if (groupId != item.getGroupId()) {
236                 continue;
237             }
238             groupDevicesList.add(item);
239             groupDevicesList.addAll(item.getMemberDevice());
240         }
241         return groupDevicesList;
242     }
243 
getFirstMemberDevice(int groupId)244     public CachedBluetoothDevice getFirstMemberDevice(int groupId) {
245         List<CachedBluetoothDevice> members = getGroupDevicesFromAllOfDevicesList(groupId);
246         if (members.isEmpty())
247             return null;
248 
249         CachedBluetoothDevice firstMember = members.get(0);
250         log("getFirstMemberDevice: groupId=" + groupId
251                 + " address=" + firstMember.getDevice().getAnonymizedAddress());
252         return firstMember;
253     }
254 
255     @VisibleForTesting
getPreferredMainDevice(int groupId, List<CachedBluetoothDevice> groupDevicesList)256     CachedBluetoothDevice getPreferredMainDevice(int groupId,
257             List<CachedBluetoothDevice> groupDevicesList) {
258         // How to select the preferred main device?
259         // 1. The DUAL mode connected device which has A2DP/HFP and LE audio.
260         // 2. One of connected LE device in the list. Default is the lead device from LE profile.
261         // 3. If there is no connected device, then reset the relationship. Set the DUAL mode
262         // deviced as the main device. Otherwise, set any one of the device.
263         if (groupDevicesList == null || groupDevicesList.isEmpty()) {
264             return null;
265         }
266 
267         CachedBluetoothDevice dualModeDevice = groupDevicesList.stream()
268                 .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
269                         .anyMatch(profile -> profile instanceof LeAudioProfile))
270                 .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
271                         .anyMatch(profile -> profile instanceof A2dpProfile
272                                 || profile instanceof HeadsetProfile))
273                 .findFirst().orElse(null);
274         if (dualModeDevice != null && dualModeDevice.isConnected()) {
275             log("getPreferredMainDevice: The connected DUAL mode device");
276             return dualModeDevice;
277         }
278 
279         final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
280         final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
281         final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
282         final BluetoothDevice leAudioLeadDevice = (leAudioProfile != null && isAtLeastT())
283                 ? leAudioProfile.getConnectedGroupLeadDevice(groupId) : null;
284 
285         if (leAudioLeadDevice != null) {
286             log("getPreferredMainDevice: The LeadDevice from LE profile is "
287                     + leAudioLeadDevice.getAnonymizedAddress());
288         }
289         CachedBluetoothDevice leAudioLeadCachedDevice =
290                 leAudioLeadDevice != null ? deviceManager.findDevice(leAudioLeadDevice) : null;
291         if (leAudioLeadCachedDevice == null) {
292             log("getPreferredMainDevice: The LeadDevice is not in the all of devices list");
293         } else if (leAudioLeadCachedDevice.isConnected()) {
294             log("getPreferredMainDevice: The connected LeadDevice from LE profile");
295             return leAudioLeadCachedDevice;
296         }
297         CachedBluetoothDevice oneOfConnectedDevices = groupDevicesList.stream()
298                 .filter(cachedDevice -> cachedDevice.isConnected())
299                 .findFirst().orElse(null);
300         if (oneOfConnectedDevices != null) {
301             log("getPreferredMainDevice: One of the connected devices.");
302             return oneOfConnectedDevices;
303         }
304 
305         if (dualModeDevice != null) {
306             log("getPreferredMainDevice: The DUAL mode device.");
307             return dualModeDevice;
308         }
309         // last
310         if (!groupDevicesList.isEmpty()) {
311             log("getPreferredMainDevice: One of the group devices.");
312             return groupDevicesList.get(0);
313         }
314         return null;
315     }
316 
317     @VisibleForTesting
addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice)318     boolean addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice) {
319         boolean hasChanged = false;
320         if (preferredMainDevice == null) {
321             log("addMemberDevicesIntoMainDevice: No main device. Do nothing.");
322             return hasChanged;
323         }
324 
325         // If the current main device is not preferred main device, then set it as new main device.
326         // Otherwise, do nothing.
327         BluetoothDevice bluetoothDeviceOfPreferredMainDevice = preferredMainDevice.getDevice();
328         CachedBluetoothDevice mainDeviceOfPreferredMainDevice = findMainDevice(preferredMainDevice);
329         boolean hasPreferredMainDeviceAlreadyBeenMainDevice =
330                 mainDeviceOfPreferredMainDevice == null;
331 
332         if (!hasPreferredMainDeviceAlreadyBeenMainDevice) {
333             // preferredMainDevice has not been the main device.
334             // switch relationship between the mainDeviceOfPreferredMainDevice and
335             // PreferredMainDevice
336 
337             log("addMemberDevicesIntoMainDevice: The PreferredMainDevice have the mainDevice. "
338                     + "Do switch relationship between the mainDeviceOfPreferredMainDevice and "
339                     + "PreferredMainDevice");
340             // To switch content and dispatch to notify UI change
341             mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfPreferredMainDevice);
342             mainDeviceOfPreferredMainDevice.switchMemberDeviceContent(preferredMainDevice);
343             mainDeviceOfPreferredMainDevice.refresh();
344             // It is necessary to do remove and add for updating the mapping on
345             // preference and device
346             mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfPreferredMainDevice);
347             hasChanged = true;
348         }
349 
350         // If the mCachedDevices List at CachedBluetoothDeviceManager has multiple items which are
351         // the same groupId, then combine them and also keep the preferred main device as main
352         // device.
353         List<CachedBluetoothDevice> topLevelOfGroupDevicesList = mCachedDevices.stream()
354                 .filter(device -> device.getGroupId() == groupId)
355                 .collect(Collectors.toList());
356         boolean haveMultiMainDevicesInAllOfDevicesList = topLevelOfGroupDevicesList.size() > 1;
357         // Update the new main of CachedBluetoothDevice, since it may be changed in above step.
358         final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
359         preferredMainDevice = deviceManager.findDevice(bluetoothDeviceOfPreferredMainDevice);
360         if (haveMultiMainDevicesInAllOfDevicesList) {
361             // put another devices into main device.
362             for (CachedBluetoothDevice deviceItem : topLevelOfGroupDevicesList) {
363                 if (deviceItem.getDevice() == null || deviceItem.getDevice().equals(
364                         bluetoothDeviceOfPreferredMainDevice)) {
365                     continue;
366                 }
367 
368                 Set<CachedBluetoothDevice> memberSet = deviceItem.getMemberDevice();
369                 for (CachedBluetoothDevice memberSetItem : memberSet) {
370                     if (!memberSetItem.equals(preferredMainDevice)) {
371                         preferredMainDevice.addMemberDevice(memberSetItem);
372                     }
373                 }
374                 memberSet.clear();
375                 preferredMainDevice.addMemberDevice(deviceItem);
376                 mCachedDevices.remove(deviceItem);
377                 mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem);
378                 hasChanged = true;
379             }
380         }
381         if (hasChanged) {
382             log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
383                     + mCachedDevices);
384         }
385         return hasChanged;
386     }
387 
log(String msg)388     private void log(String msg) {
389         if (DEBUG) {
390             Log.d(TAG, msg);
391         }
392     }
393 }
394