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.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
19 
20 import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice;
21 
22 import android.annotation.NonNull;
23 import android.media.AudioDeviceAttributes;
24 import android.media.AudioDeviceInfo;
25 import android.text.TextUtils;
26 import android.util.ArrayMap;
27 import android.util.ArraySet;
28 import android.util.SparseArray;
29 import android.util.SparseIntArray;
30 import android.util.Xml;
31 
32 import com.android.car.audio.CarAudioContext.AudioContext;
33 import com.android.internal.util.Preconditions;
34 
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Objects;
45 import java.util.Set;
46 import java.util.stream.Collectors;
47 
48 /**
49  * A helper class loads all audio zones from the configuration XML file.
50  */
51 /* package */ class CarAudioZonesHelper {
52     private static final String NAMESPACE = null;
53     private static final String TAG_ROOT = "carAudioConfiguration";
54     private static final String TAG_AUDIO_ZONES = "zones";
55     private static final String TAG_AUDIO_ZONE = "zone";
56     private static final String TAG_VOLUME_GROUPS = "volumeGroups";
57     private static final String TAG_VOLUME_GROUP = "group";
58     private static final String TAG_AUDIO_DEVICE = "device";
59     private static final String TAG_CONTEXT = "context";
60     private static final String ATTR_VERSION = "version";
61     private static final String ATTR_IS_PRIMARY = "isPrimary";
62     private static final String ATTR_ZONE_NAME = "name";
63     private static final String ATTR_DEVICE_ADDRESS = "address";
64     private static final String ATTR_CONTEXT_NAME = "context";
65     private static final String ATTR_ZONE_ID = "audioZoneId";
66     private static final String ATTR_OCCUPANT_ZONE_ID = "occupantZoneId";
67     private static final String TAG_INPUT_DEVICES = "inputDevices";
68     private static final String TAG_INPUT_DEVICE = "inputDevice";
69     private static final int INVALID_VERSION = -1;
70     private static final int SUPPORTED_VERSION_1 = 1;
71     private static final int SUPPORTED_VERSION_2 = 2;
72     private static final SparseIntArray SUPPORTED_VERSIONS;
73 
74 
75     private static final Map<String, Integer> CONTEXT_NAME_MAP;
76 
77     static {
78         CONTEXT_NAME_MAP = new ArrayMap<>(CarAudioContext.CONTEXTS.length);
79         CONTEXT_NAME_MAP.put("music", CarAudioContext.MUSIC);
80         CONTEXT_NAME_MAP.put("navigation", CarAudioContext.NAVIGATION);
81         CONTEXT_NAME_MAP.put("voice_command", CarAudioContext.VOICE_COMMAND);
82         CONTEXT_NAME_MAP.put("call_ring", CarAudioContext.CALL_RING);
83         CONTEXT_NAME_MAP.put("call", CarAudioContext.CALL);
84         CONTEXT_NAME_MAP.put("alarm", CarAudioContext.ALARM);
85         CONTEXT_NAME_MAP.put("notification", CarAudioContext.NOTIFICATION);
86         CONTEXT_NAME_MAP.put("system_sound", CarAudioContext.SYSTEM_SOUND);
87         CONTEXT_NAME_MAP.put("emergency", CarAudioContext.EMERGENCY);
88         CONTEXT_NAME_MAP.put("safety", CarAudioContext.SAFETY);
89         CONTEXT_NAME_MAP.put("vehicle_status", CarAudioContext.VEHICLE_STATUS);
90         CONTEXT_NAME_MAP.put("announcement", CarAudioContext.ANNOUNCEMENT);
91 
92         SUPPORTED_VERSIONS = new SparseIntArray(2);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1)93         SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2)94         SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2);
95     }
96 
97     // Same contexts as defined in android.hardware.automotive.audiocontrol.V1_0.ContextNumber
98     static final int[] LEGACY_CONTEXTS = new int[]{
99             CarAudioContext.MUSIC,
100             CarAudioContext.NAVIGATION,
101             CarAudioContext.VOICE_COMMAND,
102             CarAudioContext.CALL_RING,
103             CarAudioContext.CALL,
104             CarAudioContext.ALARM,
105             CarAudioContext.NOTIFICATION,
106             CarAudioContext.SYSTEM_SOUND
107     };
108 
isLegacyContext(@udioContext int audioContext)109     private static boolean isLegacyContext(@AudioContext int audioContext) {
110         return Arrays.binarySearch(LEGACY_CONTEXTS, audioContext) >= 0;
111     }
112 
113     private static final List<Integer> NON_LEGACY_CONTEXTS = new ArrayList<>(
114             CarAudioContext.CONTEXTS.length - LEGACY_CONTEXTS.length);
115 
116     static {
117         for (@AudioContext int audioContext : CarAudioContext.CONTEXTS) {
118             if (!isLegacyContext(audioContext)) {
119                 NON_LEGACY_CONTEXTS.add(audioContext);
120             }
121         }
122     }
123 
setNonLegacyContexts(CarVolumeGroup.Builder groupBuilder, CarAudioDeviceInfo info)124     static void setNonLegacyContexts(CarVolumeGroup.Builder groupBuilder,
125             CarAudioDeviceInfo info) {
126         for (@AudioContext int audioContext : NON_LEGACY_CONTEXTS) {
127             groupBuilder.setDeviceInfoForContext(audioContext, info);
128         }
129     }
130 
131     private final CarAudioSettings mCarAudioSettings;
132     private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo;
133     private final Map<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfoForAllInputDevices;
134     private final InputStream mInputStream;
135     private final SparseIntArray mZoneIdToOccupantZoneIdMapping;
136     private final Set<Integer> mAudioZoneIds;
137     private final Set<String> mAssignedInputAudioDevices;
138     private final boolean mUseCarVolumeGroupMute;
139 
140     private int mNextSecondaryZoneId;
141     private int mCurrentVersion;
142 
143     /**
144      * <p><b>Note: <b/> CarAudioZonesHelper is expected to be used from a single thread. This
145      * should be the same thread that originally called new CarAudioZonesHelper.
146      */
CarAudioZonesHelper(@onNull CarAudioSettings carAudioSettings, @NonNull InputStream inputStream, @NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos, @NonNull AudioDeviceInfo[] inputDeviceInfo, boolean useCarVolumeGroupMute)147     CarAudioZonesHelper(@NonNull CarAudioSettings carAudioSettings,
148             @NonNull InputStream inputStream,
149             @NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos,
150             @NonNull AudioDeviceInfo[] inputDeviceInfo, boolean useCarVolumeGroupMute) {
151         mCarAudioSettings = Objects.requireNonNull(carAudioSettings);
152         mInputStream = Objects.requireNonNull(inputStream);
153         Objects.requireNonNull(carAudioDeviceInfos);
154         Objects.requireNonNull(inputDeviceInfo);
155         mAddressToCarAudioDeviceInfo = CarAudioZonesHelper.generateAddressToInfoMap(
156                 carAudioDeviceInfos);
157         mAddressToInputAudioDeviceInfoForAllInputDevices =
158                 CarAudioZonesHelper.generateAddressToInputAudioDeviceInfoMap(inputDeviceInfo);
159         mNextSecondaryZoneId = PRIMARY_AUDIO_ZONE + 1;
160         mZoneIdToOccupantZoneIdMapping = new SparseIntArray();
161         mAudioZoneIds = new ArraySet<>();
162         mAssignedInputAudioDevices = new ArraySet<>();
163         mUseCarVolumeGroupMute = useCarVolumeGroupMute;
164     }
165 
getCarAudioZoneIdToOccupantZoneIdMapping()166     SparseIntArray getCarAudioZoneIdToOccupantZoneIdMapping() {
167         return mZoneIdToOccupantZoneIdMapping;
168     }
169 
loadAudioZones()170     SparseArray<CarAudioZone> loadAudioZones() throws IOException, XmlPullParserException {
171         return parseCarAudioZones(mInputStream);
172     }
173 
generateAddressToInfoMap( List<CarAudioDeviceInfo> carAudioDeviceInfos)174     private static Map<String, CarAudioDeviceInfo> generateAddressToInfoMap(
175             List<CarAudioDeviceInfo> carAudioDeviceInfos) {
176         return carAudioDeviceInfos.stream()
177                 .filter(info -> !TextUtils.isEmpty(info.getAddress()))
178                 .collect(Collectors.toMap(CarAudioDeviceInfo::getAddress, info -> info));
179     }
180 
generateAddressToInputAudioDeviceInfoMap( @onNull AudioDeviceInfo[] inputAudioDeviceInfos)181     private static Map<String, AudioDeviceInfo> generateAddressToInputAudioDeviceInfoMap(
182             @NonNull AudioDeviceInfo[] inputAudioDeviceInfos) {
183         Map<String, AudioDeviceInfo> deviceAddressToInputDeviceMap =
184                 new ArrayMap<>(inputAudioDeviceInfos.length);
185         for (int i = 0; i < inputAudioDeviceInfos.length; ++i) {
186             AudioDeviceInfo device = inputAudioDeviceInfos[i];
187             if (device.isSource()) {
188                 deviceAddressToInputDeviceMap.put(device.getAddress(), device);
189             }
190         }
191         return deviceAddressToInputDeviceMap;
192     }
193 
parseCarAudioZones(InputStream stream)194     private SparseArray<CarAudioZone> parseCarAudioZones(InputStream stream)
195             throws XmlPullParserException, IOException {
196         XmlPullParser parser = Xml.newPullParser();
197         parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, NAMESPACE != null);
198         parser.setInput(stream, null);
199 
200         // Ensure <carAudioConfiguration> is the root
201         parser.nextTag();
202         parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_ROOT);
203 
204         // Version check
205         final int versionNumber = Integer.parseInt(
206                 parser.getAttributeValue(NAMESPACE, ATTR_VERSION));
207 
208         if (SUPPORTED_VERSIONS.get(versionNumber, INVALID_VERSION) == INVALID_VERSION) {
209             throw new IllegalArgumentException("Latest Supported version:"
210                     + SUPPORTED_VERSION_2 + " , got version:" + versionNumber);
211         }
212 
213         mCurrentVersion = versionNumber;
214 
215         // Get all zones configured under <zones> tag
216         while (parser.next() != XmlPullParser.END_TAG) {
217             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
218             if (TAG_AUDIO_ZONES.equals(parser.getName())) {
219                 return parseAudioZones(parser);
220             } else {
221                 skip(parser);
222             }
223         }
224         throw new RuntimeException(TAG_AUDIO_ZONES + " is missing from configuration");
225     }
226 
parseAudioZones(XmlPullParser parser)227     private SparseArray<CarAudioZone> parseAudioZones(XmlPullParser parser)
228             throws XmlPullParserException, IOException {
229         SparseArray<CarAudioZone> carAudioZones = new SparseArray<>();
230 
231         while (parser.next() != XmlPullParser.END_TAG) {
232             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
233             if (TAG_AUDIO_ZONE.equals(parser.getName())) {
234                 CarAudioZone zone = parseAudioZone(parser);
235                 verifyOnlyOnePrimaryZone(zone, carAudioZones);
236                 carAudioZones.put(zone.getId(), zone);
237             } else {
238                 skip(parser);
239             }
240         }
241 
242         verifyPrimaryZonePresent(carAudioZones);
243         addRemainingMicrophonesToPrimaryZone(carAudioZones);
244         return carAudioZones;
245     }
246 
addRemainingMicrophonesToPrimaryZone(SparseArray<CarAudioZone> carAudioZones)247     private void addRemainingMicrophonesToPrimaryZone(SparseArray<CarAudioZone> carAudioZones) {
248         CarAudioZone primaryAudioZone = carAudioZones.get(PRIMARY_AUDIO_ZONE);
249         for (AudioDeviceInfo info : mAddressToInputAudioDeviceInfoForAllInputDevices.values()) {
250             if (!mAssignedInputAudioDevices.contains(info.getAddress())
251                     && isMicrophoneInputDevice(info)) {
252                 primaryAudioZone.addInputAudioDevice(new AudioDeviceAttributes(info));
253             }
254         }
255     }
256 
verifyOnlyOnePrimaryZone(CarAudioZone newZone, SparseArray<CarAudioZone> zones)257     private void verifyOnlyOnePrimaryZone(CarAudioZone newZone, SparseArray<CarAudioZone> zones) {
258         if (newZone.getId() == PRIMARY_AUDIO_ZONE && zones.contains(PRIMARY_AUDIO_ZONE)) {
259             throw new RuntimeException("More than one zone parsed with primary audio zone ID: "
260                             + PRIMARY_AUDIO_ZONE);
261         }
262     }
263 
verifyPrimaryZonePresent(SparseArray<CarAudioZone> zones)264     private void verifyPrimaryZonePresent(SparseArray<CarAudioZone> zones) {
265         if (!zones.contains(PRIMARY_AUDIO_ZONE)) {
266             throw new RuntimeException("Primary audio zone is required");
267         }
268     }
269 
parseAudioZone(XmlPullParser parser)270     private CarAudioZone parseAudioZone(XmlPullParser parser)
271             throws XmlPullParserException, IOException {
272         final boolean isPrimary = Boolean.parseBoolean(
273                 parser.getAttributeValue(NAMESPACE, ATTR_IS_PRIMARY));
274         final String zoneName = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_NAME);
275         final int audioZoneId = getZoneId(isPrimary, parser);
276         parseOccupantZoneId(audioZoneId, parser);
277         final CarAudioZone zone = new CarAudioZone(audioZoneId, zoneName);
278         while (parser.next() != XmlPullParser.END_TAG) {
279             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
280             // Expect one <volumeGroups> in one audio zone
281             if (TAG_VOLUME_GROUPS.equals(parser.getName())) {
282                 parseVolumeGroups(parser, zone);
283             } else if (TAG_INPUT_DEVICES.equals(parser.getName())) {
284                 parseInputAudioDevices(parser, zone);
285             } else {
286                 skip(parser);
287             }
288         }
289         return zone;
290     }
291 
getZoneId(boolean isPrimary, XmlPullParser parser)292     private int getZoneId(boolean isPrimary, XmlPullParser parser) {
293         String audioZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_ID);
294         if (isVersionOne()) {
295             Preconditions.checkArgument(audioZoneIdString == null,
296                     "Invalid audio attribute %s"
297                             + ", Please update car audio configurations file "
298                             + "to version to 2 to use it.", ATTR_ZONE_ID);
299             return isPrimary ? PRIMARY_AUDIO_ZONE
300                     : getNextSecondaryZoneId();
301         }
302         // Primary zone does not need to define it
303         if (isPrimary && audioZoneIdString == null) {
304             return PRIMARY_AUDIO_ZONE;
305         }
306         Objects.requireNonNull(audioZoneIdString, () ->
307                 "Requires " + ATTR_ZONE_ID + " for all audio zones.");
308         int zoneId = parsePositiveIntAttribute(ATTR_ZONE_ID, audioZoneIdString);
309         //Verify that primary zone id is PRIMARY_AUDIO_ZONE
310         if (isPrimary) {
311             Preconditions.checkArgument(zoneId == PRIMARY_AUDIO_ZONE,
312                     "Primary zone %s must be %d or it can be left empty.",
313                     ATTR_ZONE_ID, PRIMARY_AUDIO_ZONE);
314         } else {
315             Preconditions.checkArgument(zoneId != PRIMARY_AUDIO_ZONE,
316                     "%s can only be %d for primary zone.",
317                     ATTR_ZONE_ID, PRIMARY_AUDIO_ZONE);
318         }
319         validateAudioZoneIdIsUnique(zoneId);
320         return zoneId;
321     }
322 
parseOccupantZoneId(int audioZoneId, XmlPullParser parser)323     private void parseOccupantZoneId(int audioZoneId, XmlPullParser parser) {
324         String occupantZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_OCCUPANT_ZONE_ID);
325         if (isVersionOne()) {
326             Preconditions.checkArgument(occupantZoneIdString == null,
327                     "Invalid audio attribute %s"
328                             + ", Please update car audio configurations file "
329                             + "to version to 2 to use it.", ATTR_OCCUPANT_ZONE_ID);
330             return;
331         }
332         //Occupant id not required for all zones
333         if (occupantZoneIdString == null) {
334             return;
335         }
336         int occupantZoneId = parsePositiveIntAttribute(ATTR_OCCUPANT_ZONE_ID, occupantZoneIdString);
337         validateOccupantZoneIdIsUnique(occupantZoneId);
338         mZoneIdToOccupantZoneIdMapping.put(audioZoneId, occupantZoneId);
339     }
340 
parsePositiveIntAttribute(String attribute, String integerString)341     private int parsePositiveIntAttribute(String attribute, String integerString) {
342         try {
343             return Integer.parseUnsignedInt(integerString);
344         } catch (NumberFormatException | IndexOutOfBoundsException e) {
345             throw new IllegalArgumentException(attribute + " must be a positive integer, but was \""
346                     + integerString + "\" instead.", e);
347         }
348     }
349 
parseInputAudioDevices(XmlPullParser parser, CarAudioZone zone)350     private void parseInputAudioDevices(XmlPullParser parser, CarAudioZone zone)
351             throws IOException, XmlPullParserException {
352         if (isVersionOne()) {
353             throw new IllegalStateException(
354                     TAG_INPUT_DEVICES + " are not supported in car_audio_configuration.xml version "
355                             + SUPPORTED_VERSION_1);
356         }
357         while (parser.next() != XmlPullParser.END_TAG) {
358             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
359             if (TAG_INPUT_DEVICE.equals(parser.getName())) {
360                 String audioDeviceAddress =
361                         parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
362                 validateInputAudioDeviceAddress(audioDeviceAddress);
363                 AudioDeviceInfo info =
364                         mAddressToInputAudioDeviceInfoForAllInputDevices.get(audioDeviceAddress);
365                 Preconditions.checkArgument(info != null,
366                         "%s %s of %s does not exist, add input device to"
367                                 + " audio_policy_configuration.xml.",
368                         ATTR_DEVICE_ADDRESS, audioDeviceAddress, TAG_INPUT_DEVICE);
369                 zone.addInputAudioDevice(new AudioDeviceAttributes(info));
370             }
371             skip(parser);
372         }
373     }
374 
validateInputAudioDeviceAddress(String audioDeviceAddress)375     private void validateInputAudioDeviceAddress(String audioDeviceAddress) {
376         Objects.requireNonNull(audioDeviceAddress, () ->
377                 TAG_INPUT_DEVICE + " " + ATTR_DEVICE_ADDRESS + " attribute must be present.");
378         Preconditions.checkArgument(!audioDeviceAddress.isEmpty(),
379                 "%s %s attribute can not be empty.",
380                 TAG_INPUT_DEVICE, ATTR_DEVICE_ADDRESS);
381         if (mAssignedInputAudioDevices.contains(audioDeviceAddress)) {
382             throw new IllegalArgumentException(TAG_INPUT_DEVICE + " " + audioDeviceAddress
383                     + " repeats, " + TAG_INPUT_DEVICES + " can not repeat.");
384         }
385         mAssignedInputAudioDevices.add(audioDeviceAddress);
386     }
387 
validateOccupantZoneIdIsUnique(int occupantZoneId)388     private void validateOccupantZoneIdIsUnique(int occupantZoneId) {
389         if (mZoneIdToOccupantZoneIdMapping.indexOfValue(occupantZoneId) > -1) {
390             throw new IllegalArgumentException(ATTR_OCCUPANT_ZONE_ID + " " + occupantZoneId
391                     + " is already associated with a zone");
392         }
393     }
394 
validateAudioZoneIdIsUnique(int audioZoneId)395     private void validateAudioZoneIdIsUnique(int audioZoneId) {
396         if (mAudioZoneIds.contains(audioZoneId)) {
397             throw new IllegalArgumentException(ATTR_ZONE_ID + " " + audioZoneId
398                     + " is already associated with a zone");
399         }
400         mAudioZoneIds.add(audioZoneId);
401     }
402 
parseVolumeGroups(XmlPullParser parser, CarAudioZone zone)403     private void parseVolumeGroups(XmlPullParser parser, CarAudioZone zone)
404             throws XmlPullParserException, IOException {
405         int groupId = 0;
406         while (parser.next() != XmlPullParser.END_TAG) {
407             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
408             if (TAG_VOLUME_GROUP.equals(parser.getName())) {
409                 zone.addVolumeGroup(parseVolumeGroup(parser, zone.getId(), groupId));
410                 groupId++;
411             } else {
412                 skip(parser);
413             }
414         }
415     }
416 
parseVolumeGroup(XmlPullParser parser, int zoneId, int groupId)417     private CarVolumeGroup parseVolumeGroup(XmlPullParser parser, int zoneId, int groupId)
418             throws XmlPullParserException, IOException {
419         CarVolumeGroup.Builder groupBuilder =
420                 new CarVolumeGroup.Builder(zoneId, groupId, mCarAudioSettings,
421                         mUseCarVolumeGroupMute);
422         while (parser.next() != XmlPullParser.END_TAG) {
423             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
424             if (TAG_AUDIO_DEVICE.equals(parser.getName())) {
425                 String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
426                 validateOutputDeviceExist(address);
427                 parseVolumeGroupContexts(parser, groupBuilder, address);
428             } else {
429                 skip(parser);
430             }
431         }
432         return groupBuilder.build();
433     }
434 
validateOutputDeviceExist(String address)435     private void validateOutputDeviceExist(String address) {
436         if (!mAddressToCarAudioDeviceInfo.containsKey(address)) {
437             throw new IllegalStateException(String.format(
438                     "Output device address %s does not belong to any configured output device.",
439                     address));
440         }
441     }
442 
parseVolumeGroupContexts( XmlPullParser parser, CarVolumeGroup.Builder groupBuilder, String address)443     private void parseVolumeGroupContexts(
444             XmlPullParser parser, CarVolumeGroup.Builder groupBuilder, String address)
445             throws XmlPullParserException, IOException {
446         while (parser.next() != XmlPullParser.END_TAG) {
447             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
448             if (TAG_CONTEXT.equals(parser.getName())) {
449                 @AudioContext int carAudioContext = parseCarAudioContext(
450                         parser.getAttributeValue(NAMESPACE, ATTR_CONTEXT_NAME));
451                 validateCarAudioContextSupport(carAudioContext);
452                 CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
453                 groupBuilder.setDeviceInfoForContext(carAudioContext, info);
454 
455                 // If V1, default new contexts to same device as DEFAULT_AUDIO_USAGE
456                 if (isVersionOne() && carAudioContext == CarAudioService.DEFAULT_AUDIO_CONTEXT) {
457                     setNonLegacyContexts(groupBuilder, info);
458                 }
459             }
460             // Always skip to upper level since we're at the lowest.
461             skip(parser);
462         }
463     }
464 
isVersionOne()465     private boolean isVersionOne() {
466         return mCurrentVersion == SUPPORTED_VERSION_1;
467     }
468 
skip(XmlPullParser parser)469     private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
470         if (parser.getEventType() != XmlPullParser.START_TAG) {
471             throw new IllegalStateException();
472         }
473         int depth = 1;
474         while (depth != 0) {
475             switch (parser.next()) {
476                 case XmlPullParser.END_TAG:
477                     depth--;
478                     break;
479                 case XmlPullParser.START_TAG:
480                     depth++;
481                     break;
482             }
483         }
484     }
485 
parseCarAudioContext(String context)486     private static @AudioContext int parseCarAudioContext(String context) {
487         return CONTEXT_NAME_MAP.getOrDefault(context.toLowerCase(), CarAudioContext.INVALID);
488     }
489 
validateCarAudioContextSupport(@udioContext int audioContext)490     private void validateCarAudioContextSupport(@AudioContext int audioContext) {
491         if (isVersionOne() && NON_LEGACY_CONTEXTS.contains(audioContext)) {
492             throw new IllegalArgumentException(String.format(
493                     "Non-legacy audio contexts such as %s are not supported in "
494                             + "car_audio_configuration.xml version %d",
495                     CarAudioContext.toString(audioContext), SUPPORTED_VERSION_1));
496         }
497     }
498 
getNextSecondaryZoneId()499     private int getNextSecondaryZoneId() {
500         int zoneId = mNextSecondaryZoneId;
501         mNextSecondaryZoneId += 1;
502         return zoneId;
503     }
504 }
505