Files
app_Settings/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
chelseahao 9f23b4e45f [Audiosharing] Implement name and password row.
Bug: 308368124
Test: manual
Change-Id: I86d0e771ece0ea7003a50ee0cc9305814d85fecb
2024-01-26 00:59:44 +00:00

383 lines
17 KiB
Java

/*
* Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.android.settings.flags.Flags;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LeAudioProfile;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
public class AudioSharingUtils {
private static final String TAG = "AudioSharingUtils";
private static final boolean DEBUG = BluetoothUtils.D;
/**
* Fetch {@link CachedBluetoothDevice}s connected to the broadcast assistant. The devices are
* grouped by CSIP group id.
*
* @param localBtManager The BT manager to provide BT functions.
* @return A map of connected devices grouped by CSIP group id.
*/
public static Map<Integer, List<CachedBluetoothDevice>> fetchConnectedDevicesByGroupId(
LocalBluetoothManager localBtManager) {
Map<Integer, List<CachedBluetoothDevice>> groupedDevices = new HashMap<>();
if (localBtManager == null) {
Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to bt manager is null");
return groupedDevices;
}
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) {
Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to assistant profile is null");
return groupedDevices;
}
List<BluetoothDevice> connectedDevices = assistant.getConnectedDevices();
CachedBluetoothDeviceManager cacheManager = localBtManager.getCachedDeviceManager();
for (BluetoothDevice device : connectedDevices) {
CachedBluetoothDevice cachedDevice = cacheManager.findDevice(device);
if (cachedDevice == null) {
Log.d(TAG, "Skip device due to not being cached: " + device.getAnonymizedAddress());
continue;
}
int groupId = getGroupId(cachedDevice);
if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
Log.d(
TAG,
"Skip device due to no valid group id: " + device.getAnonymizedAddress());
continue;
}
if (!groupedDevices.containsKey(groupId)) {
groupedDevices.put(groupId, new ArrayList<>());
}
groupedDevices.get(groupId).add(cachedDevice);
}
if (DEBUG) {
Log.d(TAG, "fetchConnectedDevicesByGroupId: " + groupedDevices);
}
return groupedDevices;
}
/**
* Fetch a list of ordered connected lead {@link CachedBluetoothDevice}s eligible for audio
* sharing. The active device is placed in the first place if it exists. The devices can be
* filtered by whether it is already in the audio sharing session.
*
* @param localBtManager The BT manager to provide BT functions. *
* @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
* id.
* @param filterByInSharing Whether to filter the device by if is already in the sharing
* session.
* @return A list of ordered connected devices eligible for the audio sharing. The active device
* is placed in the first place if it exists.
*/
public static List<CachedBluetoothDevice> buildOrderedConnectedLeadDevices(
LocalBluetoothManager localBtManager,
Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices,
boolean filterByInSharing) {
List<CachedBluetoothDevice> orderedDevices = new ArrayList<>();
for (List<CachedBluetoothDevice> devices : groupedConnectedDevices.values()) {
CachedBluetoothDevice leadDevice = null;
for (CachedBluetoothDevice device : devices) {
if (!device.getMemberDevice().isEmpty()) {
leadDevice = device;
break;
}
}
if (leadDevice == null && !devices.isEmpty()) {
leadDevice = devices.get(0);
Log.d(
TAG,
"Empty member device, pick arbitrary device as the lead: "
+ leadDevice.getDevice().getAnonymizedAddress());
}
if (leadDevice == null) {
Log.d(TAG, "Skip due to no lead device");
continue;
}
if (filterByInSharing && !hasBroadcastSource(leadDevice, localBtManager)) {
Log.d(
TAG,
"Filtered the device due to not in sharing session: "
+ leadDevice.getDevice().getAnonymizedAddress());
continue;
}
orderedDevices.add(leadDevice);
}
orderedDevices.sort(
(CachedBluetoothDevice d1, CachedBluetoothDevice d2) -> {
// Active above not inactive
int comparison =
(isActiveLeAudioDevice(d2) ? 1 : 0)
- (isActiveLeAudioDevice(d1) ? 1 : 0);
if (comparison != 0) return comparison;
// Bonded above not bonded
comparison =
(d2.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0)
- (d1.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
if (comparison != 0) return comparison;
// Bond timestamp available above unavailable
comparison =
(d2.getBondTimestamp() != null ? 1 : 0)
- (d1.getBondTimestamp() != null ? 1 : 0);
if (comparison != 0) return comparison;
// Order by bond timestamp if it is available
// Otherwise order by device name
return d1.getBondTimestamp() != null
? d1.getBondTimestamp().compareTo(d2.getBondTimestamp())
: d1.getName().compareTo(d2.getName());
});
return orderedDevices;
}
/**
* Fetch a list of ordered connected lead {@link AudioSharingDeviceItem}s eligible for audio
* sharing. The active device is placed in the first place if it exists. The devices can be
* filtered by whether it is already in the audio sharing session.
*
* @param localBtManager The BT manager to provide BT functions. *
* @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
* id.
* @param filterByInSharing Whether to filter the device by if is already in the sharing
* session.
* @return A list of ordered connected devices eligible for the audio sharing. The active device
* is placed in the first place if it exists.
*/
public static ArrayList<AudioSharingDeviceItem> buildOrderedConnectedLeadAudioSharingDeviceItem(
LocalBluetoothManager localBtManager,
Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices,
boolean filterByInSharing) {
return buildOrderedConnectedLeadDevices(
localBtManager, groupedConnectedDevices, filterByInSharing)
.stream()
.map(device -> buildAudioSharingDeviceItem(device))
.collect(Collectors.toCollection(ArrayList::new));
}
/** Build {@link AudioSharingDeviceItem} from {@link CachedBluetoothDevice}. */
public static AudioSharingDeviceItem buildAudioSharingDeviceItem(
CachedBluetoothDevice cachedDevice) {
return new AudioSharingDeviceItem(
cachedDevice.getName(),
getGroupId(cachedDevice),
isActiveLeAudioDevice(cachedDevice));
}
/**
* Check if {@link CachedBluetoothDevice} is in an audio sharing session.
*
* @param cachedDevice The cached bluetooth device to check.
* @param localBtManager The BT manager to provide BT functions.
* @return Whether the device is in an audio sharing session.
*/
public static boolean hasBroadcastSource(
CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
if (localBtManager == null) {
Log.d(TAG, "Skip check hasBroadcastSource due to bt manager is null");
return false;
}
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) {
Log.d(TAG, "Skip check hasBroadcastSource due to assistant profile is null");
return false;
}
List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty()) {
Log.d(
TAG,
"Lead device has broadcast source, device = "
+ cachedDevice.getDevice().getAnonymizedAddress());
return true;
}
// Return true if member device is in broadcast.
for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
List<BluetoothLeBroadcastReceiveState> list =
assistant.getAllSources(device.getDevice());
if (!list.isEmpty()) {
Log.d(
TAG,
"Member device has broadcast source, device = "
+ device.getDevice().getAnonymizedAddress());
return true;
}
}
return false;
}
/**
* Check if {@link CachedBluetoothDevice} is an active le audio device.
*
* @param cachedDevice The cached bluetooth device to check.
* @return Whether the device is an active le audio device.
*/
public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
return BluetoothUtils.isActiveLeAudioDevice(cachedDevice);
}
/**
* Retrieves the one and only active Bluetooth LE Audio sink device, regardless if the device is
* currently in an audio sharing session.
*
* @param manager The LocalBluetoothManager instance used to fetch connected devices.
* @return An Optional containing the active LE Audio device, or an empty Optional if not found.
*/
public static Optional<CachedBluetoothDevice> getActiveSinkOnAssistant(
@Nullable LocalBluetoothManager manager) {
if (manager == null) {
Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
return Optional.empty();
}
var groupedDevices = fetchConnectedDevicesByGroupId(manager);
var leadDevices = buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
if (!leadDevices.isEmpty() && AudioSharingUtils.isActiveLeAudioDevice(leadDevices.get(0))) {
return Optional.of(leadDevices.get(0));
} else {
Log.w(TAG, "getActiveSinksOnAssistant(): No active lead device!");
}
return Optional.empty();
}
/** Toast message on main thread. */
public static void toastMessage(Context context, String message) {
context.getMainExecutor()
.execute(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
}
/** Returns if the le audio sharing is enabled. */
public static boolean isFeatureEnabled() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
return Flags.enableLeAudioSharing()
&& adapter.isLeAudioBroadcastSourceSupported()
== BluetoothStatusCodes.FEATURE_SUPPORTED
&& adapter.isLeAudioBroadcastAssistantSupported()
== BluetoothStatusCodes.FEATURE_SUPPORTED;
}
/** Automatically update active device if needed. */
public static void updateActiveDeviceIfNeeded(LocalBluetoothManager localBtManager) {
if (localBtManager == null) {
Log.d(TAG, "Skip updateActiveDeviceIfNeeded due to bt manager is null");
return;
}
Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices =
fetchConnectedDevicesByGroupId(localBtManager);
List<CachedBluetoothDevice> devicesInSharing =
buildOrderedConnectedLeadDevices(
localBtManager, groupedConnectedDevices, /* filterByInSharing= */ true);
if (devicesInSharing.isEmpty()) return;
List<BluetoothDevice> devices =
BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices();
CachedBluetoothDevice targetDevice = null;
// Find the earliest connected device in sharing session.
int targetDeviceIdx = -1;
for (CachedBluetoothDevice device : devicesInSharing) {
if (devices.contains(device.getDevice())) {
int idx = devices.indexOf(device.getDevice());
if (idx > targetDeviceIdx) {
targetDeviceIdx = idx;
targetDevice = device;
}
}
}
if (targetDevice != null && !isActiveLeAudioDevice(targetDevice)) {
Log.d(
TAG,
"updateActiveDeviceIfNeeded, set active device: "
+ targetDevice.getDevice().getAnonymizedAddress());
targetDevice.setActive();
} else {
Log.d(
TAG,
"updateActiveDeviceIfNeeded, skip set active device: "
+ (targetDevice == null
? "null"
: (targetDevice.getDevice().getAnonymizedAddress()
+ " is already active")));
}
}
/** Returns if the broadcast is on-going. */
public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
if (manager == null) return false;
LocalBluetoothLeBroadcast broadcast =
manager.getProfileManager().getLeAudioBroadcastProfile();
return broadcast != null && broadcast.isEnabled(null);
}
/** Stops the latest broadcast. */
public static void stopBroadcasting(LocalBluetoothManager manager) {
if (manager == null) {
Log.d(TAG, "Skip stop broadcasting due to bt manager is null");
return;
}
LocalBluetoothLeBroadcast broadcast =
manager.getProfileManager().getLeAudioBroadcastProfile();
if (broadcast == null) {
Log.d(TAG, "Skip stop broadcasting due to broadcast profile is null");
}
broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
}
/**
* Get CSIP group id for {@link CachedBluetoothDevice}.
*
* <p>If CachedBluetoothDevice#getGroupId is invalid, fetch group id from
* LeAudioProfile#getGroupId.
*/
public static int getGroupId(CachedBluetoothDevice cachedDevice) {
int groupId = cachedDevice.getGroupId();
String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress();
if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress);
return groupId;
}
for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
if (profile instanceof LeAudioProfile) {
Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress);
return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice());
}
}
Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress);
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
}