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

Commit 4c430999 authored by Chung Tang's avatar Chung Tang
Browse files

[OutputSwitcher] Implement select/deselect functions together with le audio share triggering

Bug: 385672684
Flag: com.android.media.flags.enable_output_switcher_personal_audio_sharing
Test: atest

Change-Id: Icc7b55945fbe68be963e8a64ef1b62abcdbad814
parent fd4c1498
Loading
Loading
Loading
Loading
+141 −19
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ import android.util.SparseArray;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -130,6 +131,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
    // Whether this is a TV device.
    private final boolean mIsTv;

    // Max number of devices allow for a BLE broadcast session.
    private final int mBroadcastingMaxSinks;

    // Get the singleton AudioManagerRouteController. Create a new one if it's not available yet.
    public static AudioManagerRouteController getInstance(
            @NonNull Context context,
@@ -172,6 +176,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
        mHandler = new Handler(Objects.requireNonNull(looper));
        mStrategyForMedia = Objects.requireNonNull(strategyForMedia);
        mIsTv = mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
        mBroadcastingMaxSinks =
                mContext.getResources().getInteger(R.integer.config_audio_sharing_maximum_sinks);

        mBuiltInSpeakerSuitabilityStatus =
                DeviceRouteController.getBuiltInSpeakerSuitabilityStatus(mContext);
@@ -264,17 +270,30 @@ import java.util.concurrent.CopyOnWriteArrayList;

    @Override
    @NonNull
    public List<MediaRoute2Info> getSelectableRoutes() {
        // TODO(b/421884879): Implement the select / deselect function
    public synchronized List<MediaRoute2Info> getSelectableRoutes() {
        if (currentOutputIsBLEBroadcast()) {
            return Collections.emptyList();
        } else {
            return getAvailableRoutes().stream()
                    .filter(
                            route ->
                                    !maxBroadcastDeviceReached()
                                            && !getSelectedRoutes().contains(route)
                                            && isRouteSelectable(route))
                    .toList();
        }
    }

    @Override
    @NonNull
    public List<MediaRoute2Info> getDeselectableRoutes() {
        // TODO(b/421884879): Implement the select / deselect function
    public synchronized List<MediaRoute2Info> getDeselectableRoutes() {
        if (currentOutputIsBLEBroadcast()) {
            // When broadcasting, all selected routes are deselectable.
            return getSelectedRoutes();
        } else {
            return Collections.emptyList();
        }
    }

    @Override
    @NonNull
@@ -296,6 +315,12 @@ import java.util.concurrent.CopyOnWriteArrayList;
            notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
            return;
        }

        if (com.android.media.flags.Flags.enableOutputSwitcherPersonalAudioSharing()) {
            // We need to stop broadcast when we transfer to another route
            stopBroadcastIfCurrentlySelected(requestId);
        }

        MediaRoute2InfoHolder mediaRoute2InfoHolder;
        synchronized (this) {
            mediaRoute2InfoHolder = mRouteIdToAvailableDeviceRoutes.get(routeId);
@@ -327,13 +352,43 @@ import java.util.concurrent.CopyOnWriteArrayList;
    }

    @Override
    public synchronized void selectRoute(long requestId, @NonNull String routeId) {
        // TODO(b/421884879): Implement the select / deselect function
    public void selectRoute(long requestId, @NonNull String routeId) {
        if (currentOutputIsBLEBroadcast()) {
            // Currently we do not allow selecting route when already broadcasting,
            // Ui should block user from select route as well.
            Slog.e(TAG, "Unable to select route: " + routeId + " ,requestId: " + requestId);
            notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_INVALID_COMMAND);
            return;
        }

        List<String> routeIdListForBroadcast =
                new ArrayList<>(getSelectedRoutes().stream().map(MediaRoute2Info::getId).toList());
        routeIdListForBroadcast.add(routeId);

        mHandler.post(() -> mBluetoothRouteController.startBroadcast(routeIdListForBroadcast));
    }

    @Override
    public synchronized void deselectRoute(long requestId, @NonNull String routeId) {
        // TODO(b/421884879): Implement the select / deselect function
        if (!currentOutputIsBLEBroadcast()) {
            // Unexpected result.
            Slog.e(TAG, "Unable to deselect route: " + routeId + " ,requestId: " + requestId);
            notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_INVALID_COMMAND);
            return;
        }

        mHandler.post(() -> mBluetoothRouteController.removeRouteFromBroadcast(routeId));
    }

    private void stopBroadcastIfCurrentlySelected(long requestId) {
        if (!currentOutputIsBLEBroadcast()) {
            // Unexpected result.
            Slog.e(TAG, "Unable to stop broadcast, requestId: " + requestId);
            notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_INVALID_COMMAND);
            return;
        }

        mHandler.post(mBluetoothRouteController::stopBroadcast);
    }

    @RequiresPermission(
@@ -474,7 +529,30 @@ import java.util.concurrent.CopyOnWriteArrayList;
            boolean isVolumeFixed) {
        mRouteIdToAvailableDeviceRoutes.clear();
        MediaRoute2InfoHolder newSelectedRouteHolder = null;
        List<MediaRoute2InfoHolder> newSelectedRouteInfoHoldersInBroadcast = new ArrayList<>();

        if (com.android.media.flags.Flags.enableOutputSwitcherPersonalAudioSharing()) {
            // When broadcasting, certain audioDeviceInfos from AudioManager are not reliable.
            if (selectedDeviceAttributesType == AudioDeviceInfo.TYPE_BLE_BROADCAST) {
                for (MediaRoute2Info mediaRoute2Info :
                        mBluetoothRouteController.getBroadcastingDeviceRoutes()) {
                    // Need to reconstruct MediaRoute2Info from BluetoothDeviceRoutesController
                    MediaRoute2InfoHolder newHolder =
                            MediaRoute2InfoHolder.createForAudioManagerRoute(
                                    mediaRoute2Info, AudioDeviceInfo.TYPE_BLE_HEADSET);
                    mRouteIdToAvailableDeviceRoutes.put(mediaRoute2Info.getId(), newHolder);
                    newSelectedRouteInfoHoldersInBroadcast.add(newHolder);
                }
            }
        }

        for (AudioDeviceInfo audioDeviceInfo : audioDeviceInfos) {
            if (com.android.media.flags.Flags.enableOutputSwitcherPersonalAudioSharing()) {
                if (audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BLE_BROADCAST) {
                    // Handled previously
                    continue;
                }
            }
            MediaRoute2Info mediaRoute2Info =
                    createMediaRoute2InfoFromAudioDeviceInfo(audioDeviceInfo);
            // Null means audioDeviceInfo is not a supported media output, like a phone's builtin
@@ -510,7 +588,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
            mRouteIdToAvailableDeviceRoutes.put(placeholderRouteId, placeholderRouteHolder);
        }

        if (newSelectedRouteHolder == null) {
        if (newSelectedRouteInfoHoldersInBroadcast.isEmpty() && newSelectedRouteHolder == null) {
            Slog.e(
                    TAG,
                    "Could not map this selected device attribute type to an available route: "
@@ -523,6 +601,21 @@ import java.util.concurrent.CopyOnWriteArrayList;
            // We know mRouteIdToAvailableDeviceRoutes is not empty.
            newSelectedRouteHolder = mRouteIdToAvailableDeviceRoutes.values().iterator().next();
        }

        if (!newSelectedRouteInfoHoldersInBroadcast.isEmpty()) {
            List<MediaRoute2Info> newSelectedRoutes = new ArrayList<>();
            for (MediaRoute2InfoHolder newSelectedRouteInfoHolderInBroadcast :
                    newSelectedRouteInfoHoldersInBroadcast) {
                MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo =
                        newSelectedRouteInfoHolderInBroadcast.copyWithVolumeInfo(
                                musicVolume, musicMaxVolume, isVolumeFixed);
                mRouteIdToAvailableDeviceRoutes.put(
                        newSelectedRouteInfoHolderInBroadcast.mMediaRoute2Info.getId(),
                        selectedRouteHolderWithUpdatedVolumeInfo);
                newSelectedRoutes.add(selectedRouteHolderWithUpdatedVolumeInfo.mMediaRoute2Info);
            }
            mSelectedRoutes = Collections.unmodifiableList(newSelectedRoutes);
        } else {
            MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo =
                    newSelectedRouteHolder.copyWithVolumeInfo(
                            musicVolume, musicMaxVolume, isVolumeFixed);
@@ -532,6 +625,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
            mSelectedRoutes =
                    Collections.singletonList(
                            selectedRouteHolderWithUpdatedVolumeInfo.mMediaRoute2Info);
        }

        // We only add those BT routes that we have not already obtained from audio manager (which
        // are active).
@@ -646,6 +740,34 @@ import java.util.concurrent.CopyOnWriteArrayList;
        }
    }

    /** Checks if the current output is a BLE broadcast. */
    private boolean currentOutputIsBLEBroadcast() {
        List<AudioDeviceAttributes> devicesForMedia =
                mAudioManager.getDevicesForAttributes(MEDIA_USAGE_AUDIO_ATTRIBUTES);
        return !devicesForMedia.isEmpty()
                && devicesForMedia.getFirst().getType() == AudioDeviceInfo.TYPE_BLE_BROADCAST;
    }

    /**
     * @return true if maximum number of broadcast devices reached
     */
    private boolean maxBroadcastDeviceReached() {
        return getSelectedRoutes().size() >= mBroadcastingMaxSinks;
    }

    /**
     * Checks if a given route should be displayed as selectable route. This function checks: 1) if
     * max devices for broadcast reached 2) the selected route(s) is a broadcast supported route 3)
     * the target route is a broadcast supported route.
     *
     * @param targetRoute the route for checking
     * @return true if the target route should be displayed as a selectable route
     */
    private synchronized boolean isRouteSelectable(MediaRoute2Info targetRoute) {
        return getSelectedRoutes().getFirst().getType() == MediaRoute2Info.TYPE_BLE_HEADSET
                && targetRoute.getType() == MediaRoute2Info.TYPE_BLE_HEADSET;
    }

    /**
     * Holds a {@link MediaRoute2Info} and associated information that we don't want to put in the
     * {@link MediaRoute2Info} class because it's solely necessary for the implementation of this
+67 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.media;

import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.bluetooth.BluetoothA2dp;
@@ -39,6 +40,8 @@ import android.util.Log;
import android.util.Slog;
import android.util.SparseBooleanArray;

import androidx.annotation.RequiresPermission;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.media.flags.Flags;
@@ -223,6 +226,70 @@ import java.util.stream.Collectors;
        return routes;
    }

    /**
     * Trigger {@link BluetoothProfileMonitor} to start broadcast.
     *
     * @param targetRouteIds routes ids that broadcast targeting to
     */
    protected void startBroadcast(List<String> targetRouteIds) {
        if (targetRouteIds.size() <= 1) {
            Log.e(TAG, "Unable to start broadcast, incorrect number of routes: " + targetRouteIds);
            return;
        }

        // Filter the list to only contain items with matching route ids, then
        // Map the list to BluetoothDevice list to start the broadcast.
        List<BluetoothDevice> deviceListForBroadcast = new ArrayList<>();

        // Check if routeInfo is in the target list, and prevent duplicated entries
        for (BluetoothRouteInfo routeInfo : mBluetoothRoutes.values()) {
            if (targetRouteIds.contains(routeInfo.mRoute.getId())
                    && !deviceListForBroadcast.contains(routeInfo.mBtDevice)) {
                deviceListForBroadcast.add(routeInfo.mBtDevice);
            }
        }

        mBluetoothProfileMonitor.startBroadcast(deviceListForBroadcast);
    }

    /**
     * Removes route from current broadcast.
     *
     * @param routeId route id that being removed from the broadcast.
     */
    protected void removeRouteFromBroadcast(String routeId) {
        // TODO: b/414535608 - Handle PAS with 3+ devices
        // With more than 2 devices in a broadcast, we will need to really remove it from
        // Broadcast instead of just stopping the broadcast
        mBluetoothProfileMonitor.stopBroadcast();
    }

    /** Trigger {@link BluetoothProfileMonitor} to stop broadcast. */
    protected void stopBroadcast() {
        mBluetoothProfileMonitor.stopBroadcast();
    }

    /**
     * Obtains a list of selected bluetooth route infos.
     *
     * @return list of selected bluetooth route infos.
     */
    @RequiresPermission(
            allOf = {
                Manifest.permission.BLUETOOTH_PRIVILEGED,
                Manifest.permission.BLUETOOTH_CONNECT
            })
    public List<MediaRoute2Info> getBroadcastingDeviceRoutes() {
        // Use HashSet to check and avoid duplicates devices with same routeId
        Set<String> routeIdSet = new HashSet<>();

        // Convert List<BluetoothDevice> to List<MediaRoute2Info>
        return mBluetoothProfileMonitor.getDevicesWithBroadcastSource().stream()
                .map(device -> createBluetoothRoute(device).mRoute)
                .filter(routeInfo -> routeIdSet.add(routeInfo.getId()))
                .toList();
    }

    private void notifyBluetoothRoutesUpdated() {
        mListener.onBluetoothRoutesUpdated();
    }
+49 −18
Original line number Diff line number Diff line
@@ -100,7 +100,8 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
    @Nullable
    private volatile SessionCreationOrTransferRequest mPendingTransferRequest;

    private final EventListener mOnDeviceRouteEventListener = new EventListener();
    private final DeviceRouteEventListener mDeviceRouteEventListener =
            new DeviceRouteEventListener();

    public static SystemMediaRoute2Provider create(
            Context context, UserHandle user, Looper looper) {
@@ -119,7 +120,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {

        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        mDeviceRouteController =
                DeviceRouteController.createInstance(context, looper, mOnDeviceRouteEventListener);
                DeviceRouteController.createInstance(context, looper, mDeviceRouteEventListener);
    }

    public void start() {
@@ -218,12 +219,17 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {

    @Override
    public void selectRoute(long requestId, String sessionId, String routeId) {
        // Do nothing since we don't support multiple BT yet.
        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
            // Pass params to DeviceRouteController to start the broadcast
            mDeviceRouteController.selectRoute(requestId, routeId);
        }
    }

    @Override
    public void deselectRoute(long requestId, String sessionId, String routeId) {
        // Do nothing since we don't support multiple BT yet.
        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
            mDeviceRouteController.deselectRoute(requestId, routeId);
        }
    }

    @Override
@@ -363,6 +369,16 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
                }
            }

            if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
                for (MediaRoute2Info route : mDeviceRouteController.getSelectableRoutes()) {
                    builder.addSelectableRoute(route.getId());
                }

                for (MediaRoute2Info route : mDeviceRouteController.getDeselectableRoutes()) {
                    builder.addDeselectableRoute(route.getId());
                }
            }

            if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
                var oldSessionInfo =
                        Flags.enableMirroringInMediaRouter2()
