1 /*
2  * Copyright (C) 2014 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.audiopolicy;
18 
19 import android.annotation.NonNull;
20 import android.annotation.SystemApi;
21 import android.annotation.TestApi;
22 import android.compat.annotation.UnsupportedAppUsage;
23 import android.media.AudioAttributes;
24 import android.os.Parcel;
25 import android.util.Log;
26 
27 import java.util.ArrayList;
28 import java.util.Iterator;
29 import java.util.Objects;
30 
31 
32 /**
33  * @hide
34  *
35  * Here's an example of creating a mixing rule for all media playback:
36  * <pre>
37  * AudioAttributes mediaAttr = new AudioAttributes.Builder()
38  *         .setUsage(AudioAttributes.USAGE_MEDIA)
39  *         .build();
40  * AudioMixingRule mediaRule = new AudioMixingRule.Builder()
41  *         .addRule(mediaAttr, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE)
42  *         .build();
43  * </pre>
44  */
45 @TestApi
46 @SystemApi
47 public class AudioMixingRule {
48 
AudioMixingRule(int mixType, ArrayList<AudioMixMatchCriterion> criteria, boolean allowPrivilegedPlaybackCapture)49     private AudioMixingRule(int mixType, ArrayList<AudioMixMatchCriterion> criteria,
50                             boolean allowPrivilegedPlaybackCapture) {
51         mCriteria = criteria;
52         mTargetMixType = mixType;
53         mAllowPrivilegedPlaybackCapture = allowPrivilegedPlaybackCapture;
54     }
55 
56     /**
57      * A rule requiring the usage information of the {@link AudioAttributes} to match.
58      * This mixing rule can be added with {@link Builder#addRule(AudioAttributes, int)} or
59      * {@link Builder#addMixRule(int, Object)} where the Object parameter is an instance of
60      * {@link AudioAttributes}.
61      */
62     public static final int RULE_MATCH_ATTRIBUTE_USAGE = 0x1;
63     /**
64      * A rule requiring the capture preset information of the {@link AudioAttributes} to match.
65      * This mixing rule can be added with {@link Builder#addRule(AudioAttributes, int)} or
66      * {@link Builder#addMixRule(int, Object)} where the Object parameter is an instance of
67      * {@link AudioAttributes}.
68      */
69     public static final int RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET = 0x1 << 1;
70     /**
71      * A rule requiring the UID of the audio stream to match that specified.
72      * This mixing rule can be added with {@link Builder#addMixRule(int, Object)} where the Object
73      * parameter is an instance of {@link java.lang.Integer}.
74      */
75     public static final int RULE_MATCH_UID = 0x1 << 2;
76 
77     private final static int RULE_EXCLUSION_MASK = 0x8000;
78     /**
79      * @hide
80      * A rule requiring the usage information of the {@link AudioAttributes} to differ.
81      */
82     public static final int RULE_EXCLUDE_ATTRIBUTE_USAGE =
83             RULE_EXCLUSION_MASK | RULE_MATCH_ATTRIBUTE_USAGE;
84     /**
85      * @hide
86      * A rule requiring the capture preset information of the {@link AudioAttributes} to differ.
87      */
88     public static final int RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET =
89             RULE_EXCLUSION_MASK | RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET;
90     /**
91      * @hide
92      * A rule requiring the UID information to differ.
93      */
94     public static final int RULE_EXCLUDE_UID =
95             RULE_EXCLUSION_MASK | RULE_MATCH_UID;
96 
97     /** @hide */
98     public static final class AudioMixMatchCriterion {
99         @UnsupportedAppUsage
100         final AudioAttributes mAttr;
101         @UnsupportedAppUsage
102         final int mIntProp;
103         @UnsupportedAppUsage
104         final int mRule;
105 
106         /** input parameters must be valid */
AudioMixMatchCriterion(AudioAttributes attributes, int rule)107         AudioMixMatchCriterion(AudioAttributes attributes, int rule) {
108             mAttr = attributes;
109             mIntProp = Integer.MIN_VALUE;
110             mRule = rule;
111         }
112         /** input parameters must be valid */
AudioMixMatchCriterion(Integer intProp, int rule)113         AudioMixMatchCriterion(Integer intProp, int rule) {
114             mAttr = null;
115             mIntProp = intProp.intValue();
116             mRule = rule;
117         }
118 
119         @Override
hashCode()120         public int hashCode() {
121             return Objects.hash(mAttr, mIntProp, mRule);
122         }
123 
writeToParcel(Parcel dest)124         void writeToParcel(Parcel dest) {
125             dest.writeInt(mRule);
126             final int match_rule = mRule & ~RULE_EXCLUSION_MASK;
127             switch (match_rule) {
128             case RULE_MATCH_ATTRIBUTE_USAGE:
129                 dest.writeInt(mAttr.getUsage());
130                 break;
131             case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
132                 dest.writeInt(mAttr.getCapturePreset());
133                 break;
134             case RULE_MATCH_UID:
135                 dest.writeInt(mIntProp);
136                 break;
137             default:
138                 Log.e("AudioMixMatchCriterion", "Unknown match rule" + match_rule
139                         + " when writing to Parcel");
140                 dest.writeInt(-1);
141             }
142         }
143 
getAudioAttributes()144         public AudioAttributes getAudioAttributes() { return mAttr; }
getIntProp()145         public int getIntProp() { return mIntProp; }
getRule()146         public int getRule() { return mRule; }
147     }
148 
isAffectingUsage(int usage)149     boolean isAffectingUsage(int usage) {
150         for (AudioMixMatchCriterion criterion : mCriteria) {
151             if ((criterion.mRule & RULE_MATCH_ATTRIBUTE_USAGE) != 0
152                     && criterion.mAttr != null
153                     && criterion.mAttr.getUsage() == usage) {
154                 return true;
155             }
156         }
157         return false;
158     }
159 
areCriteriaEquivalent(ArrayList<AudioMixMatchCriterion> cr1, ArrayList<AudioMixMatchCriterion> cr2)160     private static boolean areCriteriaEquivalent(ArrayList<AudioMixMatchCriterion> cr1,
161             ArrayList<AudioMixMatchCriterion> cr2) {
162         if (cr1 == null || cr2 == null) return false;
163         if (cr1 == cr2) return true;
164         if (cr1.size() != cr2.size()) return false;
165         //TODO iterate over rules to check they contain the same criterion
166         return (cr1.hashCode() == cr2.hashCode());
167     }
168 
169     private final int mTargetMixType;
getTargetMixType()170     int getTargetMixType() { return mTargetMixType; }
171     @UnsupportedAppUsage
172     private final ArrayList<AudioMixMatchCriterion> mCriteria;
173     /** @hide */
getCriteria()174     public ArrayList<AudioMixMatchCriterion> getCriteria() { return mCriteria; }
175     @UnsupportedAppUsage
176     private boolean mAllowPrivilegedPlaybackCapture = false;
177 
178     /** @hide */
allowPrivilegedPlaybackCapture()179     public boolean allowPrivilegedPlaybackCapture() {
180         return mAllowPrivilegedPlaybackCapture;
181     }
182 
183     /** @hide */
184     @Override
equals(Object o)185     public boolean equals(Object o) {
186         if (this == o) return true;
187         if (o == null || getClass() != o.getClass()) return false;
188 
189         final AudioMixingRule that = (AudioMixingRule) o;
190         return (this.mTargetMixType == that.mTargetMixType)
191                 && (areCriteriaEquivalent(this.mCriteria, that.mCriteria)
192                 && this.mAllowPrivilegedPlaybackCapture == that.mAllowPrivilegedPlaybackCapture);
193     }
194 
195     @Override
hashCode()196     public int hashCode() {
197         return Objects.hash(mTargetMixType, mCriteria, mAllowPrivilegedPlaybackCapture);
198     }
199 
isValidSystemApiRule(int rule)200     private static boolean isValidSystemApiRule(int rule) {
201         // API rules only expose the RULE_MATCH_* rules
202         switch (rule) {
203             case RULE_MATCH_ATTRIBUTE_USAGE:
204             case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
205             case RULE_MATCH_UID:
206                 return true;
207             default:
208                 return false;
209         }
210     }
isValidAttributesSystemApiRule(int rule)211     private static boolean isValidAttributesSystemApiRule(int rule) {
212         // API rules only expose the RULE_MATCH_* rules
213         switch (rule) {
214             case RULE_MATCH_ATTRIBUTE_USAGE:
215             case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
216                 return true;
217             default:
218                 return false;
219         }
220     }
221 
isValidRule(int rule)222     private static boolean isValidRule(int rule) {
223         final int match_rule = rule & ~RULE_EXCLUSION_MASK;
224         switch (match_rule) {
225             case RULE_MATCH_ATTRIBUTE_USAGE:
226             case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
227             case RULE_MATCH_UID:
228                 return true;
229             default:
230                 return false;
231         }
232     }
233 
isPlayerRule(int rule)234     private static boolean isPlayerRule(int rule) {
235         final int match_rule = rule & ~RULE_EXCLUSION_MASK;
236         switch (match_rule) {
237         case RULE_MATCH_ATTRIBUTE_USAGE:
238         case RULE_MATCH_UID:
239             return true;
240         default:
241             return false;
242         }
243     }
244 
isAudioAttributeRule(int match_rule)245     private static boolean isAudioAttributeRule(int match_rule) {
246         switch(match_rule) {
247             case RULE_MATCH_ATTRIBUTE_USAGE:
248             case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
249                 return true;
250             default:
251                 return false;
252         }
253     }
254 
255     /**
256      * Builder class for {@link AudioMixingRule} objects
257      */
258     public static class Builder {
259         private ArrayList<AudioMixMatchCriterion> mCriteria;
260         private int mTargetMixType = AudioMix.MIX_TYPE_INVALID;
261         private boolean mAllowPrivilegedPlaybackCapture = false;
262 
263         /**
264          * Constructs a new Builder with no rules.
265          */
Builder()266         public Builder() {
267             mCriteria = new ArrayList<AudioMixMatchCriterion>();
268         }
269 
270         /**
271          * Add a rule for the selection of which streams are mixed together.
272          * @param attrToMatch a non-null AudioAttributes instance for which a contradictory
273          *     rule hasn't been set yet.
274          * @param rule {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE} or
275          *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET}.
276          * @return the same Builder instance.
277          * @throws IllegalArgumentException
278          * @see #excludeRule(AudioAttributes, int)
279          */
addRule(AudioAttributes attrToMatch, int rule)280         public Builder addRule(AudioAttributes attrToMatch, int rule)
281                 throws IllegalArgumentException {
282             if (!isValidAttributesSystemApiRule(rule)) {
283                 throw new IllegalArgumentException("Illegal rule value " + rule);
284             }
285             return checkAddRuleObjInternal(rule, attrToMatch);
286         }
287 
288         /**
289          * Add a rule by exclusion for the selection of which streams are mixed together.
290          * <br>For instance the following code
291          * <br><pre>
292          * AudioAttributes mediaAttr = new AudioAttributes.Builder()
293          *         .setUsage(AudioAttributes.USAGE_MEDIA)
294          *         .build();
295          * AudioMixingRule noMediaRule = new AudioMixingRule.Builder()
296          *         .excludeRule(mediaAttr, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE)
297          *         .build();
298          * </pre>
299          * <br>will create a rule which maps to any usage value, except USAGE_MEDIA.
300          * @param attrToMatch a non-null AudioAttributes instance for which a contradictory
301          *     rule hasn't been set yet.
302          * @param rule {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE} or
303          *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET}.
304          * @return the same Builder instance.
305          * @throws IllegalArgumentException
306          * @see #addRule(AudioAttributes, int)
307          */
excludeRule(AudioAttributes attrToMatch, int rule)308         public Builder excludeRule(AudioAttributes attrToMatch, int rule)
309                 throws IllegalArgumentException {
310             if (!isValidAttributesSystemApiRule(rule)) {
311                 throw new IllegalArgumentException("Illegal rule value " + rule);
312             }
313             return checkAddRuleObjInternal(rule | RULE_EXCLUSION_MASK, attrToMatch);
314         }
315 
316         /**
317          * Add a rule for the selection of which streams are mixed together.
318          * The rule defines what the matching will be made on. It also determines the type of the
319          * property to match against.
320          * @param rule one of {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
321          *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or
322          *     {@link AudioMixingRule#RULE_MATCH_UID}.
323          * @param property see the definition of each rule for the type to use (either an
324          *     {@link AudioAttributes} or an {@link java.lang.Integer}).
325          * @return the same Builder instance.
326          * @throws IllegalArgumentException
327          * @see #excludeMixRule(int, Object)
328          */
addMixRule(int rule, Object property)329         public Builder addMixRule(int rule, Object property) throws IllegalArgumentException {
330             if (!isValidSystemApiRule(rule)) {
331                 throw new IllegalArgumentException("Illegal rule value " + rule);
332             }
333             return checkAddRuleObjInternal(rule, property);
334         }
335 
336         /**
337          * Add a rule by exclusion for the selection of which streams are mixed together.
338          * <br>For instance the following code
339          * <br><pre>
340          * AudioAttributes mediaAttr = new AudioAttributes.Builder()
341          *         .setUsage(AudioAttributes.USAGE_MEDIA)
342          *         .build();
343          * AudioMixingRule noMediaRule = new AudioMixingRule.Builder()
344          *         .addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, mediaAttr)
345          *         .excludeMixRule(AudioMixingRule.RULE_MATCH_UID, new Integer(uidToExclude)
346          *         .build();
347          * </pre>
348          * <br>will create a rule which maps to usage USAGE_MEDIA, but excludes any stream
349          * coming from the specified UID.
350          * @param rule one of {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
351          *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or
352          *     {@link AudioMixingRule#RULE_MATCH_UID}.
353          * @param property see the definition of each rule for the type to use (either an
354          *     {@link AudioAttributes} or an {@link java.lang.Integer}).
355          * @return the same Builder instance.
356          * @throws IllegalArgumentException
357          */
excludeMixRule(int rule, Object property)358         public Builder excludeMixRule(int rule, Object property) throws IllegalArgumentException {
359             if (!isValidSystemApiRule(rule)) {
360                 throw new IllegalArgumentException("Illegal rule value " + rule);
361             }
362             return checkAddRuleObjInternal(rule | RULE_EXCLUSION_MASK, property);
363         }
364 
365         /**
366          * Set if the audio of app that opted out of audio playback capture should be captured.
367          *
368          * Caller of this method with <code>true</code>, MUST abide to the restriction listed in
369          * {@link ALLOW_CAPTURE_BY_SYSTEM}, including but not limited to the captured audio
370          * can not leave the capturing app, and the quality is limited to 16k mono.
371          *
372          * The permission {@link CAPTURE_AUDIO_OUTPUT} or {@link CAPTURE_MEDIA_OUTPUT} is needed
373          * to ignore the opt-out.
374          *
375          * Only affects LOOPBACK|RENDER mix.
376          *
377          * @return the same Builder instance.
378          */
allowPrivilegedPlaybackCapture(boolean allow)379         public @NonNull Builder allowPrivilegedPlaybackCapture(boolean allow) {
380             mAllowPrivilegedPlaybackCapture = allow;
381             return this;
382         }
383 
384         /**
385          * Add or exclude a rule for the selection of which streams are mixed together.
386          * Does error checking on the parameters.
387          * @param rule
388          * @param property
389          * @return the same Builder instance.
390          * @throws IllegalArgumentException
391          */
checkAddRuleObjInternal(int rule, Object property)392         private Builder checkAddRuleObjInternal(int rule, Object property)
393                 throws IllegalArgumentException {
394             if (property == null) {
395                 throw new IllegalArgumentException("Illegal null argument for mixing rule");
396             }
397             if (!isValidRule(rule)) {
398                 throw new IllegalArgumentException("Illegal rule value " + rule);
399             }
400             final int match_rule = rule & ~RULE_EXCLUSION_MASK;
401             if (isAudioAttributeRule(match_rule)) {
402                 if (!(property instanceof AudioAttributes)) {
403                     throw new IllegalArgumentException("Invalid AudioAttributes argument");
404                 }
405                 return addRuleInternal((AudioAttributes) property, null, rule);
406             } else {
407                 // implies integer match rule
408                 if (!(property instanceof Integer)) {
409                     throw new IllegalArgumentException("Invalid Integer argument");
410                 }
411                 return addRuleInternal(null, (Integer) property, rule);
412             }
413         }
414 
415         /**
416          * Add or exclude a rule on AudioAttributes or integer property for the selection of which
417          * streams are mixed together.
418          * No rule-to-parameter type check, all done in {@link #checkAddRuleObjInternal(int, Object)}.
419          * Exceptions are thrown only when incompatible rules are added.
420          * @param attrToMatch a non-null AudioAttributes instance for which a contradictory
421          *     rule hasn't been set yet, null if not used.
422          * @param intProp an integer property to match or exclude, null if not used.
423          * @param rule one of {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_USAGE},
424          *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
425          *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or
426          *     {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET},
427          *     {@link AudioMixingRule#RULE_MATCH_UID}, {@link AudioMixingRule#RULE_EXCLUDE_UID}.
428          * @return the same Builder instance.
429          * @throws IllegalArgumentException
430          */
addRuleInternal(AudioAttributes attrToMatch, Integer intProp, int rule)431         private Builder addRuleInternal(AudioAttributes attrToMatch, Integer intProp, int rule)
432                 throws IllegalArgumentException {
433             // as rules are added to the Builder, we verify they are consistent with the type
434             // of mix being built. When adding the first rule, the mix type is MIX_TYPE_INVALID.
435             if (mTargetMixType == AudioMix.MIX_TYPE_INVALID) {
436                 if (isPlayerRule(rule)) {
437                     mTargetMixType = AudioMix.MIX_TYPE_PLAYERS;
438                 } else {
439                     mTargetMixType = AudioMix.MIX_TYPE_RECORDERS;
440                 }
441             } else if (((mTargetMixType == AudioMix.MIX_TYPE_PLAYERS) && !isPlayerRule(rule))
442                     || ((mTargetMixType == AudioMix.MIX_TYPE_RECORDERS) && isPlayerRule(rule)))
443             {
444                 throw new IllegalArgumentException("Incompatible rule for mix");
445             }
446             synchronized (mCriteria) {
447                 Iterator<AudioMixMatchCriterion> crIterator = mCriteria.iterator();
448                 final int match_rule = rule & ~RULE_EXCLUSION_MASK;
449                 while (crIterator.hasNext()) {
450                     final AudioMixMatchCriterion criterion = crIterator.next();
451 
452                     if ((criterion.mRule & ~RULE_EXCLUSION_MASK) != match_rule) {
453                         continue; // The two rules are not of the same type
454                     }
455                     switch (match_rule) {
456                         case RULE_MATCH_ATTRIBUTE_USAGE:
457                             // "usage"-based rule
458                             if (criterion.mAttr.getUsage() == attrToMatch.getUsage()) {
459                                 if (criterion.mRule == rule) {
460                                     // rule already exists, we're done
461                                     return this;
462                                 } else {
463                                     // criterion already exists with a another rule,
464                                     // it is incompatible
465                                     throw new IllegalArgumentException("Contradictory rule exists"
466                                             + " for " + attrToMatch);
467                                 }
468                             }
469                             break;
470                         case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
471                             // "capture preset"-base rule
472                             if (criterion.mAttr.getCapturePreset() == attrToMatch.getCapturePreset()) {
473                                 if (criterion.mRule == rule) {
474                                     // rule already exists, we're done
475                                     return this;
476                                 } else {
477                                     // criterion already exists with a another rule,
478                                     // it is incompatible
479                                     throw new IllegalArgumentException("Contradictory rule exists"
480                                             + " for " + attrToMatch);
481                                 }
482                             }
483                             break;
484                         case RULE_MATCH_UID:
485                             // "usage"-based rule
486                             if (criterion.mIntProp == intProp.intValue()) {
487                                 if (criterion.mRule == rule) {
488                                     // rule already exists, we're done
489                                     return this;
490                                 } else {
491                                     // criterion already exists with a another rule,
492                                     // it is incompatible
493                                     throw new IllegalArgumentException("Contradictory rule exists"
494                                             + " for UID " + intProp);
495                                 }
496                             }
497                             break;
498                     }
499                 }
500                 // rule didn't exist, add it
501                 switch (match_rule) {
502                     case RULE_MATCH_ATTRIBUTE_USAGE:
503                     case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
504                         mCriteria.add(new AudioMixMatchCriterion(attrToMatch, rule));
505                         break;
506                     case RULE_MATCH_UID:
507                         mCriteria.add(new AudioMixMatchCriterion(intProp, rule));
508                         break;
509                     default:
510                         throw new IllegalStateException("Unreachable code in addRuleInternal()");
511                 }
512             }
513             return this;
514         }
515 
addRuleFromParcel(Parcel in)516         Builder addRuleFromParcel(Parcel in) throws IllegalArgumentException {
517             final int rule = in.readInt();
518             final int match_rule = rule & ~RULE_EXCLUSION_MASK;
519             AudioAttributes attr = null;
520             Integer intProp = null;
521             switch (match_rule) {
522                 case RULE_MATCH_ATTRIBUTE_USAGE:
523                     int usage = in.readInt();
524                     attr = new AudioAttributes.Builder()
525                             .setUsage(usage).build();
526                     break;
527                 case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
528                     int preset = in.readInt();
529                     attr = new AudioAttributes.Builder()
530                             .setInternalCapturePreset(preset).build();
531                     break;
532                 case RULE_MATCH_UID:
533                     intProp = new Integer(in.readInt());
534                     break;
535                 default:
536                     // assume there was in int value to read as for now they come in pair
537                     in.readInt();
538                     throw new IllegalArgumentException("Illegal rule value " + rule + " in parcel");
539             }
540             return addRuleInternal(attr, intProp, rule);
541         }
542 
543         /**
544          * Combines all of the matching and exclusion rules that have been set and return a new
545          * {@link AudioMixingRule} object.
546          * @return a new {@link AudioMixingRule} object
547          */
build()548         public AudioMixingRule build() {
549             return new AudioMixingRule(mTargetMixType, mCriteria, mAllowPrivilegedPlaybackCapture);
550         }
551     }
552 }
553