[Audiosharing] Migrate feature from overlay to Settings
Bug: 340379827 Test: atest Change-Id: I3a88ac1d2f575f3be1f26f617479bbfd25cf6a8e
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user