diff --git a/res/values/strings.xml b/res/values/strings.xml index ee80dae7d24..5ab38a48afd 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -174,6 +174,8 @@ Left Right + + Couldn\u2019t update surroundings Audio output diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java index 90727035c0b..887c220c99f 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceController.java @@ -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 android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; import android.content.Context; import android.util.ArraySet; import android.util.Log; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -42,9 +44,12 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.widget.SeekBarPreference; +import com.android.settingslib.bluetooth.AmbientVolumeController; +import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager; import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data; +import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.VolumeControlProfile; import com.android.settingslib.core.lifecycle.Lifecycle; 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.HashBiMap; +import java.util.Map; import java.util.Set; /** A {@link BluetoothDetailsController} that manages ambient volume control preferences. */ public class BluetoothDetailsAmbientVolumePreferenceController extends BluetoothDetailsController implements Preference.OnPreferenceChangeListener, - HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener, OnStart, OnStop { + HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener, OnStart, OnStop, + AmbientVolumeController.AmbientVolumeControlCallback, BluetoothCallback { private static final boolean DEBUG = true; 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_SEPARATED = 1; + private final LocalBluetoothManager mBluetoothManager; private final Set mCachedDevices = new ArraySet<>(); private final BiMap mSideToDeviceMap = HashBiMap.create(); private final BiMap mSideToSliderMap = HashBiMap.create(); private final HearingDeviceLocalDataManager mLocalDataManager; + private final AmbientVolumeController mVolumeController; @Nullable private PreferenceCategory mDeviceControls; @Nullable private AmbientVolumePreference mPreference; + @Nullable + private Toast mToast; public BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, + @NonNull LocalBluetoothManager manager, @NonNull PreferenceFragmentCompat fragment, @NonNull CachedBluetoothDevice device, @NonNull Lifecycle lifecycle) { super(context, fragment, device, lifecycle); + mBluetoothManager = manager; mLocalDataManager = new HearingDeviceLocalDataManager(context); mLocalDataManager.setOnDeviceLocalDataChangeListener(this, ThreadUtils.getBackgroundExecutor()); + mVolumeController = new AmbientVolumeController(manager.getProfileManager(), this); } @VisibleForTesting BluetoothDetailsAmbientVolumePreferenceController(@NonNull Context context, + @NonNull LocalBluetoothManager manager, @NonNull PreferenceFragmentCompat fragment, @NonNull CachedBluetoothDevice device, @NonNull Lifecycle lifecycle, - @NonNull HearingDeviceLocalDataManager localSettings) { + @NonNull HearingDeviceLocalDataManager localSettings, + @NonNull AmbientVolumeController volumeController) { super(context, fragment, device, lifecycle); + mBluetoothManager = manager; mLocalDataManager = localSettings; + mVolumeController = volumeController; } @Override @@ -111,19 +129,33 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends @Override public void onStart() { ThreadUtils.postOnBackgroundThread(() -> { + mBluetoothManager.getEventManager().registerCallback(this); mLocalDataManager.start(); mCachedDevices.forEach(device -> { device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); + mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(), + device.getDevice()); }); }); } + @Override + public void onResume() { + refresh(); + } + + @Override + public void onPause() { + } + @Override public void onStop() { ThreadUtils.postOnBackgroundThread(() -> { + mBluetoothManager.getEventManager().unregisterCallback(this); mLocalDataManager.stop(); mCachedDevices.forEach(device -> { device.unregisterCallback(this); + mVolumeController.unregisterCallback(device.getDevice()); }); }); } @@ -133,8 +165,17 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends if (!isAvailable()) { return; } - // TODO: load data from remote - loadLocalDataToUi(); + boolean shouldShowAmbientControl = isAmbientControlAvailable(); + if (shouldShowAmbientControl) { + if (mPreference != null) { + mPreference.setVisible(true); + } + loadRemoteDataToUi(); + } else { + if (mPreference != null) { + mPreference.setVisible(false); + } + } } @Override @@ -160,19 +201,33 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends setVolumeIfValid(side, value); if (side == SIDE_UNIFIED) { - // TODO: set the value on the devices + mSideToDeviceMap.forEach((s, d) -> mVolumeController.setAmbient(d, value)); } else { - // TODO: set the value on the side device + final BluetoothDevice device = mSideToDeviceMap.get(side); + mVolumeController.setAmbient(device, value); } 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(); @@ -182,6 +237,8 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends ThreadUtils.postOnBackgroundThread(() -> mCachedDevices.forEach(device -> { 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() { mSideToDeviceMap.clear(); mCachedDevices.clear(); @@ -234,6 +326,11 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends mPreference.setOrder(ORDER_AMBIENT_VOLUME); mPreference.setOnIconClickListener(() -> { mSideToDeviceMap.forEach((s, d) -> { + // Apply previous collapsed/expanded volume to remote device + Data data = mLocalDataManager.get(d); + int volume = isControlExpanded() + ? data.ambient() : data.groupAmbient(); + mVolumeController.setAmbient(d, volume); // Update new value to local data mLocalDataManager.updateAmbientControlExpanded(d, isControlExpanded()); }); @@ -269,6 +366,16 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends /** Refreshes the control UI visibility and enabled state. */ private void refreshControlUi() { if (mPreference != null) { + boolean isAnySliderEnabled = false; + for (Map.Entry entry : mSideToDeviceMap.entrySet()) { + final int side = entry.getKey(); + final BluetoothDevice device = entry.getValue(); + final boolean enabled = isDeviceConnectedToVcp(device) + && mVolumeController.isAmbientControlAvailable(device); + isAnySliderEnabled |= enabled; + mPreference.setSliderEnabled(side, enabled); + } + mPreference.setSliderEnabled(SIDE_UNIFIED, isAnySliderEnabled); mPreference.updateLayout(); } } @@ -299,12 +406,74 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device); } final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID); - setVolumeIfValid(side, data.ambient()); - setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient()); + if (isDeviceConnectedToVcp(device)) { + 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(); + + 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(); } @@ -318,4 +487,41 @@ public class BluetoothDetailsAmbientVolumePreferenceController extends 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(); + } } diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java index 7236d8cecc8..8af08792180 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java @@ -110,7 +110,7 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon } if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) { mControllers.add(new BluetoothDetailsAmbientVolumePreferenceController(mContext, - mFragment, mCachedDevice, mLifecycle)); + mManager, mFragment, mCachedDevice, mLifecycle)); } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java index 71da4b272c6..b7aaab4527a 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAmbientVolumePreferenceControllerTest.java @@ -29,14 +29,19 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; import android.content.ContentResolver; +import android.os.Handler; import android.os.Looper; import android.provider.Settings; @@ -44,8 +49,13 @@ import androidx.preference.PreferenceCategory; import com.android.settings.testutils.shadow.ShadowThreadUtils; import com.android.settings.widget.SeekBarPreference; +import com.android.settingslib.bluetooth.AmbientVolumeController; +import com.android.settingslib.bluetooth.BluetoothEventManager; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HearingDeviceLocalDataManager; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.bluetooth.VolumeControlProfile; import org.junit.Before; import org.junit.Rule; @@ -90,6 +100,18 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends private BluetoothDevice mMemberDevice; @Mock 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; @@ -97,11 +119,29 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends public void setUp() { super.setUp(); + mContext = spy(mContext); PreferenceCategory deviceControls = new PreferenceCategory(mContext); deviceControls.setKey(KEY_HEARING_DEVICE_GROUP); mScreen.addPreference(deviceControls); - mController = new BluetoothDetailsAmbientVolumePreferenceController(mContext, mFragment, - mCachedDevice, mLifecycle, mLocalDataManager); + 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(mContext.getMainThreadHandler()).thenReturn(mTestHandler); + when(mTestHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer( + invocationOnMock -> { + invocationOnMock.getArgument(0, Runnable.class).run(); + return null; + }); } @Test @@ -128,10 +168,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends @Test public void onDeviceLocalDataChange_noMemberAndExpanded_uiCorrectAndDataUpdated() { - prepareDevice(/* hasMember= */ false, /* controlExpanded= */ true); - + prepareDevice(/* hasMember= */ false); 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(); AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); @@ -142,10 +185,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends @Test public void onDeviceLocalDataChange_noMemberAndCollapsed_uiCorrectAndDataUpdated() { - prepareDevice(/* hasMember= */ false, /* controlExpanded= */ false); - + prepareDevice(/* hasMember= */ false); 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(); AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); @@ -156,10 +202,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends @Test public void onDeviceLocalDataChange_hasMemberAndExpanded_uiCorrectAndDataUpdated() { - prepareDevice(/* hasMember= */ true, /* controlExpanded= */ true); - + prepareDevice(/* hasMember= */ true); 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(); AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); @@ -170,10 +219,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends @Test public void onDeviceLocalDataChange_hasMemberAndCollapsed_uiCorrectAndDataUpdated() { - prepareDevice(/* hasMember= */ true, /* controlExpanded= */ false); - + prepareDevice(/* hasMember= */ true); 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(); AmbientVolumePreference preference = mScreen.findPreference(KEY_AMBIENT_VOLUME); @@ -185,11 +237,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends @Test public void onStart_localDataManagerStartAndCallbackRegistered() { prepareDevice(/* hasMember= */ true); - mController.init(mScreen); + mController.onStart(); verify(mLocalDataManager, atLeastOnce()).start(); + verify(mVolumeController).registerCallback(any(Executor.class), eq(mDevice)); + verify(mVolumeController).registerCallback(any(Executor.class), eq(mMemberDevice)); verify(mCachedDevice).registerCallback(any(Executor.class), any(CachedBluetoothDevice.Callback.class)); verify(mCachedMemberDevice).registerCallback(any(Executor.class), @@ -199,11 +253,13 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends @Test public void onStop_localDataManagerStopAndCallbackUnregistered() { prepareDevice(/* hasMember= */ true); - mController.init(mScreen); + mController.onStop(); verify(mLocalDataManager).stop(); + verify(mVolumeController).unregisterCallback(mDevice); + verify(mVolumeController).unregisterCallback(mMemberDevice); verify(mCachedDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); verify(mCachedMemberDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); } @@ -211,7 +267,6 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends @Test public void onDeviceAttributesChanged_newDevice_newPreference() { prepareDevice(/* hasMember= */ false); - mController.init(mScreen); // check the right control is null before onDeviceAttributesChanged() @@ -231,16 +286,34 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends assertThat(updatedRightControl).isNotNull(); } - private void prepareDevice(boolean hasMember) { - prepareDevice(hasMember, false); + @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(); } - private void prepareDevice(boolean hasMember, boolean controlExpanded) { + 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); @@ -248,14 +321,8 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends 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); } - 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) { @@ -265,6 +332,10 @@ public class BluetoothDetailsAmbientVolumePreferenceControllerTest extends anyBoolean()); } + private AmbientVolumePreference getPreference() { + return mScreen.findPreference(KEY_AMBIENT_VOLUME); + } + @Implements(value = Settings.Global.class) public static class ShadowGlobal extends ShadowSettings.ShadowGlobal { private static final Map> sDataMap = new HashMap<>();