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

Commit 3aedb245 authored by Angela Wang's avatar Angela Wang
Browse files

Seperated ambient mute state for different devices

In previous design, we'll sync the remote devices' mute states from the
phone side since we only have one icon to control them. However, there's
a user need to have seperated mute states. As a short term solution,
we update the behavior as followings:

1. Respect remote devices' mute states without any synchronization from
   phone side
2. Show the muted icon only when both devices are muted
3. If remote mute states are different, expand the controls to let user
   clearly know which device is muted by setting the slider to minimum
   value
4. Clicking on the volume icon will mute/unmute both devices as before

Flag: EXEMPT bugfix
Bug: 412968925
Test: manually test with real devices
Test: atest AmbientVolumeUiControllerTest
Change-Id: I7d45bf7daad6b105cc61c40d57192983977b0275
parent 584acb33
Loading
Loading
Loading
Loading
+0 −6
Original line number Diff line number Diff line
@@ -405,11 +405,5 @@ public class AmbientVolumeController implements LocalBluetoothProfileManager.Ser
    }

    public record RemoteAmbientState(int gainSetting, int mute) {
        public boolean isMutable() {
            return mute != MUTE_DISABLED;
        }
        public boolean isMuted() {
            return mute == MUTE_MUTED;
        }
    }
}
+18 −12
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.settingslib.bluetooth;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;

import android.bluetooth.AudioInputControl;
import android.bluetooth.BluetoothDevice;

