diff options
8 files changed, 586 insertions, 18 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index f741f655bf7c..7b4c86207a2a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -51,7 +51,8 @@ public class CachedBluetoothDeviceManager { public CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) { mContext = context; mBtManager = localBtManager; - mHearingAidDeviceManager = new HearingAidDeviceManager(localBtManager, mCachedDevices); + mHearingAidDeviceManager = new HearingAidDeviceManager(context, localBtManager, + mCachedDevices); mCsipDeviceManager = new CsipDeviceManager(localBtManager, mCachedDevices); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java new file mode 100644 index 000000000000..d8475b3f22af --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingConstants.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 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.settingslib.bluetooth; + +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Constant values used to configure hearing aid audio routing. + * + * {@link HearingAidAudioRoutingHelper} + */ +public final class HearingAidAudioRoutingConstants { + public static final int[] CALL_ROUTING_ATTRIBUTES = new int[] { + // Stands for STRATEGY_PHONE + AudioAttributes.USAGE_VOICE_COMMUNICATION, + }; + + public static final int[] MEDIA_ROUTING_ATTRIBUTES = new int[] { + // Stands for STRATEGY_MEDIA, including USAGE_GAME, USAGE_ASSISTANT, + // USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, USAGE_ASSISTANCE_SONIFICATION + AudioAttributes.USAGE_MEDIA + }; + + public static final int[] RINGTONE_ROUTING_ATTRIBUTE = new int[] { + // Stands for STRATEGY_SONIFICATION, including USAGE_ALARM + AudioAttributes.USAGE_NOTIFICATION_RINGTONE + }; + + public static final int[] SYSTEM_SOUNDS_ROUTING_ATTRIBUTES = new int[] { + // Stands for STRATEGY_SONIFICATION_RESPECTFUL, including USAGE_NOTIFICATION_EVENT + AudioAttributes.USAGE_NOTIFICATION, + // Stands for STRATEGY_ACCESSIBILITY + AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY, + // Stands for STRATEGY_DTMF + AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING, + }; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + RoutingValue.AUTO, + RoutingValue.HEARING_DEVICE, + RoutingValue.DEVICE_SPEAKER, + }) + + public @interface RoutingValue { + int AUTO = 0; + int HEARING_DEVICE = 1; + int DEVICE_SPEAKER = 2; + } + + public static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, ""); +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java new file mode 100644 index 000000000000..c9512cd01aa3 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelper.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2023 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.settingslib.bluetooth; + +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.audiopolicy.AudioProductStrategy; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A helper class to configure the routing strategy for hearing aids. + */ +public class HearingAidAudioRoutingHelper { + + private final AudioManager mAudioManager; + + public HearingAidAudioRoutingHelper(Context context) { + mAudioManager = context.getSystemService(AudioManager.class); + } + + /** + * Gets the list of {@link AudioProductStrategy} referred by the given list of usage values + * defined in {@link AudioAttributes} + */ + public List<AudioProductStrategy> getSupportedStrategies(int[] attributeSdkUsageList) { + final List<AudioAttributes> audioAttrList = new ArrayList<>(attributeSdkUsageList.length); + for (int attributeSdkUsage : attributeSdkUsageList) { + audioAttrList.add(new AudioAttributes.Builder().setUsage(attributeSdkUsage).build()); + } + + final List<AudioProductStrategy> allStrategies = getAudioProductStrategies(); + final List<AudioProductStrategy> supportedStrategies = new ArrayList<>(); + for (AudioProductStrategy strategy : allStrategies) { + for (AudioAttributes audioAttr : audioAttrList) { + if (strategy.supportsAudioAttributes(audioAttr)) { + supportedStrategies.add(strategy); + } + } + } + + return supportedStrategies.stream().distinct().collect(Collectors.toList()); + } + + /** + * Sets the preferred device for the given strategies. + * + * @param supportedStrategies A list of {@link AudioProductStrategy} used to configure audio + * routing + * @param hearingDevice {@link AudioDeviceAttributes} of the device to be changed in audio + * routing + * @param routingValue one of value defined in + * {@link HearingAidAudioRoutingConstants.RoutingValue}, denotes routing + * destination. + * @return {code true} if the routing value successfully configure + */ + public boolean setPreferredDeviceRoutingStrategies( + List<AudioProductStrategy> supportedStrategies, AudioDeviceAttributes hearingDevice, + @HearingAidAudioRoutingConstants.RoutingValue int routingValue) { + boolean status; + switch (routingValue) { + case HearingAidAudioRoutingConstants.RoutingValue.AUTO: + status = removePreferredDeviceForStrategies(supportedStrategies); + return status; + case HearingAidAudioRoutingConstants.RoutingValue.HEARING_DEVICE: + status = removePreferredDeviceForStrategies(supportedStrategies); + status &= setPreferredDeviceForStrategies(supportedStrategies, hearingDevice); + return status; + case HearingAidAudioRoutingConstants.RoutingValue.DEVICE_SPEAKER: + status = removePreferredDeviceForStrategies(supportedStrategies); + status &= setPreferredDeviceForStrategies(supportedStrategies, + HearingAidAudioRoutingConstants.DEVICE_SPEAKER_OUT); + return status; + default: + throw new IllegalArgumentException("Unexpected routingValue: " + routingValue); + } + } + + /** + * Gets the matched hearing device {@link AudioDeviceAttributes} for {@code device}. + * + * <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} of {@code device} + * + * @param device the {@link CachedBluetoothDevice} need to be hearing aid device + * @return the requested AudioDeviceAttributes or {@code null} if not match + */ + @Nullable + public AudioDeviceAttributes getMatchedHearingDeviceAttributes(CachedBluetoothDevice device) { + if (device == null || !device.isHearingAidDevice()) { + return null; + } + + AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); + for (AudioDeviceInfo audioDevice : audioDevices) { + // ASHA for TYPE_HEARING_AID, HAP for TYPE_BLE_HEADSET + if (audioDevice.getType() == AudioDeviceInfo.TYPE_HEARING_AID + || audioDevice.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) { + if (matchAddress(device, audioDevice)) { + return new AudioDeviceAttributes(audioDevice); + } + } + } + return null; + } + + private boolean matchAddress(CachedBluetoothDevice device, AudioDeviceInfo audioDevice) { + final String audioDeviceAddress = audioDevice.getAddress(); + final CachedBluetoothDevice subDevice = device.getSubDevice(); + final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice(); + + return device.getAddress().equals(audioDeviceAddress) + || (subDevice != null && subDevice.getAddress().equals(audioDeviceAddress)) + || (!memberDevices.isEmpty() && memberDevices.stream().anyMatch( + m -> m.getAddress().equals(audioDeviceAddress))); + } + + private boolean setPreferredDeviceForStrategies(List<AudioProductStrategy> strategies, + AudioDeviceAttributes audioDevice) { + boolean status = true; + for (AudioProductStrategy strategy : strategies) { + status &= mAudioManager.setPreferredDeviceForStrategy(strategy, audioDevice); + + } + + return status; + } + + private boolean removePreferredDeviceForStrategies(List<AudioProductStrategy> strategies) { + boolean status = true; + for (AudioProductStrategy strategy : strategies) { + status &= mAudioManager.removePreferredDeviceForStrategy(strategy); + } + + return status; + } + + @VisibleForTesting + public List<AudioProductStrategy> getAudioProductStrategies() { + return AudioManager.getAudioProductStrategies(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index ebfec0aee1b7..4354e0c6e952 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -18,6 +18,11 @@ package com.android.settingslib.bluetooth; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothProfile; +import android.content.ContentResolver; +import android.content.Context; +import android.media.AudioDeviceAttributes; +import android.media.audiopolicy.AudioProductStrategy; +import android.provider.Settings; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -33,12 +38,25 @@ public class HearingAidDeviceManager { private static final String TAG = "HearingAidDeviceManager"; private static final boolean DEBUG = BluetoothUtils.D; + private final ContentResolver mContentResolver; private final LocalBluetoothManager mBtManager; private final List<CachedBluetoothDevice> mCachedDevices; - HearingAidDeviceManager(LocalBluetoothManager localBtManager, + private final HearingAidAudioRoutingHelper mRoutingHelper; + HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> CachedDevices) { + mContentResolver = context.getContentResolver(); mBtManager = localBtManager; mCachedDevices = CachedDevices; + mRoutingHelper = new HearingAidAudioRoutingHelper(context); + } + + @VisibleForTesting + HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager, + List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper) { + mContentResolver = context.getContentResolver(); + mBtManager = localBtManager; + mCachedDevices = cachedDevices; + mRoutingHelper = routingHelper; } void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice) { @@ -192,12 +210,11 @@ public class HearingAidDeviceManager { case BluetoothProfile.STATE_CONNECTED: onHiSyncIdChanged(cachedDevice.getHiSyncId()); CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice); - if (mainDevice != null){ + if (mainDevice != null) { if (mainDevice.isConnected()) { // When main device exists and in connected state, receiving sub device // connection. To refresh main device UI mainDevice.refresh(); - return true; } else { // When both Hearing Aid devices are disconnected, receiving sub device // connection. To switch content and dispatch to notify UI change @@ -207,9 +224,15 @@ public class HearingAidDeviceManager { // It is necessary to do remove and add for updating the mapping on // preference and device mBtManager.getEventManager().dispatchDeviceAdded(mainDevice); - return true; + // Only need to set first device of a set. AudioDeviceInfo for + // GET_DEVICES_OUTPUTS will not change device. + setAudioRoutingConfig(cachedDevice); } + return true; } + // Only need to set first device of a set. AudioDeviceInfo for GET_DEVICES_OUTPUTS + // will not change device. + setAudioRoutingConfig(cachedDevice); break; case BluetoothProfile.STATE_DISCONNECTED: mainDevice = findMainDevice(cachedDevice); @@ -232,13 +255,83 @@ public class HearingAidDeviceManager { // It is necessary to do remove and add for updating the mapping on // preference and device mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice); + return true; } + // Only need to clear when last device of a set get disconnected + clearAudioRoutingConfig(); break; } return false; } + private void setAudioRoutingConfig(CachedBluetoothDevice device) { + AudioDeviceAttributes hearingDeviceAttributes = + mRoutingHelper.getMatchedHearingDeviceAttributes(device); + if (hearingDeviceAttributes == null) { + Log.w(TAG, "Can not find expected AudioDeviceAttributes for hearing device: " + + device.getDevice().getAnonymizedAddress()); + return; + } + + final int callRoutingValue = Settings.Secure.getInt(mContentResolver, + Settings.Secure.HEARING_AID_CALL_ROUTING, + HearingAidAudioRoutingConstants.RoutingValue.AUTO); + final int mediaRoutingValue = Settings.Secure.getInt(mContentResolver, + Settings.Secure.HEARING_AID_MEDIA_ROUTING, + HearingAidAudioRoutingConstants.RoutingValue.AUTO); + final int ringtoneRoutingValue = Settings.Secure.getInt(mContentResolver, + Settings.Secure.HEARING_AID_RINGTONE_ROUTING, + HearingAidAudioRoutingConstants.RoutingValue.AUTO); + final int systemSoundsRoutingValue = Settings.Secure.getInt(mContentResolver, + Settings.Secure.HEARING_AID_SYSTEM_SOUNDS_ROUTING, + HearingAidAudioRoutingConstants.RoutingValue.AUTO); + + setPreferredDeviceRoutingStrategies( + HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES, + hearingDeviceAttributes, callRoutingValue); + setPreferredDeviceRoutingStrategies( + HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES, + hearingDeviceAttributes, mediaRoutingValue); + setPreferredDeviceRoutingStrategies( + HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTE, + hearingDeviceAttributes, ringtoneRoutingValue); + setPreferredDeviceRoutingStrategies( + HearingAidAudioRoutingConstants.SYSTEM_SOUNDS_ROUTING_ATTRIBUTES, + hearingDeviceAttributes, systemSoundsRoutingValue); + } + + private void clearAudioRoutingConfig() { + // Don't need to pass hearingDevice when we want to reset it (set to AUTO). + setPreferredDeviceRoutingStrategies( + HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES, + /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); + setPreferredDeviceRoutingStrategies( + HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES, + /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); + setPreferredDeviceRoutingStrategies( + HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTE, + /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); + setPreferredDeviceRoutingStrategies( + HearingAidAudioRoutingConstants.SYSTEM_SOUNDS_ROUTING_ATTRIBUTES, + /* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO); + } + + private void setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList, + AudioDeviceAttributes hearingDevice, + @HearingAidAudioRoutingConstants.RoutingValue int routingValue) { + final List<AudioProductStrategy> supportedStrategies = + mRoutingHelper.getSupportedStrategies(attributeSdkUsageList); + + final boolean status = mRoutingHelper.setPreferredDeviceRoutingStrategies( + supportedStrategies, hearingDevice, routingValue); + + if (!status) { + Log.w(TAG, "routingStrategies: " + supportedStrategies.toString() + "routingValue: " + + routingValue + " fail to configure AudioProductStrategy"); + } + } + CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { for (CachedBluetoothDevice cachedDevice : mCachedDevices) { if (isValidHiSyncId(cachedDevice.getHiSyncId())) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index a3c2e70c7da4..43e3a32c97b6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -361,6 +361,7 @@ public class LocalBluetoothProfileManager { cachedDevice.setHearingAidInfo(infoBuilder.build()); } } + HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java index f06623d0bd4a..4b3820eb0444 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java @@ -403,7 +403,7 @@ public class CachedBluetoothDeviceManagerTest { */ @Test public void updateHearingAidDevices_directToHearingAidDeviceManager() { - mHearingAidDeviceManager = spy(new HearingAidDeviceManager(mLocalBluetoothManager, + mHearingAidDeviceManager = spy(new HearingAidDeviceManager(mContext, mLocalBluetoothManager, mCachedDeviceManager.mCachedDevices)); mCachedDeviceManager.mHearingAidDeviceManager = mHearingAidDeviceManager; mCachedDeviceManager.updateHearingAidsDevices(); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java new file mode 100644 index 000000000000..8b5ea30e17fe --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidAudioRoutingHelperTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2023 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.settingslib.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.audiopolicy.AudioProductStrategy; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +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.HashSet; +import java.util.List; +import java.util.Set; + +/** Tests for {@link HearingAidAudioRoutingHelper}. */ +@RunWith(RobolectricTestRunner.class) +public class HearingAidAudioRoutingHelperTest { + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Spy + private final Context mContext = ApplicationProvider.getApplicationContext(); + private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"; + private static final String NOT_EXPECT_DEVICE_ADDRESS = "11:B2:B2:B2:B2:B2"; + + @Mock + private AudioProductStrategy mAudioStrategy; + @Spy + private AudioManager mAudioManager = mContext.getSystemService(AudioManager.class); + @Mock + private AudioDeviceInfo mAudioDeviceInfo; + @Mock + private CachedBluetoothDevice mCachedBluetoothDevice; + @Mock + private CachedBluetoothDevice mSubCachedBluetoothDevice; + private AudioDeviceAttributes mHearingDeviceAttribute; + private HearingAidAudioRoutingHelper mHelper; + + @Before + public void setUp() { + doReturn(mAudioManager).when(mContext).getSystemService(AudioManager.class); + when(mAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HEARING_AID); + when(mAudioDeviceInfo.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).thenReturn( + new AudioDeviceInfo[]{mAudioDeviceInfo}); + when(mAudioStrategy.getAudioAttributesForLegacyStreamType( + AudioManager.STREAM_MUSIC)) + .thenReturn((new AudioAttributes.Builder()).build()); + + mHearingDeviceAttribute = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_HEARING_AID, + TEST_DEVICE_ADDRESS); + mHelper = spy(new HearingAidAudioRoutingHelper(mContext)); + doReturn(List.of(mAudioStrategy)).when(mHelper).getAudioProductStrategies(); + } + + @Test + public void setPreferredDeviceRoutingStrategies_valueAuto_callRemoveStrategy() { + mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy), + mHearingDeviceAttribute, + HearingAidAudioRoutingConstants.RoutingValue.AUTO); + + verify(mAudioManager, atLeastOnce()).removePreferredDeviceForStrategy(mAudioStrategy); + } + + @Test + public void setPreferredDeviceRoutingStrategies_valueHearingDevice_callSetStrategy() { + mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy), + mHearingDeviceAttribute, + HearingAidAudioRoutingConstants.RoutingValue.HEARING_DEVICE); + + verify(mAudioManager, atLeastOnce()).setPreferredDeviceForStrategy(mAudioStrategy, + mHearingDeviceAttribute); + } + + @Test + public void setPreferredDeviceRoutingStrategies_valueDeviceSpeaker_callSetStrategy() { + final AudioDeviceAttributes speakerDevice = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, ""); + mHelper.setPreferredDeviceRoutingStrategies(List.of(mAudioStrategy), + mHearingDeviceAttribute, + HearingAidAudioRoutingConstants.RoutingValue.DEVICE_SPEAKER); + + verify(mAudioManager, atLeastOnce()).setPreferredDeviceForStrategy(mAudioStrategy, + speakerDevice); + } + + @Test + public void getMatchedHearingDeviceAttributes_mainHearingDevice_equalAddress() { + when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); + + final String targetAddress = mHelper.getMatchedHearingDeviceAttributes( + mCachedBluetoothDevice).getAddress(); + + assertThat(targetAddress).isEqualTo(mHearingDeviceAttribute.getAddress()); + } + + @Test + public void getMatchedHearingDeviceAttributes_subHearingDevice_equalAddress() { + when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + when(mCachedBluetoothDevice.getAddress()).thenReturn(NOT_EXPECT_DEVICE_ADDRESS); + when(mCachedBluetoothDevice.getSubDevice()).thenReturn(mSubCachedBluetoothDevice); + when(mSubCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + when(mSubCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); + + final String targetAddress = mHelper.getMatchedHearingDeviceAttributes( + mCachedBluetoothDevice).getAddress(); + + assertThat(targetAddress).isEqualTo(mHearingDeviceAttribute.getAddress()); + } + + @Test + public void getMatchedHearingDeviceAttributes_memberHearingDevice_equalAddress() { + when(mSubCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + when(mSubCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); + final Set<CachedBluetoothDevice> memberDevices = new HashSet<CachedBluetoothDevice>(); + memberDevices.add(mSubCachedBluetoothDevice); + when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + when(mCachedBluetoothDevice.getAddress()).thenReturn(NOT_EXPECT_DEVICE_ADDRESS); + when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberDevices); + + final String targetAddress = mHelper.getMatchedHearingDeviceAttributes( + mCachedBluetoothDevice).getAddress(); + + assertThat(targetAddress).isEqualTo(mHearingDeviceAttribute.getAddress()); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index 470d8e07f1f8..a83913693458 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -18,7 +18,12 @@ package com.android.settingslib.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -29,18 +34,32 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothProfile; import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.audiopolicy.AudioProductStrategy; import android.os.Parcel; +import androidx.test.core.app.ApplicationProvider; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; + +import java.util.List; @RunWith(RobolectricTestRunner.class) public class HearingAidDeviceManagerTest { + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + private final static long HISYNCID1 = 10; private final static long HISYNCID2 = 11; private final static String DEVICE_NAME_1 = "TestName_1"; @@ -51,6 +70,15 @@ public class HearingAidDeviceManagerTest { private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; private final BluetoothClass DEVICE_CLASS = createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE); + + private CachedBluetoothDevice mCachedDevice1; + private CachedBluetoothDevice mCachedDevice2; + private CachedBluetoothDeviceManager mCachedDeviceManager; + private HearingAidDeviceManager mHearingAidDeviceManager; + private AudioDeviceAttributes mHearingDeviceAttribute; + private final Context mContext = ApplicationProvider.getApplicationContext(); + @Spy + private HearingAidAudioRoutingHelper mHelper = new HearingAidAudioRoutingHelper(mContext); @Mock private LocalBluetoothProfileManager mLocalProfileManager; @Mock @@ -60,14 +88,12 @@ public class HearingAidDeviceManagerTest { @Mock private HearingAidProfile mHearingAidProfile; @Mock + private AudioProductStrategy mAudioStrategy; + @Mock private BluetoothDevice mDevice1; @Mock private BluetoothDevice mDevice2; - private CachedBluetoothDevice mCachedDevice1; - private CachedBluetoothDevice mCachedDevice2; - private CachedBluetoothDeviceManager mCachedDeviceManager; - private HearingAidDeviceManager mHearingAidDeviceManager; - private Context mContext; + private BluetoothClass createBtClass(int deviceClass) { Parcel p = Parcel.obtain(); @@ -81,8 +107,6 @@ public class HearingAidDeviceManagerTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; when(mDevice1.getAddress()).thenReturn(DEVICE_ADDRESS_1); when(mDevice2.getAddress()).thenReturn(DEVICE_ADDRESS_2); when(mDevice1.getName()).thenReturn(DEVICE_NAME_1); @@ -94,10 +118,18 @@ public class HearingAidDeviceManagerTest { when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalProfileManager); when(mLocalProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); - + when(mAudioStrategy.getAudioAttributesForLegacyStreamType( + AudioManager.STREAM_MUSIC)) + .thenReturn((new AudioAttributes.Builder()).build()); + doReturn(List.of(mAudioStrategy)).when(mHelper).getSupportedStrategies(any(int[].class)); + + mHearingDeviceAttribute = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_HEARING_AID, + DEVICE_ADDRESS_1); mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, mLocalBluetoothManager); - mHearingAidDeviceManager = spy(new HearingAidDeviceManager(mLocalBluetoothManager, - mCachedDeviceManager.mCachedDevices)); + mHearingAidDeviceManager = spy(new HearingAidDeviceManager(mContext, mLocalBluetoothManager, + mCachedDeviceManager.mCachedDevices, mHelper)); mCachedDevice1 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1)); mCachedDevice2 = spy(new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2)); } @@ -446,6 +478,44 @@ public class HearingAidDeviceManagerTest { } @Test + public void onProfileConnectionStateChanged_connected_callSetStrategies() { + when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn( + mHearingDeviceAttribute); + + mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, + BluetoothProfile.STATE_CONNECTED); + + verify(mHelper, atLeastOnce()).setPreferredDeviceRoutingStrategies( + eq(List.of(mAudioStrategy)), any(AudioDeviceAttributes.class), anyInt()); + } + + @Test + public void onProfileConnectionStateChanged_disconnected_callSetStrategiesWithAutoValue() { + when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn( + mHearingDeviceAttribute); + + mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, + BluetoothProfile.STATE_DISCONNECTED); + + verify(mHelper, atLeastOnce()).setPreferredDeviceRoutingStrategies( + eq(List.of(mAudioStrategy)), /* hearingDevice= */ isNull(), + eq(HearingAidAudioRoutingConstants.RoutingValue.AUTO)); + } + @Test + public void onProfileConnectionStateChanged_unpairing_callSetStrategiesWithAutoValue() { + when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn( + mHearingDeviceAttribute); + + when(mCachedDevice1.getUnpairing()).thenReturn(true); + mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, + BluetoothProfile.STATE_DISCONNECTED); + + verify(mHelper, atLeastOnce()).setPreferredDeviceRoutingStrategies( + eq(List.of(mAudioStrategy)), /* hearingDevice= */ isNull(), + eq(HearingAidAudioRoutingConstants.RoutingValue.AUTO)); + } + + @Test public void findMainDevice() { when(mCachedDevice1.getHiSyncId()).thenReturn(HISYNCID1); when(mCachedDevice2.getHiSyncId()).thenReturn(HISYNCID1); |