Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1736a010 authored by Chelsea Hao's avatar Chelsea Hao Committed by Android (Google) Code Review
Browse files

Merge changes from topic "modifySource" into main

* changes:
  Return empty set instead of `UNKNOWN_CHANNEL` when selected channel is empty.
  Notify private broadcast source data with selected channel index.
  Add selected channel index to `PrivateBroadcastReceiveData`.
  Add util functions to get and modify channel selection of BluetoothLeBroadcastMetadata.
parents 749311d4 c1f3eb83
Loading
Loading
Loading
Loading
+93 −3
Original line number Diff line number Diff line
package com.android.settingslib.bluetooth;

import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.UNKNOWN_VALUE_PLACEHOLDER;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.UNKNOWN_CHANNEL;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static com.android.settingslib.widget.AdaptiveOutlineDrawable.ICON_TYPE_ADVANCED;

import static java.util.stream.Collectors.toSet;

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastChannel;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothLeBroadcastSubgroup;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.ComponentName;
@@ -38,6 +44,7 @@ import android.util.Pair;
import android.view.InputDevice;

import androidx.annotation.DrawableRes;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
@@ -53,6 +60,7 @@ import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -60,7 +68,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class BluetoothUtils {
    private static final String TAG = "BluetoothUtils";
@@ -652,13 +659,13 @@ public class BluetoothUtils {
                        .map(deviceManager::findDevice)
                        .filter(Objects::nonNull)
                        .map(BluetoothUtils::getGroupId)
                        .collect(Collectors.toSet());
                        .collect(toSet());
        Set<Integer> activeGroupIds =
                leAudioProfile.getActiveDevices().stream()
                        .map(deviceManager::findDevice)
                        .filter(Objects::nonNull)
                        .map(BluetoothUtils::getGroupId)
                        .collect(Collectors.toSet());
                        .collect(toSet());
        int groupId = getGroupId(cachedDevice);
        return activeGroupIds.size() == 1
                && !activeGroupIds.contains(groupId)
@@ -1305,4 +1312,87 @@ public class BluetoothUtils {
            return null;
        }
    }

    /**
     * Gets the index of the first selected channel for the first subgroup for a given broadcast
     * source Id on a Bluetooth sink device.
     */
    @NonNull
    public static Set<Integer> getSelectedChannelIndex(
            @NonNull LocalBluetoothProfileManager profileManager,
            @NonNull BluetoothDevice sink, @IntRange(from = 0x00, to = 0xFF) int sourceId) {
        LocalBluetoothLeBroadcastAssistant assistant =
                profileManager.getLeAudioBroadcastAssistantProfile();
        if (assistant == null) {
            Log.w(TAG, "getSelectedChannelIndex(): assistant is null");
            return UNKNOWN_CHANNEL;
        }
        BluetoothLeBroadcastMetadata metadata = assistant.getSourceMetadata(sink, sourceId);
        if (metadata == null) {
            Log.w(TAG, "getSelectedChannelIndex(): metadata is null");
            return UNKNOWN_CHANNEL;
        }
        List<BluetoothLeBroadcastSubgroup> subgroups = metadata.getSubgroups();
        if (subgroups == null || subgroups.isEmpty()) {
            Log.d(TAG, "getSelectedChannelIndex(): subgroup is null or empty");
            return UNKNOWN_CHANNEL;
        }
        return subgroups.getFirst().getChannels().stream().filter(
                BluetoothLeBroadcastChannel::isSelected).map(
                BluetoothLeBroadcastChannel::getChannelIndex).collect(toSet());
    }

    /**
     * Sets the selected state of a specific channel index for a given broadcast source Id on a
     * Bluetooth sink device by modifying the broadcast metadata. This method assumes the channel
     * belongs to the first subgroup in the metadata.
     */
    public static void modifySelectedChannelIndex(
            @NonNull LocalBluetoothProfileManager profileManager,
            @NonNull BluetoothDevice sink, @IntRange(from = 0x00, to = 0xFF) int sourceId,
            @NonNull Set<Integer> channelIndex, boolean shouldSelect) {
        LocalBluetoothLeBroadcastAssistant assistant =
                profileManager.getLeAudioBroadcastAssistantProfile();
        if (assistant == null) {
            Log.w(TAG, "modifySelectedChannelIndex(): assistant is null");
            return;
        }
        BluetoothLeBroadcastMetadata original = assistant.getSourceMetadata(sink, sourceId);
        if (original == null) {
            Log.w(TAG, "modifySelectedChannelIndex(): metadata is null");
            return;
        }
        List<BluetoothLeBroadcastSubgroup> subgroups = new ArrayList<>(original.getSubgroups());
        if (subgroups == null || subgroups.isEmpty()) {
            Log.d(TAG, "modifySelectedChannelIndex(): subgroup is null or empty");
            return;
        }
        BluetoothLeBroadcastSubgroup firstSubgroup = subgroups.getFirst();
        List<BluetoothLeBroadcastChannel> channels = firstSubgroup.getChannels();
        if (!channels.stream().map(BluetoothLeBroadcastChannel::getChannelIndex).collect(
                toSet()).containsAll(channelIndex)) {
            Log.d(TAG, "modifySelectedChannelIndex(): no channel found for given index");
            return;
        }
        List<BluetoothLeBroadcastChannel> updatedChannels = channels.stream()
                .map(c ->
                        channelIndex.contains(c.getChannelIndex()) && c.isSelected() != shouldSelect
                        ? new BluetoothLeBroadcastChannel.Builder(c).setSelected(
                        shouldSelect).build() : c).toList();
        if (updatedChannels.equals(channels)) {
            Log.d(TAG, "modifySelectedChannelIndex(): no change needed");
            return;
        }
        BluetoothLeBroadcastSubgroup.Builder updatedSubgroupBuilder =
                new BluetoothLeBroadcastSubgroup.Builder(firstSubgroup);
        updatedSubgroupBuilder.clearChannel();
        updatedChannels.forEach(updatedSubgroupBuilder::addChannel);
        subgroups.set(0, updatedSubgroupBuilder.build());
        var updatedBuilder = new BluetoothLeBroadcastMetadata.Builder(original).clearSubgroup();
        subgroups.forEach(updatedBuilder::addSubgroup);
        BluetoothLeBroadcastMetadata updated = updatedBuilder.build();
        Log.d(TAG, "modifySelectedChannelIndex(): existedMetadata = " + original
                + " updatedMetadata = " + updated);
        assistant.modifySource(sink, sourceId, updated);
    }
}
+27 −13
Original line number Diff line number Diff line
@@ -21,7 +21,9 @@ import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import static com.android.settingslib.Utils.isAudioModeOngoingCall;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED_BY_RECEIVER;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceStateWithSelectedChannel;

