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