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

Commit 6354320f authored by Chung Tang's avatar Chung Tang
Browse files

[OutputSwitcher] Update BluetoothProfileMonitor to support Le Audio sharing

Bug: 385672684
Flag: com.android.media.flags.enable_output_switcher_personal_audio_sharing
Test: atest
Change-Id: I2f87b76bc7ae36f0820434a690f0296cd8ec11cf
parent f510a115
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -181,7 +181,11 @@ import java.util.concurrent.CopyOnWriteArrayList;

        mBluetoothRouteController =
                new BluetoothDeviceRoutesManager(
                        mContext, mHandler, btAdapter, this::rebuildAvailableRoutesAndNotify);
                        mContext,
                        mHandler,
                        looper,
                        btAdapter,
                        this::rebuildAvailableRoutesAndNotify);
        // Just build routes but don't notify. The caller may not expect the listener to be invoked
        // before this constructor has finished executing.
        rebuildAvailableRoutes();
+3 −1
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.MediaRoute2Info;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
@@ -88,13 +89,14 @@ import java.util.stream.Collectors;
    BluetoothDeviceRoutesManager(
            @NonNull Context context,
            @NonNull Handler handler,
            @NonNull Looper looper,
            @NonNull BluetoothAdapter bluetoothAdapter,
            @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) {
        this(
                context,
                handler,
                bluetoothAdapter,
                new BluetoothProfileMonitor(context, bluetoothAdapter),
                new BluetoothProfileMonitor(context, looper, bluetoothAdapter),
                listener);
    }

+403 −4
Original line number Diff line number Diff line
@@ -23,22 +23,60 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcast;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothLeBroadcastSettings;
import android.bluetooth.BluetoothLeBroadcastSubgroupSettings;
import android.bluetooth.BluetoothProfile;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.media.flags.Flags;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;

