From 1258059febb4d053a76616abd488b4b7f0c674f6 Mon Sep 17 00:00:00 2001 From: jasonwshsu Date: Wed, 13 Nov 2024 16:58:03 +0800 Subject: [PATCH] [HA Input] Add UI to support hearing device microphone change ability in device details page In this patch: * Add custom dialog preference: HearingDeviceInputRoutingPreference and its controller * Contain radio group to show 'hearing device microphone' and 'this phone's microphone' for user's preference * set/get user's preference via BluetoothDevice#isMicrophonePreferredForCalls, BluetoothDevicwe#setMicrophonePreferredForCalls * check if support HapProfile and if in AudioManager#getDevice(GET_DEVICES_INPUTS) list Bug: 349255906 Test: atest HearingDeviceInputRoutingPreferenceTest BluetoothDetailsHearingDeviceInputRoutingControllerTest BluetoothDetailsHearingDeviceControllerTest Flag: com.android.settingslib.flags.hearing_devices_input_routing_control Change-Id: I2e4dbc7fb98353ed52d0d175df4e8725df6b9a05 --- .../hearing_device_input_routing_dialog.xml | 51 ++++++ res/values/strings.xml | 10 + ...eAudioRoutingBasePreferenceController.java | 2 +- ...uetoothDetailsHearingDeviceController.java | 10 +- ...lsHearingDeviceInputRoutingController.java | 126 +++++++++++++ .../HearingDeviceInputRoutingPreference.java | 172 ++++++++++++++++++ ...ioRoutingBasePreferenceControllerTest.java | 5 +- ...othDetailsHearingDeviceControllerTest.java | 31 +++- ...aringDeviceInputRoutingControllerTest.java | 171 +++++++++++++++++ ...aringDeviceInputRoutingPreferenceTest.java | 114 ++++++++++++ 10 files changed, 687 insertions(+), 5 deletions(-) create mode 100644 res/layout/hearing_device_input_routing_dialog.xml create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingController.java create mode 100644 src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreferenceTest.java diff --git a/res/layout/hearing_device_input_routing_dialog.xml b/res/layout/hearing_device_input_routing_dialog.xml new file mode 100644 index 00000000000..266126986f5 --- /dev/null +++ b/res/layout/hearing_device_input_routing_dialog.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 8e5e04fb36c..cbff25b5b12 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -180,6 +180,16 @@ Unmute surroundings Couldn\u2019t update surroundings + + Default microphone for calls + + Default microphone + + Choose a microphone for calls. + + Hearing aid microphone + + This phone\'s microphone Audio output diff --git a/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java b/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java index 3599f4874c4..44f02f6ee3b 100644 --- a/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java +++ b/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java @@ -101,7 +101,7 @@ public abstract class HearingDeviceAudioRoutingBasePreferenceController extends final List supportedStrategies = mAudioRoutingHelper.getSupportedStrategies(audioAttributes); final AudioDeviceAttributes hearingDeviceAttributes = - mAudioRoutingHelper.getMatchedHearingDeviceAttributes(hearingDevice); + mAudioRoutingHelper.getMatchedHearingDeviceAttributesForOutput(hearingDevice); if (hearingDeviceAttributes == null) { if (DEBUG) { Log.d(TAG, diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java index 8af08792180..01f8bb4cc8e 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java @@ -42,6 +42,7 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon public static final int ORDER_HEARING_DEVICE_SETTINGS = 1; public static final int ORDER_HEARING_AIDS_PRESETS = 2; + public static final int ORDER_HEARING_DEVICE_INPUT_ROUTING = 3; public static final int ORDER_AMBIENT_VOLUME = 4; static final String KEY_HEARING_DEVICE_GROUP = "hearing_device_group"; @@ -62,10 +63,12 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon @VisibleForTesting void setSubControllers( BluetoothDetailsHearingDeviceSettingsController hearingDeviceSettingsController, - BluetoothDetailsHearingAidsPresetsController presetsController) { + BluetoothDetailsHearingAidsPresetsController presetsController, + BluetoothDetailsHearingDeviceInputRoutingController inputRoutingController) { mControllers.clear(); mControllers.add(hearingDeviceSettingsController); mControllers.add(presetsController); + mControllers.add(inputRoutingController); } @Override @@ -112,6 +115,11 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon mControllers.add(new BluetoothDetailsAmbientVolumePreferenceController(mContext, mManager, mFragment, mCachedDevice, mLifecycle)); } + if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { + mControllers.add( + new BluetoothDetailsHearingDeviceInputRoutingController(mContext, mFragment, + mCachedDevice, mLifecycle)); + } } @NonNull diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingController.java new file mode 100644 index 00000000000..6c9a075672a --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingController.java @@ -0,0 +1,126 @@ +/* + * 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_DEVICE_INPUT_ROUTING; + +import android.content.Context; +import android.media.AudioManager; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.bluetooth.HearingDeviceInputRoutingPreference.InputRoutingValue; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.HapClientProfile; +import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants; +import com.android.settingslib.bluetooth.HearingAidAudioRoutingHelper; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.Arrays; + +/** + * The controller of the hearing device input routing + * + *

