[Audiosharing] Migrate feature from overlay to Settings

Bug: 340379827
Test: atest
Change-Id: I3a88ac1d2f575f3be1f26f617479bbfd25cf6a8e
This commit is contained in:
Yiyi Shen
2024-05-14 12:50:20 +08:00
parent a719e78a4c
commit e5bd60a0cf
133 changed files with 18497 additions and 275 deletions

View File

@@ -0,0 +1,355 @@
/*
* 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.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
import static java.util.Collections.emptyList;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.utils.ThreadUtils;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import javax.annotation.Nullable;
/**
* A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices.
*/
public class AudioStreamsHelper {
private static final String TAG = "AudioStreamsHelper";
private static final boolean DEBUG = BluetoothUtils.D;
private final @Nullable LocalBluetoothManager mBluetoothManager;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
mBluetoothManager = bluetoothManager;
mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
}
/**
* Adds the specified LE broadcast source to all active sinks.
*
* @param source The LE broadcast metadata representing the audio source.
*/
void addSource(BluetoothLeBroadcastMetadata source) {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "addSource(): LeBroadcastAssistant is null!");
return;
}
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
for (var sink :
getConnectedBluetoothDevices(
mBluetoothManager, /* inSharingOnly= */ false)) {
if (DEBUG) {
Log.d(
TAG,
"addSource(): join broadcast broadcastId"
+ " : "
+ source.getBroadcastId()
+ " sink : "
+ sink.getAddress());
}
mLeBroadcastAssistant.addSource(sink, source, false);
}
});
}
/** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
void removeSource(int broadcastId) {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
return;
}
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
for (var sink :
getConnectedBluetoothDevices(
mBluetoothManager, /* inSharingOnly= */ true)) {
if (DEBUG) {
Log.d(
TAG,
"removeSource(): remove all sources with broadcast id :"
+ broadcastId
+ " from sink : "
+ sink.getAddress());
}
mLeBroadcastAssistant.getAllSources(sink).stream()
.filter(state -> state.getBroadcastId() == broadcastId)
.forEach(
state ->
mLeBroadcastAssistant.removeSource(
sink, state.getSourceId()));
}
});
}
/** Retrieves a list of all LE broadcast receive states from active sinks. */
@VisibleForTesting
public List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
return emptyList();
}
return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
.filter(AudioStreamsHelper::isConnected)
.toList();
}
@Nullable
LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
return mLeBroadcastAssistant;
}
/** Checks the connectivity status based on the provided broadcast receive state. */
public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0);
}
static boolean isBadCode(BluetoothLeBroadcastReceiveState state) {
return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
&& state.getBigEncryptionState()
== BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE;
}
/**
* Returns a {@code CachedBluetoothDevice} that is either connected to a broadcast source or is
* a connected LE device.
*/
public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(
@androidx.annotation.Nullable LocalBluetoothManager manager) {
if (manager == null) {
Log.w(
TAG,
"getCachedBluetoothDeviceInSharingOrLeConnected(): LocalBluetoothManager is"
+ " null!");
return Optional.empty();
}
var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
var leadDevices =
AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
if (leadDevices.isEmpty()) {
Log.w(TAG, "getCachedBluetoothDeviceInSharingOrLeConnected(): No lead device!");
return Optional.empty();
}
var deviceHasSource =
leadDevices.stream()
.filter(device -> hasConnectedBroadcastSource(device, manager))
.findFirst();
if (deviceHasSource.isPresent()) {
Log.d(
TAG,
"getCachedBluetoothDeviceInSharingOrLeConnected(): Device has connected source"
+ " found: "
+ deviceHasSource.get().getAddress());
return deviceHasSource;
}
Log.d(
TAG,
"getCachedBluetoothDeviceInSharingOrLeConnected(): Device connected found: "
+ leadDevices.get(0).getAddress());
return Optional.of(leadDevices.get(0));
}
/** Returns a {@code CachedBluetoothDevice} that has a connected broadcast source. */
static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharing(
@androidx.annotation.Nullable LocalBluetoothManager manager) {
if (manager == null) {
Log.w(TAG, "getCachedBluetoothDeviceInSharing(): LocalBluetoothManager is null!");
return Optional.empty();
}
var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
var leadDevices =
AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
if (leadDevices.isEmpty()) {
Log.w(TAG, "getCachedBluetoothDeviceInSharing(): No lead device!");
return Optional.empty();
}
return leadDevices.stream()
.filter(device -> hasConnectedBroadcastSource(device, manager))
.findFirst();
}
/**
* Check if {@link CachedBluetoothDevice} has connected to a broadcast source.
*
* @param cachedDevice The cached bluetooth device to check.
* @param localBtManager The BT manager to provide BT functions.
* @return Whether the device has connected to a broadcast source.
*/
private static boolean hasConnectedBroadcastSource(
CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
if (localBtManager == null) {
Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null");
return false;
}
LocalBluetoothLeBroadcastAssistant assistant =
localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) {
Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null");
return false;
}
List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty()
&& sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) {
Log.d(
TAG,
"Lead device has connected 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() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) {
Log.d(
TAG,
"Member device has connected broadcast source, device = "
+ device.getDevice().getAnonymizedAddress());
return true;
}
}
return false;
}
/**
* Retrieves a list of connected Bluetooth devices that belongs to one {@link
* CachedBluetoothDevice} that's either connected to a broadcast source or is a connected LE
* audio device.
*/
static List<BluetoothDevice> getConnectedBluetoothDevices(
@Nullable LocalBluetoothManager manager, boolean inSharingOnly) {
if (manager == null) {
Log.w(TAG, "getConnectedBluetoothDevices(): LocalBluetoothManager is null!");
return emptyList();
}
var leBroadcastAssistant = getLeBroadcastAssistant(manager);
if (leBroadcastAssistant == null) {
Log.w(TAG, "getConnectedBluetoothDevices(): LeBroadcastAssistant is null!");
return emptyList();
}
List<BluetoothDevice> connectedDevices =
leBroadcastAssistant.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED});
Optional<CachedBluetoothDevice> cachedBluetoothDevice =
inSharingOnly
? getCachedBluetoothDeviceInSharing(manager)
: getCachedBluetoothDeviceInSharingOrLeConnected(manager);
List<BluetoothDevice> bluetoothDevices =
cachedBluetoothDevice
.map(
c ->
Stream.concat(
Stream.of(c.getDevice()),
c.getMemberDevice().stream()
.map(
CachedBluetoothDevice
::getDevice))
.filter(connectedDevices::contains)
.toList())
.orElse(emptyList());
Log.d(TAG, "getConnectedBluetoothDevices() devices: " + bluetoothDevices);
return bluetoothDevices;
}
private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
@Nullable LocalBluetoothManager manager) {
if (manager == null) {
Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!");
return null;
}
LocalBluetoothProfileManager profileManager = manager.getProfileManager();
if (profileManager == null) {
Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!");
return null;
}
return profileManager.getLeAudioBroadcastAssistantProfile();
}
static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
// TODO(b/331547596): prioritize broadcastName
Optional<String> optionalProgramInfo =
source.getSubgroups().stream()
.map(subgroup -> subgroup.getContentMetadata().getProgramInfo())
.filter(programInfo -> !Strings.isNullOrEmpty(programInfo))
.findFirst();
return optionalProgramInfo.orElseGet(
() -> {
String broadcastName = source.getBroadcastName();
if (broadcastName != null && !broadcastName.isEmpty()) {
return broadcastName;
} else {
return "Broadcast Id: " + source.getBroadcastId();
}
});
}
static String getBroadcastName(BluetoothLeBroadcastReceiveState state) {
return state.getSubgroupMetadata().stream()
.map(BluetoothLeAudioContentMetadata::getProgramInfo)
.filter(i -> !Strings.isNullOrEmpty(i))
.findFirst()
.orElse("Broadcast Id: " + state.getBroadcastId());
}
void startMediaService(Context context, int audioStreamBroadcastId, String title) {
List<BluetoothDevice> devices =
getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true);
if (devices.isEmpty()) {
return;
}
var intent = new Intent(context, AudioStreamMediaService.class);
intent.putExtra(BROADCAST_ID, audioStreamBroadcastId);
intent.putExtra(BROADCAST_TITLE, title);
intent.putParcelableArrayListExtra(DEVICES, new ArrayList<>(devices));
context.startService(intent);
}
}