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

Commit 6cce84e0 authored by Yiyi Shen's avatar Yiyi Shen Committed by Android (Google) Code Review
Browse files

Merge "[OutputSwitcher] Set and update the volume changes for PAS sinks" into main

parents 1b9f146b b85b6daf
Loading
Loading
Loading
Loading
+40 −3
Original line number Diff line number Diff line
@@ -423,6 +423,31 @@ import java.util.concurrent.CopyOnWriteArrayList;
        return true;
    }

    @Override
    public void setVolume(long requestId, @NonNull String routeId, int volume) {
        mHandler.post(() -> setVolumeOnHandler(requestId, routeId, volume));
    }

    private void setVolumeOnHandler(long requestId, @NonNull String routeId, int volume) {
        if (currentOutputIsBLEBroadcast()) {
            if (mBluetoothRouteController.isMediaOnlyRouteInBroadcast(routeId)) {
                // Media only device (device can only listen to broadcast source) volume
                // is controlled by volume control profile.
                boolean result = mBluetoothRouteController.setRouteVolume(routeId, volume);
                if (!result) {
                    notifyRequestFailed(
                            requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
                }
            } else {
                // Primary device (device can listen to call and broadcast source)
                // volume is bundled and controlled by music stream.
                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
            }
        } else {
            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
        }
    }

    private Runnable getTransferActionForRoute(MediaRoute2InfoHolder mediaRoute2InfoHolder) {
        if (mediaRoute2InfoHolder.mCorrespondsToInactiveBluetoothRoute) {
            String deviceAddress = mediaRoute2InfoHolder.mMediaRoute2Info.getAddress();
@@ -625,9 +650,21 @@ import java.util.concurrent.CopyOnWriteArrayList;
            List<MediaRoute2Info> newSelectedRoutes = new ArrayList<>();
            for (MediaRoute2InfoHolder newSelectedRouteInfoHolderInBroadcast :
                    newSelectedRouteInfoHoldersInBroadcast) {
                MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo =
                MediaRoute2Info routeInfo = newSelectedRouteInfoHolderInBroadcast.mMediaRoute2Info;
                MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo;
                if (routeInfo.getVolume() != BluetoothProfileMonitor.INVALID_VOLUME
                        && routeInfo.getVolumeMax()
                                == BluetoothProfileMonitor.MAXIMUM_DEVICE_VOLUME) {
                    selectedRouteHolderWithUpdatedVolumeInfo =
                            newSelectedRouteInfoHolderInBroadcast.copyWithVolumeInfo(
                                    routeInfo.getVolume(), routeInfo.getVolumeMax(), isVolumeFixed);
                } else {
                    // Volume is not available from BT volume control profile, use music stream
                    // volume by default.
                    selectedRouteHolderWithUpdatedVolumeInfo =
                            newSelectedRouteInfoHolderInBroadcast.copyWithVolumeInfo(
                                    musicVolume, musicMaxVolume, isVolumeFixed);
                }
                mRouteIdToAvailableDeviceRoutes.put(
                        newSelectedRouteInfoHolderInBroadcast.mMediaRoute2Info.getId(),
                        selectedRouteHolderWithUpdatedVolumeInfo);
+56 −11
Original line number Diff line number Diff line
@@ -76,6 +76,13 @@ import java.util.stream.Collectors;
        void onBluetoothRoutesUpdated();
    }

    /** Interface for receiving events about Broadcast sinks volume changes. */
    interface OnBroadcastSinkVolumeChangedListener {

        /** Called when Bluetooth sink volume in broadcast has changed. */
        void onBroadcastSinkVolumeChanged();
    }

    @NonNull
    private final AdapterStateChangedReceiver mAdapterStateChangedReceiver =
            new AdapterStateChangedReceiver();
@@ -122,7 +129,7 @@ import java.util.stream.Collectors;

    public void start(UserHandle user, @NonNull BluetoothRoutesUpdatedListener listener) {
        mListener = listener;
        mBluetoothProfileMonitor.start();
        mBluetoothProfileMonitor.start(() -> listener.onBluetoothRoutesUpdated());

        IntentFilter adapterStateChangedIntentFilter = new IntentFilter();

@@ -179,6 +186,31 @@ import java.util.stream.Collectors;
        mBluetoothAdapter.setActiveDevice(btRouteInfo.mBtDevice, ACTIVE_DEVICE_AUDIO);
    }

    public synchronized boolean isMediaOnlyRouteInBroadcast(@NonNull String routeId) {
        for (BluetoothRouteInfo info : mBluetoothRoutes.values()) {
            if (info.mRoute.getId().equals(routeId)) {
                if (mBluetoothProfileMonitor.isMediaOnlyDeviceInBroadcast(info.mBtDevice)) {
                    return true;
                }
            }
        }
        return false;
    }

    public synchronized boolean setRouteVolume(@NonNull String routeId, int volume) {
        boolean volumeUpdated = false;
        for (BluetoothRouteInfo info : mBluetoothRoutes.values()) {
            if (info.mRoute.getId().equals(routeId)) {
                // There could be multiple BT devices for the same route id, for example, LE Audio
                // devices, hearing aids.
                mBluetoothProfileMonitor.setDeviceVolume(
                        info.mBtDevice, volume, /* isGroupOp= */ false);
                volumeUpdated = true;
            }
        }
        return volumeUpdated;
    }

    private void updateBluetoothRoutes() {
        Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices();

@@ -199,7 +231,8 @@ import java.util.stream.Collectors;
                                            BluetoothDevice::getAddress, Function.identity()));
            for (BluetoothDevice device : bondedDevices) {
                if (device.isConnected()) {
                    BluetoothRouteInfo newBtRoute = createBluetoothRoute(device);
                    BluetoothRouteInfo newBtRoute =
                            createBluetoothRoute(device, /* setVolume= */ false);
                    if (newBtRoute.mConnectedProfiles.size() > 0) {
                        mBluetoothRoutes.put(device.getAddress(), newBtRoute);
                    }
@@ -283,7 +316,13 @@ import java.util.stream.Collectors;

        // Convert List<BluetoothDevice> to List<MediaRoute2Info>
        return mBluetoothProfileMonitor.getDevicesWithBroadcastSource().stream()
                .map(device -> createBluetoothRoute(device).mRoute)
                .map(
                        device ->
                                createBluetoothRoute(
                                                device,
                                                /* setVolume= */ mBluetoothProfileMonitor
                                                        .isMediaOnlyDeviceInBroadcast(device))
                                        .mRoute)
                .filter(routeInfo -> routeIdSet.add(routeInfo.getId()))
                .toList();
    }
@@ -307,9 +346,8 @@ import java.util.stream.Collectors;
     * bluetooth devices individually, since the audio stack refers to a bluetooth device group by
     * any of its member devices.
     */
    private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) {
        BluetoothRouteInfo
                newBtRoute = new BluetoothRouteInfo();
    private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device, boolean setVolume) {
        BluetoothRouteInfo newBtRoute = new BluetoothRouteInfo();
        newBtRoute.mBtDevice = device;
        String deviceName = getDeviceName(device);

@@ -317,9 +355,7 @@ import java.util.stream.Collectors;
        String routeId = getRouteIdForType(device, type);

        newBtRoute.mConnectedProfiles = getConnectedProfiles(device);
        // Note that volume is only relevant for active bluetooth routes, and those are managed via
        // AudioManager.
        newBtRoute.mRoute =
        MediaRoute2Info.Builder routeInfoBuilder =
                new MediaRoute2Info.Builder(routeId, deviceName)
                        .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO)
                        .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK)
@@ -329,8 +365,17 @@ import java.util.stream.Collectors;
                                        .getText(R.string.bluetooth_a2dp_audio_route_name)
                                        .toString())
                        .setType(type)
                        .setAddress(device.getAddress())
                        .build();
                        .setAddress(device.getAddress());
        // Note that volume is only relevant for active bluetooth routes, and those are managed via
        // AudioManager.
        // The only exception is media only devices in broadcast, the volume is fetched from
        // bluetooth volume control profile.
        if (setVolume) {
            routeInfoBuilder
                    .setVolume(mBluetoothProfileMonitor.getDeviceVolume(device))
                    .setVolumeMax(BluetoothProfileMonitor.MAXIMUM_DEVICE_VOLUME);
        }
        newBtRoute.mRoute = routeInfoBuilder.build();
        return newBtRoute;
    }

