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