1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.car.audio; 17 18 import static android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; 19 import static android.media.AudioAttributes.USAGE_MEDIA; 20 21 import static com.android.car.audio.CarAudioContext.AudioContext; 22 23 import static org.junit.Assert.assertThrows; 24 import static org.mockito.Mockito.any; 25 import static org.mockito.Mockito.anyInt; 26 import static org.mockito.Mockito.eq; 27 import static org.mockito.Mockito.mock; 28 import static org.mockito.Mockito.times; 29 import static org.mockito.Mockito.verify; 30 import static org.mockito.Mockito.when; 31 32 import android.car.media.CarAudioManager; 33 import android.car.test.AbstractExpectableTestCase; 34 import android.media.AudioAttributes; 35 import android.media.AudioDeviceAttributes; 36 import android.media.AudioDeviceInfo; 37 import android.media.AudioFormat; 38 import android.media.AudioSystem; 39 import android.media.audiopolicy.AudioMix; 40 import android.media.audiopolicy.AudioPolicy; 41 import android.util.ArrayMap; 42 import android.util.SparseArray; 43 44 import com.google.common.collect.ImmutableList; 45 46 import org.junit.Test; 47 import org.junit.runner.RunWith; 48 import org.mockito.ArgumentCaptor; 49 import org.mockito.Mock; 50 import org.mockito.Mockito; 51 import org.mockito.junit.MockitoJUnitRunner; 52 53 import java.util.ArrayList; 54 import java.util.List; 55 import java.util.Map; 56 57 @RunWith(MockitoJUnitRunner.class) 58 public final class CarAudioDynamicRoutingTest extends AbstractExpectableTestCase { 59 private static final String MUSIC_ADDRESS = "bus0_music"; 60 private static final String NAV_ADDRESS = "bus1_nav"; 61 62 private static final AudioAttributes TEST_MEDIA_ATTRIBUTE = 63 CarAudioContext.getAudioAttributeFromUsage(USAGE_MEDIA); 64 private static final AudioAttributes TEST_NAVIGATION_ATTRIBUTE = 65 CarAudioContext.getAudioAttributeFromUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE); 66 67 private static final CarAudioContext TEST_CAR_AUDIO_CONTEXT = 68 new CarAudioContext(CarAudioContext.getAllContextsInfo(), 69 /* useCoreAudioRouting= */ false); 70 71 private static final @AudioContext int TEST_MEDIA_CONTEXT = 72 TEST_CAR_AUDIO_CONTEXT.getContextForAudioAttribute(TEST_MEDIA_ATTRIBUTE); 73 private static final @AudioContext int TEST_NAVIGATION_CONTEXT = 74 TEST_CAR_AUDIO_CONTEXT.getContextForAudioAttribute(TEST_NAVIGATION_ATTRIBUTE); 75 76 @Mock 77 private AudioManagerWrapper mAudioManager; 78 79 @Test setupAudioDynamicRouting()80 public void setupAudioDynamicRouting() { 81 AudioPolicy.Builder mockBuilder = Mockito.mock(AudioPolicy.Builder.class); 82 SparseArray<CarAudioZone> zones = getTestCarAudioZones(/* defaultConfigSelected= */ false, 83 /* secondaryConfigSelected= */ true); 84 85 CarAudioDynamicRouting.setupAudioDynamicRouting(TEST_CAR_AUDIO_CONTEXT, mAudioManager, 86 mockBuilder, zones); 87 88 ArgumentCaptor<AudioMix> audioMixCaptor = ArgumentCaptor.forClass(AudioMix.class); 89 verify(mockBuilder, times(2)).addMix(audioMixCaptor.capture()); 90 AudioMix audioMix = audioMixCaptor.getValue(); 91 expectWithMessage("Music address registered").that(audioMix.getRegistration()) 92 .isEqualTo(MUSIC_ADDRESS); 93 expectWithMessage("Contains Music device") 94 .that(audioMix.isRoutedToDevice(AudioSystem.DEVICE_OUT_BUS, MUSIC_ADDRESS)) 95 .isTrue(); 96 expectWithMessage("Does not contain Nav device") 97 .that(audioMix.isRoutedToDevice(AudioSystem.DEVICE_OUT_BUS, NAV_ADDRESS)) 98 .isFalse(); 99 expectWithMessage("Affected media usage") 100 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_MEDIA)) 101 .isTrue(); 102 expectWithMessage("Affected game usage") 103 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_GAME)) 104 .isTrue(); 105 expectWithMessage("Affected unknown usage") 106 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_UNKNOWN)) 107 .isTrue(); 108 expectWithMessage("Non-affected voice comm usage") 109 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)) 110 .isFalse(); 111 expectWithMessage("Non-affected voice comm signalling usage") 112 .that(audioMix.isAffectingUsage( 113 AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)) 114 .isFalse(); 115 expectWithMessage("Non-affected alarm usage") 116 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ALARM)) 117 .isFalse(); 118 expectWithMessage("Non-affected notification usage") 119 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_NOTIFICATION)) 120 .isFalse(); 121 expectWithMessage("Non-affected notification ringtone usage") 122 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)) 123 .isFalse(); 124 expectWithMessage("Non-affected notification event usage") 125 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)) 126 .isFalse(); 127 expectWithMessage("Non-affected assistance accessibility usage") 128 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)) 129 .isFalse(); 130 expectWithMessage("Non-affected nav guidance usage") 131 .that(audioMix.isAffectingUsage( 132 AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)) 133 .isFalse(); 134 expectWithMessage("Non-affected assistance sonification usage") 135 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)) 136 .isFalse(); 137 expectWithMessage("Non-affected assistant usage") 138 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ASSISTANT)) 139 .isFalse(); 140 expectWithMessage("Non-affected call assistant usage") 141 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_CALL_ASSISTANT)) 142 .isFalse(); 143 expectWithMessage("Non-affected emergency usage") 144 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_EMERGENCY)) 145 .isFalse(); 146 expectWithMessage("Non-affected safety usage") 147 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_SAFETY)) 148 .isFalse(); 149 expectWithMessage("Non-affected vehicle status usage") 150 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_VEHICLE_STATUS)) 151 .isFalse(); 152 expectWithMessage("Non-affected announcement usage") 153 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ANNOUNCEMENT)) 154 .isFalse(); 155 } 156 157 @Test setupAudioDynamicRouting_withDefaultConfigSelected()158 public void setupAudioDynamicRouting_withDefaultConfigSelected() { 159 AudioPolicy.Builder mockBuilder = Mockito.mock(AudioPolicy.Builder.class); 160 SparseArray<CarAudioZone> zones = getTestCarAudioZones(/* defaultConfigSelected= */ true, 161 /* secondaryConfigSelected= */ false); 162 163 CarAudioDynamicRouting.setupAudioDynamicRouting(TEST_CAR_AUDIO_CONTEXT, mAudioManager, 164 mockBuilder, zones); 165 166 ArgumentCaptor<AudioMix> audioMixCaptor = ArgumentCaptor.forClass(AudioMix.class); 167 verify(mockBuilder, times(1)).addMix(audioMixCaptor.capture()); 168 AudioMix audioMix = audioMixCaptor.getValue(); 169 expectWithMessage("Music address registered with default config selected") 170 .that(audioMix.getRegistration()).isEqualTo(MUSIC_ADDRESS); 171 expectWithMessage("Contains Music device with default config selected") 172 .that(audioMix.isRoutedToDevice(AudioSystem.DEVICE_OUT_BUS, MUSIC_ADDRESS)) 173 .isTrue(); 174 expectWithMessage("Does not contain Nav device with default config selected") 175 .that(audioMix.isRoutedToDevice(AudioSystem.DEVICE_OUT_BUS, NAV_ADDRESS)) 176 .isFalse(); 177 expectWithMessage("Affected media usage with default config selected") 178 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_MEDIA)) 179 .isTrue(); 180 expectWithMessage("Affected game usage with default config selected") 181 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_GAME)) 182 .isTrue(); 183 expectWithMessage("Affected unknown usage with default config selected") 184 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_UNKNOWN)) 185 .isTrue(); 186 expectWithMessage("Non-affected voice comm usage with default config selected") 187 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)) 188 .isFalse(); 189 expectWithMessage( 190 "Non-affected voice comm signalling usage with default config selected") 191 .that(audioMix.isAffectingUsage( 192 AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)) 193 .isFalse(); 194 expectWithMessage("Non-affected alarm usage") 195 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ALARM)) 196 .isFalse(); 197 expectWithMessage("Non-affected notification usage with default config selected") 198 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_NOTIFICATION)) 199 .isFalse(); 200 expectWithMessage( 201 "Non-affected notification ringtone usage with default config selected") 202 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)) 203 .isFalse(); 204 expectWithMessage("Non-affected notification event usage with default config selected") 205 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)) 206 .isFalse(); 207 expectWithMessage( 208 "Non-affected assistance accessibility usage with default config selected") 209 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)) 210 .isFalse(); 211 expectWithMessage("Non-affected nav guidance usage") 212 .that(audioMix.isAffectingUsage( 213 AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)) 214 .isFalse(); 215 expectWithMessage("Non-affected assistance sonification usage") 216 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)) 217 .isFalse(); 218 expectWithMessage("Non-affected assistant usage with default config selected") 219 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ASSISTANT)) 220 .isFalse(); 221 expectWithMessage("Non-affected call assistant usage with default config selected") 222 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_CALL_ASSISTANT)) 223 .isFalse(); 224 expectWithMessage("Non-affected emergency usage with default config selected") 225 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_EMERGENCY)) 226 .isFalse(); 227 expectWithMessage("Non-affected safety usage with default config selected") 228 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_SAFETY)) 229 .isFalse(); 230 expectWithMessage("Non-affected vehicle status usage") 231 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_VEHICLE_STATUS)) 232 .isFalse(); 233 expectWithMessage("Non-affected announcement usage with default config selected") 234 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_ANNOUNCEMENT)) 235 .isFalse(); 236 } 237 238 @Test setupAudioDynamicRouting_withoutSelectedConfig()239 public void setupAudioDynamicRouting_withoutSelectedConfig() { 240 AudioPolicy.Builder mockBuilder = Mockito.mock(AudioPolicy.Builder.class); 241 SparseArray<CarAudioZone> zones = getTestCarAudioZones(/* defaultConfigSelected= */ false, 242 /* secondaryConfigSelected= */ false); 243 244 IllegalStateException thrown = assertThrows(IllegalStateException.class, 245 () -> CarAudioDynamicRouting.setupAudioDynamicRouting(TEST_CAR_AUDIO_CONTEXT, 246 mAudioManager, mockBuilder, zones)); 247 248 expectWithMessage("No selected config exception").that(thrown).hasMessageThat() 249 .contains("Selected configuration for zone"); 250 } 251 252 @Test setupAudioDynamicRoutingForMirrorDevice()253 public void setupAudioDynamicRoutingForMirrorDevice() { 254 AudioPolicy.Builder mockBuilder = Mockito.mock(AudioPolicy.Builder.class); 255 AudioDeviceInfo mirrorDevice = Mockito.mock(AudioDeviceInfo.class); 256 CarAudioDeviceInfo carMirrorDeviceInfo = getCarAudioDeviceInfo(MUSIC_ADDRESS, 257 /* canBeRoutedWithDynamicPolicyMix= */ true, mirrorDevice); 258 when(mAudioManager.getDevices(anyInt())).thenReturn( 259 new AudioDeviceInfo[]{mirrorDevice}); 260 261 262 CarAudioDynamicRouting.setupAudioDynamicRoutingForMirrorDevice(mockBuilder, 263 List.of(carMirrorDeviceInfo), mAudioManager); 264 265 ArgumentCaptor<AudioMix> audioMixCaptor = ArgumentCaptor.forClass(AudioMix.class); 266 267 verify(mockBuilder).addMix(audioMixCaptor.capture()); 268 AudioMix audioMix = audioMixCaptor.getValue(); 269 expectWithMessage("Music address registered").that(audioMix.getRegistration()) 270 .isEqualTo(MUSIC_ADDRESS); 271 expectWithMessage("Contains Music device") 272 .that(audioMix.isRoutedToDevice(AudioSystem.DEVICE_OUT_BUS, MUSIC_ADDRESS)) 273 .isTrue(); 274 expectWithMessage("Affected media usage") 275 .that(audioMix.isAffectingUsage(AudioAttributes.USAGE_MEDIA)) 276 .isTrue(); 277 } 278 getTestCarAudioZones(boolean defaultConfigSelected, boolean secondaryConfigSelected)279 private SparseArray<CarAudioZone> getTestCarAudioZones(boolean defaultConfigSelected, 280 boolean secondaryConfigSelected) { 281 AudioDeviceInfo musicDeviceInfo = Mockito.mock(AudioDeviceInfo.class); 282 CarAudioDeviceInfo musicCarAudioDeviceInfo = getCarAudioDeviceInfo(MUSIC_ADDRESS, 283 /* canBeRoutedWithDynamicPolicyMix= */ true, musicDeviceInfo); 284 AudioDeviceInfo navDeviceInfo = Mockito.mock(AudioDeviceInfo.class); 285 CarAudioDeviceInfo navCarAudioDeviceInfo = getCarAudioDeviceInfo(NAV_ADDRESS, 286 /* canBeRoutedWithDynamicPolicyMix= */ false, navDeviceInfo); 287 when(mAudioManager.getDevices(anyInt())).thenReturn( 288 new AudioDeviceInfo[]{musicDeviceInfo, navDeviceInfo}); 289 CarVolumeGroup mockMusicGroup = new VolumeGroupBuilder() 290 .addDeviceAddressAndContexts(TEST_MEDIA_CONTEXT, MUSIC_ADDRESS) 291 .addCarAudioDeviceInfoMock(musicCarAudioDeviceInfo) 292 .build(); 293 CarVolumeGroup mockNavGroupRoutingOnMusic = new VolumeGroupBuilder() 294 .addDeviceAddressAndContexts(TEST_NAVIGATION_CONTEXT, NAV_ADDRESS) 295 .addCarAudioDeviceInfoMock(navCarAudioDeviceInfo) 296 .build(); 297 CarAudioZoneConfig defaultAudioZoneConfig = 298 new CarAudioZoneConfig.Builder("Default zone config 0", 299 CarAudioManager.PRIMARY_AUDIO_ZONE, /* zoneConfigId= */ 0, 300 /* isDefault= */ true).addVolumeGroup(mockMusicGroup) 301 .addVolumeGroup(mockNavGroupRoutingOnMusic) 302 .build(); 303 defaultAudioZoneConfig.setIsSelected(defaultConfigSelected); 304 CarAudioZoneConfig secondaryAudioZoneConfig = 305 new CarAudioZoneConfig.Builder("Selected zone config 0", 306 CarAudioManager.PRIMARY_AUDIO_ZONE, /* zoneConfigId= */ 1, 307 /* isDefault= */ false).addVolumeGroup(mockMusicGroup) 308 .addVolumeGroup(mockNavGroupRoutingOnMusic) 309 .build(); 310 secondaryAudioZoneConfig.setIsSelected(secondaryConfigSelected); 311 CarAudioZone carAudioZone = new CarAudioZone(TEST_CAR_AUDIO_CONTEXT, "Primary zone", 312 CarAudioManager.PRIMARY_AUDIO_ZONE); 313 carAudioZone.addZoneConfig(defaultAudioZoneConfig); 314 carAudioZone.addZoneConfig(secondaryAudioZoneConfig); 315 SparseArray<CarAudioZone> zones = new SparseArray<>(); 316 zones.put(CarAudioManager.PRIMARY_AUDIO_ZONE, carAudioZone); 317 return zones; 318 } 319 getCarAudioDeviceInfo(String address, boolean canBeRoutedWithDynamicPolicyMix, AudioDeviceInfo audioDeviceInfo)320 private CarAudioDeviceInfo getCarAudioDeviceInfo(String address, 321 boolean canBeRoutedWithDynamicPolicyMix, AudioDeviceInfo audioDeviceInfo) { 322 when(audioDeviceInfo.isSink()).thenReturn(true); 323 when(audioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_BUS); 324 when(audioDeviceInfo.getAddress()).thenReturn(address); 325 AudioDeviceAttributes deviceAttributes = new AudioDeviceAttributes(audioDeviceInfo); 326 CarAudioDeviceInfo carAudioDeviceInfo = Mockito.mock(CarAudioDeviceInfo.class); 327 when(carAudioDeviceInfo.getAddress()).thenReturn(address); 328 when(carAudioDeviceInfo.getAudioDevice()).thenReturn(deviceAttributes); 329 when(carAudioDeviceInfo.getEncodingFormat()) 330 .thenReturn(AudioFormat.ENCODING_PCM_16BIT); 331 when(carAudioDeviceInfo.getChannelCount()).thenReturn(2); 332 when(carAudioDeviceInfo.canBeRoutedWithDynamicPolicyMix()).thenReturn( 333 canBeRoutedWithDynamicPolicyMix); 334 return carAudioDeviceInfo; 335 } 336 337 private static final class VolumeGroupBuilder { 338 private SparseArray<String> mDeviceAddresses = new SparseArray<>(); 339 private CarAudioDeviceInfo mCarAudioDeviceInfoMock; 340 private ArrayMap<String, List<Integer>> mUsagesDeviceAddresses = new ArrayMap<>(); 341 private boolean mIsActive = true; 342 addDeviceAddressAndContexts(@udioContext int context, String address)343 VolumeGroupBuilder addDeviceAddressAndContexts(@AudioContext int context, String address) { 344 mDeviceAddresses.put(context, address); 345 return this; 346 } 347 addCarAudioDeviceInfoMock(CarAudioDeviceInfo infoMock)348 VolumeGroupBuilder addCarAudioDeviceInfoMock(CarAudioDeviceInfo infoMock) { 349 mCarAudioDeviceInfoMock = infoMock; 350 return this; 351 } 352 build()353 CarVolumeGroup build() { 354 CarVolumeGroup carVolumeGroup = mock(CarVolumeGroup.class); 355 Map<String, ArrayList<Integer>> addressToContexts = new ArrayMap<>(); 356 @AudioContext int[] contexts = new int[mDeviceAddresses.size()]; 357 358 for (int index = 0; index < mDeviceAddresses.size(); index++) { 359 @AudioContext int context = mDeviceAddresses.keyAt(index); 360 String address = mDeviceAddresses.get(context); 361 when(carVolumeGroup.getAddressForContext(context)).thenReturn(address); 362 if (!addressToContexts.containsKey(address)) { 363 addressToContexts.put(address, new ArrayList<>()); 364 } 365 addressToContexts.get(address).add(context); 366 contexts[index] = context; 367 } 368 369 for (int index = 0; index < mUsagesDeviceAddresses.size(); index++) { 370 String address = mUsagesDeviceAddresses.keyAt(index); 371 List<Integer> usagesForAddress = mUsagesDeviceAddresses.get(address); 372 when(carVolumeGroup.getAllSupportedUsagesForAddress(eq(address))) 373 .thenReturn(usagesForAddress); 374 } 375 376 when(carVolumeGroup.getContexts()).thenReturn(contexts); 377 378 for (String address : addressToContexts.keySet()) { 379 when(carVolumeGroup.getContextsForAddress(address)) 380 .thenReturn(ImmutableList.copyOf(addressToContexts.get(address))); 381 } 382 when(carVolumeGroup.getAddresses()) 383 .thenReturn(ImmutableList.copyOf(addressToContexts.keySet())); 384 385 when(carVolumeGroup.getCarAudioDeviceInfoForAddress(any())) 386 .thenReturn(mCarAudioDeviceInfoMock); 387 when(carVolumeGroup.isActive()).thenReturn(mIsActive); 388 389 return carVolumeGroup; 390 } 391 } 392 } 393