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         return getCachedDevice(groupId) != null;
222     }
223 
224     @VisibleForTesting
getGroupDevicesFromAllOfDevicesList(int groupId)225     List<CachedBluetoothDevice> getGroupDevicesFromAllOfDevicesList(int groupId) {
226         List<CachedBluetoothDevice> groupDevicesList = new ArrayList<>();
227         if (!isValidGroupId(groupId)) {
228             return groupDevicesList;
229         }
230         for (CachedBluetoothDevice item : mCachedDevices) {
231             if (groupId != item.getGroupId()) {
232                 continue;
233             }
234             groupDevicesList.add(item);
235             groupDevicesList.addAll(item.getMemberDevice());
236         }
237         return groupDevicesList;
238     }
239 
getFirstMemberDevice(int groupId)240     public CachedBluetoothDevice getFirstMemberDevice(int groupId) {
241         List<CachedBluetoothDevice> members = getGroupDevicesFromAllOfDevicesList(groupId);
242         if (members.isEmpty())
243             return null;
244 
245         CachedBluetoothDevice firstMember = members.get(0);
246         log("getFirstMemberDevice: groupId=" + groupId
247                 + " address=" + firstMember.getDevice().getAnonymizedAddress());
248         return firstMember;
249     }
250 
251     @VisibleForTesting
getPreferredMainDevice(int groupId, List<CachedBluetoothDevice> groupDevicesList)252     CachedBluetoothDevice getPreferredMainDevice(int groupId,
253             List<CachedBluetoothDevice> groupDevicesList) {
254         // How to select the preferred main device?
255         // 1. The DUAL mode connected device which has A2DP/HFP and LE audio.
256         // 2. One of connected LE device in the list. Default is the lead device from LE profile.
257         // 3. If there is no connected device, then reset the relationship. Set the DUAL mode
258         // deviced as the main device. Otherwise, set any one of the device.
259         if (groupDevicesList == null || groupDevicesList.isEmpty()) {
260             return null;
261         }
262 
263         CachedBluetoothDevice dualModeDevice = groupDevicesList.stream()
264                 .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
265                         .anyMatch(profile -> profile instanceof LeAudioProfile))
266                 .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
267                         .anyMatch(profile -> profile instanceof A2dpProfile
268                                 || profile instanceof HeadsetProfile))
269                 .findFirst().orElse(null);
270         if (isDeviceConnected(dualModeDevice)) {
271             log("getPreferredMainDevice: The connected DUAL mode device");
272             return dualModeDevice;
273         }
274 
275         final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
276         final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
277         final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
278         final BluetoothDevice leAudioLeadDevice = (leAudioProfile != null && isAtLeastT())
279                 ? leAudioProfile.getConnectedGroupLeadDevice(groupId) : null;
280 
281         if (leAudioLeadDevice != null) {
282             log("getPreferredMainDevice: The LeadDevice from LE profile is "
283                     + leAudioLeadDevice.getAnonymizedAddress());
284         }
285         CachedBluetoothDevice leAudioLeadCachedDevice =
286                 leAudioLeadDevice != null ? deviceManager.findDevice(leAudioLeadDevice) : null;
287         if (leAudioLeadCachedDevice == null) {
288             log("getPreferredMainDevice: The LeadDevice is not in the all of devices list");
289         } else if (isDeviceConnected(leAudioLeadCachedDevice)) {
290             log("getPreferredMainDevice: The connected LeadDevice from LE profile");
291             return leAudioLeadCachedDevice;
292         }
293         CachedBluetoothDevice oneOfConnectedDevices =
294                 groupDevicesList.stream()
295                         .filter(cachedDevice -> isDeviceConnected(cachedDevice))
296                         .findFirst()
297                         .orElse(null);
298         if (oneOfConnectedDevices != null) {
299             log("getPreferredMainDevice: One of the connected devices.");
300             return oneOfConnectedDevices;
301         }
302 
303         if (dualModeDevice != null) {
304             log("getPreferredMainDevice: The DUAL mode device.");
305             return dualModeDevice;
306         }
307         // last
308         if (!groupDevicesList.isEmpty()) {
309             log("getPreferredMainDevice: One of the group devices.");
310             return groupDevicesList.get(0);
311         }
312         return null;
313     }
314 
315     @VisibleForTesting
addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice)316     boolean addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice) {
317         boolean hasChanged = false;
318         if (preferredMainDevice == null) {
319             log("addMemberDevicesIntoMainDevice: No main device. Do nothing.");
320             return hasChanged;
321         }
322 
323         // If the current main device is not preferred main device, then set it as new main device.
324         // Otherwise, do nothing.
325         BluetoothDevice bluetoothDeviceOfPreferredMainDevice = preferredMainDevice.getDevice();
326         CachedBluetoothDevice mainDeviceOfPreferredMainDevice = findMainDevice(preferredMainDevice);
327         boolean hasPreferredMainDeviceAlreadyBeenMainDevice =
328                 mainDeviceOfPreferredMainDevice == null;
329 
330         if (!hasPreferredMainDeviceAlreadyBeenMainDevice) {
331             // preferredMainDevice has not been the main device.
332             // switch relationship between the mainDeviceOfPreferredMainDevice and
333             // PreferredMainDevice
334 
335             log("addMemberDevicesIntoMainDevice: The PreferredMainDevice have the mainDevice. "
336                     + "Do switch relationship between the mainDeviceOfPreferredMainDevice and "
337                     + "PreferredMainDevice");
338             // To switch content and dispatch to notify UI change
339             mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfPreferredMainDevice);
340             mainDeviceOfPreferredMainDevice.switchMemberDeviceContent(preferredMainDevice);
341             mainDeviceOfPreferredMainDevice.refresh();
342             // It is necessary to do remove and add for updating the mapping on
343             // preference and device
344             mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfPreferredMainDevice);
345             hasChanged = true;
346         }
347 
348         // If the mCachedDevices List at CachedBluetoothDeviceManager has multiple items which are
349         // the same groupId, then combine them and also keep the preferred main device as main
350         // device.
351         List<CachedBluetoothDevice> topLevelOfGroupDevicesList = mCachedDevices.stream()
352                 .filter(device -> device.getGroupId() == groupId)
353                 .collect(Collectors.toList());
354         boolean haveMultiMainDevicesInAllOfDevicesList = topLevelOfGroupDevicesList.size() > 1;
355         // Update the new main of CachedBluetoothDevice, since it may be changed in above step.
356         final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
357         preferredMainDevice = deviceManager.findDevice(bluetoothDeviceOfPreferredMainDevice);
358         if (haveMultiMainDevicesInAllOfDevicesList) {
359             // put another devices into main device.
360             for (CachedBluetoothDevice deviceItem : topLevelOfGroupDevicesList) {
361                 if (deviceItem.getDevice() == null || deviceItem.getDevice().equals(
362                         bluetoothDeviceOfPreferredMainDevice)) {
363                     continue;
364                 }
365 
366                 Set<CachedBluetoothDevice> memberSet = deviceItem.getMemberDevice();
367                 for (CachedBluetoothDevice memberSetItem : memberSet) {
368                     if (!memberSetItem.equals(preferredMainDevice)) {
369                         preferredMainDevice.addMemberDevice(memberSetItem);
370                     }
371                 }
372                 memberSet.clear();
373                 preferredMainDevice.addMemberDevice(deviceItem);
374                 mCachedDevices.remove(deviceItem);
375                 mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem);
376                 hasChanged = true;
377             }
378         }
379         if (hasChanged) {
380             log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
381                     + mCachedDevices);
382         }
383         return hasChanged;
384     }
385 
log(String msg)386     private void log(String msg) {
387         if (DEBUG) {
388             Log.d(TAG, msg);
389         }
390     }
391 
isDeviceConnected(CachedBluetoothDevice cachedDevice)392     private boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
393         if (cachedDevice == null) {
394             return false;
395         }
396         final BluetoothDevice device = cachedDevice.getDevice();
397         return cachedDevice.isConnected()
398                 && device.getBondState() == BluetoothDevice.BOND_BONDED
399                 && device.isConnected();
400     }
401 }
402