• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.car.oem.utils;
18 
19 import static com.android.car.oem.focus.FocusInteraction.INTERACTION_CONCURRENT;
20 import static com.android.car.oem.focus.FocusInteraction.INTERACTION_EXCLUSIVE;
21 import static com.android.car.oem.focus.FocusInteraction.INTERACTION_REJECT;
22 import static com.android.car.oem.utils.AudioUtils.getAudioAttributeFromUsage;
23 
24 import android.annotation.Nullable;
25 import android.media.AudioAttributes;
26 import android.util.ArrayMap;
27 import android.util.ArraySet;
28 import android.util.Slog;
29 import android.util.Xml;
30 
31 import com.android.internal.util.Preconditions;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.Objects;
42 
43 public final class OemCarServiceHelper {
44     private static final String TAG = OemCarServiceHelper.class.getSimpleName();
45 
46     // Per documentation if namespace is disabled, it must be null
47     private static final String NO_NAMESPACE = null;
48     private static final String NO_TAG = null;
49     private static final String TAG_ROOT_VOLUME = "oemAudioVolumeConfiguration";
50     private static final String TAG_ROOT_DUCKING = "oemAudioDuckingConfigurations";
51     private static final String TAG_ROOT_FOCUS = "oemAudioFocusConfigurations";
52     private static final String TAG_ATTRIBUTE = "attribute";
53     private static final String TAG_DUCK = "duck";
54     private static final String TAG_EXCLUSIVE = "exclusive";
55     private static final String TAG_REJECT = "reject";
56     private static final String TAG_CONCURRENT = "concurrent";
57     private static final String TAG_VOLUME_PRIORITY = "volumePriorities";
58     private static final String TAG_DUCK_INTERACTIONS = "duckingInteractions";
59     private static final String TAG_DUCK_INTERACTION = "duckingInteraction";
60     private static final String TAG_FOCUS_INTERACTIONS = "focusInteractions";
61     private static final String TAG_FOCUS_INTERACTION = "focusInteraction";
62     private static final String ATTR_USAGE = "usage";
63     private static final int DEPRECATED_AUDIO_ATTRIBUTES = 3;
64     private static final int SYSTEM_AUDIO_ATTRIBUTES = 4;
65     // Size of suppressible usages minus deprecated suppressible usages plus system usages
66     private static final int TOTAL_AUDIO_ATTRIBUTES =
67             AudioAttributes.SUPPRESSIBLE_USAGES.size() - DEPRECATED_AUDIO_ATTRIBUTES
68                     + SYSTEM_AUDIO_ATTRIBUTES;
69 
70     private @Nullable List<AudioAttributes> mVolumePriorities;
71     // A mapping from AudioAttributes to other AudioAttributes that is either reject, exclusive,
72     // or concurrent with values -1, 1, and 2 respectively
73     private @Nullable ArrayMap<AudioAttributes, ArrayMap<AudioAttributes, Integer>>
74             mCurrentHolderToIncomingFocusInteractions;
75     private @Nullable ArrayMap<AudioAttributes, List<AudioAttributes>> mDuckingInteractions;
76 
77     /**
78      * Parses the given inputStream and puts the priority list/interaction mapping into their
79      * respective audio management interaction list/mappings.
80      *
81      * @param inputStream The inputstream to be parsed.
82      * @throws XmlPullParserException Exception to be thrown if formatting is incorrect
83      * @throws IOException Exception to be thrown if file does not exist
84      */
parseAudioManagementConfiguration(InputStream inputStream)85     public void parseAudioManagementConfiguration(InputStream inputStream)
86             throws XmlPullParserException, IOException {
87         XmlPullParser parser = Xml.newPullParser();
88         parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, NO_NAMESPACE != null);
89         parser.setInput(inputStream, /* inputEncoding= */ null);
90         // When setInput() is used with a XmlPullParser, the parser is set to the
91         // initial value of START_DOCUMENT.
92         parser.next();
93         // Per XmlPullParser documentation, null will match with any namespace and any name
94         parser.require(XmlPullParser.START_TAG, NO_NAMESPACE, NO_TAG);
95         String parserName = parser.getName();
96         if (parserName == null || (!parserName.equals(TAG_ROOT_VOLUME)
97                 && !parserName.equals(TAG_ROOT_DUCKING) && !parserName.equals(TAG_ROOT_FOCUS))) {
98             throw new XmlPullParserException("expected " + TAG_ROOT_VOLUME + " or "
99                 + TAG_ROOT_DUCKING + " or " + TAG_ROOT_FOCUS);
100         }
101 
102         while (parser.next() != XmlPullParser.END_TAG) {
103             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
104             String currentTag = parser.getName();
105             switch (currentTag) {
106                 case TAG_VOLUME_PRIORITY:
107                     mVolumePriorities = parseVolumePriority(parser);
108                     continue;
109                 case TAG_DUCK_INTERACTIONS:
110                     parseDuckingInteractions(parser);
111                     continue;
112                 case TAG_FOCUS_INTERACTIONS:
113                     parseFocusInteractions(parser);
114                     continue;
115                 default:
116                     Slog.w(TAG, "Could not match given tag");
117                     skip(parser);
118             }
119         }
120 
121         if (mDuckingInteractions != null && evaluateLoops(mDuckingInteractions)) {
122             throw new IllegalStateException("Ducking interactions contain loops");
123         }
124 
125         //TODO (b/266977493): Delete these logs when b/266977442 is fixed, strictly for debugging
126         if (mVolumePriorities != null) {
127             Slog.i(TAG, "Volume priority list is " + mVolumePriorities);
128         }
129         if (mDuckingInteractions != null) {
130             Slog.i(TAG, "Ducking interactions is: " + mDuckingInteractions);
131         }
132         if (mCurrentHolderToIncomingFocusInteractions != null) {
133             Slog.i(TAG, "focus interactions is: " + mCurrentHolderToIncomingFocusInteractions);
134             for (int i = 0; i < mCurrentHolderToIncomingFocusInteractions.size(); i++) {
135                 Slog.i(TAG, "Current focus holder is: "
136                         + mCurrentHolderToIncomingFocusInteractions.keyAt(i).usageToString());
137                 ArrayMap<AudioAttributes, Integer> focusInteraction =
138                         mCurrentHolderToIncomingFocusInteractions.valueAt(i);
139                 for (int j = 0; j < focusInteraction.size(); j++) {
140                     AudioAttributes attr = focusInteraction.keyAt(j);
141                     Slog.i(TAG, "\t focus interaction for: "
142                             + attr.usageToString() + " is: " + focusInteraction.get(attr));
143                 }
144             }
145         }
146     }
147 
148     /**
149      * Gets the volume priority list.
150      *
151      * @return The volume priority list from highest priority to lowest priority if it exists. If
152      * it was not set in the xml, returns null.
153      */
getVolumePriorityList()154     public @Nullable List<AudioAttributes> getVolumePriorityList() {
155         return mVolumePriorities;
156     }
157 
158     /**
159      * Gets the current focus holder to incoming focus holder mapping.
160      *
161      * @return The focus interaction mapping if it exists. If it was not set in the xml, returns
162      * null.
163      */
164     public @Nullable ArrayMap<AudioAttributes, ArrayMap<AudioAttributes, Integer>>
getCurrentFocusToIncomingFocusInteractions()165             getCurrentFocusToIncomingFocusInteractions() {
166         return mCurrentHolderToIncomingFocusInteractions;
167     }
168 
169     /**
170      * Gets the ducking interaction mapping.
171      *
172      * @return The ducking interaction mapping if it exists. If it was not set in the xml, returns
173      * null.
174      */
getDuckingInteractions()175     public @Nullable ArrayMap<AudioAttributes, List<AudioAttributes>> getDuckingInteractions() {
176         return mDuckingInteractions;
177     }
178 
parseFocusInteractions(XmlPullParser parser)179     private void parseFocusInteractions(XmlPullParser parser)
180             throws XmlPullParserException, IOException {
181         mCurrentHolderToIncomingFocusInteractions = new ArrayMap<>();
182         while (parser.next() != XmlPullParser.END_TAG) {
183             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
184             if (TAG_FOCUS_INTERACTION.equals(parser.getName())) {
185                 parseFocusInteraction(parser, mCurrentHolderToIncomingFocusInteractions);
186             } else {
187                 skip(parser);
188             }
189         }
190     }
191 
parseFocusInteraction(XmlPullParser parser, ArrayMap<AudioAttributes, ArrayMap<AudioAttributes, Integer>> currentHolderToIncomingFocusInteractions)192     private void parseFocusInteraction(XmlPullParser parser, ArrayMap<AudioAttributes,
193             ArrayMap<AudioAttributes, Integer>> currentHolderToIncomingFocusInteractions)
194             throws XmlPullParserException, IOException {
195         while (parser.next() != XmlPullParser.END_TAG) {
196             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
197             if (TAG_ATTRIBUTE.equals(parser.getName())) {
198                 AudioAttributes currentAttribute = getAudioAttributeFromString(
199                         parser.getAttributeValue(NO_NAMESPACE, ATTR_USAGE));
200                 // skip the focus holder since it's already been read
201                 parser.next();
202                 parseExclusiveOrRejectFocusAttributes(parser,
203                         currentHolderToIncomingFocusInteractions, currentAttribute);
204                 // There is no end tag for reading attribute
205                 return;
206             } else {
207                 skip(parser);
208             }
209         }
210     }
211 
parseExclusiveOrRejectFocusAttributes(XmlPullParser parser, ArrayMap<AudioAttributes, ArrayMap<AudioAttributes, Integer>> currentHolderToIncomingFocusInteractions, AudioAttributes focusHolder)212     private void parseExclusiveOrRejectFocusAttributes(XmlPullParser parser,
213             ArrayMap<AudioAttributes, ArrayMap<AudioAttributes, Integer>>
214                     currentHolderToIncomingFocusInteractions, AudioAttributes focusHolder)
215             throws XmlPullParserException, IOException {
216         List<AudioAttributes> exclusive = new ArrayList<>();
217         List<AudioAttributes> rejected = new ArrayList<>();
218         List<AudioAttributes> concurrent = new ArrayList<>();
219         while (parser.next() != XmlPullParser.END_TAG) {
220             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
221             String priorityType = parser.getName();
222             switch(priorityType) {
223                 case TAG_EXCLUSIVE:
224                     exclusive.addAll(parseAttributes(parser));
225                     continue;
226                 case TAG_REJECT:
227                     rejected.addAll(parseAttributes(parser));
228                     continue;
229                 case TAG_CONCURRENT:
230                     concurrent.addAll(parseAttributes(parser));
231                     continue;
232                 default:
233                     skip(parser);
234             }
235         }
236         // verify that exclusive, rejected and concurrent are disjoint
237         Preconditions.checkArgument(Collections.disjoint(exclusive, rejected));
238         Preconditions.checkArgument(Collections.disjoint(exclusive, concurrent));
239         Preconditions.checkArgument(Collections.disjoint(rejected, concurrent));
240         ArrayMap<AudioAttributes, Integer> incomingFocusInteractions = new ArrayMap<>();
241         listToMappingWithValue(exclusive, INTERACTION_EXCLUSIVE, incomingFocusInteractions);
242         listToMappingWithValue(rejected, INTERACTION_REJECT, incomingFocusInteractions);
243         listToMappingWithValue(concurrent, INTERACTION_CONCURRENT, incomingFocusInteractions);
244         Preconditions.checkArgument(incomingFocusInteractions.size() == TOTAL_AUDIO_ATTRIBUTES);
245         currentHolderToIncomingFocusInteractions.put(focusHolder, incomingFocusInteractions);
246     }
247 
parseDuckingInteractions(XmlPullParser parser)248     private void parseDuckingInteractions(XmlPullParser parser)
249             throws XmlPullParserException, IOException {
250         mDuckingInteractions = new ArrayMap<>();
251         while (parser.next() != XmlPullParser.END_TAG) {
252             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
253             if (TAG_DUCK_INTERACTION.equals(parser.getName())) {
254                 parseDuckingInteraction(parser, mDuckingInteractions);
255             } else {
256                 skip(parser);
257             }
258         }
259     }
260 
parseDuckingInteraction(XmlPullParser parser, ArrayMap<AudioAttributes, List<AudioAttributes>> duckingInteractions)261     private void parseDuckingInteraction(XmlPullParser parser, ArrayMap<AudioAttributes,
262             List<AudioAttributes>> duckingInteractions) throws XmlPullParserException, IOException {
263         while (parser.next() != XmlPullParser.END_TAG) {
264             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
265             if (TAG_ATTRIBUTE.equals(parser.getName())) {
266                 String duckingHolder = parser.getAttributeValue(NO_NAMESPACE, ATTR_USAGE);
267                 // skip the ducking holder since it's already been read
268                 parser.next();
269                 Objects.requireNonNull(duckingHolder, "requires attribute to be present");
270                 parseToDuckAttributes(parser, duckingInteractions, duckingHolder);
271                 // There is no end tag for reading attribute
272                 return;
273             } else {
274                 skip(parser);
275             }
276         }
277     }
278 
parseVolumePriority(XmlPullParser parser)279     private List<AudioAttributes> parseVolumePriority(XmlPullParser parser)
280             throws XmlPullParserException, IOException {
281         return parseAttributes(parser);
282     }
283 
parseToDuckAttributes(XmlPullParser parser, ArrayMap<AudioAttributes, List<AudioAttributes>> duckingInteractions, String duckingHolder)284     private void parseToDuckAttributes(XmlPullParser parser, ArrayMap<AudioAttributes,
285             List<AudioAttributes>> duckingInteractions, String duckingHolder)
286             throws XmlPullParserException, IOException {
287         while (parser.next() != XmlPullParser.END_TAG) {
288             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
289             if (TAG_DUCK.equals(parser.getName())) {
290                 List<AudioAttributes> duckedAttributes = parseAttributes(parser);
291                 duckingInteractions.put(getAudioAttributeFromString(duckingHolder),
292                         duckedAttributes);
293             }
294         }
295     }
296 
parseAttributes(XmlPullParser parser)297     private List<AudioAttributes> parseAttributes(XmlPullParser parser)
298             throws XmlPullParserException, IOException {
299         List<AudioAttributes> priorityList = new ArrayList<>();
300         while (parser.next() != XmlPullParser.END_TAG) {
301             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
302             if (TAG_ATTRIBUTE.equals(parser.getName())) {
303                 String audioAttributeString = parser.getAttributeValue(NO_NAMESPACE, ATTR_USAGE);
304                 Objects.requireNonNull(audioAttributeString, "requires attribute to be present");
305                 priorityList.add(getAudioAttributeFromString(audioAttributeString));
306             }
307             skip(parser);
308         }
309         return priorityList;
310     }
311 
listToMappingWithValue(List<AudioAttributes> list, int value, ArrayMap<AudioAttributes, Integer> mapping)312     private void listToMappingWithValue(List<AudioAttributes> list, int value,
313             ArrayMap<AudioAttributes, Integer> mapping) {
314         for (int i = 0; i < list.size(); i++) {
315             mapping.put(list.get(i), value);
316         }
317     }
318 
getAudioAttributeFromString(String stringAudioAttribute)319     private AudioAttributes getAudioAttributeFromString(String stringAudioAttribute) {
320         if (Objects.equals(stringAudioAttribute, "AUDIO_USAGE_NOTIFICATION_EVENT")) {
321             return getAudioAttributeFromUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT);
322         }
323         return getAudioAttributeFromUsage(AudioAttributes.xsdStringToUsage(stringAudioAttribute));
324     }
325 
skip(XmlPullParser parser)326     private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
327         if (parser.getEventType() != XmlPullParser.START_TAG) {
328             throw new IllegalStateException();
329         }
330         int depth = 1;
331         while (depth != 0) {
332             switch (parser.next()) {
333                 case XmlPullParser.END_TAG:
334                     depth--;
335                     break;
336                 case XmlPullParser.START_TAG:
337                     depth++;
338                     break;
339             }
340         }
341     }
342 
evaluateLoops(ArrayMap<AudioAttributes, List<AudioAttributes>> mapping)343     private boolean evaluateLoops(ArrayMap<AudioAttributes, List<AudioAttributes>> mapping) {
344         ArraySet<AudioAttributes> seenAttributes = new ArraySet<>();
345         for (int i = 0; i < mapping.size(); i++) {
346             if (containLoops(mapping.keyAt(i), new ArraySet<>(), seenAttributes, mapping)) {
347                 return true;
348             }
349         }
350         return false;
351     }
352 
containLoops(AudioAttributes currentAttribute, ArraySet<AudioAttributes> currentlyVisited, ArraySet<AudioAttributes> seen, ArrayMap<AudioAttributes, List<AudioAttributes>> mapping)353     private boolean containLoops(AudioAttributes currentAttribute,
354             ArraySet<AudioAttributes> currentlyVisited, ArraySet<AudioAttributes> seen,
355             ArrayMap<AudioAttributes, List<AudioAttributes>> mapping) {
356         // contains loop
357         if (currentlyVisited.contains(currentAttribute)) {
358             return true;
359         }
360         if (seen.contains(currentAttribute)) {
361             return false;
362         }
363         List<AudioAttributes> nextAttributes = mapping.getOrDefault(currentAttribute, List.of());
364         currentlyVisited.add(currentAttribute);
365         seen.add(currentAttribute);
366         for (int i = 0; i < nextAttributes.size(); i++) {
367             if (containLoops(nextAttributes.get(i), currentlyVisited, seen, mapping)) {
368                 // contains loops
369                 return true;
370             }
371         }
372         // make sure to we remove what we're currently visiting to avoid issues such as A->B->C
373         // and A->C. If B and C does not get removed, then loop will be found.
374         currentlyVisited.remove(currentAttribute);
375         return false;
376     }
377 }
378