1 /* 2 * Copyright (C) 2023 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 17 package android.media.audio.cts; 18 19 import static android.media.AudioAttributes.USAGE_MEDIA; 20 import static android.media.MediaRecorder.AudioSource.VOICE_RECOGNITION; 21 import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_INJECTOR; 22 import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_PLAYERS; 23 import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET; 24 import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE; 25 import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_AUDIO_SESSION_ID; 26 import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_UID; 27 import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET; 28 import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE; 29 import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_AUDIO_SESSION_ID; 30 import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_UID; 31 32 import static org.hamcrest.MatcherAssert.assertThat; 33 import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 34 import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; 35 import static org.junit.Assert.assertEquals; 36 import static org.junit.Assert.assertThrows; 37 38 39 import android.media.AudioAttributes; 40 import android.media.audiopolicy.AudioMixingRule; 41 import android.media.audiopolicy.AudioMixingRule.AudioMixMatchCriterion; 42 import android.os.Parcel; 43 import android.platform.test.annotations.Presubmit; 44 45 import androidx.test.ext.junit.runners.AndroidJUnit4; 46 47 import com.google.common.testing.EqualsTester; 48 49 import org.hamcrest.CustomTypeSafeMatcher; 50 import org.hamcrest.Description; 51 import org.hamcrest.Matcher; 52 import org.junit.Test; 53 import org.junit.runner.RunWith; 54 55 /** 56 * Unit tests for AudioPolicy. 57 * 58 * Run with "atest AudioMixingRuleUnitTests". 59 */ 60 @Presubmit 61 @RunWith(AndroidJUnit4.class) 62 public class AudioMixingRuleTest { 63 private static final AudioAttributes USAGE_MEDIA_AUDIO_ATTRIBUTES = 64 new AudioAttributes.Builder().setUsage(USAGE_MEDIA).build(); 65 private static final AudioAttributes CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES = 66 new AudioAttributes.Builder().setCapturePreset(VOICE_RECOGNITION).build(); 67 private static final int TEST_UID = 42; 68 private static final int OTHER_UID = 77; 69 private static final int TEST_SESSION_ID = 1234; 70 71 @Test testConstructValidRule()72 public void testConstructValidRule() { 73 AudioMixingRule rule = new AudioMixingRule.Builder() 74 .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) 75 .addMixRule(RULE_MATCH_UID, TEST_UID) 76 .excludeMixRule(RULE_MATCH_AUDIO_SESSION_ID, TEST_SESSION_ID) 77 .build(); 78 79 // Based on the rules, the mix type should fall back to MIX_ROLE_PLAYERS, 80 // since the rules are valid for both MIX_ROLE_PLAYERS & MIX_ROLE_INJECTOR. 81 assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS); 82 assertThat(rule.getCriteria(), containsInAnyOrder( 83 isAudioMixMatchUsageCriterion(USAGE_MEDIA), 84 isAudioMixMatchUidCriterion(TEST_UID), 85 isAudioMixExcludeSessionCriterion(TEST_SESSION_ID))); 86 } 87 88 @Test testConstructRuleWithConflictingCriteriaFails()89 public void testConstructRuleWithConflictingCriteriaFails() { 90 assertThrows(IllegalArgumentException.class, 91 () -> new AudioMixingRule.Builder() 92 .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) 93 .addMixRule(RULE_MATCH_UID, TEST_UID) 94 // Conflicts with previous criterion. 95 .addMixRule(RULE_EXCLUDE_UID, OTHER_UID) 96 .build()); 97 } 98 99 @Test testRuleBuilderDedupsCriteria()100 public void testRuleBuilderDedupsCriteria() { 101 AudioMixingRule rule = new AudioMixingRule.Builder() 102 .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) 103 .addMixRule(RULE_MATCH_UID, TEST_UID) 104 // Identical to previous criterion. 105 .addMixRule(RULE_MATCH_UID, TEST_UID) 106 // Identical to first criterion. 107 .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) 108 .build(); 109 110 assertThat(rule.getCriteria(), hasSize(2)); 111 assertThat(rule.getCriteria(), containsInAnyOrder( 112 isAudioMixMatchUsageCriterion(USAGE_MEDIA), 113 isAudioMixMatchUidCriterion(TEST_UID))); 114 } 115 116 @Test failsWhenAddAttributeRuleCalledWithInvalidType()117 public void failsWhenAddAttributeRuleCalledWithInvalidType() { 118 assertThrows(IllegalArgumentException.class, 119 () -> new AudioMixingRule.Builder() 120 // Rule match attribute usage requires AudioAttributes, not 121 // just the int enum value of the usage. 122 .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA) 123 .build()); 124 } 125 126 @Test failsWhenExcludeAttributeRuleCalledWithInvalidType()127 public void failsWhenExcludeAttributeRuleCalledWithInvalidType() { 128 assertThrows(IllegalArgumentException.class, 129 () -> new AudioMixingRule.Builder() 130 // Rule match attribute usage requires AudioAttributes, not 131 // just the int enum value of the usage. 132 .excludeMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA) 133 .build()); 134 } 135 136 @Test failsWhenAddIntRuleCalledWithInvalidType()137 public void failsWhenAddIntRuleCalledWithInvalidType() { 138 assertThrows(IllegalArgumentException.class, 139 () -> new AudioMixingRule.Builder() 140 // Rule match uid requires Integer not AudioAttributes. 141 .addMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES) 142 .build()); 143 } 144 145 @Test failsWhenExcludeIntRuleCalledWithInvalidType()146 public void failsWhenExcludeIntRuleCalledWithInvalidType() { 147 assertThrows(IllegalArgumentException.class, 148 () -> new AudioMixingRule.Builder() 149 // Rule match uid requires Integer not AudioAttributes. 150 .excludeMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES) 151 .build()); 152 } 153 154 @Test injectorMixTypeDeductionWithGenericRuleSucceeds()155 public void injectorMixTypeDeductionWithGenericRuleSucceeds() { 156 AudioMixingRule rule = new AudioMixingRule.Builder() 157 // UID rule can be used both with MIX_ROLE_PLAYERS and MIX_ROLE_INJECTOR. 158 .addMixRule(RULE_MATCH_UID, TEST_UID) 159 // Capture preset rule is only valid for injector, MIX_ROLE_INJECTOR should 160 // be deduced. 161 .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, 162 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) 163 .build(); 164 165 assertEquals(rule.getTargetMixRole(), MIX_ROLE_INJECTOR); 166 assertThat(rule.getCriteria(), containsInAnyOrder( 167 isAudioMixMatchUidCriterion(TEST_UID), 168 isAudioMixMatchCapturePresetCriterion(VOICE_RECOGNITION))); 169 } 170 171 @Test settingTheMixTypeToIncompatibleInjectorMixFails()172 public void settingTheMixTypeToIncompatibleInjectorMixFails() { 173 assertThrows(IllegalArgumentException.class, 174 () -> new AudioMixingRule.Builder() 175 .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, 176 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) 177 // Capture preset cannot be defined for MIX_ROLE_PLAYERS. 178 .setTargetMixRole(MIX_ROLE_PLAYERS) 179 .build()); 180 } 181 182 @Test addingPlayersOnlyRuleWithInjectorsOnlyRuleFails()183 public void addingPlayersOnlyRuleWithInjectorsOnlyRuleFails() { 184 assertThrows(IllegalArgumentException.class, 185 () -> new AudioMixingRule.Builder() 186 // MIX_ROLE_PLAYERS only rule. 187 .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) 188 // MIX ROLE_INJECTOR only rule. 189 .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, 190 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) 191 .build()); 192 } 193 194 @Test sessionIdRuleCompatibleWithPlayersMix()195 public void sessionIdRuleCompatibleWithPlayersMix() { 196 int sessionId = 42; 197 AudioMixingRule rule = new AudioMixingRule.Builder() 198 .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, sessionId) 199 .setTargetMixRole(MIX_ROLE_PLAYERS) 200 .build(); 201 202 assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS); 203 assertThat(rule.getCriteria(), containsInAnyOrder(isAudioMixSessionCriterion(sessionId))); 204 } 205 206 @Test sessionIdRuleCompatibleWithInjectorMix()207 public void sessionIdRuleCompatibleWithInjectorMix() { 208 AudioMixingRule rule = new AudioMixingRule.Builder() 209 .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, TEST_SESSION_ID) 210 .setTargetMixRole(MIX_ROLE_INJECTOR) 211 .build(); 212 213 assertEquals(rule.getTargetMixRole(), MIX_ROLE_INJECTOR); 214 assertThat(rule.getCriteria(), 215 containsInAnyOrder(isAudioMixSessionCriterion(TEST_SESSION_ID))); 216 } 217 218 @Test audioMixingRuleWithNoRulesFails()219 public void audioMixingRuleWithNoRulesFails() { 220 assertThrows(IllegalArgumentException.class, 221 () -> new AudioMixingRule.Builder().build()); 222 } 223 224 @Test audioMixMatchCriterion_equals_isCorrect()225 public void audioMixMatchCriterion_equals_isCorrect() { 226 AudioMixMatchCriterion criterionUsage = new AudioMixMatchCriterion( 227 USAGE_MEDIA_AUDIO_ATTRIBUTES, RULE_MATCH_ATTRIBUTE_USAGE); 228 AudioMixMatchCriterion criterionExcludeUsage = new AudioMixMatchCriterion( 229 USAGE_MEDIA_AUDIO_ATTRIBUTES, RULE_EXCLUDE_ATTRIBUTE_USAGE); 230 AudioMixMatchCriterion criterionCapturePreset = new AudioMixMatchCriterion( 231 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES, 232 RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET); 233 AudioMixMatchCriterion criterionExcludeCapturePreset = new AudioMixMatchCriterion( 234 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES, 235 RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET); 236 AudioMixMatchCriterion criterionUid = new AudioMixMatchCriterion(TEST_UID, RULE_MATCH_UID); 237 AudioMixMatchCriterion criterionExcludeUid = new AudioMixMatchCriterion(TEST_UID, 238 RULE_EXCLUDE_UID); 239 AudioMixMatchCriterion criterionSessionId = new AudioMixMatchCriterion(TEST_SESSION_ID, 240 RULE_MATCH_UID); 241 AudioMixMatchCriterion criterionExcludeSessionId = new AudioMixMatchCriterion( 242 TEST_SESSION_ID, RULE_EXCLUDE_UID); 243 244 final EqualsTester equalsTester = new EqualsTester(); 245 equalsTester.addEqualityGroup(criterionUsage, writeToAndFromParcel(criterionUsage)); 246 equalsTester.addEqualityGroup(criterionExcludeUsage, 247 writeToAndFromParcel(criterionExcludeUsage)); 248 equalsTester.addEqualityGroup(criterionCapturePreset, 249 writeToAndFromParcel(criterionCapturePreset)); 250 equalsTester.addEqualityGroup(criterionExcludeCapturePreset, 251 writeToAndFromParcel(criterionExcludeCapturePreset)); 252 equalsTester.addEqualityGroup(criterionUid, writeToAndFromParcel(criterionUid)); 253 equalsTester.addEqualityGroup(criterionExcludeUid, 254 writeToAndFromParcel(criterionExcludeUid)); 255 equalsTester.addEqualityGroup(criterionSessionId, writeToAndFromParcel(criterionSessionId)); 256 equalsTester.addEqualityGroup(criterionExcludeSessionId, 257 writeToAndFromParcel(criterionExcludeSessionId)); 258 259 equalsTester.testEquals(); 260 } 261 262 @Test audioMixingRule_equals_isCorrect()263 public void audioMixingRule_equals_isCorrect() { 264 final EqualsTester equalsTester = new EqualsTester(); 265 266 AudioMixingRule mixRule1 = new AudioMixingRule.Builder().addMixRule( 267 RULE_MATCH_AUDIO_SESSION_ID, TEST_SESSION_ID).excludeMixRule(RULE_MATCH_UID, 268 TEST_UID).build(); 269 AudioMixingRule mixRule2 = new AudioMixingRule.Builder().addMixRule( 270 RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, 271 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES).setTargetMixRole( 272 MIX_ROLE_INJECTOR).allowPrivilegedPlaybackCapture(true).build(); 273 AudioMixingRule mixRule3 = new AudioMixingRule.Builder().addMixRule( 274 RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, 275 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES).setTargetMixRole( 276 MIX_ROLE_INJECTOR).allowPrivilegedPlaybackCapture(false).build(); 277 AudioMixingRule mixRule4 = new AudioMixingRule.Builder().addMixRule( 278 RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, 279 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES).setTargetMixRole( 280 MIX_ROLE_INJECTOR).voiceCommunicationCaptureAllowed(true).build(); 281 282 equalsTester.addEqualityGroup(mixRule1, writeToAndFromParcel(mixRule1)); 283 equalsTester.addEqualityGroup(mixRule2, writeToAndFromParcel(mixRule2)); 284 equalsTester.addEqualityGroup(mixRule3, writeToAndFromParcel(mixRule3)); 285 equalsTester.addEqualityGroup(mixRule4, writeToAndFromParcel(mixRule4)); 286 287 equalsTester.testEquals(); 288 } 289 writeToAndFromParcel(AudioMixMatchCriterion criterion)290 private static AudioMixMatchCriterion writeToAndFromParcel(AudioMixMatchCriterion criterion) { 291 Parcel parcel = Parcel.obtain(); 292 try { 293 criterion.writeToParcel(parcel, /*parcelableFlags=*/0); 294 parcel.setDataPosition(0); 295 return AudioMixMatchCriterion.CREATOR.createFromParcel(parcel); 296 } finally { 297 parcel.recycle(); 298 } 299 } 300 writeToAndFromParcel(AudioMixingRule audioMixingRule)301 private static AudioMixingRule writeToAndFromParcel(AudioMixingRule audioMixingRule) { 302 Parcel parcel = Parcel.obtain(); 303 try { 304 audioMixingRule.writeToParcel(parcel, /*parcelableFlags=*/0); 305 parcel.setDataPosition(0); 306 return AudioMixingRule.CREATOR.createFromParcel(parcel); 307 } finally { 308 parcel.recycle(); 309 } 310 } 311 312 isAudioMixUidCriterion(int uid, boolean exclude)313 private static Matcher isAudioMixUidCriterion(int uid, boolean exclude) { 314 return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") { 315 @Override 316 public boolean matchesSafely(AudioMixMatchCriterion item) { 317 int expectedRule = exclude ? RULE_EXCLUDE_UID : RULE_MATCH_UID; 318 return item.getRule() == expectedRule && item.getIntProp() == uid; 319 } 320 321 @Override 322 public void describeMismatchSafely( 323 AudioMixMatchCriterion item, Description mismatchDescription) { 324 mismatchDescription.appendText( 325 String.format("is not %s criterion with uid %d", 326 exclude ? "exclude" : "match", uid)); 327 } 328 }; 329 } 330 331 private static Matcher isAudioMixMatchUidCriterion(int uid) { 332 return isAudioMixUidCriterion(uid, /*exclude=*/ false); 333 } 334 335 private static Matcher isAudioMixCapturePresetCriterion(int audioSource, boolean exclude) { 336 return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") { 337 @Override 338 public boolean matchesSafely(AudioMixMatchCriterion item) { 339 int expectedRule = exclude 340 ? RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET 341 : RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET; 342 AudioAttributes attributes = item.getAudioAttributes(); 343 return item.getRule() == expectedRule 344 && attributes != null && attributes.getCapturePreset() == audioSource; 345 } 346 347 @Override 348 public void describeMismatchSafely( 349 AudioMixMatchCriterion item, Description mismatchDescription) { 350 mismatchDescription.appendText( 351 String.format("is not %s criterion with capture preset %d", 352 exclude ? "exclude" : "match", audioSource)); 353 } 354 }; 355 } 356 357 private static Matcher isAudioMixMatchCapturePresetCriterion(int audioSource) { 358 return isAudioMixCapturePresetCriterion(audioSource, /*exclude=*/ false); 359 } 360 361 private static Matcher isAudioMixUsageCriterion(int usage, boolean exclude) { 362 return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("usage mix criterion") { 363 @Override 364 public boolean matchesSafely(AudioMixMatchCriterion item) { 365 int expectedRule = 366 exclude ? RULE_EXCLUDE_ATTRIBUTE_USAGE : RULE_MATCH_ATTRIBUTE_USAGE; 367 AudioAttributes attributes = item.getAudioAttributes(); 368 return item.getRule() == expectedRule 369 && attributes != null && attributes.getUsage() == usage; 370 } 371 372 @Override 373 public void describeMismatchSafely( 374 AudioMixMatchCriterion item, Description mismatchDescription) { 375 mismatchDescription.appendText( 376 String.format("is not %s criterion with usage %d", 377 exclude ? "exclude" : "match", usage)); 378 } 379 }; 380 } 381 382 private static Matcher isAudioMixMatchUsageCriterion(int usage) { 383 return isAudioMixUsageCriterion(usage, /*exclude=*/ false); 384 } 385 386 private static Matcher isAudioMixSessionCriterion(int sessionId, boolean exclude) { 387 return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("sessionId mix criterion") { 388 @Override 389 public boolean matchesSafely(AudioMixMatchCriterion item) { 390 int excludeRule = 391 exclude ? RULE_EXCLUDE_AUDIO_SESSION_ID : RULE_MATCH_AUDIO_SESSION_ID; 392 return item.getRule() == excludeRule && item.getIntProp() == sessionId; 393 } 394 395 @Override 396 public void describeMismatchSafely( 397 AudioMixMatchCriterion item, Description mismatchDescription) { 398 mismatchDescription.appendText( 399 String.format("is not %s criterion with session id %d", 400 exclude ? "exclude" : "match", sessionId)); 401 } 402 }; 403 } 404 405 private static Matcher isAudioMixSessionCriterion(int sessionId) { 406 return isAudioMixSessionCriterion(sessionId, /*exclude=*/ false); 407 } 408 409 private static Matcher isAudioMixExcludeSessionCriterion(int sessionId) { 410 return isAudioMixSessionCriterion(sessionId, /*exclude=*/ true); 411 } 412 413 } 414