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