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 
17 package com.android.audiopolicytest;
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.platform.test.annotations.Presubmit;
43 
44 import androidx.test.ext.junit.runners.AndroidJUnit4;
45 
46 import org.hamcrest.CustomTypeSafeMatcher;
47 import org.hamcrest.Description;
48 import org.hamcrest.Matcher;
49 import org.junit.Test;
50 import org.junit.runner.RunWith;
51 
52 /**
53  * Unit tests for AudioPolicy.
54  *
55  * Run with "atest AudioMixingRuleUnitTests".
56  */
57 @Presubmit
58 @RunWith(AndroidJUnit4.class)
59 public class AudioMixingRuleUnitTests {
60     private static final AudioAttributes USAGE_MEDIA_AUDIO_ATTRIBUTES =
61             new AudioAttributes.Builder().setUsage(USAGE_MEDIA).build();
62     private static final AudioAttributes CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES =
63             new AudioAttributes.Builder().setCapturePreset(VOICE_RECOGNITION).build();
64     private static final int TEST_UID = 42;
65     private static final int OTHER_UID = 77;
66     private static final int TEST_SESSION_ID = 1234;
67 
68     @Test
testConstructValidRule()69     public void testConstructValidRule() {
70         AudioMixingRule rule = new AudioMixingRule.Builder()
71                 .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES)
72                 .addMixRule(RULE_MATCH_UID, TEST_UID)
73                 .excludeMixRule(RULE_MATCH_AUDIO_SESSION_ID, TEST_SESSION_ID)
74                 .build();
75 
76         // Based on the rules, the mix type should fall back to MIX_ROLE_PLAYERS,
77         // since the rules are valid for both MIX_ROLE_PLAYERS & MIX_ROLE_INJECTOR.
78         assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS);
79         assertThat(rule.getCriteria(), containsInAnyOrder(
80                 isAudioMixMatchUsageCriterion(USAGE_MEDIA),
81                 isAudioMixMatchUidCriterion(TEST_UID),
82                 isAudioMixExcludeSessionCriterion(TEST_SESSION_ID)));
83     }
84 
85     @Test
testConstructRuleWithConflictingCriteriaFails()86     public void testConstructRuleWithConflictingCriteriaFails() {
87         assertThrows(IllegalArgumentException.class,
88                 () -> new AudioMixingRule.Builder()
89                         .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES)
90                         .addMixRule(RULE_MATCH_UID, TEST_UID)
91                         // Conflicts with previous criterion.
92                         .addMixRule(RULE_EXCLUDE_UID, OTHER_UID)
93                         .build());
94     }
95 
96     @Test
testRuleBuilderDedupsCriteria()97     public void testRuleBuilderDedupsCriteria() {
98         AudioMixingRule rule = new AudioMixingRule.Builder()
99                 .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES)
100                 .addMixRule(RULE_MATCH_UID, TEST_UID)
101                 // Identical to previous criterion.
102                 .addMixRule(RULE_MATCH_UID, TEST_UID)
103                 // Identical to first criterion.
104                 .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES)
105                 .build();
106 
107         assertThat(rule.getCriteria(), hasSize(2));
108         assertThat(rule.getCriteria(), containsInAnyOrder(
109                 isAudioMixMatchUsageCriterion(USAGE_MEDIA),
110                 isAudioMixMatchUidCriterion(TEST_UID)));
111     }
112 
113     @Test
failsWhenAddAttributeRuleCalledWithInvalidType()114     public void failsWhenAddAttributeRuleCalledWithInvalidType() {
115         assertThrows(IllegalArgumentException.class,
116                 () -> new AudioMixingRule.Builder()
117                         // Rule match attribute usage requires AudioAttributes, not
118                         // just the int enum value of the usage.
119                         .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA)
120                         .build());
121     }
122 
123     @Test
failsWhenExcludeAttributeRuleCalledWithInvalidType()124     public void failsWhenExcludeAttributeRuleCalledWithInvalidType() {
125         assertThrows(IllegalArgumentException.class,
126                 () -> new AudioMixingRule.Builder()
127                         // Rule match attribute usage requires AudioAttributes, not
128                         // just the int enum value of the usage.
129                         .excludeMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA)
130                         .build());
131     }
132 
133     @Test
failsWhenAddIntRuleCalledWithInvalidType()134     public void failsWhenAddIntRuleCalledWithInvalidType() {
135         assertThrows(IllegalArgumentException.class,
136                 () -> new AudioMixingRule.Builder()
137                         // Rule match uid requires Integer not AudioAttributes.
138                         .addMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES)
139                         .build());
140     }
141 
142     @Test
failsWhenExcludeIntRuleCalledWithInvalidType()143     public void failsWhenExcludeIntRuleCalledWithInvalidType() {
144         assertThrows(IllegalArgumentException.class,
145                 () -> new AudioMixingRule.Builder()
146                         // Rule match uid requires Integer not AudioAttributes.
147                         .excludeMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES)
148                         .build());
149     }
150 
151     @Test
injectorMixTypeDeductionWithGenericRuleSucceeds()152     public void injectorMixTypeDeductionWithGenericRuleSucceeds() {
153         AudioMixingRule rule = new AudioMixingRule.Builder()
154                 // UID rule can be used both with MIX_ROLE_PLAYERS and MIX_ROLE_INJECTOR.
155                 .addMixRule(RULE_MATCH_UID, TEST_UID)
156                 // Capture preset rule is only valid for injector, MIX_ROLE_INJECTOR should
157                 // be deduced.
158                 .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET,
159                         CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES)
160                 .build();
161 
162         assertEquals(rule.getTargetMixRole(), MIX_ROLE_INJECTOR);
163         assertThat(rule.getCriteria(), containsInAnyOrder(
164                 isAudioMixMatchUidCriterion(TEST_UID),
165                 isAudioMixMatchCapturePresetCriterion(VOICE_RECOGNITION)));
166     }
167 
168     @Test
settingTheMixTypeToIncompatibleInjectorMixFails()169     public void settingTheMixTypeToIncompatibleInjectorMixFails() {
170         assertThrows(IllegalArgumentException.class,
171                 () -> new AudioMixingRule.Builder()
172                         .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET,
173                                 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES)
174                         // Capture preset cannot be defined for MIX_ROLE_PLAYERS.
175                         .setTargetMixRole(MIX_ROLE_PLAYERS)
176                         .build());
177     }
178 
179     @Test
addingPlayersOnlyRuleWithInjectorsOnlyRuleFails()180     public void addingPlayersOnlyRuleWithInjectorsOnlyRuleFails() {
181         assertThrows(IllegalArgumentException.class,
182                 () -> new AudioMixingRule.Builder()
183                         // MIX_ROLE_PLAYERS only rule.
184                         .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES)
185                         // MIX ROLE_INJECTOR only rule.
186                         .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET,
187                                 CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES)
188                         .build());
189     }
190 
191     @Test
sessionIdRuleCompatibleWithPlayersMix()192     public void sessionIdRuleCompatibleWithPlayersMix() {
193         int sessionId = 42;
194         AudioMixingRule rule = new AudioMixingRule.Builder()
195                 .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, sessionId)
196                 .setTargetMixRole(MIX_ROLE_PLAYERS)
197                 .build();
198 
199         assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS);
200         assertThat(rule.getCriteria(), containsInAnyOrder(isAudioMixSessionCriterion(sessionId)));
201     }
202 
203     @Test
sessionIdRuleCompatibleWithInjectorMix()204     public void sessionIdRuleCompatibleWithInjectorMix() {
205         AudioMixingRule rule = new AudioMixingRule.Builder()
206                 .addMixRule(RULE_MATCH_AUDIO_SESSION_ID, TEST_SESSION_ID)
207                 .setTargetMixRole(MIX_ROLE_INJECTOR)
208                 .build();
209 
210         assertEquals(rule.getTargetMixRole(), MIX_ROLE_INJECTOR);
211         assertThat(rule.getCriteria(),
212                 containsInAnyOrder(isAudioMixSessionCriterion(TEST_SESSION_ID)));
213     }
214 
215     @Test
audioMixingRuleWithNoRulesFails()216     public void audioMixingRuleWithNoRulesFails() {
217         assertThrows(IllegalArgumentException.class,
218                 () -> new AudioMixingRule.Builder().build());
219     }
220 
221 
isAudioMixUidCriterion(int uid, boolean exclude)222     private static Matcher isAudioMixUidCriterion(int uid, boolean exclude) {
223         return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") {
224             @Override
225             public boolean matchesSafely(AudioMixMatchCriterion item) {
226                 int expectedRule = exclude ? RULE_EXCLUDE_UID : RULE_MATCH_UID;
227                 return item.getRule() == expectedRule && item.getIntProp() == uid;
228             }
229 
230             @Override
231             public void describeMismatchSafely(
232                     AudioMixMatchCriterion item, Description mismatchDescription) {
233                 mismatchDescription.appendText(
234                         String.format("is not %s criterion with uid %d",
235                                 exclude ? "exclude" : "match", uid));
236             }
237         };
238     }
239 
240     private static Matcher isAudioMixMatchUidCriterion(int uid) {
241         return isAudioMixUidCriterion(uid, /*exclude=*/ false);
242     }
243 
244     private static Matcher isAudioMixCapturePresetCriterion(int audioSource, boolean exclude) {
245         return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") {
246             @Override
247             public boolean matchesSafely(AudioMixMatchCriterion item) {
248                 int expectedRule = exclude
249                         ? RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET
250                         : RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET;
251                 AudioAttributes attributes = item.getAudioAttributes();
252                 return item.getRule() == expectedRule
253                         && attributes != null && attributes.getCapturePreset() == audioSource;
254             }
255 
256             @Override
257             public void describeMismatchSafely(
258                     AudioMixMatchCriterion item, Description mismatchDescription) {
259                 mismatchDescription.appendText(
260                         String.format("is not %s criterion with capture preset %d",
261                                 exclude ? "exclude" : "match", audioSource));
262             }
263         };
264     }
265 
266     private static Matcher isAudioMixMatchCapturePresetCriterion(int audioSource) {
267         return isAudioMixCapturePresetCriterion(audioSource, /*exclude=*/ false);
268     }
269 
270     private static Matcher isAudioMixUsageCriterion(int usage, boolean exclude) {
271         return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("usage mix criterion") {
272             @Override
273             public boolean matchesSafely(AudioMixMatchCriterion item) {
274                 int expectedRule =
275                         exclude ? RULE_EXCLUDE_ATTRIBUTE_USAGE : RULE_MATCH_ATTRIBUTE_USAGE;
276                 AudioAttributes attributes = item.getAudioAttributes();
277                 return item.getRule() == expectedRule
278                         && attributes != null && attributes.getUsage() == usage;
279             }
280 
281             @Override
282             public void describeMismatchSafely(
283                     AudioMixMatchCriterion item, Description mismatchDescription) {
284                 mismatchDescription.appendText(
285                         String.format("is not %s criterion with usage %d",
286                                 exclude ? "exclude" : "match", usage));
287             }
288         };
289     }
290 
291     private static Matcher isAudioMixMatchUsageCriterion(int usage) {
292         return isAudioMixUsageCriterion(usage, /*exclude=*/ false);
293     }
294 
295     private static Matcher isAudioMixSessionCriterion(int sessionId, boolean exclude) {
296         return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("sessionId mix criterion") {
297             @Override
298             public boolean matchesSafely(AudioMixMatchCriterion item) {
299                 int excludeRule =
300                         exclude ? RULE_EXCLUDE_AUDIO_SESSION_ID : RULE_MATCH_AUDIO_SESSION_ID;
301                 return item.getRule() == excludeRule && item.getIntProp() == sessionId;
302             }
303 
304             @Override
305             public void describeMismatchSafely(
306                     AudioMixMatchCriterion item, Description mismatchDescription) {
307                 mismatchDescription.appendText(
308                         String.format("is not %s criterion with session id %d",
309                         exclude ? "exclude" : "match", sessionId));
310             }
311         };
312     }
313 
314     private static Matcher isAudioMixSessionCriterion(int sessionId) {
315         return isAudioMixSessionCriterion(sessionId, /*exclude=*/ false);
316     }
317 
318     private static Matcher isAudioMixExcludeSessionCriterion(int sessionId) {
319         return isAudioMixSessionCriterion(sessionId, /*exclude=*/ true);
320     }
321 
322 }
323