diff --git a/res/values/strings.xml b/res/values/strings.xml index 1fa41eb41b7..92157f3e9b1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -156,6 +156,8 @@ Shortcut, hearing aid compatibility Presets + + Couldn\u2019t update preset Audio output diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java index 208f8d0afbd..43721f4cf92 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java @@ -19,15 +19,18 @@ package com.android.settings.bluetooth; import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.ORDER_HEARING_AIDS_PRESETS; +import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHapPresetInfo; import android.content.Context; import android.text.TextUtils; import android.util.Log; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; @@ -88,8 +91,53 @@ public class BluetoothDetailsHearingAidsPresetsController extends @Override public boolean onPreferenceChange(@NonNull Preference preference, @Nullable Object newValue) { if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { - // TODO(b/300015207): Update the settings to remote device - return true; + if (newValue instanceof final String value + && preference instanceof final ListPreference listPreference) { + final int index = listPreference.findIndexOfValue(value); + final String presetName = listPreference.getEntries()[index].toString(); + final int presetIndex = Integer.parseInt( + listPreference.getEntryValues()[index].toString()); + listPreference.setSummary(presetName); + boolean supportSynchronizedPresets = mHapClientProfile.supportsSynchronizedPresets( + mCachedDevice.getDevice()); + int hapGroupId = mHapClientProfile.getHapGroup(mCachedDevice.getDevice()); + if (supportSynchronizedPresets + && hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { + if (DEBUG) { + Log.d(TAG, "onPreferenceChange, selectPresetForGroup " + + ", presetName: " + presetName + + ", presetIndex: " + presetIndex + + ", hapGroupId: " + hapGroupId + + ", device: " + mCachedDevice.getAddress()); + } + mHapClientProfile.selectPresetForGroup(hapGroupId, presetIndex); + } else { + if (DEBUG) { + Log.d(TAG, "onPreferenceChange, selectPreset " + + ", presetName: " + presetName + + ", presetIndex: " + presetIndex + + ", device: " + mCachedDevice.getAddress()); + } + mHapClientProfile.selectPreset(mCachedDevice.getDevice(), presetIndex); + final CachedBluetoothDevice subDevice = mCachedDevice.getSubDevice(); + if (subDevice != null) { + if (DEBUG) { + Log.d(TAG, "onPreferenceChange, selectPreset for subDevice" + + ", device: " + subDevice.getAddress()); + } + mHapClientProfile.selectPreset(subDevice.getDevice(), presetIndex); + } + for (final CachedBluetoothDevice memberDevice : + mCachedDevice.getMemberDevice()) { + if (DEBUG) { + Log.d(TAG, "onPreferenceChange, selectPreset for memberDevice" + + ", device: " + memberDevice.getAddress()); + } + mHapClientProfile.selectPreset(memberDevice.getDevice(), presetIndex); + } + } + return true; + } } return false; } @@ -115,12 +163,29 @@ public class BluetoothDetailsHearingAidsPresetsController extends return; } mPreference.setEnabled(mCachedDevice.isConnectedHapClientDevice()); - // TODO(b/300015207): Load preset from remote and show in UI + + loadAllPresetInfo(); + if (mPreference.getEntries().length == 0) { + mPreference.setEnabled(false); + } else { + int activePresetIndex = mHapClientProfile.getActivePresetIndex( + mCachedDevice.getDevice()); + if (activePresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) { + mPreference.setValue(Integer.toString(activePresetIndex)); + mPreference.setSummary(mPreference.getEntry()); + } else { + mPreference.setSummary(null); + } + } } @Override public boolean isAvailable() { - return false; + if (mHapClientProfile == null) { + return false; + } + return mCachedDevice.getProfiles().stream().anyMatch( + profile -> profile instanceof HapClientProfile); } @Override @@ -130,7 +195,7 @@ public class BluetoothDetailsHearingAidsPresetsController extends Log.d(TAG, "onPresetSelected, device: " + device.getAddress() + ", presetIndex: " + presetIndex + ", reason: " + reason); } - // TODO(b/300015207): Update the UI + mContext.getMainExecutor().execute(this::refresh); } } @@ -142,7 +207,10 @@ public class BluetoothDetailsHearingAidsPresetsController extends "onPresetSelectionFailed, device: " + device.getAddress() + ", reason: " + reason); } - // TODO(b/300015207): Update the UI + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); } } @@ -153,7 +221,10 @@ public class BluetoothDetailsHearingAidsPresetsController extends Log.d(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId + ", reason: " + reason); } - // TODO(b/300015207): Update the UI + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); } } @@ -166,7 +237,7 @@ public class BluetoothDetailsHearingAidsPresetsController extends + ", reason: " + reason + ", infoList: " + presetInfoList); } - // TODO(b/300015207): Update the UI + mContext.getMainExecutor().execute(this::refresh); } } @@ -178,7 +249,10 @@ public class BluetoothDetailsHearingAidsPresetsController extends "onSetPresetNameFailed, device: " + device.getAddress() + ", reason: " + reason); } - // TODO(b/300015207): Update the UI + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); } } @@ -189,7 +263,10 @@ public class BluetoothDetailsHearingAidsPresetsController extends Log.d(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId + ", reason: " + reason); } - // TODO(b/300015207): Update the UI + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); } } @@ -201,4 +278,31 @@ public class BluetoothDetailsHearingAidsPresetsController extends preference.setOnPreferenceChangeListener(this); return preference; } + + private void loadAllPresetInfo() { + if (mPreference == null) { + return; + } + List infoList = mHapClientProfile.getAllPresetInfo( + mCachedDevice.getDevice()); + CharSequence[] presetNames = new CharSequence[infoList.size()]; + CharSequence[] presetIndexes = new CharSequence[infoList.size()]; + for (int i = 0; i < infoList.size(); i++) { + presetNames[i] = infoList.get(i).getName(); + presetIndexes[i] = Integer.toString(infoList.get(i).getIndex()); + } + mPreference.setEntries(presetNames); + mPreference.setEntryValues(presetIndexes); + } + + @VisibleForTesting + @Nullable + ListPreference getPreference() { + return mPreference; + } + + void showErrorToast() { + Toast.makeText(mContext, R.string.bluetooth_hearing_aids_presets_error, + Toast.LENGTH_SHORT).show(); + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java index 3a898c150e1..c08bb98e55b 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java @@ -16,21 +16,29 @@ package com.android.settings.bluetooth; +import static android.bluetooth.BluetoothCsipSetCoordinator.GROUP_ID_INVALID; +import static android.bluetooth.BluetoothHapClient.PRESET_INDEX_UNAVAILABLE; + import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; import static com.android.settings.bluetooth.BluetoothDetailsHearingAidsPresetsController.KEY_HEARING_AIDS_PRESETS; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +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 android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapClient; +import android.bluetooth.BluetoothHapPresetInfo; import androidx.preference.ListPreference; import androidx.preference.PreferenceCategory; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HapClientProfile; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; @@ -43,7 +51,9 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; +import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; /** Tests for {@link BluetoothDetailsHearingAidsPresetsController}. */ @@ -53,6 +63,7 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends private static final int TEST_PRESET_INDEX = 1; private static final String TEST_PRESET_NAME = "test_preset"; + private static final int TEST_HAP_GROUP_ID = 1; @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @@ -63,6 +74,10 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends private LocalBluetoothProfileManager mProfileManager; @Mock private HapClientProfile mHapClientProfile; + @Mock + private CachedBluetoothDevice mCachedChildDevice; + @Mock + private BluetoothDevice mChildDevice; private BluetoothDetailsHearingAidsPresetsController mController; @@ -73,6 +88,8 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile); when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + when(mCachedDevice.isConnectedHapClientDevice()).thenReturn(true); + when(mCachedChildDevice.getDevice()).thenReturn(mChildDevice); PreferenceCategory deviceControls = new PreferenceCategory(mContext); deviceControls.setKey(KEY_HEARING_DEVICE_GROUP); mScreen.addPreference(deviceControls); @@ -81,6 +98,20 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends mController.init(mScreen); } + @Test + public void isAvailable_supportHap_returnTrue() { + when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_notSupportHap_returnFalse() { + when(mCachedDevice.getProfiles()).thenReturn(new ArrayList<>()); + + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void onResume_registerCallback() { mController.onResume(); @@ -96,7 +127,6 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends verify(mHapClientProfile).unregisterCallback(any(BluetoothHapClient.Callback.class)); } - @Test public void onPreferenceChange_keyMatched_verifyStatusUpdated() { final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); @@ -105,6 +135,7 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends String.valueOf(TEST_PRESET_INDEX)); assertThat(handled).isTrue(); + verify(presetPreference).setSummary(TEST_PRESET_NAME); } @Test @@ -115,6 +146,116 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends presetPreference, String.valueOf(TEST_PRESET_INDEX)); assertThat(handled).isFalse(); + verify(presetPreference, never()).setSummary(any()); + } + + @Test + public void onPreferenceChange_supportGroupOperation_validGroupId_verifySelectPresetForGroup() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(true); + when(mHapClientProfile.getHapGroup(mDevice)).thenReturn(TEST_HAP_GROUP_ID); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPresetForGroup(TEST_HAP_GROUP_ID, TEST_PRESET_INDEX); + } + + @Test + public void onPreferenceChange_notSupportGroupOperation_verifySelectPreset() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(false); + when(mHapClientProfile.getHapGroup(mDevice)).thenReturn(TEST_HAP_GROUP_ID); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPreset(mDevice, TEST_PRESET_INDEX); + } + + @Test + public void onPreferenceChange_invalidGroupId_verifySelectPreset() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(true); + when(mHapClientProfile.getHapGroup(mDevice)).thenReturn(GROUP_ID_INVALID); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPreset(mDevice, TEST_PRESET_INDEX); + } + + @Test + public void onPreferenceChange_notSupportGroupOperation_hasSubDevice_verifyStatusUpdated() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(false); + when(mCachedDevice.getSubDevice()).thenReturn(mCachedChildDevice); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPreset(mDevice, TEST_PRESET_INDEX); + verify(mHapClientProfile).selectPreset(mChildDevice, TEST_PRESET_INDEX); + } + + @Test + public void onPreferenceChange_notSupportGroupOperation_hasMemberDevice_verifyStatusUpdated() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice)).thenReturn(false); + when(mCachedDevice.getMemberDevice()).thenReturn(Set.of(mCachedChildDevice)); + + mController.onPreferenceChange(presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + verify(mHapClientProfile).selectPreset(mDevice, TEST_PRESET_INDEX); + verify(mHapClientProfile).selectPreset(mChildDevice, TEST_PRESET_INDEX); + } + + @Test + public void refresh_emptyPresetInfo_preferenceDisabled() { + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(new ArrayList<>()); + + mController.refresh(); + + assertThat(mController.getPreference()).isNotNull(); + assertThat(mController.getPreference().isEnabled()).isFalse(); + } + + @Test + public void refresh_validPresetInfo_preferenceEnabled() { + BluetoothHapPresetInfo info = getTestPresetInfo(); + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); + + mController.refresh(); + + assertThat(mController.getPreference()).isNotNull(); + assertThat(mController.getPreference().isEnabled()).isTrue(); + } + + @Test + public void refresh_invalidActivePresetIndex_summaryIsNull() { + BluetoothHapPresetInfo info = getTestPresetInfo(); + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); + when(mHapClientProfile.getActivePresetIndex(mDevice)).thenReturn(PRESET_INDEX_UNAVAILABLE); + + mController.refresh(); + + assertThat(mController.getPreference()).isNotNull(); + assertThat(mController.getPreference().getSummary()).isNull(); + } + + @Test + public void refresh_validActivePresetIndex_summaryIsNotNull() { + BluetoothHapPresetInfo info = getTestPresetInfo(); + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); + when(mHapClientProfile.getActivePresetIndex(mDevice)).thenReturn(TEST_PRESET_INDEX); + + mController.refresh(); + + assertThat(mController.getPreference()).isNotNull(); + assertThat(mController.getPreference().getSummary()).isNotNull(); + } + + private BluetoothHapPresetInfo getTestPresetInfo() { + BluetoothHapPresetInfo info = mock(BluetoothHapPresetInfo.class); + when(info.getName()).thenReturn(TEST_PRESET_NAME); + when(info.getIndex()).thenReturn(TEST_PRESET_INDEX); + return info; } private ListPreference getTestPresetPreference(String key) {