+114 −7
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothLeBroadcastSettings;
import android.bluetooth.BluetoothLeBroadcastSubgroupSettings;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothVolumeControl;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Handler;
@@ -46,6 +47,7 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;

/* package */ class BluetoothProfileMonitor {
@@ -53,7 +55,14 @@ import java.util.concurrent.ThreadLocalRandom;
    private static final String TAG = BluetoothProfileMonitor.class.getSimpleName();

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

    /* package */ static final int INVALID_VOLUME = -1;
    /* package */ static final int MAXIMUM_DEVICE_VOLUME = 255;
    // TODO(b/397568136): remove reading primary group id from SettingsProvider once
    //  adopt_primary_group_management_api_v2  is rolled out
    private static final String KEY_PRIMARY_GROUP_ID =
            "bluetooth_le_broadcast_fallback_active_group_id";

    private static final int INVALID_BROADCAST_ID = 0;
    private static final String UNDERLINE = "_";
    private static final int DEFAULT_CODE_MAX = 9999;
    private static final int DEFAULT_CODE_MIN = 1000;
@@ -69,6 +78,7 @@ import java.util.concurrent.ThreadLocalRandom;
    @NonNull
    private final ProfileListener mProfileListener = new ProfileListener();
    @NonNull private final BroadcastCallback mBroadcastCallback = new BroadcastCallback();
    @NonNull private final VolumeControlCallback mVolumeCallback = new VolumeControlCallback();

    @NonNull
    private final BroadcastAssistantCallback mBroadcastAssistantCallback =
@@ -86,11 +96,20 @@ import java.util.concurrent.ThreadLocalRandom;
    private BluetoothLeAudio mLeAudioProfile;

    @GuardedBy("this")
    @Nullable
    private BluetoothLeBroadcast mBroadcastProfile;

    private BluetoothLeBroadcastAssistant mAssistantProfile;
    @Nullable private BluetoothLeBroadcastAssistant mAssistantProfile;
    @Nullable private BluetoothVolumeControl mVolumeProfile;

    private final List<BluetoothDevice> mDevicesToAdd = new ArrayList<>();
    private int mBroadcastId = 0;
    private int mBroadcastId = INVALID_BROADCAST_ID;
    private final ConcurrentHashMap<BluetoothDevice, Integer> mVolumeMap =
            new ConcurrentHashMap<>();

    @NonNull
    private BluetoothDeviceRoutesManager.OnBroadcastSinkVolumeChangedListener
            mVolumeChangedListener;

    BluetoothProfileMonitor(
            @NonNull Context context,
@@ -99,9 +118,15 @@ import java.util.concurrent.ThreadLocalRandom;
        mContext = Objects.requireNonNull(context);
        mHandler = new Handler(Objects.requireNonNull(looper));
        mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter);
        // no-op listener, will be overridden in start()
        mVolumeChangedListener = () -> {};
    }

    /* package */ void start() {
    /* package */ void start(
            @NonNull
                    BluetoothDeviceRoutesManager.OnBroadcastSinkVolumeChangedListener
                            volumeListener) {
        mVolumeChangedListener = volumeListener;
        mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP);
        mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID);
        mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO);
