Merge "[Ambient Volume] Migrate to use AmbientVolumeUiController in SettingsLib" into main

This commit is contained in:
Angela Wang
2024-12-18 17:52:47 -08:00
committed by Android (Google) Code Review
4 changed files with 164 additions and 904 deletions

View File

@@ -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.IMPORTANT_FOR_ACCESSIBILITY_YES;
import static android.view.View.VISIBLE; 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_LEFT;
import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT;
import android.bluetooth.BluetoothDevice;
import android.content.Context; import android.content.Context;
import android.util.ArrayMap;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceViewHolder; import androidx.preference.PreferenceViewHolder;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.widget.SeekBarPreference; 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 com.google.common.primitives.Ints;
import java.util.List;
import java.util.Map; 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 * separated control for devices in the same set. Toggle the expand icon will make the UI switch
* between unified and separated control. * 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. */ private static final int ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED = 0;
public interface OnIconClickListener { private static final int ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED = 1;
/** 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);
@Nullable @Nullable
private OnIconClickListener mListener; private AmbientVolumeUiListener mListener;
@Nullable @Nullable
private View mExpandIcon; private View mExpandIcon;
@Nullable @Nullable
@@ -78,27 +68,21 @@ public class AmbientVolumePreference extends PreferenceGroup {
private boolean mExpanded = false; private boolean mExpanded = false;
private boolean mMutable = false; private boolean mMutable = false;
private boolean mMuted = false; private boolean mMuted = false;
private Map<Integer, SeekBarPreference> mSideToSliderMap = new ArrayMap<>(); private final BiMap<Integer, SeekBarPreference> mSideToSliderMap = HashBiMap.create();
/**
* 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; 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) { public AmbientVolumePreference(@NonNull Context context) {
super(context, null); super(context, null);
setLayoutResource(R.layout.preference_ambient_volume); setLayoutResource(R.layout.preference_ambient_volume);
@@ -138,7 +122,8 @@ public class AmbientVolumePreference extends PreferenceGroup {
updateExpandIcon(); updateExpandIcon();
} }
void setExpandable(boolean expandable) { @Override
public void setExpandable(boolean expandable) {
mExpandable = expandable; mExpandable = expandable;
if (!mExpandable) { if (!mExpandable) {
setExpanded(false); setExpanded(false);
@@ -146,11 +131,13 @@ public class AmbientVolumePreference extends PreferenceGroup {
updateExpandIcon(); updateExpandIcon();
} }
boolean isExpandable() { @Override
public boolean isExpandable() {
return mExpandable; return mExpandable;
} }
void setExpanded(boolean expanded) { @Override
public void setExpanded(boolean expanded) {
if (!mExpandable && expanded) { if (!mExpandable && expanded) {
return; return;
} }
@@ -159,11 +146,13 @@ public class AmbientVolumePreference extends PreferenceGroup {
updateLayout(); updateLayout();
} }
boolean isExpanded() { @Override
public boolean isExpanded() {
return mExpanded; return mExpanded;
} }
void setMutable(boolean mutable) { @Override
public void setMutable(boolean mutable) {
mMutable = mutable; mMutable = mutable;
if (!mMutable) { if (!mMutable) {
mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT;
@@ -172,11 +161,13 @@ public class AmbientVolumePreference extends PreferenceGroup {
updateVolumeIcon(); updateVolumeIcon();
} }
boolean isMutable() { @Override
public boolean isMutable() {
return mMutable; return mMutable;
} }
void setMuted(boolean muted) { @Override
public void setMuted(boolean muted) {
if (!mMutable && muted) { if (!mMutable && muted) {
return; return;
} }
@@ -189,25 +180,35 @@ public class AmbientVolumePreference extends PreferenceGroup {
updateVolumeIcon(); updateVolumeIcon();
} }
boolean isMuted() { @Override
public boolean isMuted() {
return mMuted; return mMuted;
} }
void setOnIconClickListener(@Nullable OnIconClickListener listener) { @Override
public void setListener(@Nullable AmbientVolumeUiListener listener) {
mListener = listener; mListener = listener;
} }
void setSliders(Map<Integer, SeekBarPreference> sideToSliderMap) { @Override
mSideToSliderMap = sideToSliderMap; public void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap) {
for (SeekBarPreference preference : sideToSliderMap.values()) { sideToDeviceMap.forEach((side, device) ->
if (findPreference(preference.getKey()) == null) { createSlider(side, ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED + side));
addPreference(preference); 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(); updateLayout();
} }
void setSliderEnabled(int side, boolean enabled) { @Override
public void setSliderEnabled(int side, boolean enabled) {
SeekBarPreference slider = mSideToSliderMap.get(side); SeekBarPreference slider = mSideToSliderMap.get(side);
if (slider != null && slider.isEnabled() != enabled) { if (slider != null && slider.isEnabled() != enabled) {
slider.setEnabled(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); SeekBarPreference slider = mSideToSliderMap.get(side);
if (slider != null && slider.getProgress() != value) { if (slider != null && slider.getProgress() != value) {
slider.setProgress(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); SeekBarPreference slider = mSideToSliderMap.get(side);
if (slider != null) { if (slider != null) {
slider.setMin(min); slider.setMin(min);
@@ -231,7 +234,8 @@ public class AmbientVolumePreference extends PreferenceGroup {
} }
} }
void updateLayout() { @Override
public void updateLayout() {
mSideToSliderMap.forEach((side, slider) -> { mSideToSliderMap.forEach((side, slider) -> {
if (side == SIDE_UNIFIED) { if (side == SIDE_UNIFIED) {
slider.setVisible(!mExpanded); slider.setVisible(!mExpanded);
@@ -279,8 +283,7 @@ public class AmbientVolumePreference extends PreferenceGroup {
mExpandIcon.setVisibility(mExpandable ? VISIBLE : GONE); mExpandIcon.setVisibility(mExpandable ? VISIBLE : GONE);
mExpandIcon.setRotation(mExpanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED); mExpandIcon.setRotation(mExpanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED);
if (mExpandable) { if (mExpandable) {
final int stringRes = mExpanded final int stringRes = mExpanded ? R.string.bluetooth_ambient_volume_control_collapse
? R.string.bluetooth_ambient_volume_control_collapse
: R.string.bluetooth_ambient_volume_control_expand; : R.string.bluetooth_ambient_volume_control_expand;
mExpandIcon.setContentDescription(getContext().getString(stringRes)); mExpandIcon.setContentDescription(getContext().getString(stringRes));
} else { } else {
@@ -294,8 +297,7 @@ public class AmbientVolumePreference extends PreferenceGroup {
} }
mVolumeIcon.setImageLevel(mMuted ? 0 : mVolumeLevel); mVolumeIcon.setImageLevel(mMuted ? 0 : mVolumeLevel);
if (mMutable) { if (mMutable) {
final int stringRes = mMuted final int stringRes = mMuted ? R.string.bluetooth_ambient_volume_unmute
? R.string.bluetooth_ambient_volume_unmute
: R.string.bluetooth_ambient_volume_mute; : R.string.bluetooth_ambient_volume_mute;
mVolumeIcon.setContentDescription(getContext().getString(stringRes)); mVolumeIcon.setContentDescription(getContext().getString(stringRes));
mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
@@ -304,4 +306,27 @@ public class AmbientVolumePreference extends PreferenceGroup {
mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 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<Integer, SeekBarPreference> getSliders() {
return mSideToSliderMap;
}
} }

View File

@@ -16,41 +16,20 @@
package com.android.settings.bluetooth; 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.KEY_HEARING_DEVICE_GROUP;
import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.ORDER_AMBIENT_VOLUME; 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.content.Context;
import android.util.ArraySet;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.settings.R; import com.android.settingslib.bluetooth.AmbientVolumeUiController;
import com.android.settings.widget.SeekBarPreference;
import com.android.settingslib.bluetooth.AmbientVolumeController;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice; 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.LocalBluetoothManager;
import com.android.settingslib.bluetooth.VolumeControlProfile; import com.android.settingslib.bluetooth.VolumeControlProfile;
import com.android.settingslib.core.lifecycle.Lifecycle; 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.core.lifecycle.events.OnStop;
import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.utils.ThreadUtils;
import com.google.common.collect.BiMap; /** A {@link BluetoothDetailsController} that manages ambient volume preference. */
import com.google.common.collect.HashBiMap; public class BluetoothDetailsAmbientVolumePreferenceController extends BluetoothDetailsController
implements OnStart, OnStop {
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 {
private static final boolean DEBUG = true; private static final boolean DEBUG = true;
private static final String TAG = "AmbientPrefController"; private static final String TAG = "AmbientPrefController";
static final String KEY_AMBIENT_VOLUME = "ambient_volume"; static final String KEY_AMBIENT_VOLUME = "ambient_volume";
static final String KEY_AMBIENT_VOLUME_SLIDER = "ambient_volume_slider"; 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 LocalBluetoothManager mBluetoothManager;
private final Set<CachedBluetoothDevice> mCachedDevices = new ArraySet<>();
private final BiMap<Integer, BluetoothDevice> mSideToDeviceMap = HashBiMap.create();
private final BiMap<Integer, SeekBarPreference> mSideToSliderMap = HashBiMap.create();
private final HearingDeviceLocalDataManager mLocalDataManager;
private final AmbientVolumeController mVolumeController;
@Nullable
private PreferenceCategory mDeviceControls;
@Nullable @Nullable
private AmbientVolumePreference mPreference; private AmbientVolumePreference mPreference;
@Nullable @Nullable
private Toast mToast; private AmbientVolumeUiController mAmbientUiController;
public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context,
@NonNull LocalBluetoothManager manager, @NonNull LocalBluetoothManager manager,
@@ -99,45 +60,42 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
@NonNull Lifecycle lifecycle) { @NonNull Lifecycle lifecycle) {
super(context, fragment, device, lifecycle); super(context, fragment, device, lifecycle);
mBluetoothManager = manager; mBluetoothManager = manager;
mLocalDataManager = new HearingDeviceLocalDataManager(context);
mLocalDataManager.setOnDeviceLocalDataChangeListener(this,
ThreadUtils.getBackgroundExecutor());
mVolumeController = new AmbientVolumeController(manager.getProfileManager(), this);
} }
@VisibleForTesting @VisibleForTesting
BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context,
@NonNull LocalBluetoothManager manager, @NonNull LocalBluetoothManager manager,
@NonNull PreferenceFragmentCompat fragment, @NonNull PreferenceFragmentCompat fragment,
@NonNull CachedBluetoothDevice device, @NonNull CachedBluetoothDevice device,
@NonNull Lifecycle lifecycle, @NonNull Lifecycle lifecycle,
@NonNull HearingDeviceLocalDataManager localSettings, @NonNull AmbientVolumeUiController uiController) {
@NonNull AmbientVolumeController volumeController) {
super(context, fragment, device, lifecycle); super(context, fragment, device, lifecycle);
mBluetoothManager = manager; mBluetoothManager = manager;
mLocalDataManager = localSettings; mAmbientUiController = uiController;
mVolumeController = volumeController;
} }
@Override @Override
protected void init(PreferenceScreen screen) { protected void init(PreferenceScreen screen) {
mDeviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP); PreferenceCategory deviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP);
if (mDeviceControls == null) { if (deviceControls == null) {
return; 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 @Override
public void onStart() { public void onStart() {
ThreadUtils.postOnBackgroundThread(() -> { ThreadUtils.postOnBackgroundThread(() -> {
mBluetoothManager.getEventManager().registerCallback(this); if (mAmbientUiController != null) {
mLocalDataManager.start(); mAmbientUiController.start();
mCachedDevices.forEach(device -> { }
device.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(),
device.getDevice());
});
}); });
} }
@@ -153,12 +111,9 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
@Override @Override
public void onStop() { public void onStop() {
ThreadUtils.postOnBackgroundThread(() -> { ThreadUtils.postOnBackgroundThread(() -> {
mBluetoothManager.getEventManager().unregisterCallback(this); if (mAmbientUiController != null) {
mLocalDataManager.stop(); mAmbientUiController.stop();
mCachedDevices.forEach(device -> { }
device.unregisterCallback(this);
mVolumeController.unregisterCallback(device.getDevice());
});
}); });
} }
@@ -167,16 +122,8 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
if (!isAvailable()) { if (!isAvailable()) {
return; return;
} }
boolean shouldShowAmbientControl = isAmbientControlAvailable(); if (mAmbientUiController != null) {
if (shouldShowAmbientControl) { mAmbientUiController.refresh();
if (mPreference != null) {
mPreference.setVisible(true);
}
loadRemoteDataToUi();
} else {
if (mPreference != null) {
mPreference.setVisible(false);
}
} }
} }
@@ -191,424 +138,4 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
public String getPreferenceKey() { public String getPreferenceKey() {
return KEY_AMBIENT_VOLUME; 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<Integer, BluetoothDevice> entry : mSideToDeviceMap.entrySet()) {
final int side = entry.getKey();
final BluetoothDevice device = entry.getValue();
final boolean enabled = isDeviceConnectedToVcp(device)
&& mVolumeController.isAmbientControlAvailable(device);
isAnySliderEnabled |= enabled;
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();
}
} }

