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
17package android.media.audiopolicy;
18
19import android.annotation.SystemApi;
20import android.media.AudioAttributes;
21import android.os.Parcel;
22
23import java.util.ArrayList;
24import java.util.Iterator;
25import java.util.Objects;
26
27
28/**
29 * @hide
30 *
31 * Here's an example of creating a mixing rule for all media playback:
32 * <pre>
33 * AudioAttributes mediaAttr = new AudioAttributes.Builder()
34 *         .setUsage(AudioAttributes.USAGE_MEDIA)
35 *         .build();
36 * AudioMixingRule mediaRule = new AudioMixingRule.Builder()
37 *         .addRule(mediaAttr, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE)
38 *         .build();
39 * </pre>
40 */
41@SystemApi
42public class AudioMixingRule {
43
44    private AudioMixingRule(int mixType, ArrayList<AttributeMatchCriterion> criteria) {
45        mCriteria = criteria;
46        mTargetMixType = mixType;
47    }
48
49    /**
50     * A rule requiring the usage information of the {@link AudioAttributes} to match.
51     */
52    @SystemApi
53    public static final int RULE_MATCH_ATTRIBUTE_USAGE = 0x1;
54    /**
55     * A rule requiring the capture preset information of the {@link AudioAttributes} to match.
56     */
57    @SystemApi
58    public static final int RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET = 0x1 << 1;
59
60    private final static int RULE_EXCLUSION_MASK = 0x8000;
61    /**
62     * @hide
63     * A rule requiring the usage information of the {@link AudioAttributes} to differ.
64     */
65    public static final int RULE_EXCLUDE_ATTRIBUTE_USAGE =
66            RULE_EXCLUSION_MASK | RULE_MATCH_ATTRIBUTE_USAGE;
67    /**
68     * @hide
69     * A rule requiring the capture preset information of the {@link AudioAttributes} to differ.
70     */
71    public static final int RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET =
72            RULE_EXCLUSION_MASK | RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET;
73
74    static final class AttributeMatchCriterion {
75        AudioAttributes mAttr;
76        int mRule;
77
78        /** input parameters must be valid */
79        AttributeMatchCriterion(AudioAttributes attributes, int rule) {
80            mAttr = attributes;
81            mRule = rule;
82        }
83
84        @Override
85        public int hashCode() {
86            return Objects.hash(mAttr, mRule);
87        }
88
89        void writeToParcel(Parcel dest) {
90            dest.writeInt(mRule);
91            if ((mRule == RULE_MATCH_ATTRIBUTE_USAGE) || (mRule == RULE_EXCLUDE_ATTRIBUTE_USAGE)) {
92                dest.writeInt(mAttr.getUsage());
93            } else {
94                // capture preset rule
95                dest.writeInt(mAttr.getCapturePreset());
96            }
97        }
98    }
99
100    private final int mTargetMixType;
101    int getTargetMixType() { return mTargetMixType; }
102    private final ArrayList<AttributeMatchCriterion> mCriteria;
103    ArrayList<AttributeMatchCriterion> getCriteria() { return mCriteria; }
104
105    @Override
106    public int hashCode() {
107        return Objects.hash(mTargetMixType, mCriteria);
108    }
109
110    private static boolean isValidSystemApiRule(int rule) {
111        switch(rule) {
112            case RULE_MATCH_ATTRIBUTE_USAGE:
113            case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
114                return true;
115            default:
116                return false;
117        }
118    }
119
120    private static boolean isValidIntRule(int rule) {
121        switch(rule) {
122            case RULE_MATCH_ATTRIBUTE_USAGE:
123            case RULE_EXCLUDE_ATTRIBUTE_USAGE:
124            case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET:
125            case RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET:
126                return true;
127            default:
128                return false;
129        }
130    }
131
132    private static boolean isPlayerRule(int rule) {
133        return ((rule == RULE_MATCH_ATTRIBUTE_USAGE)
134                || (rule == RULE_EXCLUDE_ATTRIBUTE_USAGE));
135    }
136
137    /**
138     * Builder class for {@link AudioMixingRule} objects
139     */
140    @SystemApi
141    public static class Builder {
142        private ArrayList<AttributeMatchCriterion> mCriteria;
143        private int mTargetMixType = AudioMix.MIX_TYPE_INVALID;
144
145        /**
146         * Constructs a new Builder with no rules.
147         */
148        @SystemApi
149        public Builder() {
150            mCriteria = new ArrayList<AttributeMatchCriterion>();
151        }
152
153        /**
154         * Add a rule for the selection of which streams are mixed together.
155         * @param attrToMatch a non-null AudioAttributes instance for which a contradictory
156         *     rule hasn't been set yet.
157         * @param rule {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE} or
158         *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET}.
159         * @return the same Builder instance.
160         * @throws IllegalArgumentException
161         */
162        @SystemApi
163        public Builder addRule(AudioAttributes attrToMatch, int rule)
164                throws IllegalArgumentException {
165            if (!isValidSystemApiRule(rule)) {
166                throw new IllegalArgumentException("Illegal rule value " + rule);
167            }
168            return addRuleInt(attrToMatch, rule);
169        }
170
171        /**
172         * Add a rule by exclusion for the selection of which streams are mixed together.
173         * <br>For instance the following code
174         * <br><pre>
175         * AudioAttributes mediaAttr = new AudioAttributes.Builder()
176         *         .setUsage(AudioAttributes.USAGE_MEDIA)
177         *         .build();
178         * AudioMixingRule noMediaRule = new AudioMixingRule.Builder()
179         *         .excludeRule(mediaAttr, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE)
180         *         .build();
181         * </pre>
182         * <br>will create a rule which maps to any usage value, except USAGE_MEDIA.
183         * @param attrToMatch a non-null AudioAttributes instance for which a contradictory
184         *     rule hasn't been set yet.
185         * @param rule {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE} or
186         *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET}.
187         * @return the same Builder instance.
188         * @throws IllegalArgumentException
189         */
190        @SystemApi
191        public Builder excludeRule(AudioAttributes attrToMatch, int rule)
192                throws IllegalArgumentException {
193            if (!isValidSystemApiRule(rule)) {
194                throw new IllegalArgumentException("Illegal rule value " + rule);
195            }
196            return addRuleInt(attrToMatch, rule | RULE_EXCLUSION_MASK);
197        }
198
199        /**
200         * Add or exclude a rule for the selection of which streams are mixed together.
201         * @param attrToMatch a non-null AudioAttributes instance for which a contradictory
202         *     rule hasn't been set yet.
203         * @param rule one of {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_USAGE},
204         *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
205         *     {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or
206         *     {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET}.
207         * @return the same Builder instance.
208         * @throws IllegalArgumentException
209         */
210        Builder addRuleInt(AudioAttributes attrToMatch, int rule)
211                throws IllegalArgumentException {
212            if (attrToMatch == null) {
213                throw new IllegalArgumentException("Illegal null AudioAttributes argument");
214            }
215            if (!isValidIntRule(rule)) {
216                throw new IllegalArgumentException("Illegal rule value " + rule);
217            } else {
218                // as rules are added to the Builder, we verify they are consistent with the type
219                // of mix being built. When adding the first rule, the mix type is MIX_TYPE_INVALID.
220                if (mTargetMixType == AudioMix.MIX_TYPE_INVALID) {
221                    if (isPlayerRule(rule)) {
222                        mTargetMixType = AudioMix.MIX_TYPE_PLAYERS;
223                    } else {
224                        mTargetMixType = AudioMix.MIX_TYPE_RECORDERS;
225                    }
226                } else if (((mTargetMixType == AudioMix.MIX_TYPE_PLAYERS) && !isPlayerRule(rule))
227                        || ((mTargetMixType == AudioMix.MIX_TYPE_RECORDERS) && isPlayerRule(rule)))
228                {
229                    throw new IllegalArgumentException("Incompatible rule for mix");
230                }
231            }
232            synchronized (mCriteria) {
233                Iterator<AttributeMatchCriterion> crIterator = mCriteria.iterator();
234                while (crIterator.hasNext()) {
235                    final AttributeMatchCriterion criterion = crIterator.next();
236                    if ((rule == RULE_MATCH_ATTRIBUTE_USAGE)
237                            || (rule == RULE_EXCLUDE_ATTRIBUTE_USAGE)) {
238                        // "usage"-based rule
239                        if (criterion.mAttr.getUsage() == attrToMatch.getUsage()) {
240                            if (criterion.mRule == rule) {
241                                // rule already exists, we're done
242                                return this;
243                            } else {
244                                // criterion already exists with a another rule, it is incompatible
245                                throw new IllegalArgumentException("Contradictory rule exists for "
246                                        + attrToMatch);
247                            }
248                        }
249                    } else if ((rule == RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET)
250                            || (rule == RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET)) {
251                        // "capture preset"-base rule
252                        if (criterion.mAttr.getCapturePreset() == attrToMatch.getCapturePreset()) {
253                            if (criterion.mRule == rule) {
254                             // rule already exists, we're done
255                                return this;
256                            } else {
257                                // criterion already exists with a another rule, it is incompatible
258                                throw new IllegalArgumentException("Contradictory rule exists for "
259                                        + attrToMatch);
260                            }
261                        }
262                    }
263                }
264                // rule didn't exist, add it
265                mCriteria.add(new AttributeMatchCriterion(attrToMatch, rule));
266            }
267            return this;
268        }
269
270        Builder addRuleFromParcel(Parcel in) throws IllegalArgumentException {
271            int rule = in.readInt();
272            AudioAttributes attr;
273            if ((rule == RULE_MATCH_ATTRIBUTE_USAGE) || (rule == RULE_EXCLUDE_ATTRIBUTE_USAGE)) {
274                int usage = in.readInt();
275                attr = new AudioAttributes.Builder()
276                        .setUsage(usage).build();
277            } else if ((rule == RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET)
278                    || (rule == RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET)) {
279                int preset = in.readInt();
280                attr = new AudioAttributes.Builder()
281                        .setInternalCapturePreset(preset).build();
282            } else {
283                in.readInt(); // assume there was in int value to read as for now they come in pair
284                throw new IllegalArgumentException("Illegal rule value " + rule + " in parcel");
285            }
286            return addRuleInt(attr, rule);
287        }
288
289        /**
290         * Combines all of the matching and exclusion rules that have been set and return a new
291         * {@link AudioMixingRule} object.
292         * @return a new {@link AudioMixingRule} object
293         */
294        public AudioMixingRule build() {
295            return new AudioMixingRule(mTargetMixType, mCriteria);
296        }
297    }
298}
299