[Ambient Volume] Show value with remote data

Sync local data with remote data when UI need to refresh and set the
corresponding local value to remote when the control expanded/collapsed.

Flag: com.android.settingslib.flags.hearing_devices_ambient_volume_control
Bug: 357878944
Test: atest BluetoothDetailsAmbientVolumePreferenceControllerTest
Change-Id: If748e696eb62b199d4fd9abafa2300d301a8079c
This commit is contained in:
Angela Wang
2024-11-11 07:23:46 +00:00
parent c2ca7dadd9
commit 46537a6576
4 changed files with 315 additions and 36 deletions

View File

@@ -174,6 +174,8 @@
<string name="bluetooth_ambient_volume_control_left">Left</string> <string name="bluetooth_ambient_volume_control_left">Left</string>
<!-- Connected devices settings. The text to show the control is for right side device. [CHAR LIMIT=30] --> <!-- Connected devices settings. The text to show the control is for right side device. [CHAR LIMIT=30] -->
<string name="bluetooth_ambient_volume_control_right">Right</string> <string name="bluetooth_ambient_volume_control_right">Right</string>
<!-- Message when changing ambient state failed. [CHAR LIMIT=NONE] -->
<string name="bluetooth_ambient_volume_error">Couldn\u2019t update surroundings</string>
<!-- Connected devices settings. Title of the preference to show the entrance of the audio output page. It can change different types of audio are played on phone or other bluetooth devices. [CHAR LIMIT=35] --> <!-- Connected devices settings. Title of the preference to show the entrance of the audio output page. It can change different types of audio are played on phone or other bluetooth devices. [CHAR LIMIT=35] -->
<string name="bluetooth_audio_routing_title">Audio output</string> <string name="bluetooth_audio_routing_title">Audio output</string>
<!-- Title for bluetooth audio routing page footer. [CHAR LIMIT=30] --> <!-- Title for bluetooth audio routing page footer. [CHAR LIMIT=30] -->

View File

