diff --git a/res/values/strings.xml b/res/values/strings.xml index 2037323c8d4..2f0e1d93005 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -150,14 +150,14 @@ 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 + + Presets + + Couldn\u2019t update preset 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/BluetoothDetailsHearingAidsPresetsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java new file mode 100644 index 00000000000..43721f4cf92 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java @@ -0,0 +1,308 @@ +/* + * 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.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; +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())) { + 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; + } + + @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()); + + 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() { + if (mHapClientProfile == null) { + return false; + } + return mCachedDevice.getProfiles().stream().anyMatch( + profile -> profile instanceof HapClientProfile); + } + + @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); + } + mContext.getMainExecutor().execute(this::refresh); + } + } + + @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); + } + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); + } + } + + @Override + public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) { + if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId + + ", reason: " + reason); + } + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); + } + } + + @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); + } + mContext.getMainExecutor().execute(this::refresh); + } + } + + @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); + } + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); + } + } + + @Override + public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) { + if (hapGroupId == mHapClientProfile.getHapGroup(mCachedDevice.getDevice())) { + if (DEBUG) { + Log.d(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId + + ", reason: " + reason); + } + mContext.getMainExecutor().execute(() -> { + refresh(); + showErrorToast(); + }); + } + } + + 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; + } + + 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/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java new file mode 100644 index 00000000000..3703b7180af --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java @@ -0,0 +1,116 @@ +/* + * 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.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; + +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 { + + 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, + BluetoothDetailsHearingAidsPresetsController presetsController) { + mControllers.clear(); + mControllers.add(hearingDeviceSettingsController); + mControllers.add(presetsController); + } + + @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)); + } + if (Flags.enableHearingAidPresetControl()) { + mControllers.add(new BluetoothDetailsHearingAidsPresetsController(mContext, mFragment, + mManager, 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 71% rename from src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java rename to src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java index 162abc78aef..7e5f3b1a78f 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceSettingsController.java @@ -16,6 +16,9 @@ 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; @@ -36,15 +39,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 +58,40 @@ 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.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)); 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..87b2c6b65d0 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, mManager, + mCachedDevice, lifecycle); + controllers.add(hearingDeviceController); + hearingDeviceController.initSubControllers(isLaunchFromHearingDevicePage()); + controllers.addAll(hearingDeviceController.getSubControllers()); } return controllers; } 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..c08bb98e55b --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java @@ -0,0 +1,270 @@ +/* + * 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 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; + +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.ArrayList; +import java.util.List; +import java.util.Set; +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"; + private static final int TEST_HAP_GROUP_ID = 1; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private LocalBluetoothManager mLocalManager; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private HapClientProfile mHapClientProfile; + @Mock + private CachedBluetoothDevice mCachedChildDevice; + @Mock + private BluetoothDevice mChildDevice; + + 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)); + when(mCachedDevice.isConnectedHapClientDevice()).thenReturn(true); + when(mCachedChildDevice.getDevice()).thenReturn(mChildDevice); + 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 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(); + + 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(); + verify(presetPreference).setSummary(TEST_PRESET_NAME); + } + + @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(); + 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) { + 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 new file mode 100644 index 00000000000..2a50f892add --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.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.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; +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 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; + + @Override + public void setUp() { + super.setUp(); + + when(mLocalManager.getProfileManager()).thenReturn(mProfileManager); + mHearingDeviceController = new BluetoothDetailsHearingDeviceController(mContext, + mFragment, mLocalManager, mCachedDevice, mLifecycle); + mHearingDeviceController.setSubControllers(mHearingDeviceSettingsController, + mPresetsController); + } + + @Test + public void isAvailable_hearingDeviceSettingsAvailable_returnTrue() { + when(mHearingDeviceSettingsController.isAvailable()).thenReturn(true); + + 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(); + } + + + @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(); + } + + @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(); + } +} 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() {