• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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