import androidx.annotation.NonNull;
@@ -105,20 +106,9 @@ public interface AmbientVolumeUi {
    /** @return if the UI is in expanded mode. */
    boolean isControlExpanded();

    /**
     * Sets if the UI is capable to mute the ambient of the remote device.
     *
     * <p> If the value is {@code false}, it implies the remote device ambient will always be
     * unmute and can not be mute from the UI
     */
    void setMutable(boolean mutable);

    /** @return if the UI is capable to mute the ambient of remote device. */
    boolean isMutable();

    /** Sets if the UI shows mute state. */
    void setMuted(boolean muted);

    /** @return if the UI shows mute state */
    boolean isMuted();

@@ -149,7 +139,7 @@ public interface AmbientVolumeUi {
    void setSliderEnabled(int side, boolean enabled);

    /**
     * Sets the slider value.
     * Sets the slider's value.
     *
     * @param side the side of the slider
     * @param value the ambient value
@@ -165,6 +155,22 @@ public interface AmbientVolumeUi {
     */
    void setSliderRange(int side, int min, int max);

    /**
     * Sets the slider's mute state.
     *
     * @param side the side of the slider
     * @param muteState the mute state, see {@link AudioInputControl.Mute}
     */
    void setSliderMuteState(int side, int muteState);

    /**
     * Gets the slider's mute state.
     *
     * @param side the side of the slider
     * @return the mute state, see {@link AudioInputControl.Mute}
     */
    int getSliderMuteState(int side);

    /** Updates the UI according to current state. */
    void updateLayout();
}
+70 −64
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.settingslib.bluetooth;

import static android.bluetooth.AudioInputControl.MUTE_DISABLED;
import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED;
import static android.bluetooth.AudioInputControl.MUTE_MUTED;
import static android.bluetooth.BluetoothDevice.BOND_BONDED;
@@ -65,6 +66,7 @@ public class AmbientVolumeUiController implements

    private final Set<CachedBluetoothDevice> mCachedDevices = new ArraySet<>();
    private final BiMap<Integer, BluetoothDevice> mSideToDeviceMap = HashBiMap.create();
    private final Set<Integer> mRangeInitializedSliderSides = new ArraySet<>();
    private CachedBluetoothDevice mCachedDevice;
    private boolean mShowUiWhenLocalDataExist = true;

@@ -173,7 +175,7 @@ public class AmbientVolumeUiController implements
    @Override
    public void onExpandIconClick() {
        mSideToDeviceMap.forEach((s, d) -> {
            if (!mAmbientLayout.isMuted()) {
            if (!isDeviceMuted(d)) {
                // Apply previous collapsed/expanded volume to remote device
                HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(d);
                int volume = mAmbientLayout.isControlExpanded()
@@ -181,8 +183,7 @@ public class AmbientVolumeUiController implements
                mVolumeController.setAmbient(d, volume);
            }
            // Update new value to local data
            mLocalDataManager.updateAmbientControlExpanded(d,
                    mAmbientLayout.isControlExpanded());
            mLocalDataManager.updateAmbientControlExpanded(d, mAmbientLayout.isControlExpanded());
        });
        mLocalDataManager.flush();
    }
@@ -213,15 +214,27 @@ public class AmbientVolumeUiController implements
            }
        };

        boolean performUnmuteAction = false;
        if (side == SIDE_UNIFIED) {
            if (mAmbientLayout.isMuted()) {
            // User drag on the volume slider when muted. Unmute the devices first.
            mAmbientLayout.setMuted(false);

                // User drag on the unified slider when muted. Unmute all devices first.
                mAmbientLayout.setSliderMuteState(side, MUTE_NOT_MUTED);
                for (BluetoothDevice device : mSideToDeviceMap.values()) {
                    mVolumeController.setMuted(device, false);
                }
            // Restore the value before muted
            loadLocalDataToUi();
                performUnmuteAction = true;
            }
        } else {
            final BluetoothDevice device = mSideToDeviceMap.get(side);
            if (isDeviceMuted(device)) {
                // User drag on the slider when muted. Unmute the device first.
                mAmbientLayout.setSliderMuteState(side, MUTE_NOT_MUTED);
                mVolumeController.setMuted(device, false);
                performUnmuteAction = true;

            }
        }
        if (performUnmuteAction) {
            // Delay set ambient on remote device since the immediately sequential command
            // might get failed sometimes
            postDelayedOnMainThread(setAmbientRunnable, 1000L);
@@ -342,21 +355,6 @@ public class AmbientVolumeUiController implements
        mShowUiWhenLocalDataExist = shouldShow;
    }

    /** Updates the ambient sliders according to current state. */
    private void updateSliderUi() {
        boolean isAnySliderEnabled = false;
        for (Map.Entry<Integer, BluetoothDevice> entry : mSideToDeviceMap.entrySet()) {
            final int side = entry.getKey();
            final BluetoothDevice device = entry.getValue();
            final boolean enabled = isDeviceConnectedToVcp(device)
                    && mVolumeController.isAmbientControlAvailable(device);
            isAnySliderEnabled |= enabled;
            mAmbientLayout.setSliderEnabled(side, enabled);
        }
        mAmbientLayout.setSliderEnabled(SIDE_UNIFIED, isAnySliderEnabled);
        mAmbientLayout.updateLayout();
    }

    /** Sets the ambient to the corresponding control slider. */
    private void setVolumeIfValid(int side, int volume) {
        if (volume == INVALID_VOLUME) {
@@ -381,13 +379,12 @@ public class AmbientVolumeUiController implements
        if (DEBUG) {
            Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device);
        }
        if (isDeviceConnectedToVcp(device) && !mAmbientLayout.isMuted()) {
        if (isDeviceAmbientControlAvailable(device) && !isDeviceMuted(device)) {
            final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID);
            setVolumeIfValid(side, data.ambient());
            setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient());
        }
        setAmbientControlExpanded(data.ambientControlExpanded());
        updateSliderUi();
    }

    private void loadRemoteDataToUi() {
@@ -400,15 +397,30 @@ public class AmbientVolumeUiController implements
        if (DEBUG) {
            Log.d(TAG, "loadRemoteDataToUi, left=" + leftState + ", right=" + rightState);
        }
        // Update ambient range. This should be done first since the muted state and enabled state
        // will set the value to minimum value
        mSideToDeviceMap.forEach((side, device) -> {
            if (!mRangeInitializedSliderSides.contains(side)) {
                int ambientMax = mVolumeController.getAmbientMax(device);
                int ambientMin = mVolumeController.getAmbientMin(device);
                if (ambientMin != ambientMax) {
                    mAmbientLayout.setSliderRange(side, ambientMin, ambientMax);
                    mAmbientLayout.setSliderRange(SIDE_UNIFIED, ambientMin, ambientMax);
                    mRangeInitializedSliderSides.add(side);
                }
            }
        });

        // Check the remote mute state to decide if we need to expand the control. This should be
        // done before updating ambient value since it'll affect the controls expanded state
        final int leftMuteState = leftState != null ? leftState.mute() : MUTE_DISABLED;
        final int rightMuteState = rightState != null ? rightState.mute() : MUTE_DISABLED;
        if (leftMuteState != MUTE_DISABLED && rightMuteState != MUTE_DISABLED
                && leftMuteState != rightMuteState) {
            // Expand the controls if two devices are mutable but with different mute states
            setAmbientControlExpanded(true);
        }

        // Update ambient volume
        final int leftAmbient = leftState != null ? leftState.gainSetting() : INVALID_VOLUME;
        final int rightAmbient = rightState != null ? rightState.gainSetting() : INVALID_VOLUME;
@@ -416,8 +428,9 @@ public class AmbientVolumeUiController implements
            setVolumeIfValid(SIDE_LEFT, leftAmbient);
            setVolumeIfValid(SIDE_RIGHT, rightAmbient);
        } else {
            if (leftAmbient != rightAmbient && leftAmbient != INVALID_VOLUME
                    && rightAmbient != INVALID_VOLUME) {
            if (leftAmbient != INVALID_VOLUME && rightAmbient != INVALID_VOLUME
                    && leftAmbient != rightAmbient) {
                // Expand the controls if two devices have different ambient values
                setVolumeIfValid(SIDE_LEFT, leftAmbient);
                setVolumeIfValid(SIDE_RIGHT, rightAmbient);
                setAmbientControlExpanded(true);
@@ -429,25 +442,24 @@ public class AmbientVolumeUiController implements
        // Initialize local data between side and group value
        initLocalAmbientDataIfNeeded();

        // Update mute state
        boolean mutable = true;
        boolean muted = true;
        if (isDeviceConnectedToVcp(leftDevice) && leftState != null) {
            mutable &= leftState.isMutable();
            muted &= leftState.isMuted();
        }
        if (isDeviceConnectedToVcp(rightDevice) && rightState != null) {
            mutable &= rightState.isMutable();
            muted &= rightState.isMuted();
        }
        mAmbientLayout.setMutable(mutable);
        mAmbientLayout.setMuted(muted);

        // Ensure remote device mute state is synced
        syncMuteStateIfNeeded(leftDevice, leftState, muted);
        syncMuteStateIfNeeded(rightDevice, rightState, muted);
        // Update slider mute state. This should be done after loading remote ambient into local
        // database since we'll show minimum value of the slider instead of the remote value if the
        // device is muted
        mAmbientLayout.setSliderMuteState(SIDE_LEFT, leftMuteState);
        mAmbientLayout.setSliderMuteState(SIDE_RIGHT, rightMuteState);

        updateSliderUi();
        // Update slider enabled state. This should be done after loading remote ambient into local
        // database since we'll show minimum value of the slider instead of the remote value if the
        // slider is not enabled.
        boolean isAnySliderEnabled = false;
        for (Map.Entry<Integer, BluetoothDevice> entry : mSideToDeviceMap.entrySet()) {
            final int side = entry.getKey();
            final BluetoothDevice device = entry.getValue();
            final boolean enabled = isDeviceAmbientControlAvailable(device);
            isAnySliderEnabled |= enabled;
            mAmbientLayout.setSliderEnabled(side, enabled);
        }
        mAmbientLayout.setSliderEnabled(SIDE_UNIFIED, isAnySliderEnabled);
    }

    private void setAmbientControlExpanded(boolean expanded) {
@@ -469,7 +481,7 @@ public class AmbientVolumeUiController implements
                }
            }
            // Found remote ambient control points
            if (mVolumeController.isAmbientControlAvailable(device)) {
            if (isDeviceAmbientControlAvailable(device)) {
                return true;
            }
        }
@@ -499,19 +511,13 @@ public class AmbientVolumeUiController implements
        mLocalDataManager.flush();
    }

    private void syncMuteStateIfNeeded(@Nullable BluetoothDevice device,
            @Nullable AmbientVolumeController.RemoteAmbientState state, boolean muted) {
        if (isDeviceConnectedToVcp(device) && state != null && state.isMutable()) {
            if (state.isMuted() != muted) {
                mVolumeController.setMuted(device, muted);
            }
        }
    private boolean isDeviceMuted(BluetoothDevice device) {
        final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID);
        return mAmbientLayout.getSliderMuteState(side) == MUTE_MUTED;
    }

    private boolean isDeviceConnectedToVcp(@Nullable BluetoothDevice device) {
        return device != null && device.isConnected()
                && mProfileManager.getVolumeControlProfile().getConnectionStatus(device)
                == BluetoothProfile.STATE_CONNECTED;
    private boolean isDeviceAmbientControlAvailable(BluetoothDevice device) {
        return device.isConnected() && mVolumeController.isAmbientControlAvailable(device);
    }

    private void postOnMainThread(Runnable runnable) {
+4 −17
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.settingslib.bluetooth;

import static android.bluetooth.AudioInputControl.MUTE_DISABLED;
import static android.bluetooth.AudioInputControl.MUTE_MUTED;
import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED;
import static android.bluetooth.BluetoothDevice.BOND_BONDED;
@@ -257,26 +256,14 @@ public class AmbientVolumeUiControllerTest {
    }

    @Test
    public void refresh_oneSideNotMutable_controlNotMutableAndNotMuted() {
        prepareRemoteData(mDevice, 10, MUTE_DISABLED);
        prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED);

        mController.refresh();

        verify(mAmbientLayout).setMutable(false);
        verify(mAmbientLayout).setMuted(false);
    }

    @Test
    public void refresh_oneSideNotMuted_controlNotMutedAndSyncToRemote() {
    public void refresh_leftAndRightDifferentMuteState_expandControl() {
        prepareRemoteData(mDevice, 10, MUTE_MUTED);
        prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED);
        prepareRemoteData(mMemberDevice, 10, MUTE_NOT_MUTED);
        when(mAmbientLayout.isControlExpanded()).thenReturn(false);

        mController.refresh();

        verify(mAmbientLayout).setMutable(true);
        verify(mAmbientLayout).setMuted(false);
        verify(mVolumeController).setMuted(mDevice, false);
        verify(mAmbientLayout).setControlExpanded(true);
    }

    private void prepareDevice(boolean hasMember) {