@@ -28,9 +28,11 @@ import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_R
import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME; import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context; import android.content.Context;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.Log; import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -42,9 +44,12 @@ import androidx.preference.PreferenceScreen;
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.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;
import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data; import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data;
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;
import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStart;
@@ -54,12 +59,14 @@ import com.android.settingslib.utils.ThreadUtils;
import com.google.common.collect.BiMap; import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap; import com.google.common.collect.HashBiMap;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** A {@link BluetoothDetailsController} that manages ambient volume control preferences. */ /** A {@link BluetoothDetailsController} that manages ambient volume control preferences. */
public class BluetoothDetailsAmbientVolumePreferenceController extends public class BluetoothDetailsAmbientVolumePreferenceController extends
BluetoothDetailsController implements Preference.OnPreferenceChangeListener, BluetoothDetailsController implements Preference.OnPreferenceChangeListener,
HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener, OnStart, OnStop { 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";
@@ -69,34 +76,45 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
private static final int ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED = 0; private static final int ORDER_AMBIENT_VOLUME_CONTROL_UNIFIED = 0;
private static final int ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED = 1; private static final int ORDER_AMBIENT_VOLUME_CONTROL_SEPARATED = 1;
private final LocalBluetoothManager mBluetoothManager;
private final Set<CachedBluetoothDevice> mCachedDevices = new ArraySet<>(); private final Set<CachedBluetoothDevice> mCachedDevices = new ArraySet<>();
private final BiMap<Integer, BluetoothDevice> mSideToDeviceMap = HashBiMap.create(); private final BiMap<Integer, BluetoothDevice> mSideToDeviceMap = HashBiMap.create();
private final BiMap<Integer, SeekBarPreference> mSideToSliderMap = HashBiMap.create(); private final BiMap<Integer, SeekBarPreference> mSideToSliderMap = HashBiMap.create();
private final HearingDeviceLocalDataManager mLocalDataManager; private final HearingDeviceLocalDataManager mLocalDataManager;
private final AmbientVolumeController mVolumeController;
@Nullable @Nullable
private PreferenceCategory mDeviceControls; private PreferenceCategory mDeviceControls;
@Nullable @Nullable
private AmbientVolumePreference mPreference; private AmbientVolumePreference mPreference;
@Nullable
private Toast mToast;
public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context,
@NonNull LocalBluetoothManager manager,
@NonNull PreferenceFragmentCompat fragment, @NonNull PreferenceFragmentCompat fragment,
@NonNull CachedBluetoothDevice device, @NonNull CachedBluetoothDevice device,
@NonNull Lifecycle lifecycle) { @NonNull Lifecycle lifecycle) {
super(context, fragment, device, lifecycle); super(context, fragment, device, lifecycle);
mBluetoothManager = manager;
mLocalDataManager = new HearingDeviceLocalDataManager(context); mLocalDataManager = new HearingDeviceLocalDataManager(context);
mLocalDataManager.setOnDeviceLocalDataChangeListener(this, mLocalDataManager.setOnDeviceLocalDataChangeListener(this,
ThreadUtils.getBackgroundExecutor()); ThreadUtils.getBackgroundExecutor());
mVolumeController = new AmbientVolumeController(manager.getProfileManager(), this);
} }
@VisibleForTesting @VisibleForTesting
BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context,
@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 HearingDeviceLocalDataManager localSettings,
@NonNull AmbientVolumeController volumeController) {
super(context, fragment, device, lifecycle); super(context, fragment, device, lifecycle);
mBluetoothManager = manager;
mLocalDataManager = localSettings; mLocalDataManager = localSettings;
mVolumeController = volumeController;
} }
@Override @Override
@@ -111,19 +129,33 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
@Override @Override
public void onStart() { public void onStart() {
ThreadUtils.postOnBackgroundThread(() -> { ThreadUtils.postOnBackgroundThread(() -> {
mBluetoothManager.getEventManager().registerCallback(this);
mLocalDataManager.start(); mLocalDataManager.start();
mCachedDevices.forEach(device -> { mCachedDevices.forEach(device -> {
device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); device.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(),
device.getDevice());
}); });
}); });
} }
@Override
public void onResume() {
refresh();
}
@Override
public void onPause() {
}
@Override @Override
public void onStop() { public void onStop() {
ThreadUtils.postOnBackgroundThread(() -> { ThreadUtils.postOnBackgroundThread(() -> {
mBluetoothManager.getEventManager().unregisterCallback(this);
mLocalDataManager.stop(); mLocalDataManager.stop();
mCachedDevices.forEach(device -> { mCachedDevices.forEach(device -> {
device.unregisterCallback(this); device.unregisterCallback(this);
mVolumeController.unregisterCallback(device.getDevice());
}); });
}); });
} }
@@ -133,8 +165,17 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
if (!isAvailable()) { if (!isAvailable()) {
return; return;
} }
// TODO: load data from remote boolean shouldShowAmbientControl = isAmbientControlAvailable();
loadLocalDataToUi(); if (shouldShowAmbientControl) {
if (mPreference != null) {
mPreference.setVisible(true);
}
loadRemoteDataToUi();
} else {
if (mPreference != null) {
mPreference.setVisible(false);
}
}
} }
@Override @Override
@@ -160,19 +201,33 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
setVolumeIfValid(side, value); setVolumeIfValid(side, value);
if (side == SIDE_UNIFIED) { if (side == SIDE_UNIFIED) {
// TODO: set the value on the devices mSideToDeviceMap.forEach((s, d) -> mVolumeController.setAmbient(d, value));
} else { } else {
// TODO: set the value on the side device final BluetoothDevice device = mSideToDeviceMap.get(side);
mVolumeController.setAmbient(device, value);
} }
return true; return true;
} }
return false; 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 @Override
public void onDeviceAttributesChanged() { public void onDeviceAttributesChanged() {
mCachedDevices.forEach(device -> { mCachedDevices.forEach(device -> {
device.unregisterCallback(this); device.unregisterCallback(this);
mVolumeController.unregisterCallback(device.getDevice());
}); });
mContext.getMainExecutor().execute(() -> { mContext.getMainExecutor().execute(() -> {
loadDevices(); loadDevices();
@@ -182,6 +237,8 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
ThreadUtils.postOnBackgroundThread(() -> ThreadUtils.postOnBackgroundThread(() ->
mCachedDevices.forEach(device -> { mCachedDevices.forEach(device -> {
device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); device.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(),
device.getDevice());
}) })
); );
}); });
@@ -201,6 +258,41 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
} }
} }
@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 onCommandFailed(@NonNull BluetoothDevice device) {
Log.w(TAG, "onCommandFailed, device:" + device);
mContext.getMainExecutor().execute(() -> {
showErrorToast();
refresh();
});
}
private void loadDevices() { private void loadDevices() {
mSideToDeviceMap.clear(); mSideToDeviceMap.clear();
mCachedDevices.clear(); mCachedDevices.clear();
@@ -234,6 +326,11 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
mPreference.setOrder(ORDER_AMBIENT_VOLUME); mPreference.setOrder(ORDER_AMBIENT_VOLUME);
mPreference.setOnIconClickListener(() -> { mPreference.setOnIconClickListener(() -> {
mSideToDeviceMap.forEach((s, d) -> { mSideToDeviceMap.forEach((s, d) -> {
// Apply previous collapsed/expanded volume to remote device
Data data = mLocalDataManager.get(d);
int volume = isControlExpanded()
? data.ambient() : data.groupAmbient();
mVolumeController.setAmbient(d, volume);
// Update new value to local data // Update new value to local data
mLocalDataManager.updateAmbientControlExpanded(d, isControlExpanded()); mLocalDataManager.updateAmbientControlExpanded(d, isControlExpanded());
}); });
@@ -269,6 +366,16 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
/** Refreshes the control UI visibility and enabled state. */ /** Refreshes the control UI visibility and enabled state. */
private void refreshControlUi() { private void refreshControlUi() {
if (mPreference != null) { 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(); mPreference.updateLayout();
} }
} }
@@ -299,12 +406,74 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device); Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device);
} }
final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID); final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID);
setVolumeIfValid(side, data.ambient()); if (isDeviceConnectedToVcp(device)) {
setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient()); setVolumeIfValid(side, data.ambient());
setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient());
}
setControlExpanded(data.ambientControlExpanded()); setControlExpanded(data.ambientControlExpanded());
refreshControlUi(); 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();
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() { private boolean isControlExpanded() {
return mPreference != null && mPreference.isExpanded(); return mPreference != null && mPreference.isExpanded();
} }
@@ -318,4 +487,41 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends
mLocalDataManager.updateAmbientControlExpanded(d, expanded); mLocalDataManager.updateAmbientControlExpanded(d, expanded);
}); });
} }
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 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