@@ -444,22 +460,13 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
                    .setSystemSession(true);

            List<MediaRoute2Info> selectedRoutes = mDeviceRouteController.getSelectedRoutes();
            List<String> transferableRoutes = new ArrayList<>();
            mSelectedRouteIds = selectedRoutes.stream().map(MediaRoute2Info::getId).toList();

            var defaultRouteBuilder =
                    new MediaRoute2Info.Builder(
                                    MediaRoute2Info.ROUTE_ID_DEFAULT, selectedRoutes.getFirst())
                            .setSystemRoute(true)
                            .setProviderId(mUniqueId);
            if (Flags.hideBtAddressFromAppsWithoutBtPermission()) {
                defaultRouteBuilder.setAddress(null); // We clear the address field.
            }
            mDefaultRoute = defaultRouteBuilder.build();
            List<String> transferableRoutes = new ArrayList<>();

            for (String selectedRouteId : mSelectedRouteIds) {
                builder.addSelectedRoute(selectedRouteId);
            }

            for (MediaRoute2Info route : mDeviceRouteController.getAvailableRoutes()) {
                String routeId = route.getId();
                if (!mSelectedRouteIds.contains(routeId)) {
@@ -471,6 +478,27 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
                builder.addTransferableRoute(route);
            }

            if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
                for (MediaRoute2Info route : mDeviceRouteController.getSelectableRoutes()) {
                    builder.addSelectableRoute(route.getId());
                }

                for (MediaRoute2Info route : mDeviceRouteController.getDeselectableRoutes()) {
                    builder.addDeselectableRoute(route.getId());
                }
            }

            // Handle the default route
            var defaultRouteBuilder =
                    new MediaRoute2Info.Builder(
                                    MediaRoute2Info.ROUTE_ID_DEFAULT, selectedRoutes.getFirst())
                            .setSystemRoute(true)
                            .setProviderId(mUniqueId);
            if (Flags.hideBtAddressFromAppsWithoutBtPermission()) {
                defaultRouteBuilder.setAddress(null); // We clear the address field.
            }
            mDefaultRoute = defaultRouteBuilder.build();

            if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
                int transferReason = RoutingSessionInfo.TRANSFER_REASON_FALLBACK;
                UserHandle transferInitiatorUserHandle = null;
@@ -592,8 +620,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {

        List<String> selectedRoutes = sessionInfo.getSelectedRoutes();

        if (!Flags.enableOutputSwitcherPersonalAudioSharing()) {
            if (selectedRoutes.size() != 1) {
            throw new IllegalStateException("Selected routes list should contain only 1 route id.");
                throw new IllegalStateException(
                        "Selected routes list should contain only 1 route id.");
            }
        }

        String oldSelectedRouteId = MediaRouter2Utils.getOriginalId(selectedRoutes.get(0));
@@ -641,7 +672,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
        publishProviderState();
    }

    private class EventListener implements DeviceRouteController.EventListener {
    private class DeviceRouteEventListener implements DeviceRouteController.EventListener {

        @Override
        public void onDeviceRouteChanged() {