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

Commit 0c4d7aec authored by Rongxuan Liu's avatar Rongxuan Liu Committed by Gerrit Code Review
Browse files

Merge "[le audio] Add Volume Control APIs for connected devices in broadcast" into main

parents f2a768a6 2d3a1f68
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
@@ -1089,12 +1089,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