View File

@@ -26,8 +26,10 @@ import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_R
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothDevice;
import android.content.Context; import android.content.Context;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.view.View; import android.view.View;
@@ -40,6 +42,7 @@ import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.widget.SeekBarPreference; import com.android.settings.widget.SeekBarPreference;
import com.android.settingslib.bluetooth.AmbientVolumeUi;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -69,14 +72,14 @@ public class AmbientVolumePreferenceTest {
@Spy @Spy
private Context mContext = ApplicationProvider.getApplicationContext(); private Context mContext = ApplicationProvider.getApplicationContext();
@Mock @Mock
private AmbientVolumePreference.OnIconClickListener mListener; private AmbientVolumeUi.AmbientVolumeUiListener mListener;
@Mock @Mock
private View mItemView; private View mItemView;
private AmbientVolumePreference mPreference; private AmbientVolumePreference mPreference;
private ImageView mExpandIcon; private ImageView mExpandIcon;
private ImageView mVolumeIcon; private ImageView mVolumeIcon;
private final Map<Integer, SeekBarPreference> mSideToSlidersMap = new ArrayMap<>(); private final Map<Integer, BluetoothDevice> mSideToDeviceMap = new ArrayMap<>();
@Before @Before
public void setUp() { public void setUp() {
@@ -84,13 +87,27 @@ public class AmbientVolumePreferenceTest {
PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext); PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext);
mPreference = new AmbientVolumePreference(mContext); mPreference = new AmbientVolumePreference(mContext);
mPreference.setKey(KEY_AMBIENT_VOLUME); mPreference.setKey(KEY_AMBIENT_VOLUME);
mPreference.setOnIconClickListener(mListener); mPreference.setListener(mListener);
mPreference.setExpandable(true); mPreference.setExpandable(true);
mPreference.setMutable(true); mPreference.setMutable(true);
preferenceScreen.addPreference(mPreference); preferenceScreen.addPreference(mPreference);
prepareSliders(); prepareDevices();
mPreference.setSliders(mSideToSlidersMap); 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); mExpandIcon = new ImageView(mContext);
mVolumeIcon = new ImageView(mContext); mVolumeIcon = new ImageView(mContext);
@@ -206,33 +223,16 @@ public class AmbientVolumePreferenceTest {
private void assertControlUiCorrect() { private void assertControlUiCorrect() {
final boolean expanded = mPreference.isExpanded(); final boolean expanded = mPreference.isExpanded();
assertThat(mSideToSlidersMap.get(SIDE_UNIFIED).isVisible()).isEqualTo(!expanded); Map<Integer, SeekBarPreference> sliders = mPreference.getSliders();
assertThat(mSideToSlidersMap.get(SIDE_LEFT).isVisible()).isEqualTo(expanded); assertThat(sliders.get(SIDE_UNIFIED).isVisible()).isEqualTo(!expanded);
assertThat(mSideToSlidersMap.get(SIDE_RIGHT).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; final float rotation = expanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED;
assertThat(mExpandIcon.getRotation()).isEqualTo(rotation); assertThat(mExpandIcon.getRotation()).isEqualTo(rotation);
} }
private void prepareSliders() { private void prepareDevices() {
prepareSlider(SIDE_UNIFIED); mSideToDeviceMap.put(SIDE_LEFT, mock(BluetoothDevice.class));
prepareSlider(SIDE_LEFT); mSideToDeviceMap.put(SIDE_RIGHT, mock(BluetoothDevice.class));
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);
} }
} }

