From 4af270b231be69f40f1fb124c92c9a58df276c6a Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Tue, 27 Feb 2024 13:39:37 +0000 Subject: [PATCH 1/3] Separate category controller out of HearingDeviceControlsController We're going to add more different device controls (such as hearing aids presets, volume offset controls or microphone volume controls) into "device_controls_general" PreferenceCategory. It's better to keep the category controller separated from the child controller to better maintain the visibility of the whole category and have a clearer stucture of these controllers. Bug: 300015207 Test: atest BluetoothDetailsHearingDeviceControllerTest Test: atest BluetoothDetailsHearingDeviceSettingsControllerTest Test: atest BluetoothDeviceDetailsFragmentTest Change-Id: I7f35b02a1120aefa8307e500f7abfce3b8055fbf --- res/values/strings.xml | 12 +-- res/xml/bluetooth_device_details_fragment.xml | 2 +- ...uetoothDetailsHearingDeviceController.java | 102 ++++++++++++++++++ ...tailsHearingDeviceSettingsController.java} | 36 ++++--- .../BluetoothDeviceDetailsFragment.java | 20 ++-- ...othDetailsHearingDeviceControllerTest.java | 83 ++++++++++++++ ...sHearingDeviceSettingsControllerTest.java} | 19 ++-- .../BluetoothDeviceDetailsFragmentTest.java | 6 +- 8 files changed, 232 insertions(+), 48 deletions(-) create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java rename src/com/android/settings/bluetooth/{BluetoothDetailsHearingDeviceControlsController.java => BluetoothDetailsHearingDeviceSettingsController.java} (75%) create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java rename tests/robotests/src/com/android/settings/bluetooth/{BluetoothDetailsHearingDeviceControlsControllerTest.java => BluetoothDetailsHearingDeviceSettingsControllerTest.java} (81%) diff --git a/res/values/strings.xml b/res/values/strings.xml index cf13dfbf8d2..3f01af0b9cf 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -150,14 +150,10 @@ Pair right ear Pair left ear - - For all available hearing devices - - More hearing device settings - - Change cross-device settings like shortcut, and telecoil controls - - For this device + + Hearing device settings + + Shortcut, hearing aid compatibility Audio output diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml index d260554f937..91f73a70b6e 100644 --- a/res/xml/bluetooth_device_details_fragment.xml +++ b/res/xml/bluetooth_device_details_fragment.xml @@ -69,7 +69,7 @@ android:key="device_companion_apps"/> + android:key="hearing_device_group" /> diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java new file mode 100644 index 00000000000..27a4cb17aa1 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; + +/** + * The controller of the hearing device controls. + * + *

Note: It is responsible for creating the sub-controllers inside this preference + * category controller. + */ +public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsController { + static final String KEY_HEARING_DEVICE_GROUP = "hearing_device_group"; + + private final List mControllers = new ArrayList<>(); + private Lifecycle mLifecycle; + + public BluetoothDetailsHearingDeviceController(@NonNull Context context, + @NonNull PreferenceFragmentCompat fragment, + @NonNull CachedBluetoothDevice device, + @NonNull Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + mLifecycle = lifecycle; + } + + @VisibleForTesting + void setSubControllers( + BluetoothDetailsHearingDeviceSettingsController hearingDeviceSettingsController) { + mControllers.clear(); + mControllers.add(hearingDeviceSettingsController); + } + + @Override + public boolean isAvailable() { + return mControllers.stream().anyMatch(BluetoothDetailsController::isAvailable); + } + + @Override + @NonNull + public String getPreferenceKey() { + return KEY_HEARING_DEVICE_GROUP; + } + + @Override + protected void init(PreferenceScreen screen) { + + } + + @Override + protected void refresh() { + + } + + /** + * Initiates the sub controllers controlled by this group controller. + * + *

Note: The caller must call this method when creating this class. + * + * @param isLaunchFromHearingDevicePage a boolean that determines if the caller is launch from + * hearing device page + */ + void initSubControllers(boolean isLaunchFromHearingDevicePage) { + mControllers.clear(); + // Don't need to show the entrance to hearing device page when launched from the same page + if (!isLaunchFromHearingDevicePage) { + mControllers.add(new BluetoothDetailsHearingDeviceSettingsController(mContext, + mFragment, mCachedDevice, mLifecycle)); + } + } + + @NonNull + public List getSubControllers() { + return mControllers; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java similarity index 75% rename from src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java rename to src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java index 162abc78aef..b381cc4b2b8 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java @@ -16,6 +16,8 @@ package com.android.settings.bluetooth; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; + import android.content.Context; import android.text.TextUtils; @@ -36,15 +38,13 @@ import com.google.common.annotations.VisibleForTesting; /** * The controller of the hearing device settings to launch Hearing device page. */ -public class BluetoothDetailsHearingDeviceControlsController extends BluetoothDetailsController +public class BluetoothDetailsHearingDeviceSettingsController extends BluetoothDetailsController implements Preference.OnPreferenceClickListener { @VisibleForTesting - static final String KEY_DEVICE_CONTROLS_GENERAL_GROUP = "device_controls_general"; - @VisibleForTesting - static final String KEY_HEARING_DEVICE_CONTROLS = "hearing_device_controls"; + static final String KEY_HEARING_DEVICE_SETTINGS = "hearing_device_settings"; - public BluetoothDetailsHearingDeviceControlsController(Context context, + public BluetoothDetailsHearingDeviceSettingsController(Context context, PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) { super(context, fragment, device, lifecycle); lifecycle.addObserver(this); @@ -57,37 +57,39 @@ public class BluetoothDetailsHearingDeviceControlsController extends BluetoothDe @Override protected void init(PreferenceScreen screen) { - if (!mCachedDevice.isHearingAidDevice()) { + if (!isAvailable()) { return; } - - final PreferenceCategory prefCategory = screen.findPreference(getPreferenceKey()); - final Preference pref = createHearingDeviceControlsPreference(prefCategory.getContext()); - prefCategory.addPreference(pref); + final PreferenceCategory group = screen.findPreference(KEY_HEARING_DEVICE_GROUP); + final Preference pref = createHearingDeviceSettingsPreference(group.getContext()); + group.addPreference(pref); } @Override - protected void refresh() {} + protected void refresh() { + + } @Override public String getPreferenceKey() { - return KEY_DEVICE_CONTROLS_GENERAL_GROUP; + return KEY_HEARING_DEVICE_SETTINGS; } @Override public boolean onPreferenceClick(Preference preference) { - if (TextUtils.equals(preference.getKey(), KEY_HEARING_DEVICE_CONTROLS)) { + if (TextUtils.equals(preference.getKey(), KEY_HEARING_DEVICE_SETTINGS)) { launchAccessibilityHearingDeviceSettings(); return true; } return false; } - private Preference createHearingDeviceControlsPreference(Context context) { + private Preference createHearingDeviceSettingsPreference(Context context) { final ArrowPreference preference = new ArrowPreference(context); - preference.setKey(KEY_HEARING_DEVICE_CONTROLS); - preference.setTitle(context.getString(R.string.bluetooth_device_controls_title)); - preference.setSummary(context.getString(R.string.bluetooth_device_controls_summary)); + preference.setKey(KEY_HEARING_DEVICE_SETTINGS); + preference.setTitle(context.getString(R.string.bluetooth_hearing_device_settings_title)); + preference.setSummary( + context.getString(R.string.bluetooth_hearing_device_settings_summary)); preference.setOnPreferenceClickListener(this); return preference; diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index 9c68c9cc870..dae1e086115 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -326,16 +326,16 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment lifecycle)); controllers.add(new BluetoothDetailsPairOtherController(context, this, mCachedDevice, lifecycle)); - // Don't need to show hearing device again when launched from the same page. - if (!isLaunchFromHearingDevicePage()) { - controllers.add(new BluetoothDetailsHearingDeviceControlsController(context, this, - mCachedDevice, lifecycle)); - } - controllers.add(new BluetoothDetailsDataSyncController(context, this, - mCachedDevice, lifecycle)); - controllers.add( - new BluetoothDetailsExtraOptionsController( - context, this, mCachedDevice, lifecycle)); + controllers.add(new BluetoothDetailsDataSyncController(context, this, mCachedDevice, + lifecycle)); + controllers.add(new BluetoothDetailsExtraOptionsController(context, this, mCachedDevice, + lifecycle)); + BluetoothDetailsHearingDeviceController hearingDeviceController = + new BluetoothDetailsHearingDeviceController(context, this, mCachedDevice, + lifecycle); + controllers.add(hearingDeviceController); + hearingDeviceController.initSubControllers(isLaunchFromHearingDevicePage()); + controllers.addAll(hearingDeviceController.getSubControllers()); } return controllers; } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java new file mode 100644 index 00000000000..d5284b4c438 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link BluetoothDetailsHearingDeviceController}. */ +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsHearingDeviceControllerTest extends + BluetoothDetailsControllerTestBase { + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private BluetoothDetailsHearingDeviceController mHearingDeviceController; + + @Mock + private BluetoothDetailsHearingDeviceSettingsController mHearingDeviceSettingsController; + + @Override + public void setUp() { + super.setUp(); + + mHearingDeviceController = new BluetoothDetailsHearingDeviceController(mContext, + mFragment, mCachedDevice, mLifecycle); + mHearingDeviceController.setSubControllers(mHearingDeviceSettingsController); + } + + @Test + public void isAvailable_hearingDeviceSettingsAvailable_returnTrue() { + when(mHearingDeviceSettingsController.isAvailable()).thenReturn(true); + + assertThat(mHearingDeviceController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_noControllersAvailable_returnFalse() { + when(mHearingDeviceSettingsController.isAvailable()).thenReturn(false); + + assertThat(mHearingDeviceController.isAvailable()).isFalse(); + } + + + @Test + public void initSubControllers_launchFromHearingDevicePage_hearingDeviceSettingsNotExist() { + mHearingDeviceController.initSubControllers(true); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingDeviceSettingsController)).isFalse(); + } + + @Test + public void initSubControllers_notLaunchFromHearingDevicePage_hearingDeviceSettingsExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingDeviceSettingsController)).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsControllerTest.java similarity index 81% rename from tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java rename to tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsControllerTest.java index 364d299e519..b420717d397 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsControllerTest.java @@ -39,23 +39,24 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; -/** Tests for {@link BluetoothDetailsHearingDeviceControlsController}. */ +/** Tests for {@link BluetoothDetailsHearingDeviceSettingsController}. */ @RunWith(RobolectricTestRunner.class) -public class BluetoothDetailsHearingDeviceControlsControllerTest extends +public class BluetoothDetailsHearingDeviceSettingsControllerTest extends BluetoothDetailsControllerTestBase { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Captor private ArgumentCaptor mIntentArgumentCaptor; - private BluetoothDetailsHearingDeviceControlsController mController; + private BluetoothDetailsHearingDeviceSettingsController mController; @Override public void setUp() { super.setUp(); FakeFeatureFactory.setupForTest(); - mController = new BluetoothDetailsHearingDeviceControlsController(mActivity, mFragment, + mController = new BluetoothDetailsHearingDeviceSettingsController(mActivity, mFragment, mCachedDevice, mLifecycle); when(mCachedDevice.isHearingAidDevice()).thenReturn(true); } @@ -75,12 +76,12 @@ public class BluetoothDetailsHearingDeviceControlsControllerTest extends } @Test - public void onPreferenceClick_hearingDeviceControlsKey_LaunchExpectedFragment() { - final Preference hearingControlsKeyPreference = new Preference(mContext); - hearingControlsKeyPreference.setKey( - BluetoothDetailsHearingDeviceControlsController.KEY_HEARING_DEVICE_CONTROLS); + public void onPreferenceClick_hearingDeviceSettingsKey_launchExpectedFragment() { + final Preference hearingDeviceSettingsPreference = new Preference(mContext); + hearingDeviceSettingsPreference.setKey( + BluetoothDetailsHearingDeviceSettingsController.KEY_HEARING_DEVICE_SETTINGS); - mController.onPreferenceClick(hearingControlsKeyPreference); + mController.onPreferenceClick(hearingDeviceSettingsPreference); assertStartActivityWithExpectedFragment(mActivity, AccessibilityHearingAidsFragment.class.getName()); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java index fc72c412b6e..50aa7719ccb 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java @@ -18,7 +18,7 @@ package com.android.settings.bluetooth; import static android.bluetooth.BluetoothDevice.BOND_NONE; -import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceControlsController.KEY_DEVICE_CONTROLS_GENERAL_GROUP; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceSettingsController.KEY_HEARING_DEVICE_SETTINGS; import static com.google.common.truth.Truth.assertThat; @@ -237,7 +237,7 @@ public class BluetoothDeviceDetailsFragmentTest { assertThat(controllerList.stream() .anyMatch(controller -> controller.getPreferenceKey().equals( - KEY_DEVICE_CONTROLS_GENERAL_GROUP))).isFalse(); + KEY_HEARING_DEVICE_SETTINGS))).isFalse(); } @Test @@ -253,7 +253,7 @@ public class BluetoothDeviceDetailsFragmentTest { assertThat(controllerList.stream() .anyMatch(controller -> controller.getPreferenceKey().equals( - KEY_DEVICE_CONTROLS_GENERAL_GROUP))).isTrue(); + KEY_HEARING_DEVICE_SETTINGS))).isTrue(); } private InputDevice createInputDeviceWithMatchingBluetoothAddress() { From 82e4ed3bd1a48e9ab73def091ba31b6556ac02ac Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Tue, 27 Feb 2024 13:41:56 +0000 Subject: [PATCH 2/3] Selects presets in device details page (1/2) Enables users to select their presets in Bluetooth device details page if the device supports HAP. This CL only contains the UI elements. The full functionality will be introduce in the next CL. Bug: 300015207 Test: atest BluetoothDetailsHearingDeviceControllerTest Test: atest BluetoothDetailsHearingAidsPresetsControllerTest Change-Id: I1ab4781191b0c9e1033a29c30ca61671878bb7e1 --- res/values/strings.xml | 2 + ...thDetailsHearingAidsPresetsController.java | 204 ++++++++++++++++++ ...uetoothDetailsHearingDeviceController.java | 16 +- ...etailsHearingDeviceSettingsController.java | 2 + .../BluetoothDeviceDetailsFragment.java | 4 +- ...tailsHearingAidsPresetsControllerTest.java | 129 +++++++++++ ...othDetailsHearingDeviceControllerTest.java | 52 ++++- 7 files changed, 403 insertions(+), 6 deletions(-) create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 3f01af0b9cf..1fa41eb41b7 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -154,6 +154,8 @@ Hearing device settings Shortcut, hearing aid compatibility + + Presets Audio output diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java new file mode 100644 index 00000000000..208f8d0afbd --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.BluetoothDevice; +import android.bluetooth.BluetoothHapClient; +import android.bluetooth.BluetoothHapPresetInfo; +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.HapClientProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.events.OnPause; +import com.android.settingslib.core.lifecycle.events.OnResume; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.List; + +/** + * The controller of the hearing aid presets. + */ +public class BluetoothDetailsHearingAidsPresetsController extends + BluetoothDetailsController implements Preference.OnPreferenceChangeListener, + BluetoothHapClient.Callback, OnResume, OnPause { + + private static final boolean DEBUG = true; + private static final String TAG = "BluetoothDetailsHearingAidsPresetsController"; + static final String KEY_HEARING_AIDS_PRESETS = "hearing_aids_presets"; + + private final HapClientProfile mHapClientProfile; + @Nullable + private ListPreference mPreference; + + public BluetoothDetailsHearingAidsPresetsController(@NonNull Context context, + @NonNull PreferenceFragmentCompat fragment, + @NonNull LocalBluetoothManager manager, + @NonNull CachedBluetoothDevice device, + @NonNull Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + mHapClientProfile = manager.getProfileManager().getHapClientProfile(); + } + + @Override + public void onResume() { + super.onResume(); + if (mHapClientProfile != null) { + mHapClientProfile.registerCallback(ThreadUtils.getBackgroundExecutor(), this); + } + } + + @Override + public void onPause() { + if (mHapClientProfile != null) { + mHapClientProfile.unregisterCallback(this); + } + super.onPause(); + } + + @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; + } + return false; + } + + @Nullable + @Override + public String getPreferenceKey() { + return KEY_HEARING_AIDS_PRESETS; + } + + @Override + protected void init(PreferenceScreen screen) { + PreferenceCategory deviceControls = screen.findPreference(KEY_HEARING_DEVICE_GROUP); + if (deviceControls != null) { + mPreference = createPresetPreference(deviceControls.getContext()); + deviceControls.addPreference(mPreference); + } + } + + @Override + protected void refresh() { + if (!isAvailable() || mPreference == null) { + return; + } + mPreference.setEnabled(mCachedDevice.isConnectedHapClientDevice()); + // TODO(b/300015207): Load preset from remote and show in UI + } + + @Override + public boolean isAvailable() { + return false; + } + + @Override + public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex, int reason) { + if (device.equals(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onPresetSelected, device: " + device.getAddress() + + ", presetIndex: " + presetIndex + ", reason: " + reason); + } + // TODO(b/300015207): Update the UI + } + } + + @Override + public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) { + if (device.equals(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, + "onPresetSelectionFailed, device: " + device.getAddress() + + ", reason: " + reason); + } + // TODO(b/300015207): Update the UI + } + } + + @Override + public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) { + if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId + + ", reason: " + reason); + } + // TODO(b/300015207): Update the UI + } + } + + @Override + public void onPresetInfoChanged(@NonNull BluetoothDevice device, + @NonNull List presetInfoList, int reason) { + if (device.equals(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onPresetInfoChanged, device: " + device.getAddress() + + ", reason: " + reason + + ", infoList: " + presetInfoList); + } + // TODO(b/300015207): Update the UI + } + } + + @Override + public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) { + if (device.equals(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, + "onSetPresetNameFailed, device: " + device.getAddress() + + ", reason: " + reason); + } + // TODO(b/300015207): Update the UI + } + } + + @Override + public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) { + if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId + + ", reason: " + reason); + } + // TODO(b/300015207): Update the UI + } + } + + private ListPreference createPresetPreference(Context context) { + ListPreference preference = new ListPreference(context); + preference.setKey(KEY_HEARING_AIDS_PRESETS); + preference.setOrder(ORDER_HEARING_AIDS_PRESETS); + preference.setTitle(context.getString(R.string.bluetooth_hearing_aids_presets)); + preference.setOnPreferenceChangeListener(this); + return preference; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java index 27a4cb17aa1..3703b7180af 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java @@ -22,7 +22,9 @@ import androidx.annotation.NonNull; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; +import com.android.settings.accessibility.Flags; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.lifecycle.Lifecycle; import com.google.common.annotations.VisibleForTesting; @@ -37,24 +39,32 @@ import java.util.List; * category controller. */ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsController { + + public static final int ORDER_HEARING_DEVICE_SETTINGS = 1; + public static final int ORDER_HEARING_AIDS_PRESETS = 2; static final String KEY_HEARING_DEVICE_GROUP = "hearing_device_group"; private final List mControllers = new ArrayList<>(); private Lifecycle mLifecycle; + private LocalBluetoothManager mManager; public BluetoothDetailsHearingDeviceController(@NonNull Context context, @NonNull PreferenceFragmentCompat fragment, + @NonNull LocalBluetoothManager manager, @NonNull CachedBluetoothDevice device, @NonNull Lifecycle lifecycle) { super(context, fragment, device, lifecycle); + mManager = manager; mLifecycle = lifecycle; } @VisibleForTesting void setSubControllers( - BluetoothDetailsHearingDeviceSettingsController hearingDeviceSettingsController) { + BluetoothDetailsHearingDeviceSettingsController hearingDeviceSettingsController, + BluetoothDetailsHearingAidsPresetsController presetsController) { mControllers.clear(); mControllers.add(hearingDeviceSettingsController); + mControllers.add(presetsController); } @Override @@ -93,6 +103,10 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon mControllers.add(new BluetoothDetailsHearingDeviceSettingsController(mContext, mFragment, mCachedDevice, mLifecycle)); } + if (Flags.enableHearingAidPresetControl()) { + mControllers.add(new BluetoothDetailsHearingAidsPresetsController(mContext, mFragment, + mManager, mCachedDevice, mLifecycle)); + } } @NonNull diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java index b381cc4b2b8..7e5f3b1a78f 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java @@ -17,6 +17,7 @@ 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_DEVICE_SETTINGS; import android.content.Context; import android.text.TextUtils; @@ -87,6 +88,7 @@ public class BluetoothDetailsHearingDeviceSettingsController extends BluetoothDe private Preference createHearingDeviceSettingsPreference(Context context) { final ArrowPreference preference = new ArrowPreference(context); preference.setKey(KEY_HEARING_DEVICE_SETTINGS); + preference.setOrder(ORDER_HEARING_DEVICE_SETTINGS); preference.setTitle(context.getString(R.string.bluetooth_hearing_device_settings_title)); preference.setSummary( context.getString(R.string.bluetooth_hearing_device_settings_summary)); diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index dae1e086115..87b2c6b65d0 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -331,8 +331,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment controllers.add(new BluetoothDetailsExtraOptionsController(context, this, mCachedDevice, lifecycle)); BluetoothDetailsHearingDeviceController hearingDeviceController = - new BluetoothDetailsHearingDeviceController(context, this, mCachedDevice, - lifecycle); + new BluetoothDetailsHearingDeviceController(context, this, mManager, + mCachedDevice, lifecycle); controllers.add(hearingDeviceController); hearingDeviceController.initSubControllers(isLaunchFromHearingDevicePage()); controllers.addAll(hearingDeviceController.getSubControllers()); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java new file mode 100644 index 00000000000..3a898c150e1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +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.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothHapClient; + +import androidx.preference.ListPreference; +import androidx.preference.PreferenceCategory; + +import com.android.settingslib.bluetooth.HapClientProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +import java.util.List; +import java.util.concurrent.Executor; + +/** Tests for {@link BluetoothDetailsHearingAidsPresetsController}. */ +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsHearingAidsPresetsControllerTest extends + BluetoothDetailsControllerTestBase { + + private static final int TEST_PRESET_INDEX = 1; + private static final String TEST_PRESET_NAME = "test_preset"; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private LocalBluetoothManager mLocalManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private HapClientProfile mHapClientProfile; + + private BluetoothDetailsHearingAidsPresetsController mController; + + @Override + public void setUp() { + super.setUp(); + + when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + PreferenceCategory deviceControls = new PreferenceCategory(mContext); + deviceControls.setKey(KEY_HEARING_DEVICE_GROUP); + mScreen.addPreference(deviceControls); + mController = new BluetoothDetailsHearingAidsPresetsController(mContext, mFragment, + mLocalManager, mCachedDevice, mLifecycle); + mController.init(mScreen); + } + + @Test + public void onResume_registerCallback() { + mController.onResume(); + + verify(mHapClientProfile).registerCallback(any(Executor.class), + any(BluetoothHapClient.Callback.class)); + } + + @Test + public void onPause_unregisterCallback() { + mController.onPause(); + + verify(mHapClientProfile).unregisterCallback(any(BluetoothHapClient.Callback.class)); + } + + + @Test + public void onPreferenceChange_keyMatched_verifyStatusUpdated() { + final ListPreference presetPreference = getTestPresetPreference(KEY_HEARING_AIDS_PRESETS); + + boolean handled = mController.onPreferenceChange(presetPreference, + String.valueOf(TEST_PRESET_INDEX)); + + assertThat(handled).isTrue(); + } + + @Test + public void onPreferenceChange_keyNotMatched_doNothing() { + final ListPreference presetPreference = getTestPresetPreference("wrong_key"); + + boolean handled = mController.onPreferenceChange( + presetPreference, String.valueOf(TEST_PRESET_INDEX)); + + assertThat(handled).isFalse(); + } + + private ListPreference getTestPresetPreference(String key) { + final ListPreference presetPreference = spy(new ListPreference(mContext)); + when(presetPreference.findIndexOfValue(String.valueOf(TEST_PRESET_INDEX))).thenReturn(0); + when(presetPreference.getEntries()).thenReturn(new CharSequence[]{TEST_PRESET_NAME}); + when(presetPreference.getEntryValues()).thenReturn( + new CharSequence[]{String.valueOf(TEST_PRESET_INDEX)}); + presetPreference.setKey(key); + return presetPreference; + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java index d5284b4c438..2a50f892add 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java @@ -20,6 +20,15 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.when; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import com.android.settings.accessibility.Flags; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,11 +42,20 @@ import org.robolectric.RobolectricTestRunner; public class BluetoothDetailsHearingDeviceControllerTest extends BluetoothDetailsControllerTestBase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + @Mock + private LocalBluetoothManager mLocalManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock private BluetoothDetailsHearingDeviceController mHearingDeviceController; - + @Mock + private BluetoothDetailsHearingAidsPresetsController mPresetsController; @Mock private BluetoothDetailsHearingDeviceSettingsController mHearingDeviceSettingsController; @@ -45,9 +63,11 @@ public class BluetoothDetailsHearingDeviceControllerTest extends public void setUp() { super.setUp(); + when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); mHearingDeviceController = new BluetoothDetailsHearingDeviceController(mContext, - mFragment, mCachedDevice, mLifecycle); - mHearingDeviceController.setSubControllers(mHearingDeviceSettingsController); + mFragment, mLocalManager, mCachedDevice, mLifecycle); + mHearingDeviceController.setSubControllers(mHearingDeviceSettingsController, + mPresetsController); } @Test @@ -57,9 +77,17 @@ public class BluetoothDetailsHearingDeviceControllerTest extends assertThat(mHearingDeviceController.isAvailable()).isTrue(); } + @Test + public void isAvailable_presetsControlsAvailable_returnTrue() { + when(mPresetsController.isAvailable()).thenReturn(true); + + assertThat(mHearingDeviceController.isAvailable()).isTrue(); + } + @Test public void isAvailable_noControllersAvailable_returnFalse() { when(mHearingDeviceSettingsController.isAvailable()).thenReturn(false); + when(mPresetsController.isAvailable()).thenReturn(false); assertThat(mHearingDeviceController.isAvailable()).isFalse(); } @@ -80,4 +108,22 @@ public class BluetoothDetailsHearingDeviceControllerTest extends assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( c -> c instanceof BluetoothDetailsHearingDeviceSettingsController)).isTrue(); } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_HEARING_AID_PRESET_CONTROL) + public void initSubControllers_flagEnabled_presetControllerExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingAidsPresetsController)).isTrue(); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_HEARING_AID_PRESET_CONTROL) + public void initSubControllers_flagDisabled_presetControllerNotExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingAidsPresetsController)).isFalse(); + } } From b054b05b788a80093ed57b4cc4e8443a563524c5 Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Mon, 22 Jan 2024 07:11:27 +0000 Subject: [PATCH 3/3] Selects presets in device details page (2/2) Updates UI and sets preset to remote device when corresponding callback is called. Bug: 300015207 Test: atest BluetoothDetailsHearingAidsPresetsControllerTest Change-Id: Ic013b96acaa6161b861fbae32ddfd77387f9bc47 --- res/values/strings.xml | 2 + ...thDetailsHearingAidsPresetsController.java | 124 +++++++++++++-- ...tailsHearingAidsPresetsControllerTest.java | 143 +++++++++++++++++- 3 files changed, 258 insertions(+), 11 deletions(-) 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) {