@@ -110,7 +110,7 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon
} }
if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) { if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) {
mControllers.add(new BluetoothDetailsAmbientVolumePreferenceController(mContext, mControllers.add(new BluetoothDetailsAmbientVolumePreferenceController(mContext,
mFragment, mCachedDevice, mLifecycle)); mManager, mFragment, mCachedDevice, mLifecycle));
} }
} }

View File

@@ -29,14 +29,19 @@ 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.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce; 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.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf; import static org.robolectric.Shadows.shadowOf;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.provider.Settings; import android.provider.Settings;
@@ -44,8 +49,13 @@ 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.settings.widget.SeekBarPreference;
import com.android.settingslib.bluetooth.AmbientVolumeController;
import com.android.settingslib.bluetooth.BluetoothEventManager;
import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager; import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.bluetooth.VolumeControlProfile;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -90,6 +100,18 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
private BluetoothDevice mMemberDevice; private BluetoothDevice mMemberDevice;
@Mock @Mock
private HearingDeviceLocalDataManager mLocalDataManager; private HearingDeviceLocalDataManager mLocalDataManager;
@Mock
private LocalBluetoothManager mBluetoothManager;
@Mock
private BluetoothEventManager mEventManager;
@Mock
private LocalBluetoothProfileManager mProfileManager;
@Mock
private VolumeControlProfile mVolumeControlProfile;
@Mock
private AmbientVolumeController mVolumeController;
@Mock
private Handler mTestHandler;
private BluetoothDetailsAmbientVolumePreferenceController mController; private BluetoothDetailsAmbientVolumePreferenceController mController;
@@ -97,11 +119,29 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
public void setUp() { public void setUp() {
super.setUp(); super.setUp();
mContext = spy(mContext);
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 = new BluetoothDetailsAmbientVolumePreferenceController(mContext, mFragment, mController = spy(
mCachedDevice, mLifecycle, mLocalDataManager); 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(mContext.getMainThreadHandler()).thenReturn(mTestHandler);
when(mTestHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
invocationOnMock -> {
invocationOnMock.getArgument(0, Runnable.class).run();
return null;
});
} }
@Test @Test
@@ -128,10 +168,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
@Test @Test
public void onDeviceLocalDataChange_noMemberAndExpanded_uiCorrectAndDataUpdated() { public void onDeviceLocalDataChange_noMemberAndExpanded_uiCorrectAndDataUpdated() {
prepareDevice(/* hasMember= */ false, /* controlExpanded= */ true); prepareDevice(/* hasMember= */ false);
mController.init(mScreen); mController.init(mScreen);
mController.onDeviceLocalDataChange(TEST_ADDRESS, prepareEmptyData()); 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(); shadowOf(Looper.getMainLooper()).idle();
AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
@@ -142,10 +185,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
@Test @Test
public void onDeviceLocalDataChange_noMemberAndCollapsed_uiCorrectAndDataUpdated() { public void onDeviceLocalDataChange_noMemberAndCollapsed_uiCorrectAndDataUpdated() {
prepareDevice(/* hasMember= */ false, /* controlExpanded= */ false); prepareDevice(/* hasMember= */ false);
mController.init(mScreen); mController.init(mScreen);
mController.onDeviceLocalDataChange(TEST_ADDRESS, prepareEmptyData()); 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(); shadowOf(Looper.getMainLooper()).idle();
AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
@@ -156,10 +202,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
@Test @Test
public void onDeviceLocalDataChange_hasMemberAndExpanded_uiCorrectAndDataUpdated() { public void onDeviceLocalDataChange_hasMemberAndExpanded_uiCorrectAndDataUpdated() {
prepareDevice(/* hasMember= */ true, /* controlExpanded= */ true); prepareDevice(/* hasMember= */ true);
mController.init(mScreen); mController.init(mScreen);
mController.onDeviceLocalDataChange(TEST_ADDRESS, prepareEmptyData()); 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(); shadowOf(Looper.getMainLooper()).idle();
AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
@@ -170,10 +219,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
@Test @Test
public void onDeviceLocalDataChange_hasMemberAndCollapsed_uiCorrectAndDataUpdated() { public void onDeviceLocalDataChange_hasMemberAndCollapsed_uiCorrectAndDataUpdated() {
prepareDevice(/* hasMember= */ true, /* controlExpanded= */ false); prepareDevice(/* hasMember= */ true);
mController.init(mScreen); mController.init(mScreen);
mController.onDeviceLocalDataChange(TEST_ADDRESS, prepareEmptyData()); 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(); shadowOf(Looper.getMainLooper()).idle();
AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME);
@@ -185,11 +237,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
@Test @Test
public void onStart_localDataManagerStartAndCallbackRegistered() { public void onStart_localDataManagerStartAndCallbackRegistered() {
prepareDevice(/* hasMember= */ true); prepareDevice(/* hasMember= */ true);
mController.init(mScreen); mController.init(mScreen);
mController.onStart(); mController.onStart();
verify(mLocalDataManager, atLeastOnce()).start(); 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), verify(mCachedDevice).registerCallback(any(Executor.class),
any(CachedBluetoothDevice.Callback.class)); any(CachedBluetoothDevice.Callback.class));
verify(mCachedMemberDevice).registerCallback(any(Executor.class), verify(mCachedMemberDevice).registerCallback(any(Executor.class),
@@ -199,11 +253,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
@Test @Test
public void onStop_localDataManagerStopAndCallbackUnregistered() { public void onStop_localDataManagerStopAndCallbackUnregistered() {
prepareDevice(/* hasMember= */ true); prepareDevice(/* hasMember= */ true);
mController.init(mScreen); mController.init(mScreen);
mController.onStop(); mController.onStop();
verify(mLocalDataManager).stop(); verify(mLocalDataManager).stop();
verify(mVolumeController).unregisterCallback(mDevice);
verify(mVolumeController).unregisterCallback(mMemberDevice);
verify(mCachedDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); verify(mCachedDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class));
verify(mCachedMemberDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); verify(mCachedMemberDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class));
} }
@@ -211,7 +267,6 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
@Test @Test
public void onDeviceAttributesChanged_newDevice_newPreference() { public void onDeviceAttributesChanged_newDevice_newPreference() {
prepareDevice(/* hasMember= */ false); prepareDevice(/* hasMember= */ false);
mController.init(mScreen); mController.init(mScreen);
// check the right control is null before onDeviceAttributesChanged() // check the right control is null before onDeviceAttributesChanged()
@@ -231,16 +286,34 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
assertThat(updatedRightControl).isNotNull(); assertThat(updatedRightControl).isNotNull();
} }
private void prepareDevice(boolean hasMember) { @Test
prepareDevice(hasMember, false); 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();
} }
private void prepareDevice(boolean hasMember, boolean controlExpanded) { private void prepareDevice(boolean hasMember) {
when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT); when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT);
when(mCachedDevice.getDevice()).thenReturn(mDevice); when(mCachedDevice.getDevice()).thenReturn(mDevice);
when(mCachedDevice.getBondState()).thenReturn(BOND_BONDED); when(mCachedDevice.getBondState()).thenReturn(BOND_BONDED);
when(mDevice.getAddress()).thenReturn(TEST_ADDRESS); when(mDevice.getAddress()).thenReturn(TEST_ADDRESS);
when(mDevice.getAnonymizedAddress()).thenReturn(TEST_ADDRESS); when(mDevice.getAnonymizedAddress()).thenReturn(TEST_ADDRESS);
when(mDevice.isConnected()).thenReturn(true);
if (hasMember) { if (hasMember) {
when(mCachedDevice.getMemberDevice()).thenReturn(Set.of(mCachedMemberDevice)); when(mCachedDevice.getMemberDevice()).thenReturn(Set.of(mCachedMemberDevice));
when(mCachedMemberDevice.getDeviceSide()).thenReturn(SIDE_RIGHT); when(mCachedMemberDevice.getDeviceSide()).thenReturn(SIDE_RIGHT);
@@ -248,14 +321,8 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
when(mCachedMemberDevice.getBondState()).thenReturn(BOND_BONDED); when(mCachedMemberDevice.getBondState()).thenReturn(BOND_BONDED);
when(mMemberDevice.getAddress()).thenReturn(TEST_MEMBER_ADDRESS); when(mMemberDevice.getAddress()).thenReturn(TEST_MEMBER_ADDRESS);
when(mMemberDevice.getAnonymizedAddress()).thenReturn(TEST_MEMBER_ADDRESS); when(mMemberDevice.getAnonymizedAddress()).thenReturn(TEST_MEMBER_ADDRESS);
when(mMemberDevice.isConnected()).thenReturn(true);
} }
HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder()
.ambient(0).groupAmbient(0).ambientControlExpanded(controlExpanded).build();
when(mLocalDataManager.get(any(BluetoothDevice.class))).thenReturn(data);
}
private HearingDeviceLocalDataManager.Data prepareEmptyData() {
return new HearingDeviceLocalDataManager.Data.Builder().build();
} }
private void verifyDeviceDataUpdated(BluetoothDevice device) { private void verifyDeviceDataUpdated(BluetoothDevice device) {
@@ -265,6 +332,10 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends
anyBoolean()); anyBoolean());
} }
private AmbientVolumePreference getPreference() {
return mScreen.findPreference(KEY_AMBIENT_VOLUME);
}
@Implements(value = Settings.Global.class) @Implements(value = Settings.Global.class)
public static class ShadowGlobal extends ShadowSettings.ShadowGlobal { public static class ShadowGlobal extends ShadowSettings.ShadowGlobal {
private static final Map<ContentResolver, Map<String, String>> sDataMap = new HashMap<>(); private static final Map<ContentResolver, Map<String, String>> sDataMap = new HashMap<>();