@@ -110,6 +135,8 @@ import java.util.concurrent.ThreadLocalRandom;
                    mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST);
            mBluetoothAdapter.getProfileProxy(
                    mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
            mBluetoothAdapter.getProfileProxy(
                    mContext, mProfileListener, BluetoothProfile.VOLUME_CONTROL);
        }
    }

@@ -128,8 +155,8 @@ import java.util.concurrent.ThreadLocalRandom;
                    bluetoothProfile = mHearingAidProfile;
                    break;
                default:
                    throw new IllegalArgumentException(profile
                            + " is not supported as Bluetooth profile");
                    throw new IllegalArgumentException(
                            profile + " is not supported as Bluetooth profile");
            }
        }

@@ -391,6 +418,64 @@ import java.util.concurrent.ThreadLocalRandom;
                .build();
    }

    /* package */ boolean isDeviceInBroadcast(@NonNull BluetoothDevice device) {
        return mAssistantProfile != null
                && mBroadcastId != INVALID_BROADCAST_ID
                && mAssistantProfile.getAllSources(device).stream()
                        .anyMatch(source -> source.getBroadcastId() == mBroadcastId);
    }

    /* package */ void setDeviceVolume(
            @NonNull BluetoothDevice device, int volume, boolean isGroupOp) {
        if (mVolumeProfile != null) {
            mVolumeProfile.setDeviceVolume(device, volume, isGroupOp);
        }
    }

    /* package */ int getDeviceVolume(@NonNull BluetoothDevice device) {
        return isDeviceInBroadcast(device)
                ? mVolumeMap.getOrDefault(device, INVALID_VOLUME)
                : INVALID_VOLUME;
    }

    /**
     * Check if the BT device is the media only device in broadcast.
     *
     * <p>There are two types of sinks in the broadcast session, primary sink and media only sink.
     *
     * <p>Primary sink is the sink can listen to the call, usually it is the one belongs to the
     * broadcast owner.
     *
     * <p>Media only sink can only listen to audio shared by the broadcaster, including media and
     * notification.
     */
    /* package */ boolean isMediaOnlyDeviceInBroadcast(@NonNull BluetoothDevice device) {
        // Media only device, other than primary device, can only listen to the broadcast content
        // and is not the default one to listen to the call.
        long groupId = getGroupId(BluetoothProfile.LE_AUDIO, device);
        if (groupId == GROUP_ID_NO_GROUP) {
            Slog.d(TAG, "isMediaOnlyDeviceInBroadcast, invalid group id");
            return false;
        }
        long primaryGroupId = GROUP_ID_NO_GROUP;
        if (com.android.settingslib.flags.Flags.adoptPrimaryGroupManagementApiV2()) {
            if (mLeAudioProfile != null) {
                primaryGroupId = mLeAudioProfile.getBroadcastToUnicastFallbackGroup();
            }
        } else {
            // TODO(b/397568136): remove reading primary group id from SettingsProvider once
            //  adopt_primary_group_management_api_v2 is rolled out
            ContentResolver contentResolver = mContext.getContentResolver();
            primaryGroupId =
                    Settings.Secure.getIntForUser(
                            contentResolver,
                            KEY_PRIMARY_GROUP_ID,
                            (int) GROUP_ID_NO_GROUP,
                            contentResolver.getUserId());
        }
        return groupId != primaryGroupId;
    }

    private final class ProfileListener implements BluetoothProfile.ServiceListener {
        @Override
        public void onServiceConnected(int profile, BluetoothProfile proxy) {
@@ -418,6 +503,12 @@ import java.util.concurrent.ThreadLocalRandom;
                                    mHandler::post, mBroadcastAssistantCallback);
                        }
                        break;
                    case BluetoothProfile.VOLUME_CONTROL:
                        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
                            mVolumeProfile = (BluetoothVolumeControl) proxy;
                            mVolumeProfile.registerCallback(mHandler::post, mVolumeCallback);
                        }
                        break;
                }
            }
        }
