From fc79e749550cbee1dd2d184a3764f01f77f3c3d5 Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Wed, 11 Dec 2024 06:25:41 +0000 Subject: [PATCH] [Ambient Volume] Migrate to use AmbientVolumeUiController in SettingsLib Moove the common ui logic code into settingslib for using in both settings and systemui. Flag: com.android.settingslib.flags.hearing_devices_ambient_volume_control Bug: 357878944 Test: atest AmbientVolumePreferenceTest Test: atest BluetoothDetailsAmbientVolumePreferenceControllerTest Change-Id: I97d5ac2d1862fed7249af8b35f04fa36fc47d16d --- .../bluetooth/AmbientVolumePreference.java | 147 +++-- ...ailsAmbientVolumePreferenceController.java | 525 +----------------- .../AmbientVolumePreferenceTest.java | 58 +- ...AmbientVolumePreferenceControllerTest.java | 338 +---------- 4 files changed, 164 insertions(+), 904 deletions(-) diff --git a/src/com/android/settings/bluetooth/AmbientVolumePreference.java b/src/com/android/settings/bluetooth/AmbientVolumePreference.java index e916c046df6..8196edf0bd8 100644 --- a/src/com/android/settings/bluetooth/AmbientVolumePreference.java +++ b/src/com/android/settings/bluetooth/AmbientVolumePreference.java @@ -21,25 +21,29 @@ 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.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 android.bluetooth.BluetoothDevice; import android.content.Context; -import android.util.ArrayMap; import android.view.View; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceViewHolder; import com.android.settings.R; import com.android.settings.widget.SeekBarPreference; +import com.android.settingslib.bluetooth.AmbientVolumeUi; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; import com.google.common.primitives.Ints; -import java.util.List; import java.util.Map; /** @@ -49,27 +53,13 @@ import java.util.Map; * 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 { +public class AmbientVolumePreference extends PreferenceGroup implements AmbientVolumeUi { - /** 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(); - - /** 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 VALID_SIDES = List.of(SIDE_UNIFIED, SIDE_LEFT, SIDE_RIGHT); + private static final int ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED = 0; + private static final int ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED = 1; @Nullable - private OnIconClickListener mListener; + private AmbientVolumeUiListener mListener; @Nullable private View mExpandIcon; @Nullable @@ -78,27 +68,21 @@ public class AmbientVolumePreference extends PreferenceGroup { private boolean mExpanded = false; private boolean mMutable = false; private boolean mMuted = false; - private Map mSideToSliderMap = new ArrayMap<>(); - - /** - * Ambient volume level for hearing device ambient control icon - *

- * 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. - *

- * To represent the combined left/right levels with a single value, the following calculation - * is used: - * finalLevel = (leftLevel * 5) + rightLevel - * For example: - *

    - *
  • If left level is 2 and right level is 3, the final level will be 13 (2 * 5 + 3)
  • - *
  • If both left and right levels are 0, the final level will be 0
  • - *
  • If both left and right levels are 4, the final level will be 24
  • - *
- */ + private final BiMap mSideToSliderMap = HashBiMap.create(); private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; + private final OnPreferenceChangeListener mPreferenceChangeListener = + (slider, v) -> { + if (slider instanceof SeekBarPreference && v instanceof final Integer value) { + final Integer side = mSideToSliderMap.inverse().get(slider); + if (mListener != null && side != null) { + mListener.onSliderValueChange(side, value); + } + return true; + } + return false; + }; + public AmbientVolumePreference(@NonNull Context context) { super(context, null); setLayoutResource(R.layout.preference_ambient_volume); @@ -138,7 +122,8 @@ public class AmbientVolumePreference extends PreferenceGroup { updateExpandIcon(); } - void setExpandable(boolean expandable) { + @Override + public void setExpandable(boolean expandable) { mExpandable = expandable; if (!mExpandable) { setExpanded(false); @@ -146,11 +131,13 @@ public class AmbientVolumePreference extends PreferenceGroup { updateExpandIcon(); } - boolean isExpandable() { + @Override + public boolean isExpandable() { return mExpandable; } - void setExpanded(boolean expanded) { + @Override + public void setExpanded(boolean expanded) { if (!mExpandable && expanded) { return; } @@ -159,11 +146,13 @@ public class AmbientVolumePreference extends PreferenceGroup { updateLayout(); } - boolean isExpanded() { + @Override + public boolean isExpanded() { return mExpanded; } - void setMutable(boolean mutable) { + @Override + public void setMutable(boolean mutable) { mMutable = mutable; if (!mMutable) { mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; @@ -172,11 +161,13 @@ public class AmbientVolumePreference extends PreferenceGroup { updateVolumeIcon(); } - boolean isMutable() { + @Override + public boolean isMutable() { return mMutable; } - void setMuted(boolean muted) { + @Override + public void setMuted(boolean muted) { if (!mMutable && muted) { return; } @@ -189,25 +180,35 @@ public class AmbientVolumePreference extends PreferenceGroup { updateVolumeIcon(); } - boolean isMuted() { + @Override + public boolean isMuted() { return mMuted; } - void setOnIconClickListener(@Nullable OnIconClickListener listener) { + @Override + public void setListener(@Nullable AmbientVolumeUiListener listener) { mListener = listener; } - void setSliders(Map sideToSliderMap) { - mSideToSliderMap = sideToSliderMap; - for (SeekBarPreference preference : sideToSliderMap.values()) { - if (findPreference(preference.getKey()) == null) { - addPreference(preference); + @Override + public void setupSliders(@NonNull Map sideToDeviceMap) { + sideToDeviceMap.forEach((side, device) -> + createSlider(side, ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED + side)); + createSlider(SIDE_UNIFIED, ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED); + + if (!mSideToSliderMap.isEmpty()) { + for (int side : VALID_SIDES) { + final SeekBarPreference slider = mSideToSliderMap.get(side); + if (slider != null && findPreference(slider.getKey()) == null) { + addPreference(slider); + } } } updateLayout(); } - void setSliderEnabled(int side, boolean enabled) { + @Override + public void setSliderEnabled(int side, boolean enabled) { SeekBarPreference slider = mSideToSliderMap.get(side); if (slider != null && slider.isEnabled() != enabled) { slider.setEnabled(enabled); @@ -215,7 +216,8 @@ public class AmbientVolumePreference extends PreferenceGroup { } } - void setSliderValue(int side, int value) { + @Override + public void setSliderValue(int side, int value) { SeekBarPreference slider = mSideToSliderMap.get(side); if (slider != null && slider.getProgress() != value) { slider.setProgress(value); @@ -223,7 +225,8 @@ public class AmbientVolumePreference extends PreferenceGroup { } } - void setSliderRange(int side, int min, int max) { + @Override + public void setSliderRange(int side, int min, int max) { SeekBarPreference slider = mSideToSliderMap.get(side); if (slider != null) { slider.setMin(min); @@ -231,7 +234,8 @@ public class AmbientVolumePreference extends PreferenceGroup { } } - void updateLayout() { + @Override + public void updateLayout() { mSideToSliderMap.forEach((side, slider) -> { if (side == SIDE_UNIFIED) { slider.setVisible(!mExpanded); @@ -279,8 +283,7 @@ public class AmbientVolumePreference extends PreferenceGroup { 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 + final int stringRes = mExpanded ? R.string.bluetooth_ambient_volume_control_collapse : R.string.bluetooth_ambient_volume_control_expand; mExpandIcon.setContentDescription(getContext().getString(stringRes)); } else { @@ -294,8 +297,7 @@ public class AmbientVolumePreference extends PreferenceGroup { } mVolumeIcon.setImageLevel(mMuted ? 0 : mVolumeLevel); if (mMutable) { - final int stringRes = mMuted - ? R.string.bluetooth_ambient_volume_unmute + 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); @@ -304,4 +306,27 @@ public class AmbientVolumePreference extends PreferenceGroup { mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } } + + private void createSlider(int side, int order) { + if (mSideToSliderMap.containsKey(side)) { + return; + } + SeekBarPreference slider = new SeekBarPreference(getContext()); + slider.setKey(KEY_AMBIENT_VOLUME_SLIDER + "_" + side); + slider.setOrder(order); + slider.setOnPreferenceChangeListener(mPreferenceChangeListener); + if (side == SIDE_LEFT) { + slider.setTitle( + getContext().getString(R.string.bluetooth_ambient_volume_control_left)); + } else if (side == SIDE_RIGHT) { + slider.setTitle( + getContext().getString(R.string.bluetooth_ambient_volume_control_right)); + } + mSideToSliderMap.put(side, slider); + } + + @VisibleForTesting + Map getSliders() { + return mSideToSliderMap; + } } diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java index f237ffe50c3..4b0b5d4d309 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java @@ -16,41 +16,20 @@ 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; -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 static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; -import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; -import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothProfile; import android.content.Context; -import android.util.ArraySet; -import android.util.Log; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; -import com.android.settings.R; -import com.android.settings.widget.SeekBarPreference; -import com.android.settingslib.bluetooth.AmbientVolumeController; -import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.AmbientVolumeUiController; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager; -import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.VolumeControlProfile; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -58,39 +37,21 @@ import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; import com.android.settingslib.utils.ThreadUtils; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; - -import java.util.Map; -import java.util.Set; - -/** A {@link BluetoothDetailsController} that manages ambient volume control preferences. */ -public class BluetoothDetailsAmbientVolumePreferenceController extends - BluetoothDetailsController implements Preference.OnPreferenceChangeListener, - HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener, OnStart, OnStop, - AmbientVolumeController.AmbientVolumeControlCallback, BluetoothCallback { +/** A {@link BluetoothDetailsController} that manages ambient volume preference. */ +public class BluetoothDetailsAmbientVolumePreferenceController extends BluetoothDetailsController + implements OnStart, OnStop { 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 LocalBluetoothManager mBluetoothManager; - private final Set mCachedDevices = new ArraySet<>(); - private final BiMap mSideToDeviceMap = HashBiMap.create(); - private final BiMap mSideToSliderMap = HashBiMap.create(); - private final HearingDeviceLocalDataManager mLocalDataManager; - private final AmbientVolumeController mVolumeController; - - @Nullable - private PreferenceCategory mDeviceControls; @Nullable private AmbientVolumePreference mPreference; @Nullable - private Toast mToast; + private AmbientVolumeUiController mAmbientUiController; public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, @NonNull LocalBluetoothManager manager, @@ -99,45 +60,42 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends @NonNull Lifecycle lifecycle) { super(context, fragment, device, lifecycle); mBluetoothManager = manager; - mLocalDataManager = new HearingDeviceLocalDataManager(context); - mLocalDataManager.setOnDeviceLocalDataChangeListener(this, - ThreadUtils.getBackgroundExecutor()); - mVolumeController = new AmbientVolumeController(manager.getProfileManager(), this); } @VisibleForTesting - BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, + public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, @NonNull LocalBluetoothManager manager, @NonNull PreferenceFragmentCompat fragment, @NonNull CachedBluetoothDevice device, @NonNull Lifecycle lifecycle, - @NonNull HearingDeviceLocalDataManager localSettings, - @NonNull AmbientVolumeController volumeController) { + @NonNull AmbientVolumeUiController uiController) { super(context, fragment, device, lifecycle); mBluetoothManager = manager; - mLocalDataManager = localSettings; - mVolumeController = volumeController; + mAmbientUiController = uiController; } @Override protected void init(PreferenceScreen screen) { - mDeviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP); - if (mDeviceControls == null) { + PreferenceCategory deviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP); + if (deviceControls == null) { return; } - loadDevices(); + mPreference = new AmbientVolumePreference(deviceControls.getContext()); + mPreference.setKey(KEY_AMBIENT_VOLUME); + mPreference.setOrder(ORDER_AMBIENT_VOLUME); + deviceControls.addPreference(mPreference); + + mAmbientUiController = new AmbientVolumeUiController(mContext, mBluetoothManager, + mPreference); + mAmbientUiController.loadDevice(mCachedDevice); } @Override public void onStart() { ThreadUtils.postOnBackgroundThread(() -> { - mBluetoothManager.getEventManager().registerCallback(this); - mLocalDataManager.start(); - mCachedDevices.forEach(device -> { - device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); - mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(), - device.getDevice()); - }); + if (mAmbientUiController != null) { + mAmbientUiController.start(); + } }); } @@ -153,12 +111,9 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends @Override public void onStop() { ThreadUtils.postOnBackgroundThread(() -> { - mBluetoothManager.getEventManager().unregisterCallback(this); - mLocalDataManager.stop(); - mCachedDevices.forEach(device -> { - device.unregisterCallback(this); - mVolumeController.unregisterCallback(device.getDevice()); - }); + if (mAmbientUiController != null) { + mAmbientUiController.stop(); + } }); } @@ -167,16 +122,8 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends if (!isAvailable()) { return; } - boolean shouldShowAmbientControl = isAmbientControlAvailable(); - if (shouldShowAmbientControl) { - if (mPreference != null) { - mPreference.setVisible(true); - } - loadRemoteDataToUi(); - } else { - if (mPreference != null) { - mPreference.setVisible(false); - } + if (mAmbientUiController != null) { + mAmbientUiController.refresh(); } } @@ -191,424 +138,4 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends 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); - } - setVolumeIfValid(side, 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 { - setAmbientRunnable.run(); - } - return true; - } - return false; - } - - @Override - public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice, - int state, int bluetoothProfile) { - if (bluetoothProfile == BluetoothProfile.VOLUME_CONTROL - && state == BluetoothProfile.STATE_CONNECTED - && mCachedDevices.contains(cachedDevice)) { - // After VCP connected, AICS may not ready yet and still return invalid value, delay - // a while to wait AICS ready as a workaround - mContext.getMainThreadHandler().postDelayed(this::refresh, 1000L); - } - } - - @Override - public void onDeviceAttributesChanged() { - mCachedDevices.forEach(device -> { - device.unregisterCallback(this); - mVolumeController.unregisterCallback(device.getDevice()); - }); - mContext.getMainExecutor().execute(() -> { - loadDevices(); - if (!mCachedDevices.isEmpty()) { - refresh(); - } - ThreadUtils.postOnBackgroundThread(() -> - mCachedDevices.forEach(device -> { - device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); - mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(), - device.getDevice()); - }) - ); - }); - } - - @Override - public void onDeviceLocalDataChange(@NonNull String address, @Nullable Data data) { - if (data == null) { - // The local data is removed because the device is unpaired, do nothing - return; - } - for (BluetoothDevice device : mSideToDeviceMap.values()) { - if (device.getAnonymizedAddress().equals(address)) { - mContext.getMainExecutor().execute(() -> loadLocalDataToUi(device)); - return; - } - } - } - - @Override - public void onVolumeControlServiceConnected() { - mCachedDevices.forEach( - device -> mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(), - device.getDevice())); - } - - @Override - public void onAmbientChanged(@NonNull BluetoothDevice device, int gainSettings) { - if (DEBUG) { - Log.d(TAG, "onAmbientChanged, value:" + gainSettings + ", device:" + device); - } - Data data = mLocalDataManager.get(device); - boolean isInitiatedFromUi = (isControlExpanded() && data.ambient() == gainSettings) - || (!isControlExpanded() && data.groupAmbient() == gainSettings); - if (isInitiatedFromUi) { - // The change is initiated from UI, no need to update UI - return; - } - - // We have to check if we need to expand the controls by getting all remote - // device's ambient value, delay for a while to wait all remote devices update - // to the latest value to avoid unnecessary expand action. - mContext.getMainThreadHandler().postDelayed(this::refresh, 1200L); - } - - @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(() -> { - showErrorToast(); - refresh(); - }); - } - - private void loadDevices() { - mSideToDeviceMap.clear(); - mCachedDevices.clear(); - if (VALID_SIDES.contains(mCachedDevice.getDeviceSide()) - && mCachedDevice.getBondState() == BOND_BONDED) { - mSideToDeviceMap.put(mCachedDevice.getDeviceSide(), mCachedDevice.getDevice()); - mCachedDevices.add(mCachedDevice); - } - for (CachedBluetoothDevice memberDevice : mCachedDevice.getMemberDevice()) { - if (VALID_SIDES.contains(memberDevice.getDeviceSide()) - && memberDevice.getBondState() == BOND_BONDED) { - 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); - 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); - } - } - - 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); - if (side == SIDE_LEFT) { - preference.setTitle(mContext.getString(R.string.bluetooth_ambient_volume_control_left)); - } else if (side == SIDE_RIGHT) { - preference.setTitle( - mContext.getString(R.string.bluetooth_ambient_volume_control_right)); - } - mSideToSliderMap.put(side, preference); - } - - /** Refreshes the control UI visibility and enabled state. */ - private void refreshControlUi() { - if (mPreference != null) { - boolean isAnySliderEnabled = false; - for (Map.Entry entry : mSideToDeviceMap.entrySet()) { - final int side = entry.getKey(); - final BluetoothDevice device = entry.getValue(); - final boolean enabled = isDeviceConnectedToVcp(device) - && mVolumeController.isAmbientControlAvailable(device); - isAnySliderEnabled |= enabled; - mPreference.setSliderEnabled(side, enabled); - } - mPreference.setSliderEnabled(SIDE_UNIFIED, isAnySliderEnabled); - mPreference.updateLayout(); - } - } - - /** Sets the volume to the corresponding control slider. */ - private void setVolumeIfValid(int side, int volume) { - if (volume == INVALID_VOLUME) { - return; - } - if (mPreference != null) { - mPreference.setSliderValue(side, volume); - } - // Update new value to local data - if (side == SIDE_UNIFIED) { - mSideToDeviceMap.forEach((s, d) -> mLocalDataManager.updateGroupAmbient(d, volume)); - } else { - mLocalDataManager.updateAmbient(mSideToDeviceMap.get(side), volume); - } - } - - private void loadLocalDataToUi() { - mSideToDeviceMap.forEach((s, d) -> loadLocalDataToUi(d)); - } - - private void loadLocalDataToUi(BluetoothDevice device) { - final Data data = mLocalDataManager.get(device); - if (DEBUG) { - Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device); - } - final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID); - if (isDeviceConnectedToVcp(device) && !isControlMuted()) { - setVolumeIfValid(side, data.ambient()); - setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient()); - } - setControlExpanded(data.ambientControlExpanded()); - refreshControlUi(); - } - - private void loadRemoteDataToUi() { - BluetoothDevice leftDevice = mSideToDeviceMap.get(SIDE_LEFT); - AmbientVolumeController.RemoteAmbientState leftState = - mVolumeController.refreshAmbientState(leftDevice); - BluetoothDevice rightDevice = mSideToDeviceMap.get(SIDE_RIGHT); - AmbientVolumeController.RemoteAmbientState rightState = - mVolumeController.refreshAmbientState(rightDevice); - if (DEBUG) { - Log.d(TAG, "loadRemoteDataToUi, left=" + leftState + ", right=" + rightState); - } - - if (mPreference != null) { - mSideToDeviceMap.forEach((side, device) -> { - int ambientMax = mVolumeController.getAmbientMax(device); - int ambientMin = mVolumeController.getAmbientMin(device); - if (ambientMin != ambientMax) { - mPreference.setSliderRange(side, ambientMin, ambientMax); - mPreference.setSliderRange(SIDE_UNIFIED, ambientMin, ambientMax); - } - }); - } - - // Update ambient volume - final int leftAmbient = leftState != null ? leftState.gainSetting() : INVALID_VOLUME; - final int rightAmbient = rightState != null ? rightState.gainSetting() : INVALID_VOLUME; - if (isControlExpanded()) { - setVolumeIfValid(SIDE_LEFT, leftAmbient); - setVolumeIfValid(SIDE_RIGHT, rightAmbient); - } else { - if (leftAmbient != rightAmbient && leftAmbient != INVALID_VOLUME - && rightAmbient != INVALID_VOLUME) { - setVolumeIfValid(SIDE_LEFT, leftAmbient); - setVolumeIfValid(SIDE_RIGHT, rightAmbient); - setControlExpanded(true); - } else { - int unifiedAmbient = leftAmbient != INVALID_VOLUME ? leftAmbient : rightAmbient; - setVolumeIfValid(SIDE_UNIFIED, unifiedAmbient); - } - } - // 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(); - } - - /** Check if any device in the group has valid ambient control points */ - private boolean isAmbientControlAvailable() { - for (BluetoothDevice device : mSideToDeviceMap.values()) { - // Found ambient local data for this device, show the ambient control - if (mLocalDataManager.get(device).hasAmbientData()) { - return true; - } - // Found remote ambient control points on this device, show the ambient control - if (mVolumeController.isAmbientControlAvailable(device)) { - return true; - } - } - return false; - } - - private boolean isControlExpanded() { - return mPreference != null && mPreference.isExpanded(); - } - - private void setControlExpanded(boolean expanded) { - if (mPreference != null && mPreference.isExpanded() != expanded) { - mPreference.setExpanded(expanded); - } - mSideToDeviceMap.forEach((s, d) -> { - // Update new value to local data - mLocalDataManager.updateAmbientControlExpanded(d, expanded); - }); - } - - private boolean isControlMuted() { - return mPreference != null && mPreference.isMuted(); - } - - private void initLocalDataIfNeeded() { - int smallerVolumeAmongGroup = Integer.MAX_VALUE; - for (BluetoothDevice device : mSideToDeviceMap.values()) { - Data data = mLocalDataManager.get(device); - if (data.ambient() != INVALID_VOLUME) { - smallerVolumeAmongGroup = Math.min(data.ambient(), smallerVolumeAmongGroup); - } else if (data.groupAmbient() != INVALID_VOLUME) { - // Initialize side ambient from group ambient value - mLocalDataManager.updateAmbient(device, data.groupAmbient()); - } - } - if (smallerVolumeAmongGroup != Integer.MAX_VALUE) { - for (BluetoothDevice device : mSideToDeviceMap.values()) { - Data data = mLocalDataManager.get(device); - if (data.groupAmbient() == INVALID_VOLUME) { - // Initialize group ambient from smaller side ambient value - mLocalDataManager.updateGroupAmbient(device, smallerVolumeAmongGroup); - } - } - } - } - - 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() - .getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED; - } - - private void showErrorToast() { - if (mToast != null) { - mToast.cancel(); - } - mToast = Toast.makeText(mContext, R.string.bluetooth_ambient_volume_error, - Toast.LENGTH_SHORT); - mToast.show(); - } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java index ec406c45503..115f642d19b 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java @@ -26,8 +26,10 @@ import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_R import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.bluetooth.BluetoothDevice; import android.content.Context; import android.util.ArrayMap; import android.view.View; @@ -40,6 +42,7 @@ import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.widget.SeekBarPreference; +import com.android.settingslib.bluetooth.AmbientVolumeUi; import org.junit.Before; import org.junit.Rule; @@ -69,14 +72,14 @@ public class AmbientVolumePreferenceTest { @Spy private Context mContext = ApplicationProvider.getApplicationContext(); @Mock - private AmbientVolumePreference.OnIconClickListener mListener; + private AmbientVolumeUi.AmbientVolumeUiListener mListener; @Mock private View mItemView; private AmbientVolumePreference mPreference; private ImageView mExpandIcon; private ImageView mVolumeIcon; - private final Map mSideToSlidersMap = new ArrayMap<>(); + private final Map mSideToDeviceMap = new ArrayMap<>(); @Before public void setUp() { @@ -84,13 +87,27 @@ public class AmbientVolumePreferenceTest { PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext); mPreference = new AmbientVolumePreference(mContext); mPreference.setKey(KEY_AMBIENT_VOLUME); - mPreference.setOnIconClickListener(mListener); + mPreference.setListener(mListener); mPreference.setExpandable(true); mPreference.setMutable(true); preferenceScreen.addPreference(mPreference); - prepareSliders(); - mPreference.setSliders(mSideToSlidersMap); + prepareDevices(); + mPreference.setupSliders(mSideToDeviceMap); + mPreference.getSliders().forEach((side, slider) -> { + 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); + } + }); mExpandIcon = new ImageView(mContext); mVolumeIcon = new ImageView(mContext); @@ -206,33 +223,16 @@ public class AmbientVolumePreferenceTest { 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); + Map sliders = mPreference.getSliders(); + assertThat(sliders.get(SIDE_UNIFIED).isVisible()).isEqualTo(!expanded); + assertThat(sliders.get(SIDE_LEFT).isVisible()).isEqualTo(expanded); + assertThat(sliders.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); - 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); + private void prepareDevices() { + mSideToDeviceMap.put(SIDE_LEFT, mock(BluetoothDevice.class)); + mSideToDeviceMap.put(SIDE_RIGHT, mock(BluetoothDevice.class)); } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java index 975d3b491aa..fb10d09f350 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java @@ -16,46 +16,25 @@ 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; -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.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.robolectric.Shadows.shadowOf; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothProfile; -import android.content.ContentResolver; import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; import androidx.preference.PreferenceCategory; import com.android.settings.testutils.shadow.ShadowThreadUtils; -import com.android.settings.widget.SeekBarPreference; -import com.android.settingslib.bluetooth.AmbientVolumeController; +import com.android.settingslib.bluetooth.AmbientVolumeUiController; import com.android.settingslib.bluetooth.BluetoothEventManager; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.bluetooth.VolumeControlProfile; @@ -69,41 +48,19 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; -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; /** Tests for {@link BluetoothDetailsAmbientVolumePreferenceController}. */ @RunWith(RobolectricTestRunner.class) @Config(shadows = { - BluetoothDetailsAmbientVolumePreferenceControllerTest.ShadowGlobal.class, 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; - @Mock - private HearingDeviceLocalDataManager mLocalDataManager; @Mock private LocalBluetoothManager mBluetoothManager; @Mock @@ -113,9 +70,9 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends @Mock private VolumeControlProfile mVolumeControlProfile; @Mock - private AmbientVolumeController mVolumeController; - @Mock private Handler mTestHandler; + @Mock + private AmbientVolumeUiController mUiController; private BluetoothDetailsAmbientVolumePreferenceController mController; @@ -124,24 +81,16 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends super.setUp(); mContext = spy(mContext); + + when(mBluetoothManager.getProfileManager()).thenReturn(mProfileManager); + when(mBluetoothManager.getEventManager()).thenReturn(mEventManager); + mController = spy( + new BluetoothDetailsAmbientVolumePreferenceController(mContext, mBluetoothManager, + mFragment, mCachedDevice, mLifecycle, mUiController)); + PreferenceCategory deviceControls = new PreferenceCategory(mContext); deviceControls.setKey(KEY_HEARING_DEVICE_GROUP); mScreen.addPreference(deviceControls); - mController = spy( - new BluetoothDetailsAmbientVolumePreferenceController(mContext, mBluetoothManager, - mFragment, mCachedDevice, mLifecycle, mLocalDataManager, - mVolumeController)); - - when(mBluetoothManager.getEventManager()).thenReturn(mEventManager); - when(mBluetoothManager.getProfileManager()).thenReturn(mProfileManager); - when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControlProfile); - when(mVolumeControlProfile.getConnectionStatus(mDevice)).thenReturn( - 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( @@ -152,283 +101,42 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends } @Test - public void init_deviceWithoutMember_controlNotExpandable() { - prepareDevice(/* hasMember= */ false); - + public void init_preferenceAdded() { 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); + public void refresh_deviceNotSupportVcp_verifyUiControllerNoRefresh() { + when(mCachedDevice.getProfiles()).thenReturn(List.of()); - mController.init(mScreen); + mController.refresh(); - AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); - assertThat(preference).isNotNull(); - assertThat(preference.isExpandable()).isTrue(); + verify(mUiController, never()).refresh(); } @Test - public void onDeviceLocalDataChange_noMemberAndExpanded_uiCorrectAndDataUpdated() { - prepareDevice(/* hasMember= */ false); - mController.init(mScreen); - HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder() - .ambient(0).groupAmbient(0).ambientControlExpanded(true).build(); - when(mLocalDataManager.get(mDevice)).thenReturn(data); + public void refresh_deviceSupportVcp_verifyUiControllerRefresh() { + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); - mController.onDeviceLocalDataChange(TEST_ADDRESS, data); - shadowOf(Looper.getMainLooper()).idle(); + mController.refresh(); - AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); - assertThat(preference).isNotNull(); - assertThat(preference.isExpanded()).isFalse(); - verifyDeviceDataUpdated(mDevice); + verify(mUiController).refresh(); } @Test - public void onDeviceLocalDataChange_noMemberAndCollapsed_uiCorrectAndDataUpdated() { - prepareDevice(/* hasMember= */ false); - mController.init(mScreen); - HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder() - .ambient(0).groupAmbient(0).ambientControlExpanded(false).build(); - when(mLocalDataManager.get(mDevice)).thenReturn(data); - - mController.onDeviceLocalDataChange(TEST_ADDRESS, data); - shadowOf(Looper.getMainLooper()).idle(); - - AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); - assertThat(preference).isNotNull(); - assertThat(preference.isExpanded()).isFalse(); - verifyDeviceDataUpdated(mDevice); - } - - @Test - public void onDeviceLocalDataChange_hasMemberAndExpanded_uiCorrectAndDataUpdated() { - prepareDevice(/* hasMember= */ true); - mController.init(mScreen); - HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder() - .ambient(0).groupAmbient(0).ambientControlExpanded(true).build(); - when(mLocalDataManager.get(mDevice)).thenReturn(data); - - mController.onDeviceLocalDataChange(TEST_ADDRESS, data); - shadowOf(Looper.getMainLooper()).idle(); - - AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); - assertThat(preference).isNotNull(); - assertThat(preference.isExpanded()).isTrue(); - verifyDeviceDataUpdated(mDevice); - } - - @Test - public void onDeviceLocalDataChange_hasMemberAndCollapsed_uiCorrectAndDataUpdated() { - prepareDevice(/* hasMember= */ true); - mController.init(mScreen); - HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder() - .ambient(0).groupAmbient(0).ambientControlExpanded(false).build(); - when(mLocalDataManager.get(mDevice)).thenReturn(data); - - mController.onDeviceLocalDataChange(TEST_ADDRESS, data); - shadowOf(Looper.getMainLooper()).idle(); - - AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); - assertThat(preference).isNotNull(); - assertThat(preference.isExpanded()).isFalse(); - verifyDeviceDataUpdated(mDevice); - } - - @Test - public void onStart_localDataManagerStartAndCallbackRegistered() { - prepareDevice(/* hasMember= */ true); - mController.init(mScreen); - + public void onStart_verifyUiControllerStart() { mController.onStart(); - verify(mLocalDataManager, atLeastOnce()).start(); - verify(mVolumeController).registerCallback(any(Executor.class), eq(mDevice)); - verify(mVolumeController).registerCallback(any(Executor.class), eq(mMemberDevice)); - verify(mCachedDevice).registerCallback(any(Executor.class), - any(CachedBluetoothDevice.Callback.class)); - verify(mCachedMemberDevice).registerCallback(any(Executor.class), - any(CachedBluetoothDevice.Callback.class)); + verify(mUiController).start(); } @Test - public void onStop_localDataManagerStopAndCallbackUnregistered() { - prepareDevice(/* hasMember= */ true); - mController.init(mScreen); - + public void onStop_verifyUiControllerStop() { mController.onStop(); - verify(mLocalDataManager).stop(); - verify(mVolumeController).unregisterCallback(mDevice); - verify(mVolumeController).unregisterCallback(mMemberDevice); - verify(mCachedDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); - verify(mCachedMemberDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); - } - - @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(); - } - - @Test - public void onAmbientChanged_refreshWhenNotInitiateFromUi() { - prepareDevice(/* hasMember= */ false); - mController.init(mScreen); - final int testAmbient = 10; - HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder() - .ambient(testAmbient) - .groupAmbient(testAmbient) - .ambientControlExpanded(false) - .build(); - when(mLocalDataManager.get(mDevice)).thenReturn(data); - getPreference().setExpanded(true); - - mController.onAmbientChanged(mDevice, testAmbient); - verify(mController, never()).refresh(); - - final int updatedTestAmbient = 20; - mController.onAmbientChanged(mDevice, updatedTestAmbient); - 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); - when(mCachedDevice.getBondState()).thenReturn(BOND_BONDED); - when(mDevice.getAddress()).thenReturn(TEST_ADDRESS); - when(mDevice.getAnonymizedAddress()).thenReturn(TEST_ADDRESS); - when(mDevice.isConnected()).thenReturn(true); - if (hasMember) { - when(mCachedDevice.getMemberDevice()).thenReturn(Set.of(mCachedMemberDevice)); - when(mCachedMemberDevice.getDeviceSide()).thenReturn(SIDE_RIGHT); - when(mCachedMemberDevice.getDevice()).thenReturn(mMemberDevice); - when(mCachedMemberDevice.getBondState()).thenReturn(BOND_BONDED); - when(mMemberDevice.getAddress()).thenReturn(TEST_MEMBER_ADDRESS); - when(mMemberDevice.getAnonymizedAddress()).thenReturn(TEST_MEMBER_ADDRESS); - when(mMemberDevice.isConnected()).thenReturn(true); - } - } - - 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()); - verify(mLocalDataManager, atLeastOnce()).updateAmbientControlExpanded(eq(device), - anyBoolean()); - } - - private AmbientVolumePreference getPreference() { - return mScreen.findPreference(KEY_AMBIENT_VOLUME); - } - - @Implements(value = Settings.Global.class) - public static class ShadowGlobal extends ShadowSettings.ShadowGlobal { - private static final Map> sDataMap = new HashMap<>(); - - @Implementation - protected static boolean putStringForUser( - ContentResolver cr, String name, String value, int userHandle) { - get(cr).put(name, value); - return true; - } - - @Implementation - protected static String getStringForUser(ContentResolver cr, String name, int userHandle) { - return get(cr).get(name); - } - - private static Map get(ContentResolver cr) { - return sDataMap.computeIfAbsent(cr, k -> new HashMap<>()); - } + verify(mUiController).stop(); } }