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();
+ }
}