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