/* package */ class BluetoothProfileMonitor {

    private static final String TAG = BluetoothProfileMonitor.class.getSimpleName();

    /* package */ static final long GROUP_ID_NO_GROUP = -1L;

    private static final String UNDERLINE = "_";
    private static final int DEFAULT_CODE_MAX = 9999;
    private static final int DEFAULT_CODE_MIN = 1000;
    private static final int MIN_NO_DEVICES_FOR_BROADCAST = 2;
    private static final String VALID_PASSWORD_CHARACTERS =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,"
                    + ".<>?/";
    private static final int PASSWORD_LENGTH = 16;
    private static final int BROADCAST_NAME_PREFIX_MAX_LENGTH = 27;
    private static final String DEFAULT_BROADCAST_NAME_PREFIX = "Broadcast";
    private static final String IMPROVE_COMPATIBILITY_HIGH_QUALITY = "1";

    @NonNull
    private final ProfileListener mProfileListener = new ProfileListener();
    @NonNull private final BroadcastCallback mBroadcastCallback = new BroadcastCallback();

    @NonNull
    private final Context mContext;
    @NonNull
    private final BluetoothAdapter mBluetoothAdapter;
    private final BroadcastAssistantCallback mBroadcastAssistantCallback =
            new BroadcastAssistantCallback();

    @NonNull private final Context mContext;
    @NonNull private final Handler mHandler;
    @NonNull private final BluetoothAdapter mBluetoothAdapter;

    @Nullable
    private BluetoothA2dp mA2dpProfile;
@@ -47,9 +85,19 @@ import java.util.Objects;
    @Nullable
    private BluetoothLeAudio mLeAudioProfile;

    BluetoothProfileMonitor(@NonNull Context context,
    @GuardedBy("this")
    private BluetoothLeBroadcast mBroadcastProfile;

    private BluetoothLeBroadcastAssistant mAssistantProfile;
    private final List<BluetoothDevice> mDevicesToAdd = new ArrayList<>();
    private int mBroadcastId = 0;

    BluetoothProfileMonitor(
            @NonNull Context context,
            @NonNull Looper looper,
            @NonNull BluetoothAdapter bluetoothAdapter) {
        mContext = Objects.requireNonNull(context);
        mHandler = new Handler(Objects.requireNonNull(looper));
        mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter);
    }

@@ -57,6 +105,12 @@ import java.util.Objects;
        mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP);
        mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID);
        mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO);
        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
            mBluetoothAdapter.getProfileProxy(
                    mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST);
            mBluetoothAdapter.getProfileProxy(
                    mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
        }
    }

    /* package */ boolean isProfileSupported(int profile, @NonNull BluetoothDevice device) {
@@ -104,6 +158,240 @@ import java.util.Objects;
        }
    }

    /**
     * Starts broadcast and connect to given bluetooth devices.
     *
     * @param devices Bluetooth devices that are going to connect the to broadcast
     */
    public synchronized void startBroadcast(List<BluetoothDevice> devices) {
        if (devices.size() < MIN_NO_DEVICES_FOR_BROADCAST) {
            Slog.e(
                    TAG,
                    "Insufficient number of device to start broadcast: need to have at least "
                            + MIN_NO_DEVICES_FOR_BROADCAST
                            + " device(s) to start broadcast, current no. of selected device(s) = "
                            + devices.size());
            return;
        }

        if (mBroadcastProfile == null) {
            // BluetoothLeBroadcast is not initialized properly
            Slog.e(TAG, "Fail to start broadcast, LeBroadcast is null");
            return;
        }

        if (mBroadcastProfile.getAllBroadcastMetadata().size() >= getMaximumNumberOfBroadcasts()) {
            Slog.e(
                    TAG,
                    "Fail to start broadcast, current number of broadcasting group: "
                            + mBroadcastProfile.getAllBroadcastMetadata().size()
                            + ", exceeds the maximum allowed: "
                            + getMaximumNumberOfBroadcasts());
            return;
        }

        // Store the broadcast name so that program info and broadcast setting can use the same
        // value.
        String broadcastName = getBroadcastName();

        // Current broadcast framework only support one subgroup
        BluetoothLeBroadcastSubgroupSettings subgroupSettings =
                buildBroadcastSubgroupSettings(
                        /* language= */ null,
                        getProgramInfo(broadcastName),
                        isImproveQualityFlagEnabled());
        BluetoothLeBroadcastSettings settings =
                buildBroadcastSettings(
                        /* isPublic= */ true, // TODO(b/421062071): Set to false after framework
                        // fix.
                        broadcastName,
                        getBroadcastCode(),
                        List.of(subgroupSettings));

        mDevicesToAdd.clear();
        mDevicesToAdd.addAll(devices);
        mBroadcastProfile.startBroadcast(settings);
    }

    /** Stops the broadcast. */
    public synchronized void stopBroadcast() {
        if (mBroadcastProfile != null) {
            mBroadcastProfile.stopBroadcast(mBroadcastId);
        } else {
            Slog.e(TAG, "Fail to stop broadcast, LeBroadcast is null");
        }
    }

    /**
     * Obtains selected bluetooth devices from broadcast assistant that are broadcasting.
     *
     * @return list of selected {@link BluetoothDevice}
     */
    public List<BluetoothDevice> getDevicesWithBroadcastSource() {
        if (mAssistantProfile == null) {
            return List.of();
        }

        return mAssistantProfile.getConnectedDevices().stream()
                .filter(
                        device ->
                                mAssistantProfile.getAllSources(device).stream()
                                        .anyMatch(
                                                source -> source.getBroadcastId() == mBroadcastId))
                .toList();
    }

    /**
     * Gets the maximum number of Broadcast Isochronous Group supported on this device from
     * broadcast profile
     *
     * @return value of the maximum number of Broadcast Isochronous Group supported on this device
     *     from broadcast profile
     */
    public int getMaximumNumberOfBroadcasts() {
        return mBroadcastProfile.getMaximumNumberOfBroadcasts();
    }

    /**
     * Perform add device as broadcast source to {@link BluetoothLeBroadcastAssistant}. Devices will
     * then receive audio broadcast
     *
     * @param deviceList - List of {@link BluetoothDevice} for broadcast
     * @param metadata - broadcast metadata that obtained from broadcast object
     */
    private void addSourceToDevices(
            List<BluetoothDevice> deviceList, BluetoothLeBroadcastMetadata metadata) {
        if (mAssistantProfile == null) {
            Slog.d(TAG, "BroadcastAssistant is null");
            return;
        }

        if (metadata == null) {
            Slog.d(TAG, "BroadcastMetadata is null");
            return;
        }

        for (BluetoothDevice device : deviceList) {
            mAssistantProfile.addSource(device, metadata, /* isGroupOp= */ false);
        }
    }

    @Nullable
    private byte[] getBroadcastCode() {
        ContentResolver contentResolver = mContext.getContentResolver();

        String prefBroadcastCode =
                Settings.Secure.getStringForUser(
                        contentResolver,
                        Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE,
                        contentResolver.getUserId());

        byte[] broadcastCode =
                (prefBroadcastCode == null)
                        ? generateRandomPassword().getBytes(StandardCharsets.UTF_8)
                        : prefBroadcastCode.getBytes(StandardCharsets.UTF_8);

        return (broadcastCode != null && broadcastCode.length > 0) ? broadcastCode : null;
    }

    @NonNull
    private String getBroadcastName() {
        ContentResolver contentResolver = mContext.getContentResolver();
        String settingBroadcastName =
                Settings.Secure.getStringForUser(
                        contentResolver,
                        Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME,
                        contentResolver.getUserId());

        if (settingBroadcastName == null || settingBroadcastName.isEmpty()) {
            int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX);
            String name = BluetoothAdapter.getDefaultAdapter().getName();
            if (name == null || name.isEmpty()) {
                name = DEFAULT_BROADCAST_NAME_PREFIX;
            }
            return (name.length() < BROADCAST_NAME_PREFIX_MAX_LENGTH
                            ? name
                            : name.substring(0, BROADCAST_NAME_PREFIX_MAX_LENGTH))
                    + UNDERLINE
                    + postfix;
        }
        return settingBroadcastName;
    }

    @NonNull
    private String getProgramInfo(@NonNull String defaultProgramInfo) {
        ContentResolver contentResolver = mContext.getContentResolver();

        String programInfo =
                Settings.Secure.getStringForUser(
                        contentResolver,
                        Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO,
                        contentResolver.getUserId());

        if (programInfo == null || programInfo.isEmpty()) {
            return defaultProgramInfo;
        }

        return programInfo;
    }

    private static String generateRandomPassword() {
        SecureRandom random = new SecureRandom();
        StringBuilder stringBuilder = new StringBuilder(PASSWORD_LENGTH);

        for (int i = 0; i < PASSWORD_LENGTH; i++) {
            int randomIndex = random.nextInt(VALID_PASSWORD_CHARACTERS.length());
            stringBuilder.append(VALID_PASSWORD_CHARACTERS.charAt(randomIndex));
        }

        return stringBuilder.toString();
    }

    private boolean isImproveQualityFlagEnabled() {
        ContentResolver contentResolver = mContext.getContentResolver();
        // BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY takes only "1" and "0" in string only. Check
        // android.provider.settings.validators.SecureSettingsValidators for mode details.
        return IMPROVE_COMPATIBILITY_HIGH_QUALITY.equals(
                Settings.Secure.getStringForUser(
                        contentResolver,
                        Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY,
                        contentResolver.getUserId()));
    }

    private BluetoothLeBroadcastSettings buildBroadcastSettings(
            boolean isPublic,
            String broadcastName,
            byte[] broadcastCode,
            List<BluetoothLeBroadcastSubgroupSettings> subgroupSettingsList) {
        BluetoothLeBroadcastSettings.Builder builder =
                new BluetoothLeBroadcastSettings.Builder()
                        .setPublicBroadcast(isPublic)
                        .setBroadcastName(broadcastName)
                        .setBroadcastCode(broadcastCode);
        for (BluetoothLeBroadcastSubgroupSettings subgroupSettings : subgroupSettingsList) {
            builder.addSubgroupSettings(subgroupSettings);
        }
        return builder.build();
    }

    private BluetoothLeBroadcastSubgroupSettings buildBroadcastSubgroupSettings(
            String language, String programInfo, boolean improveCompatibility) {
        BluetoothLeAudioContentMetadata metadata =
                new BluetoothLeAudioContentMetadata.Builder()
                        .setLanguage(language)
                        .setProgramInfo(programInfo)
                        .build();
        // Current broadcast framework only support one subgroup, thus we still maintain the latest
        // metadata to keep legacy UI working.
        return new BluetoothLeBroadcastSubgroupSettings.Builder()
                .setPreferredQuality(
                        improveCompatibility
                                ? BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD
                                : BluetoothLeBroadcastSubgroupSettings.QUALITY_HIGH)
                .setContentMetadata(metadata)
                .build();
    }

    private final class ProfileListener implements BluetoothProfile.ServiceListener {
        @Override
        public void onServiceConnected(int profile, BluetoothProfile proxy) {
@@ -118,6 +406,19 @@ import java.util.Objects;
                    case BluetoothProfile.LE_AUDIO:
                        mLeAudioProfile = (BluetoothLeAudio) proxy;
                        break;
                    case BluetoothProfile.LE_AUDIO_BROADCAST:
                        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
                            mBroadcastProfile = (BluetoothLeBroadcast) proxy;
                            mBroadcastProfile.registerCallback(mHandler::post, mBroadcastCallback);
                        }
                        break;
                    case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT:
                        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
                            mAssistantProfile = (BluetoothLeBroadcastAssistant) proxy;
                            mAssistantProfile.registerCallback(
                                    mHandler::post, mBroadcastAssistantCallback);
                        }
                        break;
                }
            }
        }