View File

@@ -16,46 +16,25 @@
package com.android.settings.bluetooth; 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;
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.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 com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; 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.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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.Handler;
import android.os.Looper;
import android.provider.Settings;
import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceCategory;
import com.android.settings.testutils.shadow.ShadowThreadUtils; import com.android.settings.testutils.shadow.ShadowThreadUtils;
import com.android.settings.widget.SeekBarPreference; import com.android.settingslib.bluetooth.AmbientVolumeUiController;
import com.android.settingslib.bluetooth.AmbientVolumeController;
import com.android.settingslib.bluetooth.BluetoothEventManager; 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.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.bluetooth.VolumeControlProfile; import com.android.settingslib.bluetooth.VolumeControlProfile;
@@ -69,41 +48,19 @@ import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule; import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; 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.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
/** Tests for {@link BluetoothDetailsAmbientVolumePreferenceController}. */ /** Tests for {@link BluetoothDetailsAmbientVolumePreferenceController}. */
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config(shadows = { @Config(shadows = {
BluetoothDetailsAmbientVolumePreferenceControllerTest.ShadowGlobal.class,
ShadowThreadUtils.class ShadowThreadUtils.class
}) })
public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
BluetoothDetailsControllerTestBase { BluetoothDetailsControllerTestBase {
@Rule @Rule
public final MockitoRule mMockitoRule = MockitoJUnit.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 @Mock
private LocalBluetoothManager mBluetoothManager; private LocalBluetoothManager mBluetoothManager;
@Mock @Mock
@@ -113,9 +70,9 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
@Mock @Mock
private VolumeControlProfile mVolumeControlProfile; private VolumeControlProfile mVolumeControlProfile;
@Mock @Mock
private AmbientVolumeController mVolumeController;
@Mock
private Handler mTestHandler; private Handler mTestHandler;
@Mock
private AmbientVolumeUiController mUiController;
private BluetoothDetailsAmbientVolumePreferenceController mController; private BluetoothDetailsAmbientVolumePreferenceController mController;
@@ -124,24 +81,16 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
super.setUp(); super.setUp();
mContext = spy(mContext); 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); PreferenceCategory deviceControls = new PreferenceCategory(mContext);
deviceControls.setKey(KEY_HEARING_DEVICE_GROUP); deviceControls.setKey(KEY_HEARING_DEVICE_GROUP);
mScreen.addPreference(deviceControls); 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(mContext.getMainThreadHandler()).thenReturn(mTestHandler);
when(mTestHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer( when(mTestHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
@@ -152,283 +101,42 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
} }
@Test @Test
public void init_deviceWithoutMember_controlNotExpandable() { public void init_preferenceAdded() {
prepareDevice(/* hasMember= */ false);
mController.init(mScreen); mController.init(mScreen);
AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
assertThat(preference).isNotNull(); assertThat(preference).isNotNull();
assertThat(preference.isExpandable()).isFalse();
} }
@Test @Test
public void init_deviceWithMember_controlExpandable() { public void refresh_deviceNotSupportVcp_verifyUiControllerNoRefresh() {
prepareDevice(/* hasMember= */ true); when(mCachedDevice.getProfiles()).thenReturn(List.of());
mController.init(mScreen); mController.refresh();
AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); verify(mUiController, never()).refresh();
assertThat(preference).isNotNull();
assertThat(preference.isExpandable()).isTrue();
} }
@Test @Test
public void onDeviceLocalDataChange_noMemberAndExpanded_uiCorrectAndDataUpdated() { public void refresh_deviceSupportVcp_verifyUiControllerRefresh() {
prepareDevice(/* hasMember= */ false); when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile));
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); mController.refresh();
shadowOf(Looper.getMainLooper()).idle();
AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); verify(mUiController).refresh();
assertThat(preference).isNotNull();
assertThat(preference.isExpanded()).isFalse();
verifyDeviceDataUpdated(mDevice);
} }
@Test @Test
public void onDeviceLocalDataChange_noMemberAndCollapsed_uiCorrectAndDataUpdated() { public void onStart_verifyUiControllerStart() {
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);
mController.onStart(); mController.onStart();
verify(mLocalDataManager, atLeastOnce()).start(); verify(mUiController).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));
} }
@Test @Test
public void onStop_localDataManagerStopAndCallbackUnregistered() { public void onStop_verifyUiControllerStop() {
prepareDevice(/* hasMember= */ true);
mController.init(mScreen);
mController.onStop(); mController.onStop();
verify(mLocalDataManager).stop(); verify(mUiController).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<ContentResolver, Map<String, String>> 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<String, String> get(ContentResolver cr) {
return sDataMap.computeIfAbsent(cr, k -> new HashMap<>());
}
} }
} }