import static java.util.stream.Collectors.toList;

@@ -144,6 +146,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
            };
    private final Context mContext;
    private final CachedBluetoothDeviceManager mDeviceManager;
    private final LocalBluetoothProfileManager mProfileManager;
    private final boolean mHysteresisModeFixAvailable;
    private final boolean mIsWorkProfile;
    private BluetoothLeBroadcast mServiceBroadcast;
@@ -426,6 +429,12 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
                        Log.d(TAG, "Skip notifyPrivateBroadcastReceived for work profile.");
                        return;
                    }
                    String packageName = mContext.getPackageName();
                    if (!packageName.equals(SYSUI_PKG)) {
                        Log.d(TAG,
                                "Skip notifyPrivateBroadcastReceived, not triggered by SystemUI.");
                        return;
                    }
                    if (state.getBroadcastId() == mBroadcastId
                            || !mLocalSinksPendingSourceRemoval.isEmpty()) {
                        Log.d(TAG,
@@ -433,9 +442,14 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
                                        + "triggered by personal audio sharing.");
                        return;
                    }
                    var sourceState = LocalBluetoothLeBroadcastAssistant.getLocalSourceState(state);
                    var sourceStateAndSelectedChannel = getLocalSourceStateWithSelectedChannel(
                            mProfileManager, sink, sourceId, state);
                    var sourceState = sourceStateAndSelectedChannel.first;
                    var selectedChannel = sourceStateAndSelectedChannel.second;
                    if (sourceState == STREAMING || sourceState == DECRYPTION_FAILED
                            || (mHysteresisModeFixAvailable && sourceState == PAUSED)) {
                            || (mHysteresisModeFixAvailable && sourceState == PAUSED)
                            || (Flags.audioStreamPlayPauseByModifySource()
                            && sourceState == PAUSED_BY_RECEIVER)) {
                        List<BluetoothLeAudioContentMetadata> subgroupMetadata =
                                state.getSubgroupMetadata();
                        String programInfo = subgroupMetadata.isEmpty() ? ""
@@ -445,7 +459,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
                                sourceId,
                                state.getBroadcastId(),
                                programInfo == null ? "" : programInfo,
                                sourceState);
                                sourceState,
                                selectedChannel);
                    }
                }
            };
