1 /* 2 * Copyright (C) 2018 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 package com.android.car.audio; 17 18 import static android.car.PlatformVersion.VERSION_CODES.UPSIDE_DOWN_CAKE_0; 19 import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE; 20 import static android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT; 21 22 import static com.android.car.audio.CarAudioService.CAR_DEFAULT_AUDIO_ATTRIBUTE; 23 import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice; 24 25 import static java.util.Locale.ROOT; 26 27 import android.annotation.NonNull; 28 import android.car.builtin.media.AudioManagerHelper; 29 import android.media.AudioAttributes; 30 import android.media.AudioDeviceAttributes; 31 import android.media.AudioDeviceInfo; 32 import android.media.AudioManager; 33 import android.media.audiopolicy.AudioProductStrategy; 34 import android.text.TextUtils; 35 import android.util.ArrayMap; 36 import android.util.ArraySet; 37 import android.util.SparseArray; 38 import android.util.SparseIntArray; 39 import android.util.Xml; 40 41 import com.android.car.audio.CarAudioContext.AudioContext; 42 import com.android.car.internal.util.VersionUtils; 43 import com.android.internal.util.Preconditions; 44 45 import org.xmlpull.v1.XmlPullParser; 46 import org.xmlpull.v1.XmlPullParserException; 47 48 import java.io.IOException; 49 import java.io.InputStream; 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.MissingResourceException; 54 import java.util.Objects; 55 import java.util.Set; 56 import java.util.stream.Collectors; 57 58 /** 59 * A helper class loads all audio zones from the configuration XML file. 60 */ 61 /* package */ final class CarAudioZonesHelper { 62 private static final String NAMESPACE = null; 63 private static final String TAG_ROOT = "carAudioConfiguration"; 64 65 private static final String TAG_OEM_CONTEXTS = "oemContexts"; 66 private static final String TAG_OEM_CONTEXT = "oemContext"; 67 private static final String TAG_AUDIO_ATTRIBUTES = "audioAttributes"; 68 private static final String TAG_AUDIO_ATTRIBUTE = "audioAttribute"; 69 private static final String TAG_USAGE = "usage"; 70 private static final String ATTR_USAGE_VALUE = "value"; 71 private static final String ATTR_NAME = "name"; 72 private static final String ATTR_CONTENT_TYPE = "contentType"; 73 private static final String ATTR_USAGE = "usage"; 74 private static final String ATTR_TAGS = "tags"; 75 76 private static final String TAG_AUDIO_ZONES = "zones"; 77 private static final String TAG_AUDIO_ZONE = "zone"; 78 private static final String TAG_AUDIO_ZONE_CONFIGS = "zoneConfigs"; 79 private static final String TAG_AUDIO_ZONE_CONFIG = "zoneConfig"; 80 private static final String TAG_VOLUME_GROUPS = "volumeGroups"; 81 private static final String TAG_VOLUME_GROUP = "group"; 82 private static final String TAG_AUDIO_DEVICE = "device"; 83 private static final String TAG_CONTEXT = "context"; 84 private static final String ATTR_VERSION = "version"; 85 private static final String ATTR_IS_PRIMARY = "isPrimary"; 86 private static final String ATTR_IS_CONFIG_DEFAULT = "isDefault"; 87 private static final String ATTR_ZONE_NAME = "name"; 88 private static final String ATTR_CONFIG_NAME = "name"; 89 private static final String ATTR_DEVICE_ADDRESS = "address"; 90 private static final String ATTR_CONTEXT_NAME = "context"; 91 private static final String ATTR_ZONE_ID = "audioZoneId"; 92 private static final String ATTR_OCCUPANT_ZONE_ID = "occupantZoneId"; 93 private static final String TAG_INPUT_DEVICES = "inputDevices"; 94 private static final String TAG_INPUT_DEVICE = "inputDevice"; 95 private static final String TAG_MIRRORING_DEVICES = "mirroringDevices"; 96 private static final String TAG_MIRRORING_DEVICE = "mirroringDevice"; 97 private static final int INVALID_VERSION = -1; 98 private static final int SUPPORTED_VERSION_1 = 1; 99 private static final int SUPPORTED_VERSION_2 = 2; 100 private static final int SUPPORTED_VERSION_3 = 3; 101 private static final SparseIntArray SUPPORTED_VERSIONS; 102 103 static { 104 SUPPORTED_VERSIONS = new SparseIntArray(3); SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1)105 SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1); SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2)106 SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2); SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_3, SUPPORTED_VERSION_3)107 SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_3, SUPPORTED_VERSION_3); 108 } 109 110 private final AudioManager mAudioManager; 111 private final CarAudioSettings mCarAudioSettings; 112 private final List<CarAudioContextInfo> mCarAudioContextInfos = new ArrayList<>(); 113 private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo; 114 private final Map<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfoForAllInputDevices; 115 private final InputStream mInputStream; 116 private final SparseIntArray mZoneIdToOccupantZoneIdMapping; 117 private final Set<Integer> mAudioZoneIds; 118 private final Set<String> mAssignedInputAudioDevices; 119 private final Set<String> mAudioZoneConfigNames; 120 private final boolean mUseCarVolumeGroupMute; 121 private final boolean mUseCoreAudioVolume; 122 private final boolean mUseCoreAudioRouting; 123 private final List<CarAudioDeviceInfo> mMirroringDevices = new ArrayList<>(); 124 125 private final ArrayMap<String, Integer> mContextNameToId = new ArrayMap<>(); 126 private CarAudioContext mCarAudioContext; 127 private int mNextSecondaryZoneId; 128 private int mCurrentVersion; 129 130 /** 131 * <p><b>Note: <b/> CarAudioZonesHelper is expected to be used from a single thread. This 132 * should be the same thread that originally called new CarAudioZonesHelper. 133 */ CarAudioZonesHelper(AudioManager audioManager, @NonNull CarAudioSettings carAudioSettings, @NonNull InputStream inputStream, @NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos, @NonNull AudioDeviceInfo[] inputDeviceInfo, boolean useCarVolumeGroupMute, boolean useCoreAudioVolume, boolean useCoreAudioRouting)134 CarAudioZonesHelper(AudioManager audioManager, 135 @NonNull CarAudioSettings carAudioSettings, 136 @NonNull InputStream inputStream, 137 @NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos, 138 @NonNull AudioDeviceInfo[] inputDeviceInfo, boolean useCarVolumeGroupMute, 139 boolean useCoreAudioVolume, boolean useCoreAudioRouting) { 140 mAudioManager = Objects.requireNonNull(audioManager, 141 "Audio manager cannot be null"); 142 mCarAudioSettings = Objects.requireNonNull(carAudioSettings); 143 mInputStream = Objects.requireNonNull(inputStream); 144 Objects.requireNonNull(carAudioDeviceInfos); 145 Objects.requireNonNull(inputDeviceInfo); 146 mAddressToCarAudioDeviceInfo = CarAudioZonesHelper.generateAddressToInfoMap( 147 carAudioDeviceInfos); 148 mAddressToInputAudioDeviceInfoForAllInputDevices = 149 CarAudioZonesHelper.generateAddressToInputAudioDeviceInfoMap(inputDeviceInfo); 150 mNextSecondaryZoneId = PRIMARY_AUDIO_ZONE + 1; 151 mZoneIdToOccupantZoneIdMapping = new SparseIntArray(); 152 mAudioZoneIds = new ArraySet<>(); 153 mAssignedInputAudioDevices = new ArraySet<>(); 154 mAudioZoneConfigNames = new ArraySet<>(); 155 mUseCarVolumeGroupMute = useCarVolumeGroupMute; 156 mUseCoreAudioVolume = useCoreAudioVolume; 157 mUseCoreAudioRouting = useCoreAudioRouting; 158 } 159 getCarAudioZoneIdToOccupantZoneIdMapping()160 SparseIntArray getCarAudioZoneIdToOccupantZoneIdMapping() { 161 return mZoneIdToOccupantZoneIdMapping; 162 } 163 loadAudioZones()164 SparseArray<CarAudioZone> loadAudioZones() throws IOException, XmlPullParserException { 165 return parseCarAudioZones(mInputStream); 166 } 167 generateAddressToInfoMap( List<CarAudioDeviceInfo> carAudioDeviceInfos)168 private static Map<String, CarAudioDeviceInfo> generateAddressToInfoMap( 169 List<CarAudioDeviceInfo> carAudioDeviceInfos) { 170 return carAudioDeviceInfos.stream() 171 .filter(info -> !TextUtils.isEmpty(info.getAddress())) 172 .collect(Collectors.toMap(CarAudioDeviceInfo::getAddress, info -> info)); 173 } 174 generateAddressToInputAudioDeviceInfoMap( @onNull AudioDeviceInfo[] inputAudioDeviceInfos)175 private static Map<String, AudioDeviceInfo> generateAddressToInputAudioDeviceInfoMap( 176 @NonNull AudioDeviceInfo[] inputAudioDeviceInfos) { 177 Map<String, AudioDeviceInfo> deviceAddressToInputDeviceMap = 178 new ArrayMap<>(inputAudioDeviceInfos.length); 179 for (int i = 0; i < inputAudioDeviceInfos.length; ++i) { 180 AudioDeviceInfo device = inputAudioDeviceInfos[i]; 181 if (device.isSource()) { 182 deviceAddressToInputDeviceMap.put(device.getAddress(), device); 183 } 184 } 185 return deviceAddressToInputDeviceMap; 186 } 187 parseCarAudioZones(InputStream stream)188 private SparseArray<CarAudioZone> parseCarAudioZones(InputStream stream) 189 throws XmlPullParserException, IOException { 190 XmlPullParser parser = Xml.newPullParser(); 191 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, NAMESPACE != null); 192 parser.setInput(stream, null); 193 194 // Ensure <carAudioConfiguration> is the root 195 parser.nextTag(); 196 parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_ROOT); 197 198 // Version check 199 final int versionNumber = Integer.parseInt( 200 parser.getAttributeValue(NAMESPACE, ATTR_VERSION)); 201 202 if (SUPPORTED_VERSIONS.get(versionNumber, INVALID_VERSION) == INVALID_VERSION) { 203 throw new IllegalArgumentException("Latest Supported version:" 204 + SUPPORTED_VERSION_3 + " , got version:" + versionNumber); 205 } 206 207 mCurrentVersion = versionNumber; 208 // Get all zones configured under <zones> tag 209 while (parser.next() != XmlPullParser.END_TAG) { 210 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 211 if (TAG_OEM_CONTEXTS.equals(parser.getName())) { 212 parseCarAudioContexts(parser); 213 } else if (TAG_MIRRORING_DEVICES.equals(parser.getName())) { 214 parseMirroringDevices(parser); 215 } else if (TAG_AUDIO_ZONES.equals(parser.getName())) { 216 loadCarAudioContexts(); 217 return parseAudioZones(parser); 218 } else { 219 skip(parser); 220 } 221 } 222 throw new MissingResourceException(TAG_AUDIO_ZONES + " is missing from configuration", 223 "", TAG_AUDIO_ZONES); 224 } 225 parseMirroringDevices(XmlPullParser parser)226 private void parseMirroringDevices(XmlPullParser parser) 227 throws XmlPullParserException, IOException { 228 if (isVersionLessThanThree()) { 229 throw new IllegalStateException( 230 TAG_MIRRORING_DEVICES + " are not supported in car_audio_configuration.xml" 231 + " version " + mCurrentVersion + ". Must be at least version " 232 + SUPPORTED_VERSION_3); 233 } 234 235 while (parser.next() != XmlPullParser.END_TAG) { 236 if (parser.getEventType() != XmlPullParser.START_TAG) { 237 continue; 238 } 239 if (TAG_MIRRORING_DEVICE.equals(parser.getName())) { 240 parseMirroringDevice(parser); 241 } 242 skip(parser); 243 } 244 } 245 parseMirroringDevice(XmlPullParser parser)246 private void parseMirroringDevice(XmlPullParser parser) { 247 String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS); 248 validateOutputDeviceExist(address); 249 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address); 250 if (mMirroringDevices.contains(info)) { 251 throw new IllegalArgumentException(TAG_MIRRORING_DEVICE + " " + address 252 + " repeats, " + TAG_MIRRORING_DEVICES + " can not repeat."); 253 } 254 mMirroringDevices.add(info); 255 } 256 loadCarAudioContexts()257 private void loadCarAudioContexts() { 258 if (isVersionLessThanThree() || mCarAudioContextInfos.isEmpty()) { 259 mCarAudioContextInfos.addAll(CarAudioContext.getAllContextsInfo()); 260 } 261 for (int index = 0; index < mCarAudioContextInfos.size(); index++) { 262 CarAudioContextInfo info = mCarAudioContextInfos.get(index); 263 mContextNameToId.put(info.getName().toLowerCase(ROOT), info.getId()); 264 } 265 mCarAudioContext = new CarAudioContext(mCarAudioContextInfos, mUseCoreAudioRouting); 266 } 267 parseCarAudioContexts(XmlPullParser parser)268 private void parseCarAudioContexts(XmlPullParser parser) 269 throws XmlPullParserException, IOException { 270 int contextId = CarAudioContext.getInvalidContext() + 1; 271 272 while (parser.next() != XmlPullParser.END_TAG) { 273 if (parser.getEventType() != XmlPullParser.START_TAG) { 274 continue; 275 } 276 if (TAG_OEM_CONTEXT.equals(parser.getName())) { 277 parseCarAudioContext(parser, contextId); 278 contextId++; 279 } else { 280 skip(parser); 281 } 282 } 283 } 284 parseCarAudioContext(XmlPullParser parser, int contextId)285 private void parseCarAudioContext(XmlPullParser parser, int contextId) 286 throws XmlPullParserException, IOException { 287 String contextName = parser.getAttributeValue(NAMESPACE, ATTR_NAME); 288 CarAudioContextInfo context = null; 289 while (parser.next() != XmlPullParser.END_TAG) { 290 if (parser.getEventType() != XmlPullParser.START_TAG) { 291 continue; 292 } 293 if (TAG_AUDIO_ATTRIBUTES.equals(parser.getName())) { 294 List<AudioAttributes> attributes = parseAudioAttributes(parser, contextName); 295 if (mUseCoreAudioRouting) { 296 contextId = CoreAudioHelper.getStrategyForAudioAttributes(attributes.get(0)); 297 if (contextId == CoreAudioHelper.INVALID_STRATEGY) { 298 throw new IllegalArgumentException(TAG_AUDIO_ATTRIBUTES 299 + ": Cannot find strategy id for context: " 300 + contextName + " and attributes \"" + attributes.get(0) + "\" ."); 301 } 302 } 303 validateCarAudioContextAttributes(contextId, attributes, contextName); 304 context = new CarAudioContextInfo(attributes.toArray(new AudioAttributes[0]), 305 contextName, contextId); 306 mCarAudioContextInfos.add(context); 307 } else { 308 skip(parser); 309 } 310 } 311 } 312 validateCarAudioContextAttributes(int contextId, List<AudioAttributes> attributes, String contextName)313 private void validateCarAudioContextAttributes(int contextId, List<AudioAttributes> attributes, 314 String contextName) { 315 if (!mUseCoreAudioRouting) { 316 return; 317 } 318 AudioProductStrategy strategy = CoreAudioHelper.getStrategy(contextId); 319 Preconditions.checkNotNull(strategy, "No strategy for context id = %d", contextId); 320 for (int index = 0; index < attributes.size(); index++) { 321 AudioAttributes aa = attributes.get(index); 322 if (!strategy.supportsAudioAttributes(aa) 323 && !CoreAudioHelper.isDefaultStrategy(strategy.getId())) { 324 throw new IllegalArgumentException("Invalid attributes " + aa + " for context: " 325 + contextName); 326 } 327 } 328 } 329 parseAudioAttributes(XmlPullParser parser, String contextName)330 private List<AudioAttributes> parseAudioAttributes(XmlPullParser parser, String contextName) 331 throws XmlPullParserException, IOException { 332 List<AudioAttributes> supportedAttributes = new ArrayList<>(); 333 while (parser.next() != XmlPullParser.END_TAG) { 334 if (parser.getEventType() != XmlPullParser.START_TAG) { 335 continue; 336 } 337 if (TAG_USAGE.equals(parser.getName())) { 338 AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder(); 339 parseUsage(parser, attributesBuilder, ATTR_USAGE_VALUE); 340 AudioAttributes attributes = attributesBuilder.build(); 341 supportedAttributes.add(attributes); 342 } else if (TAG_AUDIO_ATTRIBUTE.equals(parser.getName())) { 343 AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder(); 344 // Usage, ContentType and tags are optional but at least one value must be 345 // provided to build a valid audio attributes 346 boolean hasValidUsage = parseUsage(parser, attributesBuilder, ATTR_USAGE); 347 boolean hasValidContentType = parseContentType(parser, attributesBuilder); 348 boolean hasValidTags = parseTags(parser, attributesBuilder); 349 if (!(hasValidUsage || hasValidContentType || hasValidTags)) { 350 throw new RuntimeException("Empty attributes for context: " + contextName); 351 } 352 AudioAttributes attributes = attributesBuilder.build(); 353 supportedAttributes.add(attributes); 354 } 355 // Always skip to upper level since we're at the lowest. 356 skip(parser); 357 } 358 if (supportedAttributes.isEmpty()) { 359 throw new IllegalArgumentException("No attributes for context: " + contextName); 360 } 361 return supportedAttributes; 362 } 363 parseUsage(XmlPullParser parser, AudioAttributes.Builder builder, String attrValue)364 private boolean parseUsage(XmlPullParser parser, AudioAttributes.Builder builder, 365 String attrValue) 366 throws XmlPullParserException, IOException { 367 String usageLiteral = parser.getAttributeValue(NAMESPACE, attrValue); 368 if (usageLiteral == null) { 369 return false; 370 } 371 int usage = AudioManagerHelper.xsdStringToUsage(usageLiteral); 372 // TODO (b/248106031): Remove once AUDIO_USAGE_NOTIFICATION_EVENT is fixed in core 373 if (Objects.equals(usageLiteral, "AUDIO_USAGE_NOTIFICATION_EVENT")) { 374 usage = USAGE_NOTIFICATION_EVENT; 375 } 376 if (AudioAttributes.isSystemUsage(usage)) { 377 builder.setSystemUsage(usage); 378 } else { 379 builder.setUsage(usage); 380 } 381 return true; 382 } 383 parseContentType(XmlPullParser parser, AudioAttributes.Builder builder)384 private boolean parseContentType(XmlPullParser parser, AudioAttributes.Builder builder) 385 throws XmlPullParserException, IOException { 386 if (!VersionUtils.isPlatformVersionAtLeastU()) { 387 throw new IllegalArgumentException("car_audio_configuration.xml tag " 388 + ATTR_CONTENT_TYPE + ", is only supported for release version " 389 + UPSIDE_DOWN_CAKE_0 + " and higher"); 390 } 391 String contentTypeLiteral = parser.getAttributeValue(NAMESPACE, ATTR_CONTENT_TYPE); 392 if (contentTypeLiteral == null) { 393 return false; 394 } 395 int contentType = AudioManagerHelper.xsdStringToContentType(contentTypeLiteral); 396 builder.setContentType(contentType); 397 return true; 398 } 399 parseTags(XmlPullParser parser, AudioAttributes.Builder builder)400 private boolean parseTags(XmlPullParser parser, AudioAttributes.Builder builder) 401 throws XmlPullParserException, IOException { 402 String tagsLiteral = parser.getAttributeValue(NAMESPACE, ATTR_TAGS); 403 if (!VersionUtils.isPlatformVersionAtLeastU()) { 404 throw new IllegalArgumentException("car_audio_configuration.xml tag " + ATTR_TAGS 405 + ", is only supported for release version " + UPSIDE_DOWN_CAKE_0 406 + " and higher"); 407 } 408 if (tagsLiteral == null) { 409 return false; 410 } 411 AudioManagerHelper.addTagToAudioAttributes(builder, tagsLiteral); 412 return true; 413 } 414 parseAudioZones(XmlPullParser parser)415 private SparseArray<CarAudioZone> parseAudioZones(XmlPullParser parser) 416 throws XmlPullParserException, IOException { 417 SparseArray<CarAudioZone> carAudioZones = new SparseArray<>(); 418 419 while (parser.next() != XmlPullParser.END_TAG) { 420 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 421 if (TAG_AUDIO_ZONE.equals(parser.getName())) { 422 CarAudioZone zone = parseAudioZone(parser); 423 verifyOnlyOnePrimaryZone(zone, carAudioZones); 424 carAudioZones.put(zone.getId(), zone); 425 } else { 426 skip(parser); 427 } 428 } 429 430 verifyPrimaryZonePresent(carAudioZones); 431 addRemainingMicrophonesToPrimaryZone(carAudioZones); 432 return carAudioZones; 433 } 434 addRemainingMicrophonesToPrimaryZone(SparseArray<CarAudioZone> carAudioZones)435 private void addRemainingMicrophonesToPrimaryZone(SparseArray<CarAudioZone> carAudioZones) { 436 CarAudioZone primaryAudioZone = carAudioZones.get(PRIMARY_AUDIO_ZONE); 437 for (AudioDeviceInfo info : mAddressToInputAudioDeviceInfoForAllInputDevices.values()) { 438 if (!mAssignedInputAudioDevices.contains(info.getAddress()) 439 && isMicrophoneInputDevice(info)) { 440 primaryAudioZone.addInputAudioDevice(new AudioDeviceAttributes(info)); 441 } 442 } 443 } 444 verifyOnlyOnePrimaryZone(CarAudioZone newZone, SparseArray<CarAudioZone> zones)445 private void verifyOnlyOnePrimaryZone(CarAudioZone newZone, SparseArray<CarAudioZone> zones) { 446 if (newZone.getId() == PRIMARY_AUDIO_ZONE && zones.contains(PRIMARY_AUDIO_ZONE)) { 447 throw new RuntimeException("More than one zone parsed with primary audio zone ID: " 448 + PRIMARY_AUDIO_ZONE); 449 } 450 } 451 verifyPrimaryZonePresent(SparseArray<CarAudioZone> zones)452 private void verifyPrimaryZonePresent(SparseArray<CarAudioZone> zones) { 453 if (!zones.contains(PRIMARY_AUDIO_ZONE)) { 454 throw new RuntimeException("Primary audio zone is required"); 455 } 456 } 457 parseAudioZone(XmlPullParser parser)458 private CarAudioZone parseAudioZone(XmlPullParser parser) 459 throws XmlPullParserException, IOException { 460 final boolean isPrimary = Boolean.parseBoolean( 461 parser.getAttributeValue(NAMESPACE, ATTR_IS_PRIMARY)); 462 final String zoneName = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_NAME); 463 final int audioZoneId = getZoneId(isPrimary, parser); 464 parseOccupantZoneId(audioZoneId, parser); 465 final CarAudioZone zone = new CarAudioZone(mCarAudioContext, zoneName, audioZoneId); 466 if (isVersionLessThanThree()) { 467 final CarAudioZoneConfig.Builder zoneConfigBuilder = new CarAudioZoneConfig.Builder( 468 zoneName, audioZoneId, /* zoneConfigId= */ 0, /* isDefault= */ true); 469 while (parser.next() != XmlPullParser.END_TAG) { 470 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 471 // Expect at least one <volumeGroups> in one audio zone 472 if (TAG_VOLUME_GROUPS.equals(parser.getName())) { 473 parseVolumeGroups(parser, zoneConfigBuilder); 474 } else if (TAG_INPUT_DEVICES.equals(parser.getName())) { 475 parseInputAudioDevices(parser, zone); 476 } else { 477 skip(parser); 478 } 479 } 480 zone.addZoneConfig(zoneConfigBuilder.build()); 481 return zone; 482 } 483 while (parser.next() != XmlPullParser.END_TAG) { 484 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 485 // Expect at least one <zoneConfigs> in one audio zone 486 if (TAG_AUDIO_ZONE_CONFIGS.equals(parser.getName())) { 487 parseZoneConfigs(parser, zone); 488 } else if (TAG_INPUT_DEVICES.equals(parser.getName())) { 489 parseInputAudioDevices(parser, zone); 490 } else { 491 skip(parser); 492 } 493 } 494 return zone; 495 } 496 getZoneId(boolean isPrimary, XmlPullParser parser)497 private int getZoneId(boolean isPrimary, XmlPullParser parser) { 498 String audioZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_ID); 499 if (isVersionOne()) { 500 Preconditions.checkArgument(audioZoneIdString == null, 501 "Invalid audio attribute %s" 502 + ", Please update car audio configurations file " 503 + "to version to 2 to use it.", ATTR_ZONE_ID); 504 return isPrimary ? PRIMARY_AUDIO_ZONE 505 : getNextSecondaryZoneId(); 506 } 507 // Primary zone does not need to define it 508 if (isPrimary && audioZoneIdString == null) { 509 return PRIMARY_AUDIO_ZONE; 510 } 511 Objects.requireNonNull(audioZoneIdString, 512 "Requires audioZoneId for all audio zones."); 513 int zoneId = parsePositiveIntAttribute(ATTR_ZONE_ID, audioZoneIdString); 514 //Verify that primary zone id is PRIMARY_AUDIO_ZONE 515 if (isPrimary) { 516 Preconditions.checkArgument(zoneId == PRIMARY_AUDIO_ZONE, 517 "Primary zone %s must be %d or it can be left empty.", 518 ATTR_ZONE_ID, PRIMARY_AUDIO_ZONE); 519 } else { 520 Preconditions.checkArgument(zoneId != PRIMARY_AUDIO_ZONE, 521 "%s can only be %d for primary zone.", 522 ATTR_ZONE_ID, PRIMARY_AUDIO_ZONE); 523 } 524 validateAudioZoneIdIsUnique(zoneId); 525 return zoneId; 526 } 527 parseOccupantZoneId(int audioZoneId, XmlPullParser parser)528 private void parseOccupantZoneId(int audioZoneId, XmlPullParser parser) { 529 String occupantZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_OCCUPANT_ZONE_ID); 530 if (isVersionOne()) { 531 Preconditions.checkArgument(occupantZoneIdString == null, 532 "Invalid audio attribute %s" 533 + ", Please update car audio configurations file " 534 + "to version to 2 to use it.", ATTR_OCCUPANT_ZONE_ID); 535 return; 536 } 537 //Occupant id not required for all zones 538 if (occupantZoneIdString == null) { 539 return; 540 } 541 int occupantZoneId = parsePositiveIntAttribute(ATTR_OCCUPANT_ZONE_ID, occupantZoneIdString); 542 validateOccupantZoneIdIsUnique(occupantZoneId); 543 mZoneIdToOccupantZoneIdMapping.put(audioZoneId, occupantZoneId); 544 } 545 parsePositiveIntAttribute(String attribute, String integerString)546 private int parsePositiveIntAttribute(String attribute, String integerString) { 547 try { 548 return Integer.parseUnsignedInt(integerString); 549 } catch (NumberFormatException | IndexOutOfBoundsException e) { 550 throw new IllegalArgumentException(attribute + " must be a positive integer, but was \"" 551 + integerString + "\" instead.", e); 552 } 553 } 554 parseInputAudioDevices(XmlPullParser parser, CarAudioZone zone)555 private void parseInputAudioDevices(XmlPullParser parser, CarAudioZone zone) 556 throws IOException, XmlPullParserException { 557 if (isVersionOne()) { 558 throw new IllegalStateException( 559 TAG_INPUT_DEVICES + " are not supported in car_audio_configuration.xml version " 560 + SUPPORTED_VERSION_1); 561 } 562 while (parser.next() != XmlPullParser.END_TAG) { 563 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 564 if (TAG_INPUT_DEVICE.equals(parser.getName())) { 565 String audioDeviceAddress = 566 parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS); 567 validateInputAudioDeviceAddress(audioDeviceAddress); 568 AudioDeviceInfo info = 569 mAddressToInputAudioDeviceInfoForAllInputDevices.get(audioDeviceAddress); 570 Preconditions.checkArgument(info != null, 571 "%s %s of %s does not exist, add input device to" 572 + " audio_policy_configuration.xml.", 573 ATTR_DEVICE_ADDRESS, audioDeviceAddress, TAG_INPUT_DEVICE); 574 zone.addInputAudioDevice(new AudioDeviceAttributes(info)); 575 } 576 skip(parser); 577 } 578 } 579 validateInputAudioDeviceAddress(String audioDeviceAddress)580 private void validateInputAudioDeviceAddress(String audioDeviceAddress) { 581 Objects.requireNonNull(audioDeviceAddress, () -> 582 TAG_INPUT_DEVICE + " " + ATTR_DEVICE_ADDRESS + " attribute must be present."); 583 Preconditions.checkArgument(!audioDeviceAddress.isEmpty(), 584 "%s %s attribute can not be empty.", 585 TAG_INPUT_DEVICE, ATTR_DEVICE_ADDRESS); 586 if (mAssignedInputAudioDevices.contains(audioDeviceAddress)) { 587 throw new IllegalArgumentException(TAG_INPUT_DEVICE + " " + audioDeviceAddress 588 + " repeats, " + TAG_INPUT_DEVICES + " can not repeat."); 589 } 590 mAssignedInputAudioDevices.add(audioDeviceAddress); 591 } 592 validateOccupantZoneIdIsUnique(int occupantZoneId)593 private void validateOccupantZoneIdIsUnique(int occupantZoneId) { 594 if (mZoneIdToOccupantZoneIdMapping.indexOfValue(occupantZoneId) > -1) { 595 throw new IllegalArgumentException(ATTR_OCCUPANT_ZONE_ID + " " + occupantZoneId 596 + " is already associated with a zone"); 597 } 598 } 599 validateAudioZoneIdIsUnique(int audioZoneId)600 private void validateAudioZoneIdIsUnique(int audioZoneId) { 601 if (mAudioZoneIds.contains(audioZoneId)) { 602 throw new IllegalArgumentException(ATTR_ZONE_ID + " " + audioZoneId 603 + " is already associated with a zone"); 604 } 605 mAudioZoneIds.add(audioZoneId); 606 } 607 parseZoneConfigs(XmlPullParser parser, CarAudioZone zone)608 private void parseZoneConfigs(XmlPullParser parser, CarAudioZone zone) 609 throws XmlPullParserException, IOException { 610 int zoneConfigId = 0; 611 while (parser.next() != XmlPullParser.END_TAG) { 612 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 613 if (TAG_AUDIO_ZONE_CONFIG.equals(parser.getName())) { 614 if (zone.getId() == PRIMARY_AUDIO_ZONE && zoneConfigId > 0) { 615 throw new IllegalArgumentException( 616 "Primary zone cannot have multiple zone configurations"); 617 } 618 parseZoneConfig(parser, zone, zoneConfigId); 619 zoneConfigId++; 620 } else { 621 skip(parser); 622 } 623 } 624 } 625 parseZoneConfig(XmlPullParser parser, CarAudioZone zone, int zoneConfigId)626 private void parseZoneConfig(XmlPullParser parser, CarAudioZone zone, int zoneConfigId) 627 throws XmlPullParserException, IOException { 628 final boolean isDefault = Boolean.parseBoolean( 629 parser.getAttributeValue(NAMESPACE, ATTR_IS_CONFIG_DEFAULT)); 630 final String zoneConfigName = parser.getAttributeValue(NAMESPACE, ATTR_CONFIG_NAME); 631 validateAudioZoneConfigName(zoneConfigName); 632 final CarAudioZoneConfig.Builder zoneConfigBuilder = new CarAudioZoneConfig.Builder( 633 zoneConfigName, zone.getId(), zoneConfigId, isDefault); 634 while (parser.next() != XmlPullParser.END_TAG) { 635 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 636 // Expect at least one <volumeGroups> in one audio zone config 637 if (TAG_VOLUME_GROUPS.equals(parser.getName())) { 638 parseVolumeGroups(parser, zoneConfigBuilder); 639 } else { 640 skip(parser); 641 } 642 } 643 zone.addZoneConfig(zoneConfigBuilder.build()); 644 } 645 validateAudioZoneConfigName(String configName)646 private void validateAudioZoneConfigName(String configName) { 647 Objects.requireNonNull(configName, TAG_AUDIO_ZONE_CONFIG + " " + ATTR_CONFIG_NAME 648 + " attribute must be present."); 649 Preconditions.checkArgument(!configName.isEmpty(), 650 "%s %s attribute can not be empty.", 651 TAG_AUDIO_ZONE_CONFIG, ATTR_CONFIG_NAME); 652 if (mAudioZoneConfigNames.contains(configName)) { 653 throw new IllegalArgumentException(ATTR_CONFIG_NAME + " " + configName 654 + " repeats, " + ATTR_CONFIG_NAME + " can not repeat."); 655 } 656 mAudioZoneConfigNames.add(configName); 657 } 658 parseVolumeGroups(XmlPullParser parser, CarAudioZoneConfig.Builder zoneConfigBuilder)659 private void parseVolumeGroups(XmlPullParser parser, 660 CarAudioZoneConfig.Builder zoneConfigBuilder) 661 throws XmlPullParserException, IOException { 662 int groupId = 0; 663 while (parser.next() != XmlPullParser.END_TAG) { 664 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 665 if (TAG_VOLUME_GROUP.equals(parser.getName())) { 666 String groupName = parser.getAttributeValue(NAMESPACE, ATTR_NAME); 667 Preconditions.checkArgument(!mUseCoreAudioVolume || groupName != null, 668 "%s %s attribute can not be empty when relying on core volume groups", 669 TAG_VOLUME_GROUP, ATTR_NAME); 670 if (groupName == null) { 671 groupName = new StringBuilder().append("config ") 672 .append(zoneConfigBuilder.getZoneConfigId()).append(" group ") 673 .append(groupId).toString(); 674 } 675 zoneConfigBuilder.addVolumeGroup(parseVolumeGroup(parser, 676 zoneConfigBuilder.getZoneId(), zoneConfigBuilder.getZoneConfigId(), 677 groupId, groupName)); 678 groupId++; 679 } else { 680 skip(parser); 681 } 682 } 683 } 684 parseVolumeGroup(XmlPullParser parser, int zoneId, int configId, int groupId, String groupName)685 private CarVolumeGroup parseVolumeGroup(XmlPullParser parser, int zoneId, int configId, 686 int groupId, String groupName) throws XmlPullParserException, IOException { 687 CarVolumeGroupFactory groupFactory = new CarVolumeGroupFactory(mAudioManager, 688 mCarAudioSettings, mCarAudioContext, zoneId, configId, groupId, groupName, 689 mUseCarVolumeGroupMute); 690 while (parser.next() != XmlPullParser.END_TAG) { 691 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 692 if (TAG_AUDIO_DEVICE.equals(parser.getName())) { 693 String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS); 694 validateOutputDeviceExist(address); 695 parseVolumeGroupContexts(parser, groupFactory, address); 696 } else { 697 skip(parser); 698 } 699 } 700 return groupFactory.getCarVolumeGroup(mUseCoreAudioVolume); 701 } 702 validateOutputDeviceExist(String address)703 private void validateOutputDeviceExist(String address) { 704 if (!mAddressToCarAudioDeviceInfo.containsKey(address)) { 705 throw new IllegalStateException(String.format( 706 "Output device address %s does not belong to any configured output device.", 707 address)); 708 } 709 } 710 parseVolumeGroupContexts( XmlPullParser parser, CarVolumeGroupFactory groupFactory, String address)711 private void parseVolumeGroupContexts( 712 XmlPullParser parser, CarVolumeGroupFactory groupFactory, String address) 713 throws XmlPullParserException, IOException { 714 while (parser.next() != XmlPullParser.END_TAG) { 715 if (parser.getEventType() != XmlPullParser.START_TAG) continue; 716 if (TAG_CONTEXT.equals(parser.getName())) { 717 @AudioContext int carAudioContextId = parseCarAudioContextId( 718 parser.getAttributeValue(NAMESPACE, ATTR_CONTEXT_NAME)); 719 validateCarAudioContextSupport(carAudioContextId); 720 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address); 721 groupFactory.setDeviceInfoForContext(carAudioContextId, info); 722 723 // If V1, default new contexts to same device as DEFAULT_AUDIO_USAGE 724 if (isVersionOne() && carAudioContextId == mCarAudioContext 725 .getContextForAudioAttribute(CAR_DEFAULT_AUDIO_ATTRIBUTE)) { 726 groupFactory.setNonLegacyContexts(info); 727 } 728 } 729 // Always skip to upper level since we're at the lowest. 730 skip(parser); 731 } 732 } 733 isVersionLessThanThree()734 private boolean isVersionLessThanThree() { 735 return mCurrentVersion < SUPPORTED_VERSION_3; 736 } 737 isVersionOne()738 private boolean isVersionOne() { 739 return mCurrentVersion == SUPPORTED_VERSION_1; 740 } 741 skip(XmlPullParser parser)742 private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { 743 if (parser.getEventType() != XmlPullParser.START_TAG) { 744 throw new IllegalStateException(); 745 } 746 int depth = 1; 747 while (depth != 0) { 748 switch (parser.next()) { 749 case XmlPullParser.END_TAG: 750 depth--; 751 break; 752 case XmlPullParser.START_TAG: 753 depth++; 754 break; 755 default: 756 break; 757 } 758 } 759 } 760 parseCarAudioContextId(String context)761 private @AudioContext int parseCarAudioContextId(String context) { 762 return mContextNameToId.getOrDefault(context.toLowerCase(ROOT), 763 CarAudioContext.getInvalidContext()); 764 } 765 validateCarAudioContextSupport(@udioContext int audioContext)766 private void validateCarAudioContextSupport(@AudioContext int audioContext) { 767 if (isVersionOne() && CarAudioContext.getCarSystemContextIds().contains(audioContext)) { 768 throw new IllegalArgumentException(String.format( 769 "Non-legacy audio contexts such as %s are not supported in " 770 + "car_audio_configuration.xml version %d", 771 mCarAudioContext.toString(audioContext), SUPPORTED_VERSION_1)); 772 } 773 } 774 getNextSecondaryZoneId()775 private int getNextSecondaryZoneId() { 776 int zoneId = mNextSecondaryZoneId; 777 mNextSecondaryZoneId += 1; 778 return zoneId; 779 } 780 getCarAudioContext()781 public CarAudioContext getCarAudioContext() { 782 return mCarAudioContext; 783 } 784 getMirrorDeviceInfos()785 public List<CarAudioDeviceInfo> getMirrorDeviceInfos() { 786 return mMirroringDevices; 787 } 788 } 789