blob: 8269b56c425bb268e3660225ceb5f00a7823bbd7 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.bluetooth;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.os.Build;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.ChecksSdkIntAtLeast;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* CsipDeviceManager manages the set of remote CSIP Bluetooth devices.
*/
public class CsipDeviceManager {
private static final String TAG = "CsipDeviceManager";
private static final boolean DEBUG = BluetoothUtils.D;
private final LocalBluetoothManager mBtManager;
private final List<CachedBluetoothDevice> mCachedDevices;
CsipDeviceManager(LocalBluetoothManager localBtManager,
List<CachedBluetoothDevice> cachedDevices) {
mBtManager = localBtManager;
mCachedDevices = cachedDevices;
};
void initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice) {
// Current it only supports the base uuid for CSIP and group this set in UI.
final int groupId = getBaseGroupId(newDevice.getDevice());
if (isValidGroupId(groupId)) {
log("initCsipDeviceIfNeeded: " + newDevice + " (group: " + groupId + ")");
// Once groupId is valid, assign groupId
newDevice.setGroupId(groupId);
}
}
private int getBaseGroupId(BluetoothDevice device) {
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
final CsipSetCoordinatorProfile profileProxy = profileManager
.getCsipSetCoordinatorProfile();
if (profileProxy != null) {
final Map<Integer, ParcelUuid> groupIdMap = profileProxy
.getGroupUuidMapByDevice(device);
if (groupIdMap == null) {
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
for (Map.Entry<Integer, ParcelUuid> entry : groupIdMap.entrySet()) {
if (entry.getValue().equals(BluetoothUuid.CAP)) {
return entry.getKey();
}
}
}
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
boolean setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice) {
final int groupId = newDevice.getGroupId();
if (isValidGroupId(groupId)) {
final CachedBluetoothDevice mainDevice = getCachedDevice(groupId);
log("setMemberDeviceIfNeeded, main: " + mainDevice + ", member: " + newDevice);
// Just add one of the coordinated set from a pair in the list that is shown in the UI.
// Once there is other devices with the same groupId, to add new device as member
// devices.
if (mainDevice != null) {
mainDevice.addMemberDevice(newDevice);
newDevice.setName(mainDevice.getName());
return true;
}
}
return false;
}
private boolean isValidGroupId(int groupId) {
return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
/**
* To find the device with {@code groupId}.
*
* @param groupId The group id
* @return if we could find a device with this {@code groupId} return this device. Otherwise,
* return null.
*/
public CachedBluetoothDevice getCachedDevice(int groupId) {
log("getCachedDevice: groupId: " + groupId);
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
if (cachedDevice.getGroupId() == groupId) {
log("getCachedDevice: found cachedDevice with the groupId: "
+ cachedDevice.getDevice().getAnonymizedAddress());
return cachedDevice;
}
}
return null;
}
// To collect all set member devices and call #onGroupIdChanged to group device by GroupId
void updateCsipDevices() {
final Set<Integer> newGroupIdSet = new HashSet<Integer>();
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
// Do nothing if GroupId has been assigned
if (!isValidGroupId(cachedDevice.getGroupId())) {
final int newGroupId = getBaseGroupId(cachedDevice.getDevice());
// Do nothing if there is no GroupId on Bluetooth device
if (isValidGroupId(newGroupId)) {
cachedDevice.setGroupId(newGroupId);
newGroupIdSet.add(newGroupId);
}
}
}
for (int groupId : newGroupIdSet) {
onGroupIdChanged(groupId);
}
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
private static boolean isAtLeastT() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
}
// Group devices by groupId
@VisibleForTesting
void onGroupIdChanged(int groupId) {
if (!isValidGroupId(groupId)) {
log("onGroupIdChanged: groupId is invalid");
return;
}
updateRelationshipOfGroupDevices(groupId);
}
// @return {@code true}, the event is processed inside the method. It is for updating
// le audio device on group relationship when receiving connected or disconnected.
// @return {@code false}, it is not le audio device or to process it same as other profiles
boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
int state) {
log("onProfileConnectionStateChangedIfProcessed: " + cachedDevice + ", state: " + state);
if (state != BluetoothProfile.STATE_CONNECTED
&& state != BluetoothProfile.STATE_DISCONNECTED) {
return false;
}
return updateRelationshipOfGroupDevices(cachedDevice.getGroupId());
}
@VisibleForTesting
boolean updateRelationshipOfGroupDevices(int groupId) {
if (!isValidGroupId(groupId)) {
log("The device is not group.");
return false;
}
log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString());
// Get the preferred main device by getPreferredMainDeviceWithoutConectionState
List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId);
CachedBluetoothDevice preferredMainDevice =
getPreferredMainDevice(groupId, groupDevicesList);
log("The preferredMainDevice= " + preferredMainDevice
+ " and the groupDevicesList of groupId= " + groupId
+ " =" + groupDevicesList);
return addMemberDevicesIntoMainDevice(groupId, preferredMainDevice);
}
CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
if (device == null || mCachedDevices == null) {
return null;
}
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (isValidGroupId(cachedDevice.getGroupId())) {
Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
if (memberSet.isEmpty()) {
continue;
}
for (CachedBluetoothDevice memberDevice : memberSet) {
if (memberDevice != null && memberDevice.equals(device)) {
return cachedDevice;
}
}
}
}
return null;
}
/**
* Check if the {@code groupId} is existed.
*
* @param groupId The group id
* @return {@code true}, if we could find a device with this {@code groupId}; Otherwise,
* return {@code false}.
*/
public boolean isExistedGroupId(int groupId) {
if (getCachedDevice(groupId) != null) {
return true;
}
return false;
}
@VisibleForTesting
List<CachedBluetoothDevice> getGroupDevicesFromAllOfDevicesList(int groupId) {
List<CachedBluetoothDevice> groupDevicesList = new ArrayList<>();
if (!isValidGroupId(groupId)) {
return groupDevicesList;
}
for (CachedBluetoothDevice item : mCachedDevices) {
if (groupId != item.getGroupId()) {
continue;
}
groupDevicesList.add(item);
groupDevicesList.addAll(item.getMemberDevice());
}
return groupDevicesList;
}
@VisibleForTesting
CachedBluetoothDevice getPreferredMainDevice(int groupId,
List<CachedBluetoothDevice> groupDevicesList) {
// How to select the preferred main device?
// 1. The DUAL mode connected device which has A2DP/HFP and LE audio.
// 2. One of connected LE device in the list. Default is the lead device from LE profile.
// 3. If there is no connected device, then reset the relationship. Set the DUAL mode
// deviced as the main device. Otherwise, set any one of the device.
if (groupDevicesList == null || groupDevicesList.isEmpty()) {
return null;
}
CachedBluetoothDevice dualModeDevice = groupDevicesList.stream()
.filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
.anyMatch(profile -> profile instanceof LeAudioProfile))
.filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
.anyMatch(profile -> profile instanceof A2dpProfile
|| profile instanceof HeadsetProfile))
.findFirst().orElse(null);
if (dualModeDevice != null && dualModeDevice.isConnected()) {
log("getPreferredMainDevice: The connected DUAL mode device");
return dualModeDevice;
}
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
final BluetoothDevice leAudioLeadDevice = (leAudioProfile != null && isAtLeastT())
? leAudioProfile.getConnectedGroupLeadDevice(groupId) : null;
if (leAudioLeadDevice != null) {
log("getPreferredMainDevice: The LeadDevice from LE profile is "
+ leAudioLeadDevice.getAnonymizedAddress());
}
CachedBluetoothDevice leAudioLeadCachedDevice =
leAudioLeadDevice != null ? deviceManager.findDevice(leAudioLeadDevice) : null;
if (leAudioLeadCachedDevice == null) {
log("getPreferredMainDevice: The LeadDevice is not in the all of devices list");
} else if (leAudioLeadCachedDevice.isConnected()) {
log("getPreferredMainDevice: The connected LeadDevice from LE profile");
return leAudioLeadCachedDevice;
}
CachedBluetoothDevice oneOfConnectedDevices = groupDevicesList.stream()
.filter(cachedDevice -> cachedDevice.isConnected())
.findFirst().orElse(null);
if (oneOfConnectedDevices != null) {
log("getPreferredMainDevice: One of the connected devices.");
return oneOfConnectedDevices;
}
if (dualModeDevice != null) {
log("getPreferredMainDevice: The DUAL mode device.");
return dualModeDevice;
}
// last
if (!groupDevicesList.isEmpty()) {
log("getPreferredMainDevice: One of the group devices.");
return groupDevicesList.get(0);
}
return null;
}
@VisibleForTesting
boolean addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice) {
boolean hasChanged = false;
if (preferredMainDevice == null) {
log("addMemberDevicesIntoMainDevice: No main device. Do nothing.");
return hasChanged;
}
// If the current main device is not preferred main device, then set it as new main device.
// Otherwise, do nothing.
BluetoothDevice bluetoothDeviceOfPreferredMainDevice = preferredMainDevice.getDevice();
CachedBluetoothDevice mainDeviceOfPreferredMainDevice = findMainDevice(preferredMainDevice);
boolean hasPreferredMainDeviceAlreadyBeenMainDevice =
mainDeviceOfPreferredMainDevice == null;
if (!hasPreferredMainDeviceAlreadyBeenMainDevice) {
// preferredMainDevice has not been the main device.
// switch relationship between the mainDeviceOfPreferredMainDevice and
// PreferredMainDevice
log("addMemberDevicesIntoMainDevice: The PreferredMainDevice have the mainDevice. "
+ "Do switch relationship between the mainDeviceOfPreferredMainDevice and "
+ "PreferredMainDevice");
// To switch content and dispatch to notify UI change
mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfPreferredMainDevice);
mainDeviceOfPreferredMainDevice.switchMemberDeviceContent(preferredMainDevice);
mainDeviceOfPreferredMainDevice.refresh();
// It is necessary to do remove and add for updating the mapping on
// preference and device
mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfPreferredMainDevice);
hasChanged = true;
}
// If the mCachedDevices List at CachedBluetoothDeviceManager has multiple items which are
// the same groupId, then combine them and also keep the preferred main device as main
// device.
List<CachedBluetoothDevice> topLevelOfGroupDevicesList = mCachedDevices.stream()
.filter(device -> device.getGroupId() == groupId)
.collect(Collectors.toList());
boolean haveMultiMainDevicesInAllOfDevicesList = topLevelOfGroupDevicesList.size() > 1;
// Update the new main of CachedBluetoothDevice, since it may be changed in above step.
final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
preferredMainDevice = deviceManager.findDevice(bluetoothDeviceOfPreferredMainDevice);
if (haveMultiMainDevicesInAllOfDevicesList) {
// put another devices into main device.
for (CachedBluetoothDevice deviceItem : topLevelOfGroupDevicesList) {
if (deviceItem.getDevice() == null || deviceItem.getDevice().equals(
bluetoothDeviceOfPreferredMainDevice)) {
continue;
}
Set<CachedBluetoothDevice> memberSet = deviceItem.getMemberDevice();
for (CachedBluetoothDevice memberSetItem : memberSet) {
if (!memberSetItem.equals(preferredMainDevice)) {
preferredMainDevice.addMemberDevice(memberSetItem);
}
}
memberSet.clear();
preferredMainDevice.addMemberDevice(deviceItem);
mCachedDevices.remove(deviceItem);
mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem);
hasChanged = true;
}
}
if (hasChanged) {
log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
+ mCachedDevices);
}
return hasChanged;
}
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
}