@@ -135,8 +436,106 @@ import java.util.Objects;
                    case BluetoothProfile.LE_AUDIO:
                        mLeAudioProfile = null;
                        break;
                    case BluetoothProfile.LE_AUDIO_BROADCAST:
                        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
                            mBroadcastProfile.unregisterCallback(mBroadcastCallback);
                            mBroadcastProfile = null;
                            mBroadcastId = 0;
                        }
                        break;
                    case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT:
                        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
                            mAssistantProfile.unregisterCallback(mBroadcastAssistantCallback);
                            mAssistantProfile = null;
                        }
                        break;
                }
            }
        }
    }

    private final class BroadcastCallback implements BluetoothLeBroadcast.Callback {
        @Override
        public void onBroadcastStarted(int reason, int broadcastId) {
            mBroadcastId = broadcastId;
        }

        @Override
        public void onBroadcastStartFailed(int reason) {
            // To prevent broadcast accidentally start when metadata change
            mDevicesToAdd.clear();
        }

        @Override
        public void onBroadcastStopped(int reason, int broadcastId) {
            mBroadcastId = 0;
        }

        @Override
        public void onBroadcastStopFailed(int reason) {}

        @Override
        public void onPlaybackStarted(int reason, int broadcastId) {}

        @Override
        public void onPlaybackStopped(int reason, int broadcastId) {}

        @Override
        public void onBroadcastUpdated(int reason, int broadcastId) {}

        @Override
        public void onBroadcastUpdateFailed(int reason, int broadcastId) {}

        @Override
        public void onBroadcastMetadataChanged(
                int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
            BluetoothProfileMonitor.this.addSourceToDevices(mDevicesToAdd, metadata);
            mDevicesToAdd.clear();
        }
    }

    private static final class BroadcastAssistantCallback
            implements BluetoothLeBroadcastAssistant.Callback {
        @Override
        public void onSearchStarted(int reason) {}

        @Override
        public void onSearchStartFailed(int reason) {}

        @Override
        public void onSearchStopped(int reason) {}

        @Override
        public void onSearchStopFailed(int reason) {}

        @Override
        public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}

        @Override
        public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {}

        @Override
        public void onSourceAddFailed(
                @NonNull BluetoothDevice sink,
                @NonNull BluetoothLeBroadcastMetadata source,
                int reason) {}

        @Override
        public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId, int reason) {}

        @Override
        public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId, int reason) {}

        @Override
        public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId, int reason) {}

        @Override
        public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId, int reason) {}

        @Override
        public void onReceiveStateChanged(
                @NonNull BluetoothDevice sink,
                int sourceId,
                @NonNull BluetoothLeBroadcastReceiveState state) {}
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@
     package="com.android.server.media.tests">

    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
    <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />

    <application android:testOnly="true" android:debuggable="true">