• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.HashSet;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.stream.Collectors;
36 
37 /**
38  * CsipDeviceManager manages the set of remote CSIP Bluetooth devices.
39  */
40 public class CsipDeviceManager {
41     private static final String TAG = "CsipDeviceManager";
42     private static final boolean DEBUG = BluetoothUtils.D;
43 
44     private final LocalBluetoothManager mBtManager;
45     private final List<CachedBluetoothDevice> mCachedDevices;
46 
CsipDeviceManager(LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> cachedDevices)47     CsipDeviceManager(LocalBluetoothManager localBtManager,
48             List<CachedBluetoothDevice> cachedDevices) {
49         mBtManager = localBtManager;
50         mCachedDevices = cachedDevices;
51     };
52 
initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice)53     void initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice) {
54         // Current it only supports the base uuid for CSIP and group this set in UI.
55         final int groupId = getBaseGroupId(newDevice.getDevice());
56         if (isValidGroupId(groupId)) {
57             log("initCsipDeviceIfNeeded: " + newDevice + " (group: " + groupId + ")");
58             // Once groupId is valid, assign groupId
59             newDevice.setGroupId(groupId);
60         }
61     }
62 
getBaseGroupId(BluetoothDevice device)63     private int getBaseGroupId(BluetoothDevice device) {
64         final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
65         final CsipSetCoordinatorProfile profileProxy = profileManager
66                 .getCsipSetCoordinatorProfile();
67         if (profileProxy != null) {
68             final Map<Integer, ParcelUuid> groupIdMap = profileProxy
69                     .getGroupUuidMapByDevice(device);
70             if (groupIdMap == null) {
71                 return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
72             }
73 
74             for (Map.Entry<Integer, ParcelUuid> entry: groupIdMap.entrySet()) {
75                 if (entry.getValue().equals(BluetoothUuid.CAP)) {
76                     return entry.getKey();
77                 }
78             }
79         }
80         return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
81     }
82 
setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice)83     boolean setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice) {
84         final int groupId = newDevice.getGroupId();
85         if (isValidGroupId(groupId)) {
86             final CachedBluetoothDevice CsipDevice = getCachedDevice(groupId);
87             log("setMemberDeviceIfNeeded, main: " + CsipDevice + ", member: " + newDevice);
88             // Just add one of the coordinated set from a pair in the list that is shown in the UI.
89             // Once there is other devices with the same groupId, to add new device as member
90             // devices.
91             if (CsipDevice != null) {
92                 CsipDevice.addMemberDevice(newDevice);
93                 newDevice.setName(CsipDevice.getName());
94                 return true;
95             }
96         }
97         return false;
98     }
99 
isValidGroupId(int groupId)100     private boolean isValidGroupId(int groupId) {
101         return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
102     }
103 
104     /**
105      * To find the device with {@code groupId}.
106      *
107      * @param groupId The group id
108      * @return if we could find a device with this {@code groupId} return this device. Otherwise,
109      * return null.
110      */
getCachedDevice(int groupId)111     public CachedBluetoothDevice getCachedDevice(int groupId) {
112         log("getCachedDevice: groupId: " + groupId);
113         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
114             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
115             if (cachedDevice.getGroupId() == groupId) {
116                 log("getCachedDevice: found cachedDevice with the groupId: "
117                         + cachedDevice.getDevice().getAnonymizedAddress());
118                 return cachedDevice;
119             }
120         }
121         return null;
122     }
123 
124     // To collect all set member devices and call #onGroupIdChanged to group device by GroupId
updateCsipDevices()125     void updateCsipDevices() {
126         final Set<Integer> newGroupIdSet = new HashSet<Integer>();
127         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
128             // Do nothing if GroupId has been assigned
129             if (!isValidGroupId(cachedDevice.getGroupId())) {
130                 final int newGroupId = getBaseGroupId(cachedDevice.getDevice());
131                 // Do nothing if there is no GroupId on Bluetooth device
132                 if (isValidGroupId(newGroupId)) {
133                     cachedDevice.setGroupId(newGroupId);
134                     newGroupIdSet.add(newGroupId);
135                 }
136             }
137         }
138         for (int groupId : newGroupIdSet) {
139             onGroupIdChanged(groupId);
140         }
141     }
142 
143     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
isAtLeastT()144     private static boolean isAtLeastT() {
145         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
146     }
147 
148     // Group devices by groupId
149     @VisibleForTesting
onGroupIdChanged(int groupId)150     void onGroupIdChanged(int groupId) {
151         if (!isValidGroupId(groupId)) {
152             log("onGroupIdChanged: groupId is invalid");
153             return;
154         }
155         log("onGroupIdChanged: mCachedDevices list =" + mCachedDevices.toString());
156         final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
157         final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
158         final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
159         final BluetoothDevice mainBluetoothDevice = (leAudioProfile != null && isAtLeastT()) ?
160                 leAudioProfile.getConnectedGroupLeadDevice(groupId) : null;
161         CachedBluetoothDevice newMainDevice =
162                 mainBluetoothDevice != null ? deviceManager.findDevice(mainBluetoothDevice) : null;
163         if (newMainDevice != null) {
164             final CachedBluetoothDevice finalNewMainDevice = newMainDevice;
165             final List<CachedBluetoothDevice> memberDevices = mCachedDevices.stream()
166                     .filter(cachedDevice -> !cachedDevice.equals(finalNewMainDevice)
167                             && cachedDevice.getGroupId() == groupId)
168                     .collect(Collectors.toList());
169             if (memberDevices == null || memberDevices.isEmpty()) {
170                 log("onGroupIdChanged: There is no member device in list.");
171                 return;
172             }
173             log("onGroupIdChanged: removed from UI device =" + memberDevices
174                     + ", with groupId=" + groupId + " mainDevice= " + newMainDevice);
175             for (CachedBluetoothDevice memberDeviceItem : memberDevices) {
176                 Set<CachedBluetoothDevice> memberSet = memberDeviceItem.getMemberDevice();
177                 if (!memberSet.isEmpty()) {
178                     log("onGroupIdChanged: Transfer the member list into new main device.");
179                     for (CachedBluetoothDevice memberListItem : memberSet) {
180                         if (!memberListItem.equals(newMainDevice)) {
181                             newMainDevice.addMemberDevice(memberListItem);
182                         }
183                     }
184                     memberSet.clear();
185                 }
186 
187                 newMainDevice.addMemberDevice(memberDeviceItem);
188                 mCachedDevices.remove(memberDeviceItem);
189                 mBtManager.getEventManager().dispatchDeviceRemoved(memberDeviceItem);
190             }
191 
192             if (!mCachedDevices.contains(newMainDevice)) {
193                 mCachedDevices.add(newMainDevice);
194                 mBtManager.getEventManager().dispatchDeviceAdded(newMainDevice);
195             }
196         } else {
197             log("onGroupIdChanged: There is no main device from the LE profile.");
198             int firstMatchedIndex = -1;
199 
200             for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
201                 final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
202                 if (cachedDevice.getGroupId() != groupId) {
203                     continue;
204                 }
205 
206                 if (firstMatchedIndex == -1) {
207                     // Found the first one
208                     firstMatchedIndex = i;
209                     newMainDevice = cachedDevice;
210                     continue;
211                 }
212 
213                 log("onGroupIdChanged: removed from UI device =" + cachedDevice
214                         + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex);
215 
216                 newMainDevice.addMemberDevice(cachedDevice);
217                 mCachedDevices.remove(i);
218                 mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
219                 break;
220             }
221         }
222     }
223 
224     // @return {@code true}, the event is processed inside the method. It is for updating
225     // le audio device on group relationship when receiving connected or disconnected.
226     // @return {@code false}, it is not le audio device or to process it same as other profiles
onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state)227     boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
228             int state) {
229         log("onProfileConnectionStateChangedIfProcessed: " + cachedDevice + ", state: " + state);
230         switch (state) {
231             case BluetoothProfile.STATE_CONNECTED:
232                 onGroupIdChanged(cachedDevice.getGroupId());
233                 CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice);
234                 if (mainDevice != null) {
235                     if (mainDevice.isConnected()) {
236                         // When main device exists and in connected state, receiving member device
237                         // connection. To refresh main device UI
238                         mainDevice.refresh();
239                         return true;
240                     } else {
241                         // When both LE Audio devices are disconnected, receiving member device
242                         // connection. To switch content and dispatch to notify UI change
243                         mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
244                         mainDevice.switchMemberDeviceContent(cachedDevice);
245                         mainDevice.refresh();
246                         // It is necessary to do remove and add for updating the mapping on
247                         // preference and device
248                         mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
249                         return true;
250                     }
251                 }
252                 break;
253             case BluetoothProfile.STATE_DISCONNECTED:
254                 mainDevice = findMainDevice(cachedDevice);
255                 if (mainDevice != null) {
256                     // When main device exists, receiving sub device disconnection
257                     // To update main device UI
258                     mainDevice.refresh();
259                     return true;
260                 }
261                 final Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
262                 if (memberSet.isEmpty()) {
263                     break;
264                 }
265 
266                 for (CachedBluetoothDevice device: memberSet) {
267                     if (device.isConnected()) {
268                         log("set device: " + device + " as the main device");
269                         // Main device is disconnected and sub device is connected
270                         // To copy data from sub device to main device
271                         mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
272                         cachedDevice.switchMemberDeviceContent(device);
273                         cachedDevice.refresh();
274                         // It is necessary to do remove and add for updating the mapping on
275                         // preference and device
276                         mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice);
277                         return true;
278                     }
279                 }
280                 break;
281             default:
282                 // Do not handle this state.
283         }
284         return false;
285     }
286 
findMainDevice(CachedBluetoothDevice device)287     CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
288         if (device == null || mCachedDevices == null) {
289             return null;
290         }
291 
292         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
293             if (isValidGroupId(cachedDevice.getGroupId())) {
294                 Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
295                 if (memberSet.isEmpty()) {
296                     continue;
297                 }
298 
299                 for (CachedBluetoothDevice memberDevice: memberSet) {
300                     if (memberDevice != null && memberDevice.equals(device)) {
301                         return cachedDevice;
302                     }
303                 }
304             }
305         }
306         return null;
307     }
308 
309     /**
310      * Check if the {@code groupId} is existed.
311      *
312      * @param groupId The group id
313      *
314      * @return {@code true}, if we could find a device with this {@code groupId}; Otherwise,
315      * return {@code false}.
316      */
isExistedGroupId(int groupId)317     public boolean isExistedGroupId(int groupId) {
318         if (getCachedDevice(groupId) != null) {
319             return true;
320         }
321 
322         return false;
323     }
324 
log(String msg)325     private void log(String msg) {
326         if (DEBUG) {
327             Log.d(TAG, msg);
328         }
329     }
330 }
331