Fix audio routing can not work after the phone get rebooted
Root Cause: setPreferredDeviceForStrategy() need to be called when hearing device get connected.
Solution:
* Call setPreferredDeviceForStrategy() when HearingAidDeviceManager#onProfileConnectionStateChangedIfProcessed()
* Extract the common functions into SettingsLib
Bug: 269122580
Test: make RunSettingsRoboTests
ROBOTEST_FILTER="(CachedBluetoothDeviceManagerTest|HearingAidAudioRoutingHelperTest|HearingAidDeviceManagerTest)"
Change-Id: I622040384f56f61c65f0daac45735789d637b703
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index f741f65..7b4c862 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 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 0000000..d8475b3
--- /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 0000000..c9512cd
--- /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 ebfec0a..4354e0c 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 @@
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 @@
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 @@
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 @@
// 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 @@
// 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 a3c2e70c7..43e3a32 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 @@
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 f06623d..4b3820e 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 @@
*/
@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 0000000..8b5ea30
--- /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 470d8e0..a839136 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 @@
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.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 @@
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 @@
@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 @@
@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 @@
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 @@
}
@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);