diff options
7 files changed, 473 insertions, 1 deletions
diff --git a/media/java/android/media/AudioDeviceInfo.java b/media/java/android/media/AudioDeviceInfo.java index 5a72b0be3405..1a3d7b7d5868 100644 --- a/media/java/android/media/AudioDeviceInfo.java +++ b/media/java/android/media/AudioDeviceInfo.java @@ -23,6 +23,8 @@ import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.util.SparseIntArray; +import com.android.internal.annotations.VisibleForTesting; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; @@ -402,7 +404,9 @@ public final class AudioDeviceInfo { private final AudioDevicePort mPort; - AudioDeviceInfo(AudioDevicePort port) { + /** @hide */ + @VisibleForTesting + public AudioDeviceInfo(AudioDevicePort port) { mPort = port; } diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java index 73bc6f96bf8b..2de8eefc4e78 100644 --- a/media/java/android/media/AudioDevicePort.java +++ b/media/java/android/media/AudioDevicePort.java @@ -20,6 +20,8 @@ import android.annotation.NonNull; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; +import com.android.aconfig.annotations.VisibleForTesting; + import java.util.Arrays; import java.util.List; @@ -38,6 +40,26 @@ import java.util.List; public class AudioDevicePort extends AudioPort { + /** @hide */ + // TODO: b/316864909 - Remove this method once there's a way to fake audio device ports further + // down the stack. + @VisibleForTesting + public static AudioDevicePort createForTesting( + int type, @NonNull String name, @NonNull String address) { + return new AudioDevicePort( + new AudioHandle(/* id= */ 0), + name, + /* samplingRates= */ null, + /* channelMasks= */ null, + /* channelIndexMasks= */ null, + /* formats= */ null, + /* gains= */ null, + type, + address, + /* encapsulationModes= */ null, + /* encapsulationMetadataTypes= */ null); + } + private final int mType; private final String mAddress; private final int[] mEncapsulationModes; diff --git a/services/tests/media/OWNERS b/services/tests/media/OWNERS new file mode 100644 index 000000000000..160767a6427c --- /dev/null +++ b/services/tests/media/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 137631 +include platform/frameworks/av:/media/janitors/media_solutions_OWNERS diff --git a/services/tests/media/mediarouterservicetest/Android.bp b/services/tests/media/mediarouterservicetest/Android.bp new file mode 100644 index 000000000000..aed3af6b69f6 --- /dev/null +++ b/services/tests/media/mediarouterservicetest/Android.bp @@ -0,0 +1,39 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "MediaRouterServiceTests", + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "androidx.test.core", + "androidx.test.rules", + "androidx.test.runner", + "compatibility-device-util-axt", + "junit", + "platform-test-annotations", + "services.core", + "truth", + ], + + platform_apis: true, + + test_suites: [ + // "device-tests", + "general-tests", + ], + + certificate: "platform", + dxflags: ["--multi-dex"], + optimize: { + enabled: false, + }, +} diff --git a/services/tests/media/mediarouterservicetest/AndroidManifest.xml b/services/tests/media/mediarouterservicetest/AndroidManifest.xml new file mode 100644 index 000000000000..fe65f868bcb3 --- /dev/null +++ b/services/tests/media/mediarouterservicetest/AndroidManifest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.media.tests"> + + <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> + <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" /> + + <application android:testOnly="true" android:debuggable="true"> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.media.tests" + android:label="Frameworks Services Tests"/> +</manifest> diff --git a/services/tests/media/mediarouterservicetest/AndroidTest.xml b/services/tests/media/mediarouterservicetest/AndroidTest.xml new file mode 100644 index 000000000000..b0656816b701 --- /dev/null +++ b/services/tests/media/mediarouterservicetest/AndroidTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Runs MediaRouter Service tests."> + <option name="test-tag" value="MediaRouterServiceTests" /> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="MediaRouterServiceTests.apk"/> + <option name="install-arg" value="-t" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.InstrumentationTest" > + <option name="package" value="com.android.server.media.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java new file mode 100644 index 000000000000..6f9b6faa0bb0 --- /dev/null +++ b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java @@ -0,0 +1,341 @@ +/* + * 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.server.media; + +import static com.android.server.media.AudioRoutingUtils.ATTRIBUTES_MEDIA; +import static com.android.server.media.AudioRoutingUtils.getMediaAudioProductStrategy; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.content.res.Resources; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; +import android.media.AudioDevicePort; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.media.MediaRoute2Info; +import android.media.audiopolicy.AudioProductStrategy; +import android.os.Looper; +import android.os.UserHandle; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@RunWith(JUnit4.class) +public class AudioPoliciesDeviceRouteControllerTest { + + private static final String FAKE_ROUTE_NAME = "fake name"; + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_SPEAKER, "name_builtin", /* address= */ null); + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_WIRED_HEADSET, "name_wired_hs", /* address= */ null); + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "name_a2dp", /* address= */ "12:34:45"); + + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_EARPIECE, /* name= */ null, /* address= */ null); + + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_NO_NAME = + createAudioDeviceInfo( + AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET, + /* name= */ null, + /* address= */ null); + + private AudioDeviceInfo mSelectedAudioDeviceInfo; + private Set<AudioDeviceInfo> mAvailableAudioDeviceInfos; + @Mock private AudioManager mMockAudioManager; + @Mock private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + private AudioPoliciesDeviceRouteController mControllerUnderTest; + private AudioDeviceCallback mAudioDeviceCallback; + private AudioProductStrategy mMediaAudioProductStrategy; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Resources mockResources = Mockito.mock(Resources.class); + when(mockResources.getText(anyInt())).thenReturn(FAKE_ROUTE_NAME); + Context realContext = InstrumentationRegistry.getInstrumentation().getContext(); + Context mockContext = Mockito.mock(Context.class); + when(mockContext.getResources()).thenReturn(mockResources); + // The bluetooth stack needs the application info, but we cannot use a spy because the + // concrete class is package private, so we just return the application info through the + // mock. + when(mockContext.getApplicationInfo()).thenReturn(realContext.getApplicationInfo()); + + // Setup the initial state so that the route controller is created in a sensible state. + mSelectedAudioDeviceInfo = FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER; + mAvailableAudioDeviceInfos = Set.of(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER); + updateMockAudioManagerState(); + mMediaAudioProductStrategy = getMediaAudioProductStrategy(); + + BluetoothAdapter btAdapter = + realContext.getSystemService(BluetoothManager.class).getAdapter(); + mControllerUnderTest = + new AudioPoliciesDeviceRouteController( + mockContext, + mMockAudioManager, + Looper.getMainLooper(), + mMediaAudioProductStrategy, + btAdapter, + mOnDeviceRouteChangedListener); + mControllerUnderTest.start(UserHandle.CURRENT_OR_SELF); + + ArgumentCaptor<AudioDeviceCallback> deviceCallbackCaptor = + ArgumentCaptor.forClass(AudioDeviceCallback.class); + verify(mMockAudioManager) + .registerAudioDeviceCallback(deviceCallbackCaptor.capture(), any()); + mAudioDeviceCallback = deviceCallbackCaptor.getValue(); + + // We clear any invocations during setup. + clearInvocations(mOnDeviceRouteChangedListener); + } + + @Test + public void getSelectedRoute_afterDevicesConnect_returnsRightSelectedRoute() { + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP); + verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged(); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ null, // Selected device doesn't change. + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); + } + + @Test + public void getSelectedRoute_afterDeviceRemovals_returnsExpectedRoutes() { + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP, + FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged(); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP); + verify(mOnDeviceRouteChangedListener, times(2)).onDeviceRouteChanged(); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); + + removeAvailableAudioDeviceInfos( + /* newSelectedDevice= */ null, + /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); + + removeAvailableAudioDeviceInfos( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER, + /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + } + + @Test + public void onAudioDevicesAdded_clearsAudioRoutingPoliciesCorrectly() { + clearInvocations(mMockAudioManager); + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ null, // Selected device doesn't change. + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE); + verifyNoMoreInteractions(mMockAudioManager); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP); + verify(mMockAudioManager).removePreferredDeviceForStrategy(mMediaAudioProductStrategy); + } + + @Test + public void getAvailableDevices_ignoresInvalidMediaOutputs() { + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ null, // Selected device doesn't change. + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE); + verifyNoMoreInteractions(mOnDeviceRouteChangedListener); + assertThat( + mControllerUnderTest.getAvailableRoutes().stream() + .map(MediaRoute2Info::getType) + .toList()) + .containsExactly(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + assertThat(mControllerUnderTest.getSelectedRoute().getType()) + .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + } + + @Test + public void transferTo_setsTheExpectedRoutingPolicy() { + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP, + FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + MediaRoute2Info builtInSpeakerRoute = + getAvailableRouteWithType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + mControllerUnderTest.transferTo(builtInSpeakerRoute.getId()); + verify(mMockAudioManager) + .setPreferredDeviceForStrategy( + mMediaAudioProductStrategy, + createAudioDeviceAttribute(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)); + + MediaRoute2Info wiredHeadsetRoute = + getAvailableRouteWithType(MediaRoute2Info.TYPE_WIRED_HEADSET); + mControllerUnderTest.transferTo(wiredHeadsetRoute.getId()); + verify(mMockAudioManager) + .setPreferredDeviceForStrategy( + mMediaAudioProductStrategy, + createAudioDeviceAttribute(AudioDeviceInfo.TYPE_WIRED_HEADSET)); + } + + @Test + public void updateVolume_propagatesCorrectlyToRouteInfo() { + when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(2); + when(mMockAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(3); + when(mMockAudioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)).thenReturn(1); + when(mMockAudioManager.isVolumeFixed()).thenReturn(false); + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET); + + MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute(); + assertThat(selectedRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADSET); + assertThat(selectedRoute.getVolume()).isEqualTo(2); + assertThat(selectedRoute.getVolumeMax()).isEqualTo(3); + assertThat(selectedRoute.getVolumeHandling()) + .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE); + + MediaRoute2Info onlyTransferrableRoute = + mControllerUnderTest.getAvailableRoutes().stream() + .filter(it -> !it.equals(selectedRoute)) + .findAny() + .orElseThrow(); + assertThat(onlyTransferrableRoute.getType()) + .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0); + assertThat(onlyTransferrableRoute.getVolumeMax()).isEqualTo(0); + assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0); + assertThat(onlyTransferrableRoute.getVolumeHandling()) + .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED); + + when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(0); + when(mMockAudioManager.isVolumeFixed()).thenReturn(true); + mControllerUnderTest.updateVolume(0); + MediaRoute2Info newSelectedRoute = mControllerUnderTest.getSelectedRoute(); + assertThat(newSelectedRoute.getVolume()).isEqualTo(0); + assertThat(newSelectedRoute.getVolumeHandling()) + .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED); + } + + @Test + public void getAvailableRoutes_whenNoProductNameIsProvided_usesTypeToPopulateName() { + assertThat(mControllerUnderTest.getSelectedRoute().getName().toString()) + .isEqualTo(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER.getProductName().toString()); + + addAvailableAudioDeviceInfo( + /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_NO_NAME, + /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_NO_NAME); + + MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute(); + assertThat(selectedRoute.getName().toString()).isEqualTo(FAKE_ROUTE_NAME); + } + + // Internal methods. + + @NonNull + private MediaRoute2Info getAvailableRouteWithType(int type) { + return mControllerUnderTest.getAvailableRoutes().stream() + .filter(it -> it.getType() == type) + .findFirst() + .orElseThrow(); + } + + private void addAvailableAudioDeviceInfo( + @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... newAvailableDevices) { + Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos); + newAvailableDeviceInfos.addAll(List.of(newAvailableDevices)); + mAvailableAudioDeviceInfos = newAvailableDeviceInfos; + if (newSelectedDevice != null) { + mSelectedAudioDeviceInfo = newSelectedDevice; + } + updateMockAudioManagerState(); + mAudioDeviceCallback.onAudioDevicesAdded(newAvailableDevices); + } + + private void removeAvailableAudioDeviceInfos( + @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... devicesToRemove) { + Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos); + List.of(devicesToRemove).forEach(newAvailableDeviceInfos::remove); + mAvailableAudioDeviceInfos = newAvailableDeviceInfos; + if (newSelectedDevice != null) { + mSelectedAudioDeviceInfo = newSelectedDevice; + } + updateMockAudioManagerState(); + mAudioDeviceCallback.onAudioDevicesRemoved(devicesToRemove); + } + + private void updateMockAudioManagerState() { + when(mMockAudioManager.getDevicesForAttributes(ATTRIBUTES_MEDIA)) + .thenReturn( + List.of(createAudioDeviceAttribute(mSelectedAudioDeviceInfo.getType()))); + when(mMockAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) + .thenReturn(mAvailableAudioDeviceInfos.toArray(new AudioDeviceInfo[0])); + } + + private static AudioDeviceAttributes createAudioDeviceAttribute(int type) { + // Address is unused. + return new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, type, /* address= */ ""); + } + + private static AudioDeviceInfo createAudioDeviceInfo( + int type, @NonNull String name, @NonNull String address) { + return new AudioDeviceInfo(AudioDevicePort.createForTesting(type, name, address)); + } +} |