@@ -462,9 +477,11 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
        }
    }

    LocalBluetoothLeBroadcast(Context context, CachedBluetoothDeviceManager deviceManager) {
    LocalBluetoothLeBroadcast(Context context, CachedBluetoothDeviceManager deviceManager,
            LocalBluetoothProfileManager profileManager) {
        mContext = context;
        mDeviceManager = deviceManager;
        mProfileManager = profileManager;
        mExecutor = Executors.newSingleThreadExecutor();
        mBuilder = new BluetoothLeAudioContentMetadata.Builder();
        mContentResolver = context.getContentResolver();
@@ -1340,20 +1357,17 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {

    private void notifyPrivateBroadcastReceived(BluetoothDevice sink, int sourceId, int broadcastId,
            String programInfo,
            LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState state) {
        String packageName = mContext.getPackageName();
        if (!packageName.equals(SYSUI_PKG)) {
            Log.d(TAG, "Skip notifyPrivateBroadcastReceived, not triggered by SystemUI.");
            return;
        }
        var data = new PrivateBroadcastReceiveData(sink, sourceId, broadcastId, programInfo, state);
            LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState state,
            Set<Integer> selectedChannelIndex) {
        var data = new PrivateBroadcastReceiveData(sink, sourceId, broadcastId, programInfo, state,
                selectedChannelIndex);
        Intent intent = new Intent(ACTION_LE_AUDIO_PRIVATE_BROADCAST_RECEIVED);
        intent.putExtra(EXTRA_PRIVATE_BROADCAST_RECEIVE_DATA, data);
        intent.setPackage(SETTINGS_PKG);
        Log.d(TAG,
                "notifyPrivateBroadcastReceived for sink = " + sink + " with sourceId = " + sourceId
                        + " state = " + state
                        + " programInfo =" + programInfo
                        + " state = " + state + " selectedChannelIndex = "
                        + selectedChannelIndex + " programInfo = " + programInfo
                        + " broadcastId = " + broadcastId);
        mContext.sendBroadcast(intent);
    }
+34 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.bluetooth.BluetoothProfile.ServiceListener;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import android.util.Pair;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
@@ -38,10 +39,13 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import com.android.settingslib.R;
import com.android.settingslib.flags.Flags;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -52,12 +56,15 @@ import java.util.concurrent.Executors;
 * BluetoothLeBroadcastAssistant.Callback} to get the result callback.
 */
public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile {
    public static final HashSet<Integer> UNKNOWN_CHANNEL = new HashSet<>(List.of(-1));

    /** A derived source state based on {@link BluetoothLeBroadcastReceiveState}. */
    public enum LocalBluetoothLeBroadcastSourceState {
        UNKNOWN,
        STREAMING,
        DECRYPTION_FAILED,
        PAUSED,
        PAUSED_BY_RECEIVER;
    }

    private static final String TAG = "LocalBluetoothLeBroadcastAssistant";
@@ -620,4 +627,31 @@ public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile
        }
        return LocalBluetoothLeBroadcastSourceState.UNKNOWN;
    }

    /**
     * Returns the source connection status with channel selected based on the provided broadcast
     * receive state and source metadata retrieved from stack.
     */
    public static @NonNull Pair<LocalBluetoothLeBroadcastSourceState, Set<Integer>>
            getLocalSourceStateWithSelectedChannel(
                @NonNull LocalBluetoothProfileManager profileManager,
                @NonNull BluetoothDevice sink,
                int sourceId,
                @NonNull BluetoothLeBroadcastReceiveState state) {
        var localSourceState = getLocalSourceState(state);
        if (!Flags.audioStreamPlayPauseByModifySource()) {
            return Pair.create(localSourceState, UNKNOWN_CHANNEL);
        }
        Set<Integer> selectedChannelIndex = BluetoothUtils.getSelectedChannelIndex(
                profileManager, sink, sourceId);
        if (localSourceState == LocalBluetoothLeBroadcastSourceState.PAUSED
                && selectedChannelIndex.isEmpty()) {
            // No channel selected meaning the user decided to de-sync to the source, we return
            // `PAUSED_BY_RECEIVER`. In contrast, having any channel selected meaning the source
            // paused itself, we return `PAUSED`.
            return Pair.create(LocalBluetoothLeBroadcastSourceState.PAUSED_BY_RECEIVER,
                    selectedChannelIndex);
        }
        return Pair.create(localSourceState, selectedChannelIndex);
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -259,7 +259,7 @@ public class LocalBluetoothProfileManager {
            if (DEBUG) {
                Log.d(TAG, "Adding local LE_AUDIO_BROADCAST profile");
            }
            mLeAudioBroadcast = new LocalBluetoothLeBroadcast(mContext, mDeviceManager);
            mLeAudioBroadcast = new LocalBluetoothLeBroadcast(mContext, mDeviceManager, this);
            // no event handler for the LE boradcast.
            mProfileNameMap.put(LocalBluetoothLeBroadcast.NAME, mLeAudioBroadcast);
        }
+12 −3
Original line number Diff line number Diff line
@@ -20,9 +20,11 @@ import android.bluetooth.BluetoothDevice
import android.os.Parcel
import android.os.Parcelable
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED_BY_RECEIVER
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.UNKNOWN_CHANNEL

/**
 * Data class representing information received in a private broadcast.
@@ -41,6 +43,7 @@ data class PrivateBroadcastReceiveData(
    val broadcastId: Int = -1,
    val programInfo: String = "",
    val state: LocalBluetoothLeBroadcastSourceState?,
    val selectedChannelIndex: Set<Int> = UNKNOWN_CHANNEL
) : Parcelable {

    override fun describeContents(): Int = 0
@@ -51,6 +54,7 @@ data class PrivateBroadcastReceiveData(
        parcel.writeInt(broadcastId)
        parcel.writeString(programInfo)
        parcel.writeSerializable(state)
        parcel.writeSerializable(java.util.HashSet(selectedChannelIndex))
    }

    companion object {
@@ -70,7 +74,11 @@ data class PrivateBroadcastReceiveData(
                            state = readSerializable(
                                LocalBluetoothLeBroadcastSourceState::class.java.classLoader,
                                LocalBluetoothLeBroadcastSourceState::class.java
                            )
                            ),
                            selectedChannelIndex = readSerializable(
                                HashSet::class.java.classLoader,
                                HashSet::class.java
                            )?.filterIsInstance<Int>()?.toHashSet() ?: UNKNOWN_CHANNEL
                        )
                    }
                override fun newArray(size: Int): Array<PrivateBroadcastReceiveData?> {
@@ -84,7 +92,8 @@ data class PrivateBroadcastReceiveData(
                    && broadcastId != -1
                    && (state == STREAMING
                    || state == PAUSED
                    || state == DECRYPTION_FAILED)
                    || state == DECRYPTION_FAILED
                    || state == PAUSED_BY_RECEIVER)
        }
    }
}
Loading