@@ -439,7 +530,7 @@ import java.util.concurrent.ThreadLocalRandom;
                        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
                            mBroadcastProfile.unregisterCallback(mBroadcastCallback);
                            mBroadcastProfile = null;
                            mBroadcastId = 0;
                            mBroadcastId = INVALID_BROADCAST_ID;
                        }
                        break;
                    case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT:
@@ -448,6 +539,12 @@ import java.util.concurrent.ThreadLocalRandom;
                            mAssistantProfile = null;
                        }
                        break;
                    case BluetoothProfile.VOLUME_CONTROL:
                        if (Flags.enableOutputSwitcherPersonalAudioSharing()) {
                            mVolumeProfile.unregisterCallback(mVolumeCallback);
                            mVolumeProfile = null;
                        }
                        break;
                }
            }
        }
@@ -537,4 +634,14 @@ import java.util.concurrent.ThreadLocalRandom;
                int sourceId,
                @NonNull BluetoothLeBroadcastReceiveState state) {}
    }

    private final class VolumeControlCallback implements BluetoothVolumeControl.Callback {
        @Override
        public void onDeviceVolumeChanged(@NonNull BluetoothDevice device, int volume) {
            mVolumeMap.put(device, volume);
            if (isMediaOnlyDeviceInBroadcast(device)) {
                mVolumeChangedListener.onBroadcastSinkVolumeChanged();
            }
        }
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -167,6 +167,15 @@ import java.util.List;
     */
    boolean updateVolume(int volume);

    /**
     * Sets device route volume.
     *
     * @param requestId identifies the request.
     * @param routeId to set the volume.
     * @param volume specifies a volume for the device route.
     */
    void setVolume(long requestId, @NonNull String routeId, int volume);

    /**
     * Starts listening for changes in the system to keep an up to date view of available and
     * selected devices.
+5 −0
Original line number Diff line number Diff line
@@ -181,6 +181,11 @@ import java.util.Objects;
        return true;
    }

    @Override
    public synchronized void setVolume(long requestId, @NonNull String routeId, int volume) {
        // Do nothing
    }

    private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) {
        int name = R.string.default_audio_route_name;
        int type = TYPE_BUILTIN_SPEAKER;
Loading