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

Commit 2d3a1f68 authored by Rongxuan Liu's avatar Rongxuan Liu
Browse files

[le audio] Add Volume Control APIs for connected devices in broadcast

This commit includes
1. Add new device volume control API and callback
2. Volume changed callback is invoked for newly registered client
3. Volume change is only updated to audio system if it was applied for
   active devices

Flagged by: leaudio_broadcast_volume_control_for_connected_devices and
enable by adb command
adb shell device_config put bluetooth com.android.bluetooth.flags.leaudio_broadcast_volume_control_for_connected_devices true

Bug: 298661871
Bug: 307408418
Test: atest BluetoothVolumeControlTest VolumeControlServiceTest
Test: manual test callback with LeAudioTestApp
Change-Id: Ibcc35c7a5e9a5cb38ed9b8373d6a128045aae36f
parent 251d9183
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -58,7 +58,8 @@ oneway interface IBluetoothVolumeControl {
    void getGroupVolume(int group_id, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
    void setGroupActive(int group_id, boolean active, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);

    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
    void setDeviceVolume(in BluetoothDevice device, int volume, boolean isGroupOp, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);

    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
    void mute(in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver);
+1 −0
Original line number Diff line number Diff line
@@ -27,4 +27,5 @@ import java.util.List;
 */
oneway interface IBluetoothVolumeControlCallback {
    void onVolumeOffsetChanged(in BluetoothDevice device, in int volumeOffset);
    void onDeviceVolumeChanged(in BluetoothDevice device, in int volume);
}
+194 −9
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@
package com.android.bluetooth.vc;

import static android.Manifest.permission.BLUETOOTH_CONNECT;

import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission;

import android.annotation.RequiresPermission;
@@ -49,15 +50,19 @@ import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.ServiceFactory;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.flags.FeatureFlags;
import com.android.bluetooth.flags.FeatureFlagsImpl;
import com.android.bluetooth.le_audio.LeAudioService;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.SynchronousResultReceiver;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

public class VolumeControlService extends ProfileService {
    private static final boolean DBG = false;
@@ -78,6 +83,7 @@ public class VolumeControlService extends ProfileService {
    private HandlerThread mStateMachinesThread;
    private BluetoothDevice mPreviousAudioDevice;
    private Handler mHandler = null;
    private FeatureFlags mFeatureFlags;

    @VisibleForTesting
    RemoteCallbackList<IBluetoothVolumeControlCallback> mCallbacks;
@@ -210,12 +216,24 @@ public class VolumeControlService extends ProfileService {
                                                                            new HashMap<>();
    private final Map<Integer, Integer> mGroupVolumeCache = new HashMap<>();
    private final Map<Integer, Boolean> mGroupMuteCache = new HashMap<>();
    private final Map<BluetoothDevice, Integer> mDeviceVolumeCache = new HashMap<>();

    private BroadcastReceiver mBondStateChangedReceiver;

    @VisibleForTesting
    ServiceFactory mFactory = new ServiceFactory();

    VolumeControlService() {
        mFeatureFlags = new FeatureFlagsImpl();
    }

    @VisibleForTesting
    VolumeControlService(Context ctx, FeatureFlags featureFlags) {
        attachBaseContext(ctx);
        mFeatureFlags = featureFlags;
        onCreate();
    }

    public static boolean isEnabled() {
        return BluetoothProperties.isProfileVcpControllerEnabled().orElse(false);
    }
@@ -270,6 +288,7 @@ public class VolumeControlService extends ProfileService {
        mAudioOffsets.clear();
        mGroupVolumeCache.clear();
        mGroupMuteCache.clear();
        mDeviceVolumeCache.clear();
        mCallbacks = new RemoteCallbackList<IBluetoothVolumeControlCallback>();

        // Mark service as started
@@ -330,6 +349,7 @@ public class VolumeControlService extends ProfileService {
        mAudioOffsets.clear();
        mGroupVolumeCache.clear();
        mGroupMuteCache.clear();
        mDeviceVolumeCache.clear();

        // Clear AdapterService, VolumeControlNativeInterface
        mAudioManager = null;
@@ -609,6 +629,41 @@ public class VolumeControlService extends ProfileService {
        mVolumeControlNativeInterface.setExtAudioOutVolumeOffset(device, 1, volumeOffset);
    }

    void setDeviceVolume(BluetoothDevice device, int volume, boolean isGroupOp) {
        if (!mFeatureFlags.leaudioBroadcastVolumeControlForConnectedDevices()) {
            return;
        }
        if (DBG) {
            Log.d(
                    TAG,
                    "setDeviceVolume: "
                            + device
                            + ", volume: "
                            + volume
                            + ", isGroupOp: "
                            + isGroupOp);
        }

        LeAudioService leAudioService = mFactory.getLeAudioService();
        if (leAudioService == null) {
            Log.e(TAG, "leAudioService not available");
            return;
        }
        int groupId = leAudioService.getGroupId(device);
        if (groupId == IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID) {
            Log.e(TAG, "Device not a part of a group");
            return;
        }

        if (isGroupOp) {
            setGroupVolume(groupId, volume);
        } else {
            Log.i(TAG, "Setting individual device volume");
            mDeviceVolumeCache.put(device, volume);
            mVolumeControlNativeInterface.setVolume(device, volume);
        }
    }

    /**
     * {@hide}
     */
@@ -657,6 +712,18 @@ public class VolumeControlService extends ProfileService {
                        IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME);
    }

    /**
     * Get device cached volume.
     *
     * @param device the device
     * @return the cached volume
     * @hide
     */
    public int getDeviceVolume(BluetoothDevice device) {
        return mDeviceVolumeCache.getOrDefault(
                device, IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME);
    }

    /**
     * This should be called by LeAudioService when LE Audio group change it
     * active state.
@@ -718,15 +785,15 @@ public class VolumeControlService extends ProfileService {
        mVolumeControlNativeInterface.unmuteGroup(groupId);
    }

    void notifyNewCallbackOfKnownVolumeOffsets(IBluetoothVolumeControlCallback callback) {
    void notifyNewCallbackOfKnownVolumeInfo(IBluetoothVolumeControlCallback callback) {
        if (DBG) {
            Log.d(TAG, "notifyNewCallbackOfKnownVolumeOffsets");
            Log.d(TAG, "notifyNewCallbackOfKnownVolumeInfo");
        }

        RemoteCallbackList<IBluetoothVolumeControlCallback> tempCallbackList =
                new RemoteCallbackList<>();
        if (tempCallbackList == null) {
            Log.w(TAG, "notifyNewCallbackOfKnownVolumeOffsets: tempCallbackList not available");
            Log.w(TAG, "notifyNewCallbackOfKnownVolumeInfo: tempCallbackList not available");
            return;
        }

@@ -736,10 +803,11 @@ public class VolumeControlService extends ProfileService {
        int n = tempCallbackList.beginBroadcast();
        if (n != 1) {
            /* There should be only one calback in this place. */
            Log.e(TAG, "notifyNewCallbackOfKnownVolumeOffsets: Shall be 1 but it is " + n);
            Log.e(TAG, "notifyNewCallbackOfKnownVolumeInfo: Shall be 1 but it is " + n);
        }

        for (int i = 0; i < n; i++) {
            // notify volume offset
            for (Map.Entry<BluetoothDevice, VolumeControlOffsetDescriptor> entry :
                    mAudioOffsets.entrySet()) {
                VolumeControlOffsetDescriptor descriptor = entry.getValue();
@@ -751,7 +819,9 @@ public class VolumeControlService extends ProfileService {
                int offset = descriptor.getFirstOffsetValue();

                if (DBG) {
                    Log.d(TAG, "notifyNewCallbackOfKnownVolumeOffsets: " + device + ", " + offset);
                    Log.d(
                            TAG,
                            "notifyNewCallbackOfKnownVolumeInfo offset: " + device + ", " + offset);
                }

                try {
@@ -760,6 +830,10 @@ public class VolumeControlService extends ProfileService {
                    continue;
                }
            }
            // notify volume level for all vc devices
            if (mFeatureFlags.leaudioBroadcastVolumeControlForConnectedDevices()) {
                notifyDevicesVolumeChanged(getDevices(), Optional.empty());
            }
        }

        tempCallbackList.finishBroadcast();
@@ -772,7 +846,7 @@ public class VolumeControlService extends ProfileService {
        /* Here we keep all the user callbacks */
        mCallbacks.register(callback);

        notifyNewCallbackOfKnownVolumeOffsets(callback);
        notifyNewCallbackOfKnownVolumeInfo(callback);
    }

    /**
@@ -832,9 +906,26 @@ public class VolumeControlService extends ProfileService {
        mGroupVolumeCache.put(groupId, volume);
        mGroupMuteCache.put(groupId, mute);

        if (mFeatureFlags.leaudioBroadcastVolumeControlForConnectedDevices()) {
            LeAudioService leAudioService = mFactory.getLeAudioService();
            if (leAudioService != null) {
                int currentlyActiveGroupId = leAudioService.getActiveGroupId();
                if (currentlyActiveGroupId == IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID
                        || groupId != currentlyActiveGroupId) {
                    Log.i(
                            TAG,
                            "Skip updating to audio system if not updating volume for current"
                                    + " active group");
                    return;
                }
            } else {
                Log.w(TAG, "leAudioService not available");
            }
        }

        int streamType = getBluetoothContextualVolumeStream();
        int flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_BLUETOOTH_ABS_VOLUME;
        mAudioManager.setStreamVolume(streamType, getDeviceVolume(streamType, volume), flags);
        mAudioManager.setStreamVolume(streamType, getAudioDeviceVolume(streamType, volume), flags);

        if (mAudioManager.isStreamMute(streamType) != mute) {
            int adjustment = mute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE;
@@ -867,6 +958,23 @@ public class VolumeControlService extends ProfileService {
        int groupVolume = getGroupVolume(groupId);
        Boolean groupMute = getGroupMute(groupId);

        if (mFeatureFlags.leaudioBroadcastVolumeControlForConnectedDevices()) {
            Log.i(TAG, "handleVolumeControlChanged: " + device + "; volume: " + volume);
            if (device == null) {
                // notify group devices volume changed
                LeAudioService leAudioService = mFactory.getLeAudioService();
                if (leAudioService != null) {
                    notifyDevicesVolumeChanged(
                            leAudioService.getGroupDevices(groupId), Optional.of(volume));
                } else {
                    Log.w(TAG, "leAudioService not available");
                }
            } else {
                // notify device volume changed
                notifyDevicesVolumeChanged(Arrays.asList(device), Optional.of(volume));
            }
        }

        if (groupVolume == IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) {
            /* We are here, because system was just started and LeAudio device just connected.
             * In such case, we take Volume stored on remote device and apply it to our cache and
@@ -933,10 +1041,10 @@ public class VolumeControlService extends ProfileService {
        }

        if (volume == IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) return -1;
        return getDeviceVolume(getBluetoothContextualVolumeStream(), volume);
        return getAudioDeviceVolume(getBluetoothContextualVolumeStream(), volume);
    }

    int getDeviceVolume(int streamType, int bleVolume) {
    int getAudioDeviceVolume(int streamType, int bleVolume) {
        int deviceMaxVolume = mAudioManager.getStreamMaxVolume(streamType);

        // TODO: Investigate what happens in classic BT when BT volume is changed to zero.
@@ -1137,6 +1245,61 @@ public class VolumeControlService extends ProfileService {
        }
    }

    /**
     * Notify devices with volume level
     *
     * <p>In case of handleVolumeControlChanged, volume level is known from native layer caller.
     * Notify the clients with the volume level directly and update the volume cache. In case of
     * newly registered callback, volume level is unknown from caller, notify the clients with
     * cached volume level from either device or group.
     *
     * @param devices list of devices to notify volume changed
     * @param volume volume level
     */
    private void notifyDevicesVolumeChanged(
            List<BluetoothDevice> devices, Optional<Integer> volume) {
        if (mCallbacks == null) {
            Log.e(TAG, "mCallbacks is null");
            return;
        }

        LeAudioService leAudioService = mFactory.getLeAudioService();
        if (leAudioService == null) {
            Log.e(TAG, "leAudioService not available");
            return;
        }

        for (BluetoothDevice dev : devices) {
            int cachedVolume = IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME;
            if (!volume.isPresent()) {
                int groupId = leAudioService.getGroupId(dev);
                if (groupId == IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID) {
                    Log.e(TAG, "Device not a part of a group");
                    continue;
                }
                // if device volume is available, notify with device volume, otherwise group volume
                cachedVolume = getDeviceVolume(dev);
                if (cachedVolume == IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) {
                    cachedVolume = getGroupVolume(groupId);
                }
            }
            int n = mCallbacks.beginBroadcast();
            for (int i = 0; i < n; i++) {
                try {
                    if (!volume.isPresent()) {
                        mCallbacks.getBroadcastItem(i).onDeviceVolumeChanged(dev, cachedVolume);
                    } else {
                        mDeviceVolumeCache.put(dev, volume.get());
                        mCallbacks.getBroadcastItem(i).onDeviceVolumeChanged(dev, volume.get());
                    }
                } catch (RemoteException e) {
                    continue;
                }
            }
            mCallbacks.finishBroadcast();
        }
    }

    // Remove state machine if the bonding for a device is removed
    private class BondStateChangedReceiver extends BroadcastReceiver {
        @Override
@@ -1451,6 +1614,28 @@ public class VolumeControlService extends ProfileService {
            }
        }

        @Override
        public void setDeviceVolume(
                BluetoothDevice device,
                int volume,
                boolean isGroupOp,
                AttributionSource source,
                SynchronousResultReceiver receiver) {
            try {
                Objects.requireNonNull(device, "device cannot be null");
                Objects.requireNonNull(source, "source cannot be null");
                Objects.requireNonNull(receiver, "receiver cannot be null");

                VolumeControlService service = getService(source);
                if (service != null) {
                    service.setDeviceVolume(device, volume, isGroupOp);
                }
                receiver.send(null);
            } catch (RuntimeException e) {
                receiver.propagateException(e);
            }
        }

        @Override
        public void setGroupVolume(int groupId, int volume, AttributionSource source,
                SynchronousResultReceiver receiver) {
+186 −15

File changed.

Preview size limit exceeded, changes collapsed.

+2 −0
Original line number Diff line number Diff line
@@ -1087,12 +1087,14 @@ package android.bluetooth {
    method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public boolean isVolumeOffsetAvailable(@NonNull android.bluetooth.BluetoothDevice);
    method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.bluetooth.BluetoothVolumeControl.Callback);
    method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public boolean setConnectionPolicy(@NonNull android.bluetooth.BluetoothDevice, int);
    method @FlaggedApi("com.android.bluetooth.flags.leaudio_broadcast_volume_control_for_connected_devices") @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void setDeviceVolume(@NonNull android.bluetooth.BluetoothDevice, @IntRange(from=0xffffff01, to=255) int, boolean);
    method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void setVolumeOffset(@NonNull android.bluetooth.BluetoothDevice, @IntRange(from=0xffffff01, to=255) int);
    method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void unregisterCallback(@NonNull android.bluetooth.BluetoothVolumeControl.Callback);
    field @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public static final String ACTION_CONNECTION_STATE_CHANGED = "android.bluetooth.volume-control.profile.action.CONNECTION_STATE_CHANGED";
  }

  public static interface BluetoothVolumeControl.Callback {
    method @FlaggedApi("com.android.bluetooth.flags.leaudio_broadcast_volume_control_for_connected_devices") public default void onDeviceVolumeChanged(@NonNull android.bluetooth.BluetoothDevice, @IntRange(from=0xffffff01, to=255) int);
    method public void onVolumeOffsetChanged(@NonNull android.bluetooth.BluetoothDevice, @IntRange(from=0xffffff01, to=255) int);
  }

Loading