[Ambient Volume] Ambient volume icon
1. Click on the icon on the header will mute/unmute the remote devices in the same set if the remote devices are both mutable.
2. Show different icon when the value of the volume slide bars changed.
Flag: com.android.settingslib.flags.hearing_devices_ambient_volume_control
Bug: 357878944
Test: atest AmbientVolumePreferenceTest
Test: atest BluetoothDetailsAmbientVolumePreferenceControllerTest
Change-Id: I829c5e08f1456c8ef9936d0314045dc8da37cd95
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5ab38a4..3300cc6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -174,6 +174,10 @@
<string name="bluetooth_ambient_volume_control_left">Left</string>
<!-- Connected devices settings. The text to show the control is for right side device. [CHAR LIMIT=30] -->
<string name="bluetooth_ambient_volume_control_right">Right</string>
+ <!-- Connected devices settings. Content description for a button, that mute ambient volume [CHAR_LIMIT=NONE] -->
+ <string name="bluetooth_ambient_volume_mute">Mute surroundings</string>
+ <!-- Connected devices settings. Content description for a button, that unmute ambient volume [CHAR LIMIT=NONE] -->
+ <string name="bluetooth_ambient_volume_unmute">Unmute surroundings</string>
<!-- Message when changing ambient state failed. [CHAR LIMIT=NONE] -->
<string name="bluetooth_ambient_volume_error">Couldn\u2019t update surroundings</string>
<!-- Connected devices settings. Title of the preference to show the entrance of the audio output page. It can change different types of audio are played on phone or other bluetooth devices. [CHAR LIMIT=35] -->
diff --git a/src/com/android/settings/bluetooth/AmbientVolumePreference.java b/src/com/android/settings/bluetooth/AmbientVolumePreference.java
index 0122203..e916c04 100644
--- a/src/com/android/settings/bluetooth/AmbientVolumePreference.java
+++ b/src/com/android/settings/bluetooth/AmbientVolumePreference.java
@@ -17,6 +17,8 @@
package com.android.settings.bluetooth;
import static android.view.View.GONE;
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO;
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
import static android.view.View.VISIBLE;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT;
@@ -25,6 +27,7 @@
import android.content.Context;
import android.util.ArrayMap;
import android.view.View;
+import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -34,6 +37,8 @@
import com.android.settings.R;
import com.android.settings.widget.SeekBarPreference;
+import com.google.common.primitives.Ints;
+
import java.util.List;
import java.util.Map;
@@ -50,10 +55,16 @@
public interface OnIconClickListener {
/** Called when the expand icon is clicked. */
void onExpandIconClick();
+
+ /** Called when the ambient volume icon is clicked. */
+ void onAmbientVolumeIconClick();
};
static final float ROTATION_COLLAPSED = 0f;
static final float ROTATION_EXPANDED = 180f;
+ static final int AMBIENT_VOLUME_LEVEL_MIN = 0;
+ static final int AMBIENT_VOLUME_LEVEL_MAX = 24;
+ static final int AMBIENT_VOLUME_LEVEL_DEFAULT = 24;
static final int SIDE_UNIFIED = 999;
static final List<Integer> VALID_SIDES = List.of(SIDE_UNIFIED, SIDE_LEFT, SIDE_RIGHT);
@@ -61,10 +72,33 @@
private OnIconClickListener mListener;
@Nullable
private View mExpandIcon;
+ @Nullable
+ private ImageView mVolumeIcon;
private boolean mExpandable = true;
private boolean mExpanded = false;
+ private boolean mMutable = false;
+ private boolean mMuted = false;
private Map<Integer, SeekBarPreference> mSideToSliderMap = new ArrayMap<>();
+ /**
+ * Ambient volume level for hearing device ambient control icon
+ * <p>
+ * This icon visually represents the current ambient gain setting.
+ * It displays separate levels for the left and right sides, each with 5 levels ranging from 0
+ * to 4.
+ * <p>
+ * To represent the combined left/right levels with a single value, the following calculation
+ * is used:
+ * finalLevel = (leftLevel * 5) + rightLevel
+ * For example:
+ * <ul>
+ * <li>If left level is 2 and right level is 3, the final level will be 13 (2 * 5 + 3)</li>
+ * <li>If both left and right levels are 0, the final level will be 0</li>
+ * <li>If both left and right levels are 4, the final level will be 24</li>
+ * </ul>
+ */
+ private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT;
+
public AmbientVolumePreference(@NonNull Context context) {
super(context, null);
setLayoutResource(R.layout.preference_ambient_volume);
@@ -79,6 +113,21 @@
holder.setDividerAllowedAbove(false);
holder.setDividerAllowedBelow(false);
+ mVolumeIcon = holder.itemView.requireViewById(com.android.internal.R.id.icon);
+ mVolumeIcon.getDrawable().mutate().setTint(getContext().getColor(
+ com.android.internal.R.color.materialColorOnPrimaryContainer));
+ final View iconView = holder.itemView.requireViewById(R.id.icon_frame);
+ iconView.setOnClickListener(v -> {
+ if (!mMutable) {
+ return;
+ }
+ setMuted(!mMuted);
+ if (mListener != null) {
+ mListener.onAmbientVolumeIconClick();
+ }
+ });
+ updateVolumeIcon();
+
mExpandIcon = holder.itemView.requireViewById(R.id.expand_icon);
mExpandIcon.setOnClickListener(v -> {
setExpanded(!mExpanded);
@@ -114,6 +163,36 @@
return mExpanded;
}
+ void setMutable(boolean mutable) {
+ mMutable = mutable;
+ if (!mMutable) {
+ mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT;
+ setMuted(false);
+ }
+ updateVolumeIcon();
+ }
+
+ boolean isMutable() {
+ return mMutable;
+ }
+
+ void setMuted(boolean muted) {
+ if (!mMutable && muted) {
+ return;
+ }
+ mMuted = muted;
+ if (mMutable && mMuted) {
+ for (SeekBarPreference slider : mSideToSliderMap.values()) {
+ slider.setProgress(slider.getMin());
+ }
+ }
+ updateVolumeIcon();
+ }
+
+ boolean isMuted() {
+ return mMuted;
+ }
+
void setOnIconClickListener(@Nullable OnIconClickListener listener) {
mListener = listener;
}
@@ -140,6 +219,7 @@
SeekBarPreference slider = mSideToSliderMap.get(side);
if (slider != null && slider.getProgress() != value) {
slider.setProgress(value);
+ updateVolumeLevel();
}
}
@@ -162,6 +242,34 @@
slider.setProgress(slider.getMin());
}
});
+ updateVolumeLevel();
+ }
+
+ private void updateVolumeLevel() {
+ int leftLevel, rightLevel;
+ if (mExpanded) {
+ leftLevel = getVolumeLevel(SIDE_LEFT);
+ rightLevel = getVolumeLevel(SIDE_RIGHT);
+ } else {
+ final int unifiedLevel = getVolumeLevel(SIDE_UNIFIED);
+ leftLevel = unifiedLevel;
+ rightLevel = unifiedLevel;
+ }
+ mVolumeLevel = Ints.constrainToRange(leftLevel * 5 + rightLevel,
+ AMBIENT_VOLUME_LEVEL_MIN, AMBIENT_VOLUME_LEVEL_MAX);
+ updateVolumeIcon();
+ }
+
+ private int getVolumeLevel(int side) {
+ SeekBarPreference slider = mSideToSliderMap.get(side);
+ if (slider == null || !slider.isEnabled()) {
+ return 0;
+ }
+ final double min = slider.getMin();
+ final double max = slider.getMax();
+ final double levelGap = (max - min) / 4.0;
+ final int value = slider.getProgress();
+ return (int) Math.ceil((value - min) / levelGap);
}
private void updateExpandIcon() {
@@ -179,4 +287,21 @@
mExpandIcon.setContentDescription(null);
}
}
+
+ private void updateVolumeIcon() {
+ if (mVolumeIcon == null) {
+ return;
+ }
+ mVolumeIcon.setImageLevel(mMuted ? 0 : mVolumeLevel);
+ if (mMutable) {
+ final int stringRes = mMuted
+ ? R.string.bluetooth_ambient_volume_unmute
+ : R.string.bluetooth_ambient_volume_mute;
+ mVolumeIcon.setContentDescription(getContext().getString(stringRes));
+ mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ } else {
+ mVolumeIcon.setContentDescription(null);
+ mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ }
+ }
}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java
index 887c220..f237ffe 100644
--- a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java
@@ -16,6 +16,8 @@
package com.android.settings.bluetooth;
+import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED;
+import static android.bluetooth.AudioInputControl.MUTE_MUTED;
import static android.bluetooth.BluetoothDevice.BOND_BONDED;
import static com.android.settings.bluetooth.AmbientVolumePreference.SIDE_UNIFIED;
@@ -180,9 +182,8 @@
@Override
public boolean isAvailable() {
- boolean isDeviceSupportVcp = mCachedDevice.getProfiles().stream().anyMatch(
+ return mCachedDevice.getProfiles().stream().anyMatch(
profile -> profile instanceof VolumeControlProfile);
- return isDeviceSupportVcp;
}
@Nullable
@@ -200,11 +201,30 @@
}
setVolumeIfValid(side, value);
- if (side == SIDE_UNIFIED) {
- mSideToDeviceMap.forEach((s, d) -> mVolumeController.setAmbient(d, value));
+ Runnable setAmbientRunnable = () -> {
+ if (side == SIDE_UNIFIED) {
+ mSideToDeviceMap.forEach((s, d) -> mVolumeController.setAmbient(d, value));
+ } else {
+ final BluetoothDevice device = mSideToDeviceMap.get(side);
+ mVolumeController.setAmbient(device, value);
+ }
+ };
+
+ if (isControlMuted()) {
+ // User drag on the volume slider when muted. Unmute the devices first.
+ if (mPreference != null) {
+ mPreference.setMuted(false);
+ }
+ for (BluetoothDevice device : mSideToDeviceMap.values()) {
+ mVolumeController.setMuted(device, false);
+ }
+ // Restore the value before muted
+ loadLocalDataToUi();
+ // Delay set ambient on remote device since the immediately sequential command
+ // might get failed sometimes
+ mContext.getMainThreadHandler().postDelayed(setAmbientRunnable, 1000L);
} else {
- final BluetoothDevice device = mSideToDeviceMap.get(side);
- mVolumeController.setAmbient(device, value);
+ setAmbientRunnable.run();
}
return true;
}
@@ -285,6 +305,24 @@
}
@Override
+ public void onMuteChanged(@NonNull BluetoothDevice device, int mute) {
+ if (DEBUG) {
+ Log.d(TAG, "onMuteChanged, mute:" + mute + ", device:" + device);
+ }
+ boolean isInitiatedFromUi = (isControlMuted() && mute == MUTE_MUTED)
+ || (!isControlMuted() && mute == MUTE_NOT_MUTED);
+ if (isInitiatedFromUi) {
+ // The change is initiated from UI, no need to update UI
+ return;
+ }
+
+ // We have to check if we need to mute the devices by getting all remote
+ // device's mute state, delay for a while to wait all remote devices update
+ // to the latest value.
+ mContext.getMainThreadHandler().postDelayed(this::refresh, 1200L);
+ }
+
+ @Override
public void onCommandFailed(@NonNull BluetoothDevice device) {
Log.w(TAG, "onCommandFailed, device:" + device);
mContext.getMainExecutor().execute(() -> {
@@ -324,17 +362,33 @@
mPreference = new AmbientVolumePreference(mDeviceControls.getContext());
mPreference.setKey(KEY_AMBIENT_VOLUME);
mPreference.setOrder(ORDER_AMBIENT_VOLUME);
- mPreference.setOnIconClickListener(() -> {
- mSideToDeviceMap.forEach((s, d) -> {
- // Apply previous collapsed/expanded volume to remote device
- Data data = mLocalDataManager.get(d);
- int volume = isControlExpanded()
- ? data.ambient() : data.groupAmbient();
- mVolumeController.setAmbient(d, volume);
- // Update new value to local data
- mLocalDataManager.updateAmbientControlExpanded(d, isControlExpanded());
- });
- });
+ mPreference.setOnIconClickListener(
+ new AmbientVolumePreference.OnIconClickListener() {
+ @Override
+ public void onExpandIconClick() {
+ mSideToDeviceMap.forEach((s, d) -> {
+ if (!isControlMuted()) {
+ // Apply previous collapsed/expanded volume to remote device
+ Data data = mLocalDataManager.get(d);
+ int volume = isControlExpanded()
+ ? data.ambient() : data.groupAmbient();
+ mVolumeController.setAmbient(d, volume);
+ }
+ // Update new value to local data
+ mLocalDataManager.updateAmbientControlExpanded(d, isControlExpanded());
+ });
+ }
+
+ @Override
+ public void onAmbientVolumeIconClick() {
+ if (!isControlMuted()) {
+ loadLocalDataToUi();
+ }
+ for (BluetoothDevice device : mSideToDeviceMap.values()) {
+ mVolumeController.setMuted(device, isControlMuted());
+ }
+ }
+ });
if (mDeviceControls.findPreference(mPreference.getKey()) == null) {
mDeviceControls.addPreference(mPreference);
}
@@ -406,7 +460,7 @@
Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device);
}
final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID);
- if (isDeviceConnectedToVcp(device)) {
+ if (isDeviceConnectedToVcp(device) && !isControlMuted()) {
setVolumeIfValid(side, data.ambient());
setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient());
}
@@ -456,6 +510,26 @@
// Initialize local data between side and group value
initLocalDataIfNeeded();
+ // 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();
+ }
+ if (mPreference != null) {
+ mPreference.setMutable(mutable);
+ mPreference.setMuted(muted);
+ }
+
+ // Ensure remote device mute state is synced
+ syncMuteStateIfNeeded(leftDevice, leftState, muted);
+ syncMuteStateIfNeeded(rightDevice, rightState, muted);
+
refreshControlUi();
}
@@ -488,6 +562,10 @@
});
}
+ private boolean isControlMuted() {
+ return mPreference != null && mPreference.isMuted();
+ }
+
private void initLocalDataIfNeeded() {
int smallerVolumeAmongGroup = Integer.MAX_VALUE;
for (BluetoothDevice device : mSideToDeviceMap.values()) {
@@ -510,6 +588,15 @@
}
}
+ 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 isDeviceConnectedToVcp(@Nullable BluetoothDevice device) {
return device != null && device.isConnected()
&& mBluetoothManager.getProfileManager().getVolumeControlProfile()
diff --git a/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java
index 75f3c9a..ec406c4 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java
@@ -57,6 +57,9 @@
@RunWith(RobolectricTestRunner.class)
public class AmbientVolumePreferenceTest {
+ private static final int TEST_LEFT_VOLUME_LEVEL = 1;
+ private static final int TEST_RIGHT_VOLUME_LEVEL = 2;
+ private static final int TEST_UNIFIED_VOLUME_LEVEL = 3;
private static final String KEY_UNIFIED_SLIDER = KEY_AMBIENT_VOLUME_SLIDER + "_" + SIDE_UNIFIED;
private static final String KEY_LEFT_SLIDER = KEY_AMBIENT_VOLUME_SLIDER + "_" + SIDE_LEFT;
private static final String KEY_RIGHT_SLIDER = KEY_AMBIENT_VOLUME_SLIDER + "_" + SIDE_RIGHT;
@@ -72,6 +75,7 @@
private AmbientVolumePreference mPreference;
private ImageView mExpandIcon;
+ private ImageView mVolumeIcon;
private final Map<Integer, SeekBarPreference> mSideToSlidersMap = new ArrayMap<>();
@Before
@@ -82,13 +86,19 @@
mPreference.setKey(KEY_AMBIENT_VOLUME);
mPreference.setOnIconClickListener(mListener);
mPreference.setExpandable(true);
+ mPreference.setMutable(true);
preferenceScreen.addPreference(mPreference);
prepareSliders();
mPreference.setSliders(mSideToSlidersMap);
mExpandIcon = new ImageView(mContext);
+ mVolumeIcon = new ImageView(mContext);
+ mVolumeIcon.setImageResource(com.android.settingslib.R.drawable.ic_ambient_volume);
+ mVolumeIcon.setImageLevel(0);
when(mItemView.requireViewById(R.id.expand_icon)).thenReturn(mExpandIcon);
+ when(mItemView.requireViewById(com.android.internal.R.id.icon)).thenReturn(mVolumeIcon);
+ when(mItemView.requireViewById(R.id.icon_frame)).thenReturn(mVolumeIcon);
PreferenceViewHolder preferenceViewHolder = PreferenceViewHolder.createInstanceForTests(
mItemView);
@@ -123,6 +133,77 @@
assertControlUiCorrect();
}
+ @Test
+ public void setMutable_mutable_clickOnMuteIconChangeMuteState() {
+ mPreference.setMutable(true);
+ mPreference.setMuted(false);
+
+ mVolumeIcon.callOnClick();
+
+ assertThat(mPreference.isMuted()).isTrue();
+ }
+
+ @Test
+ public void setMutable_notMutable_clickOnMuteIconWontChangeMuteState() {
+ mPreference.setMutable(false);
+ mPreference.setMuted(false);
+
+ mVolumeIcon.callOnClick();
+
+ assertThat(mPreference.isMuted()).isFalse();
+ }
+
+ @Test
+ public void updateLayout_mute_volumeIconIsCorrect() {
+ mPreference.setMuted(true);
+ mPreference.updateLayout();
+
+ assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(0);
+ }
+
+ @Test
+ public void updateLayout_unmuteAndExpanded_volumeIconIsCorrect() {
+ mPreference.setMuted(false);
+ mPreference.setExpanded(true);
+ mPreference.updateLayout();
+
+ int expectedLevel = calculateVolumeLevel(TEST_LEFT_VOLUME_LEVEL, TEST_RIGHT_VOLUME_LEVEL);
+ assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel);
+ }
+
+ @Test
+ public void updateLayout_unmuteAndNotExpanded_volumeIconIsCorrect() {
+ mPreference.setMuted(false);
+ mPreference.setExpanded(false);
+ mPreference.updateLayout();
+
+ int expectedLevel = calculateVolumeLevel(TEST_UNIFIED_VOLUME_LEVEL,
+ TEST_UNIFIED_VOLUME_LEVEL);
+ assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel);
+ }
+
+ @Test
+ public void setSliderEnabled_expandedAndLeftIsDisabled_volumeIconIcCorrect() {
+ mPreference.setExpanded(true);
+ mPreference.setSliderEnabled(SIDE_LEFT, false);
+
+ int expectedLevel = calculateVolumeLevel(0, TEST_RIGHT_VOLUME_LEVEL);
+ assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel);
+ }
+
+ @Test
+ public void setSliderValue_expandedAndLeftValueChanged_volumeIconIcCorrect() {
+ mPreference.setExpanded(true);
+ mPreference.setSliderValue(SIDE_LEFT, 4);
+
+ int expectedLevel = calculateVolumeLevel(4, TEST_RIGHT_VOLUME_LEVEL);
+ assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel);
+ }
+
+ private int calculateVolumeLevel(int left, int right) {
+ return left * 5 + right;
+ }
+
private void assertControlUiCorrect() {
final boolean expanded = mPreference.isExpanded();
assertThat(mSideToSlidersMap.get(SIDE_UNIFIED).isVisible()).isEqualTo(!expanded);
@@ -140,12 +221,17 @@
private void prepareSlider(int side) {
SeekBarPreference slider = new SeekBarPreference(mContext);
+ slider.setMin(0);
+ slider.setMax(4);
if (side == SIDE_LEFT) {
slider.setKey(KEY_LEFT_SLIDER);
+ slider.setProgress(TEST_LEFT_VOLUME_LEVEL);
} else if (side == SIDE_RIGHT) {
slider.setKey(KEY_RIGHT_SLIDER);
+ slider.setProgress(TEST_RIGHT_VOLUME_LEVEL);
} else {
slider.setKey(KEY_UNIFIED_SLIDER);
+ slider.setProgress(TEST_UNIFIED_VOLUME_LEVEL);
}
mSideToSlidersMap.put(side, slider);
}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java
index b7aaab4..975d3b4 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java
@@ -16,6 +16,9 @@
package com.android.settings.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;
import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME;
@@ -71,6 +74,7 @@
import org.robolectric.shadows.ShadowSettings;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -135,6 +139,9 @@
BluetoothProfile.STATE_CONNECTED);
when(mVolumeControlProfile.getConnectionStatus(mMemberDevice)).thenReturn(
BluetoothProfile.STATE_CONNECTED);
+ when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile));
+ when(mLocalDataManager.get(any(BluetoothDevice.class))).thenReturn(
+ new HearingDeviceLocalDataManager.Data.Builder().build());
when(mContext.getMainThreadHandler()).thenReturn(mTestHandler);
when(mTestHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
@@ -307,6 +314,68 @@
verify(mController).refresh();
}
+ @Test
+ public void onMuteChanged_refreshWhenNotInitiateFromUi() {
+ prepareDevice(/* hasMember= */ false);
+ mController.init(mScreen);
+ final int testMute = MUTE_NOT_MUTED;
+ AmbientVolumeController.RemoteAmbientState state =
+ new AmbientVolumeController.RemoteAmbientState(testMute, 0);
+ when(mVolumeController.refreshAmbientState(mDevice)).thenReturn(state);
+ getPreference().setMuted(false);
+
+ mController.onMuteChanged(mDevice, testMute);
+ verify(mController, never()).refresh();
+
+ final int updatedTestMute = MUTE_MUTED;
+ mController.onMuteChanged(mDevice, updatedTestMute);
+ verify(mController).refresh();
+ }
+
+ @Test
+ public void refresh_leftAndRightDifferentGainSetting_expandControl() {
+ prepareDevice(/* hasMember= */ true);
+ mController.init(mScreen);
+ prepareRemoteData(mDevice, 10, MUTE_NOT_MUTED);
+ prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED);
+ getPreference().setExpanded(false);
+
+ mController.refresh();
+
+ assertThat(getPreference().isExpanded()).isTrue();
+ }
+
+ @Test
+ public void refresh_oneSideNotMutable_controlNotMutableAndNotMuted() {
+ prepareDevice(/* hasMember= */ true);
+ mController.init(mScreen);
+ prepareRemoteData(mDevice, 10, MUTE_DISABLED);
+ prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED);
+ getPreference().setMutable(true);
+ getPreference().setMuted(true);
+
+ mController.refresh();
+
+ assertThat(getPreference().isMutable()).isFalse();
+ assertThat(getPreference().isMuted()).isFalse();
+ }
+
+ @Test
+ public void refresh_oneSideNotMuted_controlNotMutedAndSyncToRemote() {
+ prepareDevice(/* hasMember= */ true);
+ mController.init(mScreen);
+ prepareRemoteData(mDevice, 10, MUTE_MUTED);
+ prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED);
+ getPreference().setMutable(true);
+ getPreference().setMuted(true);
+
+ mController.refresh();
+
+ assertThat(getPreference().isMutable()).isTrue();
+ assertThat(getPreference().isMuted()).isFalse();
+ verify(mVolumeController).setMuted(mDevice, false);
+ }
+
private void prepareDevice(boolean hasMember) {
when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
@@ -325,6 +394,12 @@
}
}
+ private void prepareRemoteData(BluetoothDevice device, int gainSetting, int mute) {
+ when(mVolumeController.isAmbientControlAvailable(device)).thenReturn(true);
+ when(mVolumeController.refreshAmbientState(device)).thenReturn(
+ new AmbientVolumeController.RemoteAmbientState(gainSetting, mute));
+ }
+
private void verifyDeviceDataUpdated(BluetoothDevice device) {
verify(mLocalDataManager, atLeastOnce()).updateAmbient(eq(device), anyInt());
verify(mLocalDataManager, atLeastOnce()).updateGroupAmbient(eq(device), anyInt());