It manages the input routing preference and update the routing according to the value. + */ +public class BluetoothDetailsHearingDeviceInputRoutingController extends + BluetoothDetailsController implements + HearingDeviceInputRoutingPreference.InputRoutingCallback { + + private static final String TAG = "BluetoothDetailsHearingDeviceInputRoutingController"; + static final String KEY_HEARING_DEVICE_INPUT_ROUTING = "hearing_device_input_routing"; + + private final HearingAidAudioRoutingHelper mAudioRoutingHelper; + private final AudioManager mAudioManager; + + public BluetoothDetailsHearingDeviceInputRoutingController( + @NonNull Context context, + @NonNull PreferenceFragmentCompat fragment, + @NonNull CachedBluetoothDevice device, + @NonNull Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + mAudioRoutingHelper = new HearingAidAudioRoutingHelper(context); + mAudioManager = mContext.getSystemService(AudioManager.class); + } + + @Override + public boolean isAvailable() { + boolean isSupportedProfile = mCachedDevice.getProfiles().stream().anyMatch( + profile -> profile instanceof HapClientProfile); + boolean isSupportedInputDevice = Arrays.stream( + mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).anyMatch( + info -> mCachedDevice.getAddress().equals(info.getAddress())); + if (isSupportedProfile && !isSupportedInputDevice) { + Log.d(TAG, "Not supported input type hearing device."); + } + return isSupportedProfile && isSupportedInputDevice; + } + + @Override + protected void init(PreferenceScreen screen) { + PreferenceCategory hearingCategory = screen.findPreference(KEY_HEARING_DEVICE_GROUP); + if (hearingCategory != null) { + hearingCategory.addPreference( + createInputRoutingPreference(hearingCategory.getContext())); + } + } + + @Override + protected void refresh() {} + + @Nullable + @Override + public String getPreferenceKey() { + return KEY_HEARING_DEVICE_INPUT_ROUTING; + } + + private HearingDeviceInputRoutingPreference createInputRoutingPreference(Context context) { + HearingDeviceInputRoutingPreference pref = new HearingDeviceInputRoutingPreference(context); + pref.setKey(KEY_HEARING_DEVICE_INPUT_ROUTING); + pref.setOrder(ORDER_HEARING_DEVICE_INPUT_ROUTING); + pref.setTitle(context.getString(R.string.bluetooth_hearing_device_input_routing_title)); + pref.setChecked(getUserPreferredInputRoutingValue()); + pref.setInputRoutingCallback(this); + return pref; + } + + @InputRoutingValue + private int getUserPreferredInputRoutingValue() { + return mCachedDevice.getDevice().isMicrophonePreferredForCalls() + ? InputRoutingValue.HEARING_DEVICE : InputRoutingValue.BUILTIN_MIC; + } + + @Override + public void onInputRoutingUpdated(int selectedInputRoutingUiValue) { + boolean useBuiltinMic = + (selectedInputRoutingUiValue == InputRoutingValue.BUILTIN_MIC); + boolean status = mAudioRoutingHelper.setPreferredInputDeviceForCalls(mCachedDevice, + useBuiltinMic ? HearingAidAudioRoutingConstants.RoutingValue.BUILTIN_DEVICE + : HearingAidAudioRoutingConstants.RoutingValue.AUTO); + if (!status) { + Log.d(TAG, "Fail to configure setPreferredInputDeviceForCalls"); + } + mCachedDevice.getDevice().setMicrophonePreferredForCalls(!useBuiltinMic); + } +} diff --git a/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java b/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java new file mode 100644 index 00000000000..2d09f6103f1 --- /dev/null +++ b/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java @@ -0,0 +1,172 @@ +/* + * 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 android.content.DialogInterface; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RadioGroup; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; +import com.android.settingslib.CustomDialogPreferenceCompat; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Preference for controlling the input routing for hearing device. + * + *

This preference displays a dialog that allows users to choose which input device that want to + * use when using this hearing device. + */ +public class HearingDeviceInputRoutingPreference extends CustomDialogPreferenceCompat { + + /** + * Annotations for possible input routing UI for this hearing device input routing preference. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + InputRoutingValue.HEARING_DEVICE, + InputRoutingValue.BUILTIN_MIC + }) + public @interface InputRoutingValue { + int HEARING_DEVICE = 0; + int BUILTIN_MIC = 1; + } + + private static final int INVALID_ID = -1; + private final Context mContext; + private final int mFromHearingDeviceButtonId = R.id.input_from_hearing_device; + private final int mFromBuiltinMicButtonId = R.id.input_from_builtin_mic; + + @Nullable + private RadioGroup mInputRoutingGroup; + @Nullable + private InputRoutingCallback mCallback; + // Default value is hearing device as input + @InputRoutingValue + private int mSelectedInputRoutingValue = InputRoutingValue.HEARING_DEVICE; + + + public HearingDeviceInputRoutingPreference(@NonNull Context context) { + this(context, null); + } + + public HearingDeviceInputRoutingPreference(@NonNull Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + + mContext = context; + setDialogTitle(R.string.bluetooth_hearing_device_input_routing_dialog_title); + setDialogLayoutResource(R.layout.hearing_device_input_routing_dialog); + setNegativeButtonText(R.string.cancel); + setPositiveButtonText(R.string.done_button); + } + + /** + * Sets the callback to receive input routing updates. + */ + public void setInputRoutingCallback(@NonNull InputRoutingCallback callback) { + mCallback = callback; + } + + /** + * Sets the {@link InputRoutingValue} value to determine which radio button should be checked, + * and also update summary accordingly. + * + * @param inputRoutingValue The input routing value. + */ + public void setChecked(@InputRoutingValue int inputRoutingValue) { + mSelectedInputRoutingValue = inputRoutingValue; + setSummary(getSummary()); + } + + @Override + protected void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + int prevBtnId = getRadioButtonId(mSelectedInputRoutingValue); + int curBtnId = Objects.requireNonNull(mInputRoutingGroup).getCheckedRadioButtonId(); + if (prevBtnId == curBtnId) { + return; + } + + setChecked(getSelectedInputRoutingValue()); + if (mCallback != null) { + mCallback.onInputRoutingUpdated(mSelectedInputRoutingValue); + } + } + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + mInputRoutingGroup = view.requireViewById(R.id.input_routing_group); + mInputRoutingGroup.check(getRadioButtonId(mSelectedInputRoutingValue)); + } + + @Nullable + @Override + public CharSequence getSummary() { + return switch (mSelectedInputRoutingValue) { + case InputRoutingValue.HEARING_DEVICE -> mContext.getResources().getString( + R.string.bluetooth_hearing_device_input_routing_hearing_device_option); + case InputRoutingValue.BUILTIN_MIC -> mContext.getResources().getString( + R.string.bluetooth_hearing_device_input_routing_builtin_option); + default -> null; + }; + } + + private int getRadioButtonId(@InputRoutingValue int inputRoutingValue) { + return switch (inputRoutingValue) { + case InputRoutingValue.HEARING_DEVICE -> mFromHearingDeviceButtonId; + case InputRoutingValue.BUILTIN_MIC -> mFromBuiltinMicButtonId; + default -> INVALID_ID; + }; + } + + @InputRoutingValue + private int getSelectedInputRoutingValue() { + int checkedId = Objects.requireNonNull(mInputRoutingGroup).getCheckedRadioButtonId(); + if (checkedId == mFromBuiltinMicButtonId) { + return InputRoutingValue.BUILTIN_MIC; + } else { + // Should always return default value hearing device as input if something error + // happens. + return InputRoutingValue.HEARING_DEVICE; + } + } + + /** + * Callback to be invoked when input routing changes. + */ + public interface InputRoutingCallback { + + /** + * Called when the positive button is clicked and input routing is changed. + * + * @param selectedInputRoutingValue The selected input routing value. + */ + void onInputRoutingUpdated(@InputRoutingValue int selectedInputRoutingValue); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java index 4decf68d68c..bf27bf8a721 100644 --- a/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java @@ -108,7 +108,7 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { when(mBluetoothDevice.getAnonymizedAddress()).thenReturn(TEST_DEVICE_ADDRESS); when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); doReturn(hearingDeviceAttribute).when( - mAudioRoutingHelper).getMatchedHearingDeviceAttributes(any()); + mAudioRoutingHelper).getMatchedHearingDeviceAttributesForOutput(any()); when(mAudioProductStrategyMedia.getAudioAttributesForLegacyStreamType( AudioManager.STREAM_MUSIC)).thenReturn((new AudioAttributes.Builder()).build()); when(mAudioRoutingHelper.getAudioProductStrategies()).thenReturn( @@ -143,7 +143,8 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { @Test public void onPreferenceChange_noMatchedDeviceAttributes_notCallSetStrategies() { - when(mAudioRoutingHelper.getMatchedHearingDeviceAttributes(any())).thenReturn(null); + when(mAudioRoutingHelper.getMatchedHearingDeviceAttributesForOutput(any())).thenReturn( + null); verify(mAudioRoutingHelper, never()).setPreferredDeviceRoutingStrategies(any(), isNull(), anyInt()); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java index 4e3c742e284..d0177a8c2a7 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java @@ -56,6 +56,8 @@ public class BluetoothDetailsHearingDeviceControllerTest extends private BluetoothDetailsHearingAidsPresetsController mPresetsController; @Mock private BluetoothDetailsHearingDeviceSettingsController mHearingDeviceSettingsController; + @Mock + private BluetoothDetailsHearingDeviceInputRoutingController mInputRoutingController; private BluetoothDetailsHearingDeviceController mHearingDeviceController; @@ -67,7 +69,7 @@ public class BluetoothDetailsHearingDeviceControllerTest extends mHearingDeviceController = new BluetoothDetailsHearingDeviceController(mContext, mFragment, mLocalManager, mCachedDevice, mLifecycle); mHearingDeviceController.setSubControllers(mHearingDeviceSettingsController, - mPresetsController); + mPresetsController, mInputRoutingController); } @Test @@ -84,6 +86,13 @@ public class BluetoothDetailsHearingDeviceControllerTest extends assertThat(mHearingDeviceController.isAvailable()).isTrue(); } + @Test + public void isAvailable_inputRoutingControllersAvailable_returnFalse() { + when(mInputRoutingController.isAvailable()).thenReturn(true); + + assertThat(mHearingDeviceController.isAvailable()).isTrue(); + } + @Test public void isAvailable_noControllersAvailable_returnFalse() { when(mHearingDeviceSettingsController.isAvailable()).thenReturn(false); @@ -146,4 +155,24 @@ public class BluetoothDetailsHearingDeviceControllerTest extends assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( c -> c instanceof BluetoothDetailsAmbientVolumePreferenceController)).isFalse(); } + + @Test + @RequiresFlagsEnabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void initSubControllers_flagEnabled_inputRoutingControllerExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingDeviceInputRoutingController)).isTrue(); + } + + @Test + @RequiresFlagsDisabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void initSubControllers_flagDisabled_inputRoutingControllerNotExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingDeviceInputRoutingController)).isFalse(); + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingControllerTest.java new file mode 100644 index 00000000000..dc4924da6d7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingControllerTest.java @@ -0,0 +1,171 @@ +/* + * 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.BluetoothDetailsHearingDeviceInputRoutingController.KEY_HEARING_DEVICE_INPUT_ROUTING; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothDevice; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.bluetooth.HearingDeviceInputRoutingPreference.InputRoutingValue; +import com.android.settingslib.bluetooth.HapClientProfile; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +import java.util.Collections; +import java.util.List; + +/** Tests for {@link BluetoothDetailsHearingDeviceInputRoutingController}. */ + +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsHearingDeviceInputRoutingControllerTest extends + BluetoothDetailsControllerTestBase { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private static final String TEST_ADDRESS = "55:66:77:88:99:AA"; + + @Mock + private BluetoothDevice mBluetoothDevice; + @Mock + private HapClientProfile mHapClientProfile; + @Spy + private AudioManager mAudioManager; + + private BluetoothDetailsHearingDeviceInputRoutingController mController; + + @Override + public void setUp() { + super.setUp(); + + mContext = spy(ApplicationProvider.getApplicationContext()); + mAudioManager = spy(mContext.getSystemService(AudioManager.class)); + when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); + setupDevice(makeDefaultDeviceConfig()); + when(mCachedDevice.getDevice()).thenReturn(mBluetoothDevice); + PreferenceCategory deviceControls = new PreferenceCategory(mContext); + deviceControls.setKey(KEY_HEARING_DEVICE_GROUP); + mScreen.addPreference(deviceControls); + mController = new BluetoothDetailsHearingDeviceInputRoutingController(mContext, + mFragment, mCachedDevice, mLifecycle); + } + + @Test + public void init_getExpectedPreference() { + mController.init(mScreen); + + Preference pref = mScreen.findPreference(KEY_HEARING_DEVICE_INPUT_ROUTING); + assertThat(pref.getKey()).isEqualTo(KEY_HEARING_DEVICE_INPUT_ROUTING); + } + + @Test + public void init_setPreferredMicrophoneTrue_expectedSummary() { + when(mBluetoothDevice.isMicrophonePreferredForCalls()).thenReturn(true); + + mController.init(mScreen); + + Preference pref = mScreen.findPreference(KEY_HEARING_DEVICE_INPUT_ROUTING); + assertThat(pref.getSummary().toString()).isEqualTo(mContext.getString( + R.string.bluetooth_hearing_device_input_routing_hearing_device_option)); + } + + @Test + public void init_setPreferredMicrophoneFalse_expectedSummary() { + when(mBluetoothDevice.isMicrophonePreferredForCalls()).thenReturn(false); + mController.init(mScreen); + + Preference pref = mScreen.findPreference(KEY_HEARING_DEVICE_INPUT_ROUTING); + assertThat(pref.getSummary().toString()).isEqualTo(mContext.getString( + R.string.bluetooth_hearing_device_input_routing_builtin_option)); + } + + @Test + public void onInputRoutingUpdated_hearingDevice_setMicrophonePreferredForCallsTrue() { + mController.init(mScreen); + + mController.onInputRoutingUpdated(InputRoutingValue.HEARING_DEVICE); + + verify(mBluetoothDevice).setMicrophonePreferredForCalls(true); + } + + @Test + public void onInputRoutingUpdated_builtin_setMicrophonePreferredForCallsFalse() { + mController.init(mScreen); + + mController.onInputRoutingUpdated(InputRoutingValue.BUILTIN_MIC); + + verify(mBluetoothDevice).setMicrophonePreferredForCalls(false); + } + + @Test + public void isAvailable_validInput_supportHapProfile_returnTrue() { + when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS); + AudioDeviceInfo[] mockInfo = new AudioDeviceInfo[] {mockTestAddressInfo(TEST_ADDRESS)}; + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(mockInfo); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_notSupportHapProfile_returnFalse() { + when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS); + AudioDeviceInfo[] mockInfo = new AudioDeviceInfo[] {mockTestAddressInfo(TEST_ADDRESS)}; + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(mockInfo); + when(mCachedDevice.getProfiles()).thenReturn(Collections.emptyList()); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_notValidInputDevice_returnFalse() { + when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[] {}); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + + assertThat(mController.isAvailable()).isFalse(); + } + + private AudioDeviceInfo mockTestAddressInfo(String address) { + final AudioDeviceInfo info = mock(AudioDeviceInfo.class); + when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET); + when(info.getAddress()).thenReturn(address); + return info; + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreferenceTest.java new file mode 100644 index 00000000000..e5778245ef4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreferenceTest.java @@ -0,0 +1,114 @@ +/* + * 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.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RadioGroup; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.bluetooth.HearingDeviceInputRoutingPreference.InputRoutingValue; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link HearingDeviceInputRoutingPreference}. */ +@RunWith(RobolectricTestRunner.class) +public class HearingDeviceInputRoutingPreferenceTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private HearingDeviceInputRoutingPreference mPreference; + private TestInputRoutingCallback mTestInputRoutingCallback; + private View mDialogView; + + @Before + public void setup() { + mDialogView = LayoutInflater.from(mContext).inflate( + R.layout.hearing_device_input_routing_dialog, null); + mTestInputRoutingCallback = spy(new TestInputRoutingCallback()); + mPreference = new HearingDeviceInputRoutingPreference(mContext); + } + + @Test + public void onClick_checkToBuiltinMic_callbackWithBuiltinSpeaker() { + mPreference.setChecked(InputRoutingValue.HEARING_DEVICE); + mPreference.setInputRoutingCallback(mTestInputRoutingCallback); + mPreference.onBindDialogView(mDialogView); + RadioGroup radioGroup = mDialogView.requireViewById(R.id.input_routing_group); + Dialog dialog = mPreference.getDialog(); + + radioGroup.check(R.id.input_from_builtin_mic); + mPreference.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + + verify(mTestInputRoutingCallback).onInputRoutingUpdated(InputRoutingValue.BUILTIN_MIC); + } + + @Test + public void setChecked_checkNoChange_noCallback() { + mPreference.setChecked(InputRoutingValue.HEARING_DEVICE); + mPreference.setInputRoutingCallback(mTestInputRoutingCallback); + mPreference.onBindDialogView(mDialogView); + Dialog dialog = mPreference.getDialog(); + + mPreference.setChecked(InputRoutingValue.HEARING_DEVICE); + mPreference.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + + verify(mTestInputRoutingCallback, never()).onInputRoutingUpdated(anyInt()); + } + + @Test + public void setChecked_builtinMic_expectedSummary() { + mPreference.setChecked(InputRoutingValue.BUILTIN_MIC); + + assertThat(mPreference.getSummary().toString()).isEqualTo( + mContext.getString(R.string.bluetooth_hearing_device_input_routing_builtin_option)); + } + + @Test + public void setChecked_hearingDevice_expectedSummary() { + mPreference.setChecked(InputRoutingValue.HEARING_DEVICE); + + assertThat(mPreference.getSummary().toString()).isEqualTo(mContext.getString( + R.string.bluetooth_hearing_device_input_routing_hearing_device_option)); + } + + private static class TestInputRoutingCallback implements + HearingDeviceInputRoutingPreference.InputRoutingCallback { + + @Override + public void onInputRoutingUpdated(int selectedInputRoutingUiValue) {} + } +}