From 0595aed386395ffadb9e19d7b62b846217b3115c Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Thu, 31 Oct 2024 05:22:46 +0000 Subject: [PATCH] [Ambient Volume] UI of volume sliders in Settings Collapse/expand the controls when clicking on the hearder with arrow. Flag: com.android.settingslib.flags.hearing_devices_ambient_volume_control Bug: 357878944 Test: atest AmbientVolumePreferenceTest Test: atest BluetoothDetailsAmbientVolumePreferenceControllerTest Test: atest BluetoothDetailsHearingDeviceControllerTest Change-Id: I845a4397601e563ed027d7d2a0a13651e95de708 --- res/layout/preference_ambient_volume.xml | 53 +++++ res/values/strings.xml | 6 + .../bluetooth/AmbientVolumePreference.java | 182 ++++++++++++++++ ...ailsAmbientVolumePreferenceController.java | 198 ++++++++++++++++++ ...uetoothDetailsHearingDeviceController.java | 5 + .../AmbientVolumePreferenceTest.java | 152 ++++++++++++++ ...AmbientVolumePreferenceControllerTest.java | 143 +++++++++++++ ...othDetailsHearingDeviceControllerTest.java | 24 ++- 8 files changed, 761 insertions(+), 2 deletions(-) create mode 100644 res/layout/preference_ambient_volume.xml create mode 100644 src/com/android/settings/bluetooth/AmbientVolumePreference.java create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java diff --git a/res/layout/preference_ambient_volume.xml b/res/layout/preference_ambient_volume.xml new file mode 100644 index 00000000000..a8595c64dd4 --- /dev/null +++ b/res/layout/preference_ambient_volume.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index f7afbd6d9c6..c7cdd6e7af9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -164,6 +164,12 @@ There are no presets programmed by your audiologist Couldn\u2019t update preset + + Surroundings + + Expand to left and right separated controls + + Collapse to unified control Audio output diff --git a/src/com/android/settings/bluetooth/AmbientVolumePreference.java b/src/com/android/settings/bluetooth/AmbientVolumePreference.java new file mode 100644 index 00000000000..01222039a9b --- /dev/null +++ b/src/com/android/settings/bluetooth/AmbientVolumePreference.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; + +import android.content.Context; +import android.util.ArrayMap; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settings.widget.SeekBarPreference; + +import java.util.List; +import java.util.Map; + +/** + * A preference group of ambient volume controls. + * + *

It consists of a header with an expand icon and volume sliders for unified control and + * separated control for devices in the same set. Toggle the expand icon will make the UI switch + * between unified and separated control. + */ +public class AmbientVolumePreference extends PreferenceGroup { + + /** Interface definition for a callback to be invoked when the icon is clicked. */ + public interface OnIconClickListener { + /** Called when the expand icon is clicked. */ + void onExpandIconClick(); + }; + + static final float ROTATION_COLLAPSED = 0f; + static final float ROTATION_EXPANDED = 180f; + static final int SIDE_UNIFIED = 999; + static final List VALID_SIDES = List.of(SIDE_UNIFIED, SIDE_LEFT, SIDE_RIGHT); + + @Nullable + private OnIconClickListener mListener; + @Nullable + private View mExpandIcon; + private boolean mExpandable = true; + private boolean mExpanded = false; + private Map mSideToSliderMap = new ArrayMap<>(); + + public AmbientVolumePreference(@NonNull Context context) { + super(context, null); + setLayoutResource(R.layout.preference_ambient_volume); + setIcon(com.android.settingslib.R.drawable.ic_ambient_volume); + setTitle(R.string.bluetooth_ambient_volume_control); + setSelectable(false); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + holder.setDividerAllowedAbove(false); + holder.setDividerAllowedBelow(false); + + mExpandIcon = holder.itemView.requireViewById(R.id.expand_icon); + mExpandIcon.setOnClickListener(v -> { + setExpanded(!mExpanded); + if (mListener != null) { + mListener.onExpandIconClick(); + } + }); + updateExpandIcon(); + } + + void setExpandable(boolean expandable) { + mExpandable = expandable; + if (!mExpandable) { + setExpanded(false); + } + updateExpandIcon(); + } + + boolean isExpandable() { + return mExpandable; + } + + void setExpanded(boolean expanded) { + if (!mExpandable && expanded) { + return; + } + mExpanded = expanded; + updateExpandIcon(); + updateLayout(); + } + + boolean isExpanded() { + return mExpanded; + } + + void setOnIconClickListener(@Nullable OnIconClickListener listener) { + mListener = listener; + } + + void setSliders(Map sideToSliderMap) { + mSideToSliderMap = sideToSliderMap; + for (SeekBarPreference preference : sideToSliderMap.values()) { + if (findPreference(preference.getKey()) == null) { + addPreference(preference); + } + } + updateLayout(); + } + + void setSliderEnabled(int side, boolean enabled) { + SeekBarPreference slider = mSideToSliderMap.get(side); + if (slider != null && slider.isEnabled() != enabled) { + slider.setEnabled(enabled); + updateLayout(); + } + } + + void setSliderValue(int side, int value) { + SeekBarPreference slider = mSideToSliderMap.get(side); + if (slider != null && slider.getProgress() != value) { + slider.setProgress(value); + } + } + + void setSliderRange(int side, int min, int max) { + SeekBarPreference slider = mSideToSliderMap.get(side); + if (slider != null) { + slider.setMin(min); + slider.setMax(max); + } + } + + void updateLayout() { + mSideToSliderMap.forEach((side, slider) -> { + if (side == SIDE_UNIFIED) { + slider.setVisible(!mExpanded); + } else { + slider.setVisible(mExpanded); + } + if (!slider.isEnabled()) { + slider.setProgress(slider.getMin()); + } + }); + } + + private void updateExpandIcon() { + if (mExpandIcon == null) { + return; + } + mExpandIcon.setVisibility(mExpandable ? VISIBLE : GONE); + mExpandIcon.setRotation(mExpanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED); + if (mExpandable) { + final int stringRes = mExpanded + ? R.string.bluetooth_ambient_volume_control_collapse + : R.string.bluetooth_ambient_volume_control_expand; + mExpandIcon.setContentDescription(getContext().getString(stringRes)); + } else { + mExpandIcon.setContentDescription(null); + } + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java new file mode 100644 index 00000000000..0629e6e9a45 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import static com.android.settings.bluetooth.AmbientVolumePreference.SIDE_UNIFIED; +import static com.android.settings.bluetooth.AmbientVolumePreference.VALID_SIDES; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.ORDER_AMBIENT_VOLUME; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_INVALID; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.util.ArraySet; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.widget.SeekBarPreference; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.VolumeControlProfile; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.utils.ThreadUtils; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; + +import java.util.Set; + +/** A {@link BluetoothDetailsController} that manages ambient volume control preferences. */ +public class BluetoothDetailsAmbientVolumePreferenceController extends + BluetoothDetailsController implements Preference.OnPreferenceChangeListener { + + private static final boolean DEBUG = true; + private static final String TAG = "AmbientPrefController"; + + static final String KEY_AMBIENT_VOLUME = "ambient_volume"; + static final String KEY_AMBIENT_VOLUME_SLIDER = "ambient_volume_slider"; + private static final int ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED = 0; + private static final int ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED = 1; + + private final Set mCachedDevices = new ArraySet<>(); + private final BiMap mSideToDeviceMap = HashBiMap.create(); + private final BiMap mSideToSliderMap = HashBiMap.create(); + + @Nullable + private PreferenceCategory mDeviceControls; + @Nullable + private AmbientVolumePreference mPreference; + + public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, + @NonNull PreferenceFragmentCompat fragment, + @NonNull CachedBluetoothDevice device, + @NonNull Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + } + + @Override + protected void init(PreferenceScreen screen) { + mDeviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP); + if (mDeviceControls == null) { + return; + } + loadDevices(); + } + + @Override + protected void refresh() { + if (!isAvailable()) { + return; + } + // TODO: load data from remote + refreshControlUi(); + } + + @Override + public boolean isAvailable() { + boolean isDeviceSupportVcp = mCachedDevice.getProfiles().stream().anyMatch( + profile -> profile instanceof VolumeControlProfile); + return isDeviceSupportVcp; + } + + @Nullable + @Override + public String getPreferenceKey() { + return KEY_AMBIENT_VOLUME; + } + + @Override + public boolean onPreferenceChange(@NonNull Preference preference, @Nullable Object newValue) { + if (preference instanceof SeekBarPreference && newValue instanceof final Integer value) { + final int side = mSideToSliderMap.inverse().getOrDefault(preference, SIDE_INVALID); + if (DEBUG) { + Log.d(TAG, "onPreferenceChange: side=" + side + ", value=" + value); + } + if (side == SIDE_UNIFIED) { + // TODO: set the value on the devices + } else { + // TODO: set the value on the side device + } + return true; + } + return false; + } + + @Override + public void onDeviceAttributesChanged() { + mCachedDevices.forEach(device -> { + device.unregisterCallback(this); + }); + mContext.getMainExecutor().execute(() -> { + loadDevices(); + if (!mCachedDevices.isEmpty()) { + refresh(); + } + ThreadUtils.postOnBackgroundThread(() -> + mCachedDevices.forEach(device -> { + device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); + }) + ); + }); + } + + private void loadDevices() { + mSideToDeviceMap.clear(); + mCachedDevices.clear(); + if (VALID_SIDES.contains(mCachedDevice.getDeviceSide())) { + mSideToDeviceMap.put(mCachedDevice.getDeviceSide(), mCachedDevice.getDevice()); + mCachedDevices.add(mCachedDevice); + } + for (CachedBluetoothDevice memberDevice : mCachedDevice.getMemberDevice()) { + if (VALID_SIDES.contains(memberDevice.getDeviceSide())) { + mSideToDeviceMap.put(memberDevice.getDeviceSide(), memberDevice.getDevice()); + mCachedDevices.add(memberDevice); + } + } + createAmbientVolumePreference(); + createSliderPreferences(); + if (mPreference != null) { + mPreference.setExpandable(mSideToDeviceMap.size() > 1); + mPreference.setSliders((mSideToSliderMap)); + } + } + + private void createAmbientVolumePreference() { + if (mPreference != null || mDeviceControls == null) { + return; + } + mPreference = new AmbientVolumePreference(mDeviceControls.getContext()); + mPreference.setKey(KEY_AMBIENT_VOLUME); + mPreference.setOrder(ORDER_AMBIENT_VOLUME); + if (mDeviceControls.findPreference(mPreference.getKey()) == null) { + mDeviceControls.addPreference(mPreference); + } + } + + private void createSliderPreferences() { + mSideToDeviceMap.forEach((s, d) -> + createSliderPreference(s, ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED + s)); + createSliderPreference(SIDE_UNIFIED, ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED); + } + + private void createSliderPreference(int side, int order) { + if (mSideToSliderMap.containsKey(side) || mDeviceControls == null) { + return; + } + SeekBarPreference preference = new SeekBarPreference(mDeviceControls.getContext()); + preference.setKey(KEY_AMBIENT_VOLUME_SLIDER + "_" + side); + preference.setOrder(order); + preference.setOnPreferenceChangeListener(this); + mSideToSliderMap.put(side, preference); + } + + /** Refreshes the control UI visibility and enabled state. */ + private void refreshControlUi() { + if (mPreference != null) { + mPreference.updateLayout(); + } + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java index 3703b7180af..7236d8cecc8 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java @@ -42,6 +42,7 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon public static final int ORDER_HEARING_DEVICE_SETTINGS = 1; public static final int ORDER_HEARING_AIDS_PRESETS = 2; + public static final int ORDER_AMBIENT_VOLUME = 4; static final String KEY_HEARING_DEVICE_GROUP = "hearing_device_group"; private final List mControllers = new ArrayList<>(); @@ -107,6 +108,10 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon mControllers.add(new BluetoothDetailsHearingAidsPresetsController(mContext, mFragment, mManager, mCachedDevice, mLifecycle)); } + if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) { + mControllers.add(new BluetoothDetailsAmbientVolumePreferenceController(mContext, + mFragment, mCachedDevice, mLifecycle)); + } } @NonNull diff --git a/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java new file mode 100644 index 00000000000..75f3c9a1180 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import static com.android.settings.bluetooth.AmbientVolumePreference.ROTATION_COLLAPSED; +import static com.android.settings.bluetooth.AmbientVolumePreference.ROTATION_EXPANDED; +import static com.android.settings.bluetooth.AmbientVolumePreference.SIDE_UNIFIED; +import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME; +import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME_SLIDER; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.util.ArrayMap; +import android.view.View; +import android.widget.ImageView; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.widget.SeekBarPreference; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +import java.util.Map; + +/** Tests for {@link AmbientVolumePreference}. */ +@RunWith(RobolectricTestRunner.class) +public class AmbientVolumePreferenceTest { + + 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; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Spy + private Context mContext = ApplicationProvider.getApplicationContext(); + @Mock + private AmbientVolumePreference.OnIconClickListener mListener; + @Mock + private View mItemView; + + private AmbientVolumePreference mPreference; + private ImageView mExpandIcon; + private final Map mSideToSlidersMap = new ArrayMap<>(); + + @Before + public void setUp() { + PreferenceManager preferenceManager = new PreferenceManager(mContext); + PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext); + mPreference = new AmbientVolumePreference(mContext); + mPreference.setKey(KEY_AMBIENT_VOLUME); + mPreference.setOnIconClickListener(mListener); + mPreference.setExpandable(true); + preferenceScreen.addPreference(mPreference); + + prepareSliders(); + mPreference.setSliders(mSideToSlidersMap); + + mExpandIcon = new ImageView(mContext); + when(mItemView.requireViewById(R.id.expand_icon)).thenReturn(mExpandIcon); + + PreferenceViewHolder preferenceViewHolder = PreferenceViewHolder.createInstanceForTests( + mItemView); + mPreference.onBindViewHolder(preferenceViewHolder); + } + + @Test + public void setExpandable_expandable_expandIconVisible() { + mPreference.setExpandable(true); + + assertThat(mExpandIcon.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void setExpandable_notExpandable_expandIconGone() { + mPreference.setExpandable(false); + + assertThat(mExpandIcon.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void setExpanded_expanded_assertControlUiCorrect() { + mPreference.setExpanded(true); + + assertControlUiCorrect(); + } + + @Test + public void setExpanded_notExpanded_assertControlUiCorrect() { + mPreference.setExpanded(false); + + assertControlUiCorrect(); + } + + private void assertControlUiCorrect() { + final boolean expanded = mPreference.isExpanded(); + assertThat(mSideToSlidersMap.get(SIDE_UNIFIED).isVisible()).isEqualTo(!expanded); + assertThat(mSideToSlidersMap.get(SIDE_LEFT).isVisible()).isEqualTo(expanded); + assertThat(mSideToSlidersMap.get(SIDE_RIGHT).isVisible()).isEqualTo(expanded); + final float rotation = expanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED; + assertThat(mExpandIcon.getRotation()).isEqualTo(rotation); + } + + private void prepareSliders() { + prepareSlider(SIDE_UNIFIED); + prepareSlider(SIDE_LEFT); + prepareSlider(SIDE_RIGHT); + } + + private void prepareSlider(int side) { + SeekBarPreference slider = new SeekBarPreference(mContext); + if (side == SIDE_LEFT) { + slider.setKey(KEY_LEFT_SLIDER); + } else if (side == SIDE_RIGHT) { + slider.setKey(KEY_RIGHT_SLIDER); + } else { + slider.setKey(KEY_UNIFIED_SLIDER); + } + 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 new file mode 100644 index 00000000000..89209d1db26 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME; +import static com.android.settings.bluetooth.BluetoothDetailsAmbientVolumePreferenceController.KEY_AMBIENT_VOLUME_SLIDER; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.bluetooth.BluetoothDevice; +import android.os.Looper; + +import androidx.preference.PreferenceCategory; + +import com.android.settings.testutils.shadow.ShadowThreadUtils; +import com.android.settings.widget.SeekBarPreference; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Set; + +/** Tests for {@link BluetoothDetailsAmbientVolumePreferenceController}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = { + ShadowThreadUtils.class +}) +public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends + BluetoothDetailsControllerTestBase { + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + private static final String LEFT_CONTROL_KEY = KEY_AMBIENT_VOLUME_SLIDER + "_" + SIDE_LEFT; + private static final String RIGHT_CONTROL_KEY = KEY_AMBIENT_VOLUME_SLIDER + "_" + SIDE_RIGHT; + private static final String TEST_ADDRESS = "00:00:00:00:11"; + private static final String TEST_MEMBER_ADDRESS = "00:00:00:00:22"; + + @Mock + private CachedBluetoothDevice mCachedMemberDevice; + @Mock + private BluetoothDevice mDevice; + @Mock + private BluetoothDevice mMemberDevice; + + private BluetoothDetailsAmbientVolumePreferenceController mController; + + @Before + public void setUp() { + super.setUp(); + + PreferenceCategory deviceControls = new PreferenceCategory(mContext); + deviceControls.setKey(KEY_HEARING_DEVICE_GROUP); + mScreen.addPreference(deviceControls); + mController = new BluetoothDetailsAmbientVolumePreferenceController(mContext, mFragment, + mCachedDevice, mLifecycle); + } + + @Test + public void init_deviceWithoutMember_controlNotExpandable() { + prepareDevice(/* hasMember= */ false); + + mController.init(mScreen); + + AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); + assertThat(preference).isNotNull(); + assertThat(preference.isExpandable()).isFalse(); + } + + @Test + public void init_deviceWithMember_controlExpandable() { + prepareDevice(/* hasMember= */ true); + + mController.init(mScreen); + + AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); + assertThat(preference).isNotNull(); + assertThat(preference.isExpandable()).isTrue(); + } + + @Test + public void onDeviceAttributesChanged_newDevice_newPreference() { + prepareDevice(/* hasMember= */ false); + + mController.init(mScreen); + + // check the right control is null before onDeviceAttributesChanged() + SeekBarPreference leftControl = mScreen.findPreference(LEFT_CONTROL_KEY); + SeekBarPreference rightControl = mScreen.findPreference(RIGHT_CONTROL_KEY); + assertThat(leftControl).isNotNull(); + assertThat(rightControl).isNull(); + + prepareDevice(/* hasMember= */ true); + mController.onDeviceAttributesChanged(); + shadowOf(Looper.getMainLooper()).idle(); + + // check the right control is created after onDeviceAttributesChanged() + SeekBarPreference updatedLeftControl = mScreen.findPreference(LEFT_CONTROL_KEY); + SeekBarPreference updatedRightControl = mScreen.findPreference(RIGHT_CONTROL_KEY); + assertThat(updatedLeftControl).isEqualTo(leftControl); + assertThat(updatedRightControl).isNotNull(); + } + + private void prepareDevice(boolean hasMember) { + when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mDevice.getAddress()).thenReturn(TEST_ADDRESS); + if (hasMember) { + when(mCachedDevice.getMemberDevice()).thenReturn(Set.of(mCachedMemberDevice)); + when(mCachedMemberDevice.getDeviceSide()).thenReturn(SIDE_RIGHT); + when(mCachedMemberDevice.getDevice()).thenReturn(mMemberDevice); + when(mMemberDevice.getAddress()).thenReturn(TEST_MEMBER_ADDRESS); + } + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java index 2a50f892add..4e3c742e284 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java @@ -53,12 +53,12 @@ public class BluetoothDetailsHearingDeviceControllerTest extends @Mock private LocalBluetoothProfileManager mProfileManager; @Mock - private BluetoothDetailsHearingDeviceController mHearingDeviceController; - @Mock private BluetoothDetailsHearingAidsPresetsController mPresetsController; @Mock private BluetoothDetailsHearingDeviceSettingsController mHearingDeviceSettingsController; + private BluetoothDetailsHearingDeviceController mHearingDeviceController; + @Override public void setUp() { super.setUp(); @@ -126,4 +126,24 @@ public class BluetoothDetailsHearingDeviceControllerTest extends assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( c -> c instanceof BluetoothDetailsHearingAidsPresetsController)).isFalse(); } + + @Test + @RequiresFlagsEnabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_AMBIENT_VOLUME_CONTROL) + public void initSubControllers_flagEnabled_ambientVolumeControllerExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsAmbientVolumePreferenceController)).isTrue(); + } + + @Test + @RequiresFlagsDisabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_AMBIENT_VOLUME_CONTROL) + public void initSubControllers_flagDisabled_ambientVolumeControllerNotExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsAmbientVolumePreferenceController)).isFalse(); + } }